迭代数据帧并根据子串[Spark] [Scala]

时间:2018-01-30 17:49:54

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

目前我有一个输入文件(数百万条记录),其中所有记录都包含2个字符的标识符。此输入文件中的多行将连接到输出文件中的一个记录,以及如何根据标识符的连续顺序确定如何确定

例如,记录将从下面开始

  

1A
      1B
      1C
      2A
      2B
      2C
      的 1A
      1C
      2B
      2C
      的 1A
      1B
      1C

1A表示新记录的开头,因此在这种情况下输出文件将有3条记录。 " 1A"之间的所有内容将合并为一个记录

  

1A + 1B + 1C + 2A + 2B + 2C
      1A + 1C + 2B + 2C
      1A + 1B + 1C

" 1A"之间的记录数量各不相同,因此我必须迭代并检查标识符。

我不确定如何使用scala / spark来解决这种情况。 我的策略是:

  1. 将输入文件加载到数据框中。

  2. 根据记录的子字符串创建标识符列。

  3. 创建一个新列TempID和一个设置为0的变量x

  4. 迭代数据框

  5. 如果Identifier = 1A,x = x + 1

  6. TempID =变量x

  7. 然后创建一个UDF以使用相同的TempID连接记录。

    总结我的问题: 如何遍历数据帧,检查Identifier列的值,然后分配tempID(如果identifier列的值为1A,则其值增加1)

2 个答案:

答案 0 :(得分:1)

这很危险。问题是火花不能保证在元素之间保持相同的顺序,特别是因为它们可能跨越分区边界。因此,当您迭代它们时,您可以获得不同的订单。这也必须完全按顺序发生,所以在这一点上,为什么不只是完全跳过spark并将其作为常规scala代码运行,作为前处理步骤之前的火花。

我的建议是要么考虑编写自定义数据输入格式/数据源,要么可以使用“1A”作为类似于this question.的记录分隔符

答案 1 :(得分:1)

首先 - 通常“迭代”DataFrame(或Spark的其他分布式集合抽象,如RDDDataset错误不可能。该术语根本不适用。你应该使用Spark的函数转换这些集合,而不是试图迭代它们。

您可以使用窗口函数来实现目标(或 - 几乎,要遵循的详细信息)。这里的想法是(1)添加“id”列进行排序,(2)使用Window函数(基于该排序)计数以前的“1A”实例数,然后(3)使用这些“计数”作为“组ID”,将每个组的所有记录连接在一起,并按其分组:

import functions._
import spark.implicits._

// sample data:
val df = Seq("1A", "1B", "1C", "2A", "2B", "2C", "1A", "1C", "2B", "2C", "1A", "1B", "1C").toDF("val")

val result = df.withColumn("id", monotonically_increasing_id())          // add row ID
  .withColumn("isDelimiter", when($"val" === "1A", 1).otherwise(0))      // add group "delimiter" indicator
  .withColumn("groupId", sum("isDelimiter").over(Window.orderBy($"id"))) // add groupId using Window function
  .groupBy($"groupId").agg(collect_list($"val") as "list") // NOTE: order of list might not be guaranteed!
  .orderBy($"groupId").drop("groupId")                                   // removing groupId

result.show(false)
// +------------------------+
// |list                    |
// +------------------------+
// |[1A, 1B, 1C, 2A, 2B, 2C]|
// |[1A, 1C, 2B, 2C]        |
// |[1A, 1B, 1C]            |
// +------------------------+

(如果将结果作为列表不符合您的需求,我会留给您将此列转换为您需要的任何内容)

此处的主要警告collect_list不一定保证保留订单 - 一旦您使用groupBy,订单可能会丢失。所以 - 每个结果列表中的顺序可能是错误的(但是,与组的分离必然是正确的)。如果这对您很重要,可以通过收集包含"id"列的列列表并稍后使用它来对这些列表进行排序来解决此问题。

编辑:如果不解决这个警告就意识到这个答案是不完整的,并且意识到这不是微不足道的 - 这就是你如何解决它:

定义以下UDF:

val getSortedValues = udf { (input: mutable.Seq[Row]) => input
  .map { case Row (id: Long, v: String) => (id, v) }
  .sortBy(_._1)
  .map(_._2)
}

然后,将上面建议的解决方案中的行.groupBy($"groupId").agg(collect_list($"val") as "list")替换为以下行:

  .groupBy($"groupId")
  .agg(collect_list(struct($"id" as "_1", $"val" as "_2")) as "list")
  .withColumn("list", getSortedValues($"list"))

这样我们必须保留订单(以及对这些小清单进行排序的价格)。