在Apache Spark中{groupBy}之后汇总地图中的所有列值

时间:2019-09-04 11:15:28

标签: scala apache-spark apache-spark-sql

我整天都在尝试使用Dataframe,但到目前为止还没有运气。已经使用RDD做到了这一点,但是它并不是真正可读的,因此,在代码可读性方面,这种方法会更好。

使用此初始和结果DF,包括开始DF和执行.groupBy()之后我想要获得的东西。

case class SampleRow(name:String, surname:String, age:Int, city:String)
case class ResultRow(name: String, surnamesAndAges: Map[String, (Int, String)])

val df = List(
  SampleRow("Rick", "Fake", 17, "NY"),
  SampleRow("Rick", "Jordan", 18, "NY"),
  SampleRow("Sandy", "Sample", 19, "NY")
).toDF()

val resultDf = List(
  ResultRow("Rick", Map("Fake" -> (17, "NY"), "Jordan" -> (18, "NY"))),
  ResultRow("Sandy", Map("Sample" -> (19, "NY")))
).toDF()

到目前为止,我一直在尝试执行以下.groupBy ...

val resultDf = df
  .groupBy(
    Name
  )
  .agg(
    functions.map(
      selectColumn(Surname),
      functions.array(
        selectColumn(Age),
        selectColumn(City)
      )
    )
  )

但是,控制台中会提示以下内容。

Exception in thread "main" org.apache.spark.sql.AnalysisException: expression '`surname`' is neither present in the group by, nor is it an aggregate function. Add to group by or wrap in first() (or first_value) if you don't care which value you get.;;

但是,这样做将导致每个姓氏只有一个条目,而我想将它们累积在一个Map中,就像您在resultDf中看到的那样。是否有使用DF实现此目的的简单方法?

3 个答案:

答案 0 :(得分:1)

如果您不关心将数据帧类型转换为数据集(在这种情况下,ResultRow可以这样做

val grouped =df.withColumn("surnameAndAge",struct($"surname",$"age"))
.groupBy($"name")
.agg(collect_list("surnameAndAge").alias("surnamesAndAges"))

然后您可以创建一个类似于以下内容的用户定义函数

import org.apache.spark.sql._
val arrayToMap = udf[Map[String, String], Seq[Row]] {
array => array.map { 
case Row(key: String, value: String) => (key, value) }.toMap
}

现在您可以使用.withColumn并将其称为udf

val finalData = grouped.withColumn("surnamesAndAges",arrayToMap($"surnamesAndAges"))

数据框看起来像这样

finalData: org.apache.spark.sql.DataFrame = [name: string, surnamesAndAges: map<string,string>]

答案 1 :(得分:1)

您可以使用单个UDF来实现,以将数据转换为地图:

 val toMap = udf((keys: Seq[String], values1: Seq[String], values2: Seq[String]) => {
    keys.zip(values1.zip(values2)).toMap
  })



   val myResultDF = df.groupBy("name").agg(collect_list("surname") as "surname", collect_list("age") as "age", collect_list("city") as "city").withColumn("surnamesAndAges", toMap($"surname", $"age", $"city")).drop("age", "city", "surname").show(false)
+-----+--------------------------------------+
|name |surnamesAndAges                       |
+-----+--------------------------------------+
|Sandy|[Sample -> [19, NY]]                  |
|Rick |[Fake -> [17, NY], Jordan -> [18, NY]]|
+-----+--------------------------------------+

答案 2 :(得分:0)

从 Spark 2.4 开始,您不需要使用 Spark 用户定义的函数:

import org.apache.spark.sql.functions.{col, collect_set, map_from_entries, struct}

df.withColumn("mapEntry", struct(col("surname"), struct(col("age"), col("city"))))
  .groupBy("name")
  .agg(map_from_entries(collect_set("mapEntry")).as("surnameAndAges"))

说明

您首先从所需的列中添加一个包含 Map 条目的列。 Map 条目只是一个包含两列的 struct:第一列是键,第二列是值。您可以将另一个 struct 作为值。因此,在这里您的 Map 条目将使用列 surname 作为键,并使用 structagecity 作为值:

struct(col("surname"), struct(col("age"), col("city")))

然后,您使用函数 name 收集按 groupBy 键(即列 collect_set)分组的所有 Map 条目,并使用函数 map_from_entries 将此 Map 条目列表转换为 Map }