Her grubun ilk satırı nasıl seçilir?


144

Aşağıdaki gibi oluşturulan bir DataFrame var:

df.groupBy($"Hour", $"Category")
  .agg(sum($"value") as "TotalValue")
  .sort($"Hour".asc, $"TotalValue".desc))

Sonuçlar şöyle görünür:

+----+--------+----------+
|Hour|Category|TotalValue|
+----+--------+----------+
|   0|   cat26|      30.9|
|   0|   cat13|      22.1|
|   0|   cat95|      19.6|
|   0|  cat105|       1.3|
|   1|   cat67|      28.5|
|   1|    cat4|      26.8|
|   1|   cat13|      12.6|
|   1|   cat23|       5.3|
|   2|   cat56|      39.6|
|   2|   cat40|      29.7|
|   2|  cat187|      27.9|
|   2|   cat68|       9.8|
|   3|    cat8|      35.6|
| ...|    ....|      ....|
+----+--------+----------+

Gördüğünüz gibi, DataFrame Hourartan bir sırada, sonra TotalValueazalan bir sırada sıralanır.

Her grubun en üst satırını seçmek istiyorum, yani

  • Hour == 0 grubundan seçin (0, kat26,30.9)
  • Hour == 1 grubundan seçin (1, cat67,28.5)
  • Hour == 2 grubundan seçin (2, cat56,39.6)
  • ve bunun gibi

Böylece istenen çıktı:

+----+--------+----------+
|Hour|Category|TotalValue|
+----+--------+----------+
|   0|   cat26|      30.9|
|   1|   cat67|      28.5|
|   2|   cat56|      39.6|
|   3|    cat8|      35.6|
| ...|     ...|       ...|
+----+--------+----------+

Her bir grubun en üstteki N satırını da seçmek kullanışlı olabilir.

Herhangi bir yardım çok takdir edilmektedir.

Yanıtlar:


234

Pencere fonksiyonları :

Böyle bir şey hile yapmalıdır:

import org.apache.spark.sql.functions.{row_number, max, broadcast}
import org.apache.spark.sql.expressions.Window

val df = sc.parallelize(Seq(
  (0,"cat26",30.9), (0,"cat13",22.1), (0,"cat95",19.6), (0,"cat105",1.3),
  (1,"cat67",28.5), (1,"cat4",26.8), (1,"cat13",12.6), (1,"cat23",5.3),
  (2,"cat56",39.6), (2,"cat40",29.7), (2,"cat187",27.9), (2,"cat68",9.8),
  (3,"cat8",35.6))).toDF("Hour", "Category", "TotalValue")

val w = Window.partitionBy($"hour").orderBy($"TotalValue".desc)

val dfTop = df.withColumn("rn", row_number.over(w)).where($"rn" === 1).drop("rn")

dfTop.show
// +----+--------+----------+
// |Hour|Category|TotalValue|
// +----+--------+----------+
// |   0|   cat26|      30.9|
// |   1|   cat67|      28.5|
// |   2|   cat56|      39.6|
// |   3|    cat8|      35.6|
// +----+--------+----------+

Önemli veri eğriliği durumunda bu yöntem verimsiz olacaktır.

Sade SQL toplama ve ardındanjoin :

Alternatif olarak, birleştirilmiş veri çerçevesine katılabilirsiniz:

val dfMax = df.groupBy($"hour".as("max_hour")).agg(max($"TotalValue").as("max_value"))

val dfTopByJoin = df.join(broadcast(dfMax),
    ($"hour" === $"max_hour") && ($"TotalValue" === $"max_value"))
  .drop("max_hour")
  .drop("max_value")

dfTopByJoin.show

// +----+--------+----------+
// |Hour|Category|TotalValue|
// +----+--------+----------+
// |   0|   cat26|      30.9|
// |   1|   cat67|      28.5|
// |   2|   cat56|      39.6|
// |   3|    cat8|      35.6|
// +----+--------+----------+

Yinelenen değerleri korur (aynı toplam değere sahip saatte birden fazla kategori varsa). Bunları aşağıdaki gibi kaldırabilirsiniz:

dfTopByJoin
  .groupBy($"hour")
  .agg(
    first("category").alias("category"),
    first("TotalValue").alias("TotalValue"))

Üzerinde sipariş kullanmastructs :

Çok iyi test edilmemesine rağmen, birleştirme veya pencere fonksiyonları gerektirmeyen düzgün numaralar:

val dfTop = df.select($"Hour", struct($"TotalValue", $"Category").alias("vs"))
  .groupBy($"hour")
  .agg(max("vs").alias("vs"))
  .select($"Hour", $"vs.Category", $"vs.TotalValue")

dfTop.show
// +----+--------+----------+
// |Hour|Category|TotalValue|
// +----+--------+----------+
// |   0|   cat26|      30.9|
// |   1|   cat67|      28.5|
// |   2|   cat56|      39.6|
// |   3|    cat8|      35.6|
// +----+--------+----------+

