【问题标题】:Group by column "grp" and compress DataFrame - (take last not null value for each column ordering by column "ord")按列“grp”分组并压缩 DataFrame - (按列“ord”排序的每列取最后一个非空值)
【发布时间】:2019-04-08 20:08:53
【问题描述】:

假设我有以下 DataFrame:

+---+--------+---+----+----+
|grp|null_col|ord|col1|col2|
+---+--------+---+----+----+
|  1|    null|  3|null|  11|
|  2|    null|  2| xxx|  22|
|  1|    null|  1| yyy|null|
|  2|    null|  7|null|  33|
|  1|    null| 12|null|null|
|  2|    null| 19|null|  77|
|  1|    null| 10| s13|null|
|  2|    null| 11| a23|null|
+---+--------+---+----+----+

这是带有 cmets 的同一个示例 DF,按 grpord 排序:

scala> df.orderBy("grp", "ord").show
+---+--------+---+----+----+
|grp|null_col|ord|col1|col2|
+---+--------+---+----+----+
|  1|    null|  1| yyy|null|
|  1|    null|  3|null|  11|   # grp:1 - last value for `col2` (11)
|  1|    null| 10| s13|null|   # grp:1 - last value for `col1` (s13)
|  1|    null| 12|null|null|   # grp:1 - last values for `null_col`, `ord`
|  2|    null|  2| xxx|  22|   
|  2|    null|  7|null|  33|   
|  2|    null| 11| a23|null|   # grp:2 - last value for `col1` (a23)
|  2|    null| 19|null|  77|   # grp:2 - last values for `null_col`, `ord`, `col2`
+---+--------+---+----+----+

我想压缩它。 IE。按"grp" 列对它进行分组,对于每个组,按"ord" 列对行进行排序,并在每列中取最后一个not null 值(如果有的话)。

+---+--------+---+----+----+
|grp|null_col|ord|col1|col2|
+---+--------+---+----+----+
|  1|    null| 12| s13|  11|
|  2|    null| 19| a23|  77|
+---+--------+---+----+----+

我见过以下类似的问题:

但我真正的 DataFrame 有超过 250 列,所以我需要一个解决方案,我不必明确指定所有列。

我无法理解它......


MCVE:如何创建示例 DataFrame:

  1. 创建本地文件“/tmp/data.txt”并复制并粘贴 DataFrame 的上下文(如上面发布的那样)
  2. 定义function readSparkOutput():
  3. 将“/tmp/data.txt”解析为DataFrame:

    val df = readSparkOutput("file:///tmp/data.txt")
    

UPDATE:我觉得应该类似于下面的SQL:

SELECT
  grp, ord, null_col, col1, col2
FROM (
    SELECT
      grp,
      ord,
      FIRST(null_col) OVER (PARTITION BY grp ORDER BY ord DESC) as null_col,
      FIRST(col1) OVER (PARTITION BY grp ORDER BY ord DESC) as col1,
      FIRST(col2) OVER (PARTITION BY grp ORDER BY ord DESC) as col2,
      ROW_NUMBER() OVER (PARTITION BY grp ORDER BY ord DESC) as rn
    FROM table_name) as v
WHERE v.rn = 1;

我们如何动态地生成这样的 Spark 查询?

我尝试了以下简化的方法:

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

val win = Window
  .partitionBy("grp")
  .orderBy($"ord".desc)

val cols = df.columns.map(c => first(c, ignoreNulls=true).over(win).as(c))

产生:

scala> cols
res23: Array[org.apache.spark.sql.Column] = Array(first(grp, true) OVER (PARTITION BY grp ORDER BY ord DESC NULLS LAST UnspecifiedFrame) AS `grp`, first(null_col, true) OVER (PARTITION BY grp ORDER BY ord DESC NULLS LAST UnspecifiedFrame) AS `null_col`, first(ord, true) OVER (PARTITION BY grp ORDER BY ord DESC NULLS LAST UnspecifiedFrame) AS `ord`, first(col1, true) OVER (PARTITION BY grp ORDER BY ord DESC NULLS LAST UnspecifiedFrame) AS `col1`, first(col2, true) OVER (PARTITION BY grp ORDER BY ord DESC NULLS LAST UnspecifiedFrame) AS `col2`)

但我无法将其传递给df.select

