Как выбрать первую строку каждой группы?
у меня есть фрейм данных, созданный следующим образом:
df.groupBy($"Hour", $"Category")
.agg(sum($"value") as "TotalValue")
.sort($"Hour".asc, $"TotalValue".desc))
результаты выглядят так:
+----+--------+----------+
|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|
| ...| ....| ....|
+----+--------+----------+
как вы можете видеть, фрейм данных упорядочен по Hour
в порядке возрастания, затем TotalValue
в порядке убывания.
Я хотел бы выбрать верхнюю строку для каждой группы, т. е.
- из группы Час==0 выберите (0,cat26,30.9)
- из группы час= = 1 Выберите (1, cat67, 28.5)
- из группы Hour==2 select (2, cat56, 39.6)
- и так далее
таким образом, желаемый результат будет:
+----+--------+----------+
|Hour|Category|TotalValue|
+----+--------+----------+
| 0| cat26| 30.9|
| 1| cat67| 28.5|
| 2| cat56| 39.6|
| 3| cat8| 35.6|
| ...| ...| ...|
+----+--------+----------+
Это может быть удобно, чтобы иметь возможность выбрать первые n строк из каждой группы, а также.
любая помощь будет высоко ценится.
7 ответов:
окне функции:
что-то вроде этого должно сделать трюк:
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| // +----+--------+----------+
этот метод будет неэффективен в случае значительного перекоса данных.
простая агрегация SQL, за которой следует
join
:в качестве альтернативы вы можете присоединиться к агрегированному фрейму данных:
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| // +----+--------+----------+
он будет сохранять повторяющиеся значения (если есть более одной категории в час с тем же общим значением). Вы можете удалите их следующим образом:
dfTopByJoin .groupBy($"hour") .agg( first("category").alias("category"), first("TotalValue").alias("TotalValue"))
через заказ свыше
structs
:аккуратный, хотя и не очень хорошо протестированный, трюк, который не требует объединения или оконных функций:
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| // +----+--------+----------+
С API набора данных (СПАРК 1.6+, 2.0+):
зажигания 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 или более поздней версии:
df.as[Record] .groupByKey(_.Hour) .reduceGroups((x, y) => if (x.TotalValue > y.TotalValue) x else y)
последние два метода могут использовать комбинацию сторон карты и не требуется полное перемешивание, поэтому большую часть времени должна демонстрировать лучшую производительность по сравнению с оконными функциями и соединениями. Эти трости также используются со структурированной потоковой передачей в
completed
режим вывода.не используйте:
df.orderBy(...).groupBy(...).agg(first(...), ...)
может показаться, что это работает (особенно в
local
режим), но это ненадежно (Искра-16207). Кредиты на Цах Зоар на связывание соответствующих JIRA выпуск.то же самое замечание относится к
df.orderBy(...).dropDuplicates(...)
который внутренне использует эквивалентный план выполнения.
для Spark 2.0.2 с группировкой по нескольким столбцам:
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")
Это точно такой же ответ zero323, но в способе sql-запроса
предполагая, что dataframe создается и регистрируется как
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 | //+----+--------+----------+
окно функции :
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 | //+----+--------+----------+
простая агрегация SQL, за которой следует join:
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 | //+----+--------+----------+
использование упорядочивания по структурам:
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 way и не s такие же как в оригинале ответ
Решение ниже делает только один groupBy и извлекает строки вашего фрейма данных, которые содержат maxValue в одном кадре. Нет необходимости в дальнейших соединениях или окнах.
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)}}
Если фрейм данных должен быть сгруппирован по нескольким столбцам, это может помочь
val keys = List("Hour", "Category"); val selectFirstValueOfNoneGroupedColumns = df.columns .filterNot(keys.toSet) .map(_ -> "first").toMap val grouped = df.groupBy(keys.head, keys.tail: _*) .agg(selectFirstValueOfNoneGroupedColumns)
надеюсь, это поможет кому-то с подобной проблемой
здесь вы можете сделать вот так -
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
мы можем использовать функцию окна rank () (где вы выбрали бы rank = 1) ранг просто добавляет число для каждой строки группы (в этом случае это будет час)
вот пример. ( от https://github.com/jaceklaskowski/mastering-apache-spark-book/blob/master/spark-sql-functions.adoc#rank )
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| +---+------+----+