DataSet API'sı ile (Spark 1.6+, 2.0+):

Kıvılcım 1.6 :

case class Record(Hour: Integer, Category: String, TotalValue: Double)

df.as[Record]
  .groupBy($"hour")
  .reduce((x, y) => if (x.TotalValue > y.TotalValue) x else y)
  .show

// +---+--------------+
// | _1|            _2|
// +---+--------------+
// |[0]|[0,cat26,30.9]|
// |[1]|[1,cat67,28.5]|
// |[2]|[2,cat56,39.6]|
// |[3]| [3,cat8,35.6]|
// +---+--------------+

Spark 2.0 veya üstü :

df.as[Record]
  .groupByKey(_.Hour)
  .reduceGroups((x, y) => if (x.TotalValue > y.TotalValue) x else y)

Son iki yöntem, harita tarafı birleştirmeden yararlanabilir ve tam karıştırma gerektirmez, bu nedenle çoğu zaman pencere işlevleri ve birleşimlerine kıyasla daha iyi bir performans göstermelidir. Bunlar, completedçıkış modunda Yapısal Akış ile de kullanılabilir .

Kullanmayın :

df.orderBy(...).groupBy(...).agg(first(...), ...)

O (özellikle iş gibi görünebilir localmodda) ama güvenilmez (bkz SPARK-16207 için, kredi Tzach Zohar için ilgili JIRA sorunu bağlayan ve SPARK-30335 ).

Aynı not aşağıdakiler için de geçerlidir

df.orderBy(...).dropDuplicates(...)

dahili olarak eşdeğer yürütme planı kullanır.


3
Kıvılcım 1.6 beri satır yerine satır_sayısı () gibi görünüyorNumara
Adam Szałucha

Hakkında kullanma df.orderBy (...). GropBy (...). Hangi koşullarda orderBy'ye (...) güvenebiliriz? veya orderBy () öğesinin doğru sonucu vereceğinden emin olamıyorsak, başka alternatiflerimiz var mı?
Ignacio Alorre

Bir şeyi gözden kaçırıyor olabilirim, ancak genel olarak groupByKey'den kaçınmanız önerilir , bunun yerine reduceByKey kullanılmalıdır. Ayrıca, bir satır tasarruf edeceksiniz.
Thomas

3
@ GroupBy / groupByKey'den kaçınan çocuklar RDD'lerle uğraşırken, Dataset api'nin reduceByKey işlevinin bile olmadığını fark edeceksiniz.
soote


16

Birden çok sütuna göre gruplama ile Spark 2.0.2 için:

import org.apache.spark.sql.functions.row_number
import org.apache.spark.sql.expressions.Window

val w = Window.partitionBy($"col1", $"col2", $"col3").orderBy($"timestamp".desc)

val refined_df = df.withColumn("rn", row_number.over(w)).where($"rn" === 1).drop("rn")

8

Bu bir tam aynı olduğunu zero323 'ın cevabı ancak SQL sorgusu bir şekilde.

Veri çerçevesinin oluşturulduğu ve kaydedildiği varsayılarak

df.createOrReplaceTempView("table")
//+----+--------+----------+
//|Hour|Category|TotalValue|
//+----+--------+----------+
//|0   |cat26   |30.9      |
//|0   |cat13   |22.1      |
//|0   |cat95   |19.6      |
//|0   |cat105  |1.3       |
//|1   |cat67   |28.5      |
//|1   |cat4    |26.8      |
//|1   |cat13   |12.6      |
//|1   |cat23   |5.3       |
//|2   |cat56   |39.6      |
//|2   |cat40   |29.7      |
//|2   |cat187  |27.9      |
//|2   |cat68   |9.8       |
//|3   |cat8    |35.6      |
//+----+--------+----------+

Pencere fonksiyonu:

sqlContext.sql("select Hour, Category, TotalValue from (select *, row_number() OVER (PARTITION BY Hour ORDER BY TotalValue DESC) as rn  FROM table) tmp where rn = 1").show(false)
//+----+--------+----------+
//|Hour|Category|TotalValue|
//+----+--------+----------+
//|1   |cat67   |28.5      |
//|3   |cat8    |35.6      |
//|2   |cat56   |39.6      |
//|0   |cat26   |30.9      |
//+----+--------+----------+

Düz SQL toplama ve ardından birleştirme:

sqlContext.sql("select Hour, first(Category) as Category, first(TotalValue) as TotalValue from " +
  "(select Hour, Category, TotalValue from table tmp1 " +
  "join " +
  "(select Hour as max_hour, max(TotalValue) as max_value from table group by Hour) tmp2 " +
  "on " +
  "tmp1.Hour = tmp2.max_hour and tmp1.TotalValue = tmp2.max_value) tmp3 " +
  "group by tmp3.Hour")
  .show(false)
