web-dev-qa-db-fra.com

Spark DataFrame: groupBy après orderBy maintient-il cet ordre?

J'ai un Spark 2.0 dataframe example avec la structure suivante:

id, hour, count
id1, 0, 12
id1, 1, 55
..
id1, 23, 44
id2, 0, 12
id2, 1, 89
..
id2, 23, 34
etc.

Il contient 24 entrées pour chaque id (une pour chaque heure de la journée) et est ordonné par id, heure à l'aide de la fonction orderBy.

J'ai créé un agrégateur groupConcat:

  def groupConcat(separator: String, columnToConcat: Int) = new Aggregator[Row, String, String] with Serializable {
    override def zero: String = ""

    override def reduce(b: String, a: Row) = b + separator + a.get(columnToConcat)

    override def merge(b1: String, b2: String) = b1 + b2

    override def finish(b: String) = b.substring(1)

    override def bufferEncoder: Encoder[String] = Encoders.STRING

    override def outputEncoder: Encoder[String] = Encoders.STRING
  }.toColumn

Cela m'aide à concaténer des colonnes en chaînes pour obtenir cette trame de données finale:

id, hourly_count
id1, 12:55:..:44
id2, 12:89:..:34
etc.

Ma question est, si je fais example.orderBy($"id",$"hour").groupBy("id").agg(groupConcat(":",2) as "hourly_count"), cela garantit-il que les comptages horaires seront ordonnés correctement dans leurs compartiments respectifs?

J'ai lu que ce n'est pas nécessairement le cas pour les RDD (voir Spark trie par clé puis regroupe pour obtenir un ordre itérable? ), mais c'est peut-être différent pour les DataFrames?

Sinon, comment puis-je contourner ce problème?

15
Ana Todor

groupBy after orderBy ne maintient pas l'ordre, comme d'autres l'ont souligné. Ce que vous voulez faire, c'est utiliser une fonction Window - partitionner sur id et classer par heures. Vous pouvez collecter_list dessus et ensuite prendre le maximum (le plus grand) des listes résultantes car elles vont cumulativement (c'est-à-dire que la première heure n'aura que lui-même dans la liste, la deuxième heure aura 2 éléments dans la liste, et ainsi de suite).

Exemple de code complet:

import org.Apache.spark.sql.functions._
import org.Apache.spark.sql.expressions.Window
import spark.implicits._

val data = Seq(( "id1", 0, 12),
  ("id1", 1, 55),
  ("id1", 23, 44),
  ("id2", 0, 12),
  ("id2", 1, 89),
  ("id2", 23, 34)).toDF("id", "hour", "count")

    val mergeList = udf{(strings: Seq[String]) => strings.mkString(":")}
    data.withColumn("collected", collect_list($"count")
                                                    .over(Window.partitionBy("id")
                                                                 .orderBy("hour")))
            .groupBy("id")
            .agg(max($"collected").as("collected"))
            .withColumn("hourly_count", mergeList($"collected"))
            .select("id", "hourly_count").show

Cela nous maintient dans le monde DataFrame. J'ai également simplifié le code UDF utilisé par l'OP.

Production:

+---+------------+
| id|hourly_count|
+---+------------+
|id1|    12:55:44|
|id2|    12:89:34|
+---+------------+
17
Adair

Si vous souhaitez contourner l'implémentation dans Java (Scala et Python devrait être similaire):

example.orderBy(“hour”)
.groupBy(“id”)
.agg(functions.sort_array(
  functions.collect_list( 
     functions.struct(dataRow.col(“hour”),
                      dataRow.col(“count”))),false)
 .as(“hourly_count”));
5
Shyam

J'ai un cas où la commande n'est pas toujours conservée: parfois oui, surtout non.

Mon dataframe a 200 partitions fonctionnant sur Spark 1.6

df_group_sort = data.orderBy(times).groupBy(group_key).agg(
                                                  F.sort_array(F.collect_list(times)),
                                                  F.collect_list(times)
                                                           )

pour vérifier la commande, je compare les valeurs de retour de

F.sort_array(F.collect_list(times))

et

F.collect_list(times)

donner par exemple (gauche: sort_array (collect_list ()); droite: collect_list ())