scala> df.select(cols.head, cols.tail: _*).show
<console>:34: error: no `: _*' annotation allowed here
(such annotations are only allowed in arguments to *-parameters)
       df.select(cols.head, cols.tail: _*).show

又一次尝试:

scala> df.select(cols.map(col): _*).show
<console>:34: error: type mismatch;
 found   : String => org.apache.spark.sql.Column
 required: org.apache.spark.sql.Column => ?
       df.select(cols.map(col): _*).show

【问题讨论】:

  • 好吧,我猜你可以编写一个函数来接受一个 DF 和一个按列名分组的函数,然后你可以通过从 DF 模式中获取剩余的列来动态创建一个选择语句,该模式选择每个列的最大值列。
  • @TerryDactyl,感谢您的建议!不幸的是max() 不会给我我需要的东西。我需要最后一个(就“ord”列而言)而不是空值。而且我仍然不知道如何动态地制作它。如果你能把它放在答案中,我将不胜感激
  • @eliasah,感谢您的评论。您知道如何动态获取所需的数据集吗?
  • 如果你有时间,请考虑在我的帖子中权衡:stackoverflow.com/questions/53927460/… 谢谢 :)
  • @coldspeed,当然,这是一项很棒的工作! :)

标签: scala apache-spark aggregate-functions aggregation


【解决方案1】:

考虑以下方法,将窗口函数 last(c, ignoreNulls=true) 按“ord”每个“grp”排序到每个选定的列;后跟 groupBy("grp") 以获取 first agg(colFcnMap) 结果:

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

val df0 = Seq(
  (1, 3, None, Some(11)),
  (2, 2, Some("aaa"), Some(22)),
  (1, 1, Some("s12"), None),
  (2, 7, None, Some(33)),
  (1, 12, None, None),
  (2, 19, None, Some(77)),
  (1, 10, Some("s13"), None),
  (2, 11, Some("a23"), None)
).toDF("grp", "ord", "col1", "col2")

val df = df0.withColumn("null_col", lit(null))

df.orderBy("grp", "ord").show
// +---+---+----+----+--------+
// |grp|ord|col1|col2|null_col|
// +---+---+----+----+--------+
// |  1|  1| s12|null|    null|
// |  1|  3|null|  11|    null|
// |  1| 10| s13|null|    null|
// |  1| 12|null|null|    null|
// |  2|  2| aaa|  22|    null|
// |  2|  7|null|  33|    null|
// |  2| 11| a23|null|    null|
// |  2| 19|null|  77|    null|
// +---+---+----+----+--------+

val win = Window.partitionBy("grp").orderBy("ord").
  rowsBetween(0, Window.unboundedFollowing)

val nonAggCols = Array("grp")
val cols = df.columns.diff(nonAggCols)  // Columns to be aggregated

val colFcnMap = cols.zip(Array.fill(cols.size)("first")).toMap
// colFcnMap: scala.collection.immutable.Map[String,String] =
//   Map(ord -> first, col1 -> first, col2 -> first, null_col -> first)

cols.foldLeft(df)((acc, c) =>
    acc.withColumn(c, last(c, ignoreNulls=true).over(win))
  ).
  groupBy("grp").agg(colFcnMap).
  select(col("grp") :: colFcnMap.toList.map{case (c, f) => col(s"$f($c)").as(c)}: _*).
  show
// +---+---+----+----+--------+
// |grp|ord|col1|col2|null_col|
// +---+---+----+----+--------+
// |  1| 12| s13|  11|    null|
// |  2| 19| a23|  77|    null|
// +---+---+----+----+--------+

请注意,最后的 select 用于从聚合列名称中剥离函数名称(在本例中为 first())。

【讨论】:

  • 感谢您的帮助!在我的真实 DF 中,我有超过 250 个不同名称的列(它们不以“col”开头),并且一些列将只包含 NULL,就像在我的示例数据集中一样。你知道在这种情况下我该如何调整你的方法吗? PS“ord”列也应该出现在输出数据集中。
  • @MaxU,根据实际的列选择标准,有多种方法可以得出选定的列。例如,如果要聚合大多数列,则可以组合不聚合的列列表并应用diff 将它们排除在外,如扩展答案所示。
  • 我们越来越近了,谢谢! :) 不知何故,我得到了不同的结果数据集('col1': [null, null], 'col2': [null, 77],其他列都可以)。我无法使用我的文件或您答案中的示例数据集来重现您的结果...
  • @MaxU,不知道为什么你会得到不同的结果。我刚刚在新重新启动的 Spark Shell (v.2.2.1) 上使用示例数据集再次执行了确切的代码,结果与我的答案相同。
【解决方案2】:

我已经解决了一些问题,这是代码和输出

import org.apache.spark.sql.functions._
import spark.implicits._

val df0 = Seq(
  (1, 3, None, Some(11)),
  (2, 2, Some("aaa"), Some(22)),
  (1, 1, Some("s12"), None),
  (2, 7, None, Some(33)),
  (1, 12, None, None),
  (2, 19, None, Some(77)),
  (1, 10, Some("s13"), None),
  (2, 11, Some("a23"), None)
).toDF("grp", "ord", "col1", "col2")

df0.show()

//+---+---+----+----+
//|grp|ord|col1|col2|
//+---+---+----+----+
//|  1|  3|null|  11|
//|  2|  2| aaa|  22|
//|  1|  1| s12|null|
//|  2|  7|null|  33|
//|  1| 12|null|null|
//|  2| 19|null|  77|
//|  1| 10| s13|null|
//|  2| 11| a23|null|
//+---+---+----+----+

对前 2 列的数据进行排序

val df1 = df0.select("grp", "ord", "col1", "col2").orderBy("grp", "ord")

df1.show()

//+---+---+----+----+
//|grp|ord|col1|col2|
//+---+---+----+----+
//|  1|  1| s12|null|
//|  1|  3|null|  11|
//|  1| 10| s13|null|
//|  1| 12|null|null|
//|  2|  2| aaa|  22|
//|  2|  7|null|  33|
//|  2| 11| a23|null|
//|  2| 19|null|  77|
//+---+---+----+----+

val df2 = df1.groupBy("grp").agg(max("ord").alias("ord"),collect_set("col1").alias("col1"),collect_set("col2").alias("col2"))

val df3 = df2.withColumn("new_col1",$"col1".apply(size($"col1").minus(1))).withColumn("new_col2",$"col2".apply(size($"col2").minus(1)))

df3.show()

//+---+---+----------+------------+--------+--------+
//|grp|ord|      col1|        col2|new_col1|new_col2|
//+---+---+----------+------------+--------+--------+
//|  1| 12|[s12, s13]|        [11]|     s13|      11|
//|  2| 19|[aaa, a23]|[33, 22, 77]|     a23|      77|
//+---+---+----------+------------+--------+--------+

您可以使用 .drop("column_name") 删除不需要的列

【讨论】:

    【解决方案3】:

    所以这里我们按 a 分组并选择组中所有其他列的最大值:

    scala> val df = List((1,2,11), (1,1,1), (2,1,4), (2,3,5)).toDF("a", "b", "c")
    df: org.apache.spark.sql.DataFrame = [a: int, b: int ... 1 more field]
    
    scala> val aggCols = df.schema.map(_.name).filter(_ != "a").map(colName => sum(col(colName)).alias(s"max_$colName"))
    aggCols: Seq[org.apache.spark.sql.Column] = List(sum(b) AS `max_b`, sum(c) AS `max_c`)
    
    scala> df.groupBy(col("a")).agg(aggCols.head, aggCols.tail: _*)
    res0: org.apache.spark.sql.DataFrame = [a: int, max_b: bigint ... 1 more field]
    

    【讨论】:

    • 谢谢!我尝试了您的解决方案(我已经更改了聚合函数:sum() --> last()),但是对于 NULL,它不能按预期工作。你能用我的样本数据集试一试吗?
    【解决方案4】:

    我会采用与@LeoC 相同的方法,但我相信没有必要将列名作为字符串操作,我会采用更类似于 spark-sql 的答案。

    import org.apache.spark.sql.expressions.Window
    import org.apache.spark.sql.functions.{col, first, last}
    
    val win = Window.partitionBy("grp").orderBy(col("ord")).rowsBetween(0, Window.unboundedFollowing)
    
    // In case there is more than one group column
    val nonAggCols = Seq("grp")
    
    // Select columns to aggregate on
    val cols: Seq[String] = df.columns.diff(nonAggCols).toSeq
    
    // Map over selection and apply fct
    val aggregations: Seq[Column] = cols.map(c => first(col(c), ignoreNulls = true).as(c))
    
    // I'd rather cache the following step as it might get expensive
    val step1 = cols.foldLeft(df)((acc, c) => acc.withColumn(c, last(col(c), ignoreNulls = true).over(win))).cache
    
    // Finally we can aggregate our results as followed
    val results = step1.groupBy(nonAggCols.head, nonAggCols.tail: _*).agg(aggregations.head, aggregations.tail: _*)
    
    results.show
    // +---+--------+---+----+----+
    // |grp|null_col|ord|col1|col2|
    // +---+--------+---+----+----+
    // |  1|    null| 12| s13|  11|
    // |  2|    null| 19| a23|  77|
    // +---+--------+---+----+----+
    

    我希望这会有所帮助。

    编辑:您没有得到相同结果的原因是您使用的阅读器不正确。

    它将文件中的null 解释为字符串而不是null;即:

    scala> df.filter('col1.isNotNull).show
    // +---+--------+---+----+----+
    // |grp|null_col|ord|col1|col2|
    // +---+--------+---+----+----+
    // |  1|    null|  3|null|  11|
    // |  2|    null|  2| xxx|  22|
    // |  1|    null|  1| yyy|null|
    // |  2|    null|  7|null|  33|
    // |  1|    null| 12|null|null|
    // |  2|    null| 19|null|  77|
    // |  1|    null| 10| s13|null|
    // |  2|    null| 11| a23|null|
    // +---+--------+---+----+----+
    

    这是我的readSparkOutput 版本:

    def readSparkOutput(filePath: String): org.apache.spark.sql.DataFrame = {
      val step1 = spark.read
        .option("header", "true")
        .option("inferSchema", "true")
        .option("delimiter", "|")
        .option("parserLib", "UNIVOCITY")
        .option("ignoreLeadingWhiteSpace", "true")
        .option("ignoreTrailingWhiteSpace", "true")
        .option("comment", "+")
        .csv(filePath)
    
      val step2 = step1.select(step1.columns.filterNot(_.startsWith("_c")).map(step1(_)): _*)
    
      val columns = step2.columns
      columns.foldLeft(step2)((acc, c) => acc.withColumn(c, when(col(c) =!= "null" or col(c).isNotNull, col(c))))
    }
    

    【讨论】:

      【解决方案5】:

      这是你的答案(希望是我的赏金!!!)

      scala> val df = spark.sparkContext.parallelize(List(
           | (1,null.asInstanceOf[String],3,null.asInstanceOf[String],new Integer(11)),
           | (2,null.asInstanceOf[String],2,new String("xxx"),new Integer(22)),
           | (1,null.asInstanceOf[String],1,new String("yyy"),null.asInstanceOf[Integer]),
           | (2,null.asInstanceOf[String],7,null.asInstanceOf[String],new Integer(33)),
           | (1,null.asInstanceOf[String],12,null.asInstanceOf[String],null.asInstanceOf[Integer]),
           | (2,null.asInstanceOf[String],19,null.asInstanceOf[String],new Integer(77)),
           | (1,null.asInstanceOf[String],10,new String("s13"),null.asInstanceOf[Integer]),
           | (2,null.asInstanceOf[String],11,new String("a23"),null.asInstanceOf[Integer]))).toDF("grp","null_col","ord","col1","col2")
      df: org.apache.spark.sql.DataFrame = [grp: int, null_col: string ... 3 more fields]
      
      scala> df.show
      +---+--------+---+----+----+
      |grp|null_col|ord|col1|col2|
      +---+--------+---+----+----+
      |  1|    null|  3|null|  11|
      |  2|    null|  2| xxx|  22|
      |  1|    null|  1| yyy|null|
      |  2|    null|  7|null|  33|
      |  1|    null| 12|null|null|
      |  2|    null| 19|null|  77|
      |  1|    null| 10| s13|null|
      |  2|    null| 11| a23|null|
      +---+--------+---+----+----+
      

      //创建窗口规范

      scala> import org.apache.spark.sql.expressions.Window
      import org.apache.spark.sql.expressions.Window
      
      scala> val win = Window.partitionBy("grp").orderBy($"ord".desc)
      win: org.apache.spark.sql.expressions.WindowSpec = org.apache.spark.sql.expressions.WindowSpec@71878833
      

      //在所有列上使用 foldLeft 和 first over window 规范并取不同

      scala> val result = df.columns.foldLeft(df)((df, colName) => df.withColumn(colName, first(colName, ignoreNulls=true).over(win).as(colName))).distinct
      result: org.apache.spark.sql.Dataset[org.apache.spark.sql.Row] = [grp: int, null_col: string ... 3 more fields]
      
      scala> result.show
      +---+--------+---+----+----+
      |grp|null_col|ord|col1|col2|
      +---+--------+---+----+----+
      |  1|    null| 12| s13|  11|
      |  2|    null| 19| a23|  77|
      +---+--------+---+----+----+
      

      希望这会有所帮助。

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 2020-12-14
        • 1970-01-01
        • 1970-01-01
        • 2014-07-24
        • 1970-01-01
        • 2021-10-25
        • 2018-07-13
        • 2019-10-17
        相关资源
        最近更新 更多