//+----+--------+----------+
//|Hour|Category|TotalValue|
//+----+--------+----------+
//|1   |cat67   |28.5      |
//|3   |cat8    |35.6      |
//|2   |cat56   |39.6      |
//|0   |cat26   |30.9      |
//+----+--------+----------+

Yapılar üzerinden sipariş kullanma:

sqlContext.sql("select Hour, vs.Category, vs.TotalValue from (select Hour, max(struct(TotalValue, Category)) as vs from table group by Hour)").show(false)
//+----+--------+----------+
//|Hour|Category|TotalValue|
//+----+--------+----------+
//|1   |cat67   |28.5      |
//|3   |cat8    |35.6      |
//|2   |cat56   |39.6      |
//|0   |cat26   |30.9      |
//+----+--------+----------+

DataSets yolu ve yapmayın , orijinal yanıttaki ile aynıdır


2

Desen, tuşlara göre gruplandır => her gruba bir şey yap, örneğin reduce => veri çerçevesine geri

Bu durumda Dataframe soyutlamanın biraz hantal olduğunu düşündüm, bu yüzden RDD işlevselliğini kullandım

 val rdd: RDD[Row] = originalDf
  .rdd
  .groupBy(row => row.getAs[String]("grouping_row"))
  .map(iterableTuple => {
    iterableTuple._2.reduce(reduceFunction)
  })

val productDf = sqlContext.createDataFrame(rdd, originalDf.schema)

1

Aşağıdaki çözüm yalnızca bir grup yaparBir çerçevede maxValue'yu içeren veri çerçevenizin satırlarını çıkarın ve ayıklayın. Başka birleştirme veya Windows gerekmez.

import org.apache.spark.sql.Row
import org.apache.spark.sql.catalyst.encoders.RowEncoder
import org.apache.spark.sql.DataFrame

//df is the dataframe with Day, Category, TotalValue

implicit val dfEnc = RowEncoder(df.schema)

val res: DataFrame = df.groupByKey{(r) => r.getInt(0)}.mapGroups[Row]{(day: Int, rows: Iterator[Row]) => i.maxBy{(r) => r.getDouble(2)}}

Ama önce her şeyi karıştırır. Bu neredeyse bir gelişmedir (verilere bağlı olarak pencere işlevlerinden daha kötü olmayabilir).
Alper t. Turker

bir grubunuz var, bu bir karışıklığı tetikleyecek. Pencere işlevinden daha kötü değildir, çünkü bir pencere işlevinde veri çerçevesindeki her bir satır için pencereyi değerlendirir.
elghoto

1

Bunu dataframe api ile yapmanın güzel bir yolu argmax mantığını böyle kullanmaktır.

  val df = Seq(
    (0,"cat26",30.9), (0,"cat13",22.1), (0,"cat95",19.6), (0,"cat105",1.3),
    (1,"cat67",28.5), (1,"cat4",26.8), (1,"cat13",12.6), (1,"cat23",5.3),
    (2,"cat56",39.6), (2,"cat40",29.7), (2,"cat187",27.9), (2,"cat68",9.8),
    (3,"cat8",35.6)).toDF("Hour", "Category", "TotalValue")

  df.groupBy($"Hour")
    .agg(max(struct($"TotalValue", $"Category")).as("argmax"))
    .select($"Hour", $"argmax.*").show

 +----+----------+--------+
 |Hour|TotalValue|Category|
 +----+----------+--------+
 |   1|      28.5|   cat67|
 |   3|      35.6|    cat8|
 |   2|      39.6|   cat56|
 |   0|      30.9|   cat26|
 +----+----------+--------+

0

Burada bunu yapabilirsiniz -

   val data = df.groupBy("Hour").agg(first("Hour").as("_1"),first("Category").as("Category"),first("TotalValue").as("TotalValue")).drop("Hour")

data.withColumnRenamed("_1","Hour").show

-2

Rank () window fonksiyonunu kullanabiliriz (rank = 1'i seçersiniz) sıralaması sadece bir grubun her satırı için bir sayı ekler (bu durumda saat olurdu)

işte bir örnek. ( https://github.com/jaceklaskowski/mastering-apache-spark-book/blob/master/spark-sql-functions.adoc#rank adresinden )

val dataset = spark.range(9).withColumn("bucket", 'id % 3)

import org.apache.spark.sql.expressions.Window
val byBucket = Window.partitionBy('bucket).orderBy('id)

scala> dataset.withColumn("rank", rank over byBucket).show
+---+------+----+
| id|bucket|rank|
+---+------+----+
|  0|     0|   1|
|  3|     0|   2|
|  6|     0|   3|
|  1|     1|   1|
|  4|     1|   2|
|  7|     1|   3|
|  2|     2|   1|
|  5|     2|   2|
|  8|     2|   3|
+---+------+----+
Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.