2016-12-19 08:20:27.172000 2016-12-19 09:57:03.764000
2016-12-19 08:20:30.163000 2016-12-19 09:57:06.763000
2016-12-19 08:20:33.158000 2016-12-19 09:57:09.763000
2016-12-19 08:20:36.158000 2016-12-19 09:57:12.763000
2016-12-19 08:22:27.090000 2016-12-19 09:57:18.762000
2016-12-19 08:22:30.089000 2016-12-19 09:57:33.766000
2016-12-19 08:22:57.088000 2016-12-19 09:57:39.811000
2016-12-19 08:23:03.085000 2016-12-19 09:57:45.770000
2016-12-19 08:23:06.086000 2016-12-19 09:57:57.809000
2016-12-19 08:23:12.085000 2016-12-19 09:59:56.333000
2016-12-19 08:23:15.086000 2016-12-19 10:00:11.329000
2016-12-19 08:23:18.087000 2016-12-19 10:00:14.331000
2016-12-19 08:23:21.085000 2016-12-19 10:00:17.329000
2016-12-19 08:23:24.085000 2016-12-19 10:00:20.326000

La colonne de gauche est toujours triée, tandis que la colonne de droite se compose uniquement de blocs triés. Pour différentes exécutions de take (), l'ordre des blocs dans la colonne de droite est différent.

5
Kat

l'ordre peut être différent ou non, selon le nombre de partitions et la distribution des données. Nous pouvons résoudre en utilisant rdd lui-même.

Par exemple::

J'ai enregistré les exemples de données ci-dessous dans un fichier et les ai chargés dans hdfs.

1,type1,300
2,type1,100
3,type2,400
4,type2,500
5,type1,400
6,type3,560
7,type2,200
8,type3,800

et exécuté la commande ci-dessous:

sc.textFile("/spark_test/test.txt").map(x=>x.split(",")).filter(x=>x.length==3).groupBy(_(1)).mapValues(x=>x.toList.sortBy(_(2)).map(_(0)).mkString("~")).collect()

production:

Array[(String, String)] = Array((type3,6~8), (type1,2~1~5), (type2,7~3~4))

C'est-à-dire que nous avons regroupé les données par type, triées par la suite par prix, et avons concaténé les identifiants avec "~" comme séparateur. La commande ci-dessus peut être brisée comme ci-dessous:

val validData=sc.textFile("/spark_test/test.txt").map(x=>x.split(",")).filter(x=>x.length==3)

val groupedData=validData.groupBy(_(1))  //group data rdds

val sortedJoinedData=groupedData.mapValues(x=>{
   val list=x.toList
   val sortedList=list.sortBy(_(2))
   val idOnlyList=sortedList.map(_(0))
   idOnlyList.mkString("~")
}
)
sortedJoinedData.collect()

nous pouvons ensuite prendre un groupe particulier en utilisant la commande

sortedJoinedData.filter(_._1=="type1").collect()

production:

Array[(String, String)] = Array((type1,2~1~5))
1
Ashish

Non, le tri dans groupByKey ne sera pas nécessairement maintenu, mais cela est notoirement difficile à reproduire en mémoire sur un nœud. Comme cela a été dit précédemment, la manière la plus courante de procéder est lorsque les choses doivent être repartitionnées pour que le groupByKey ait lieu. J'ai réussi à reproduire cela en faisant manuellement un repartition après le sort. J'ai ensuite passé les résultats dans le groupByKey.

case class Numbered(num:Int, group:Int, otherData:Int)

// configure spark with "spark.sql.shuffle.partitions" = 2 or some other small number 

val v =
  (1 to 100000)
    // Make waaay more groups then partitions. I added an extra integer just to mess with the sort hash computation (i.e. so it won't be monotonic, not sure if needed)
    .map(Numbered(_, Random.nextInt(300), Random.nextInt(1000000))).toDS()
    // Be sure they are stored in a small number of partitions
    .repartition(2)
    .sort($"num")
    // Repartition again with a waaay bigger number then there are groups so that when things need to be merged you can get them out of order.
    .repartition(200)
    .groupByKey(_.group)
    .mapGroups {
      case (g, nums) =>
        nums             // all you need is .sortBy(_.num) here to fix the problem          
          .map(_.num)
          .mkString("~")
    }
    .collect()

// Walk through the concatenated strings. If any number ahead 
// is smaller than the number before it, you know that something
// is out of order.
v.zipWithIndex.map { case (r, i) =>
  r.split("~").map(_.toInt).foldLeft(0) { case (prev, next) =>
    if (next < prev) {
      println(s"*** Next: ${next} less then ${prev} for dataset ${i + 1} ***")
    }
    next
  }
}
0
ChoppyTheLumberjack