Spark(Scala) - 在DataFrame中恢复爆炸

时间:2018-04-02 13:54:43

标签: scala apache-spark dataframe

我最初有一个DataFrame,如下所示:

Key     Emails                      PassportNum     Age
0001    [Alan@gmail,Alan@hotmail]   passport1       23
0002    [Ben@gmail,Ben@hotmail]     passport2       28

我需要在每个电子邮件上应用一个函数,例如最后添加“_2”的虚拟函数,操作无关紧要。所以我会像这样爆炸这个专栏:

val dfExplode = df.withColumn("Email",explode($"Emails")).drop("Emails")

现在我将有一个这样的数据框:

Key     Email           PassportNum     Age
0001    Alan@gmail      passport1       23
0001    Alan@hotmail    passport1       23
0002    Ben@gmail       passport2       28
0002    Ben@hotmail     passport2       28

我对护照进行任何更改,然后我想要的是:

Key     Emails                          PassportNum     Age
0001    [Alan_2@gmail,Alan_2@hotmail]   passport1       23
0002    [Ben_2@gmail,Ben_2@hotmail]     passport2       28

我正在考虑的选项是:

dfOriginal = dfExploded.groupBy("Key","PassportNum","Age").agg(collect_set("Email").alias("Emails"))

在这种情况下,它可能不是一个糟糕的方法。但在我的实际案例中,我在单个列上执行爆炸,我有另外20个列,如PassportNum,Age ...这些将被复制。

这意味着我需要在groupBy中添加大约20列,当我真的可以通过一个列执行该组时,例如Key是唯一的。

我正在考虑将这些列添加到agg中,如下所示:

dfOriginal = dfExploded.groupBy("Key").agg(collect_set("Email").alias("Emails"),collect_set("PassportNum"),collect_set("Age"))

但我不希望它们在单个元素数组中。

是否可以在没有任何collect_*的情况下制作聚合?是否有更简单的方法来撤消explode

3 个答案:

答案 0 :(得分:3)

假设您希望保留在DataFrame世界中,那么定义一个操纵输入数组的UDF可能是值得的。将Seq作为输入并返回修改后的内容的东西。 e.g。

def myUdf = udf[Seq[String], Seq[String]] { 
    inputSeq => inputSeq.map(elem => elem + "_2")
}

df.withColumn("Emails", myUdf($"Emails"))

更好的是,您可以将确切的逻辑作为参数传递:

def myUdf(myFunc: String => String) = udf[Seq[String], Seq[String]] {
    inputSeq => inputSeq.map(myFunc)
}

df.withColumn("Emails", myUdf((email: String) => email + "_XYZ")($"Emails"))

答案 1 :(得分:1)

除了所有常见字段上的groupby之外的另一个选项是在单独的临时数据框上进行爆炸,然后从原始数据集中删除展开的列并按

重新加入分组

然而,编写一个直接操作数组而不会爆炸并收集的UDF可能更简单

def handleEmail(emails: mutable.WrappedArray[String]) = {
     emails.map(dosomething)
  }

context.udf.register("handleEmailsm"m (em:mutabe.WrappedArray[String]) => handleEmail(em))

答案 2 :(得分:1)

  
    

这意味着我需要在groupBy中添加大约20列,当我真的可以通过一个列执行该组时,例如Key是唯一的。

  

您可以通过一个简单的技巧 跳过编写每个列名,如下所示使用所有列名(或选中)除了爆炸列名称

之外,
import org.apache.spark.sql.functions._
val dfExploded = df.withColumn("Emails", explode($"Emails"))

val groupColumns = dfExploded.columns.filterNot(_.equalsIgnoreCase("Emails"))

val dfOriginal = dfExploded.groupBy(groupColumns.map(col): _*).agg(collect_set("Emails").alias("Emails"))

创建结构列

您可以使用struct inbuilt function 创建单个列,使用groupBy 中的单个列

val groupColumns = df.columns.filterNot(_.equalsIgnoreCase("Emails"))

import org.apache.spark.sql.functions._
val dfExploded = df.select(struct(groupColumns.map(col): _*).as("groupedKey"), col("Emails"))
  .withColumn("Emails", explode($"Emails"))

会给你

+-------------------+------------+
|groupedKey         |Emails      |
+-------------------+------------+
|[0001,passport1,23]|Alan@gmail  |
|[0001,passport1,23]|Alan@hotmail|
|[0002,passport2,28]|Ben@gmail   |
|[0002,passport2,28]|Ben@hotmail |
+-------------------+------------+

然后在groupBy中使用groupedKey并再次在select 中分隔它们

val dfOriginal = dfExploded.groupBy("groupedKey").agg(collect_set("Emails").alias("Emails"))
  .select($"groupedKey.*", $"Emails")