【问题标题】:Spark: Add column with map logic without using UDFSpark:使用地图逻辑添加列而不使用 UDF
【发布时间】:2020-05-17 15:41:05
【问题描述】:

基本上,我想在数据框的每一行上应用我的函数 countSimilarColumns 并将结果放在一个新列中。

我的代码如下

def main(args: Array[String]) = {
    val customerID           = "customer-1" //args(0)
    val rawData              = readFromResource("json", "/spark-test-data-copy.json")
    val flattenData          = rawData.select(flattenSchema(rawData.schema): _*)
    val referenceCustomerRow = flattenData.transform(getCustomer(customerID)).first
  }

def getCustomer(customerID: String)(dataFrame: DataFrame) = {
    dataFrame.filter($"customer" === customerID)
  }

def countSimilarColumns(first: Row, second: Row): Int = {
    if (!(first.getAs[String]("customer").equals(second.getAs[String]("customer"))))
      first.toSeq.zip(second.toSeq).count { case (x, y) => x == y }
    else
      -1
  }

我想做如下的事情。但我不知道该怎么做。

flattenData
  .withColumn(
    "similarity_score",
    flattenData.map(row => countSimilarColumns(row, referenceCustomerRow))
  )
  .show()

扁平化示例数据:

{"customer":"customer-1","att-a":"7","att-b":"3","att-c":"10","att-d":"10"}
{"customer":"customer-2","att-a":"9","att-b":"7","att-c":"12","att-d":"4"}
{"customer":"customer-3","att-a":"7","att-b":"3","att-c":"1","att-d":"10"}
{"customer":"customer-4","att-a":"9","att-b":"14","att-c":"10","att-d":"4"}

想要的输出:

+--------------------+-----------+
| customer   | similarity_score |
+--------------------+-----------+
|customer-1  |  -1    | 
|customer-2  |  0    |
|customer-3  |  3    |
|customer-4  |  1    |

UDF 是唯一的方法吗?如果是,那么我想保持我的函数 countSimilarColumns 原样,这样它就可以测试了。怎么可能? 我是 Spark/Scala 的新手。

【问题讨论】:

  • 你是如何分配相似度分数的,你有什么逻辑吗?假设如果客户 3 重复了 10 次,你是不是加了 10 作为相似度分数?
  • 我在贴的时候已经添加了countSimilarColumns这个函数。它导致该行的相似度得分。该函数检查两列的值是否相同然后计数++。我只需要在 DF 中添加一个新列。
  • 如果有错误请纠正我,假设客户列的值 customer-3 5 次,这意味着客户 3 - 5 的计数.. 是否正确?
  • 错了!你所说的是一个简单的 count() 数据框。我比较两行而不是列。并且一行可以有 x 个列,因此比较每个索引列,然后计算它们是否相同。
  • 简单来说,我想将我的函数 countSimilarColumns 应用于数据帧的每一行并将结果放入新列中

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


【解决方案1】:

您需要充分了解 DF 上的所有标准非聚合操作一次只能看到一行。因此,您必须考虑 1 行或聚合。在单行上下文中对整个 DF 调用函数 - 您的 withColumn 调用非常不寻常。当您想将行与其他行进行比较/组合时,您通常将表连接到另一个表或自身,然后使用组合表。

你想要的是类似的东西

// create DF with similarity scores AND customer, which is treated as row id here
val similarityDF = flattenData.map(row => row.getAs[String]("customer") -> countSimilarColumns(row, referenceCustomerRow)).
                   toDF("customer","similarity_score")

// join your original DF with similarityDF based on "customer"
flattenData.join(simlarityDF, usingColumn = "customer").show()

这允许您匹配两个 DF 的行。

UDF 在这里并不真正适用,因为与内置函数类似,UDF 应用于一个(或多个)特定列以生成一个值(可以使用withColumn 作为列添加),您想要的查看整个 Row。

【讨论】:

  • 这正是我所需要的。我想到了两件事。要么加入,要么以某种方式将我从 countSimilarColumns 获得的数据集添加到 flattenDataDF 但我无法同时做到这两点。我是 spark 和 scala 的新手,所以我的技能有限。你能解释一下这个语法row.getAs[String]("customer") ->它是如何变成两列的(这是我一直缺少的)。其次,我可以在同一步骤中重命名列吗?因为列的名称在similarityDF 中显示为_1 和_2。我不需要加入,因为我不需要属性。谢谢!!!
  • 我可以通过在末尾添加.toDF(Seq("customer", "similarity_score"): _*) 来做到这一点。这是在火花中做的好方法吗?
  • @Sam a -> b 相当于(a,b) - 它只是元组的语法糖,所以map 产生元组的RDD,然后用toDF 转换为DataFrame。您重命名toDF 中的列。 .toDF(Seq("customer", "similarity_score"): _*) 可以,但没必要 - :_* 语法用于传递 Seq 以使用可变参数运行。但是,由于您知道要传递的内容,toDF("customer", "similarity_score") 更简单且更具可读性。
  • 明白,谢谢!另一个问题。当我做row.getAs[String]("customer")-> 它要求隐式对话(编码器)。我听说隐式转换不是那么高效和优化。如果是这样,还有另一种方法可以不用隐式转换吗?
【解决方案2】:

flattenDataDataFrame 类型,在 flattenData 上应用 map 函数将得到 Dataset 的结果。

您将flattenData.map(row => countSimilarColumns(row, referenceCustomerRow)) 的结果传递给withColumnwithColumn 只能获取org.apache.spark.sql.Column 类型的数据

因此,如果您想在没有UDF 的情况下将上述结果添加到列中,您必须使用collect 函数,然后将其传递给lit

请检查以下代码。

flattenData
.withColumn("similarity_score",lit(
        flattenData
        .map(row => countSimilarColumns(row, referenceCustomerRow))
        .collect
        .map(_.toInt)
   )
) 

根据样本数据,添加以下逻辑。

scala> df.show(false)
+-----+-----+-----+-----+----------+
|att-a|att-b|att-c|att-d|customer  |
+-----+-----+-----+-----+----------+
|7    |3    |10   |10   |customer-1|
|9    |7    |12   |4    |customer-2|
|7    |3    |1    |10   |customer-3|
|9    |14   |10   |4    |customer-4|
+-----+-----+-----+-----+----------+


scala> val conditions = df.columns.filterNot(_ == "customer").map(c => (when(count(col(c)).over(Window.partitionBy(col(c)).orderBy(col(c).asc)) =!= 1,lit(1)).otherwise(0))).reduce(_ + _) // if row_number is 1 then adding 1 else 0 ..
conditions: org.apache.spark.sql.Column = (((CASE WHEN (NOT (count(att-a) OVER (PARTITION BY att-a ORDER BY att-a ASC NULLS FIRST unspecifiedframe$()) = 1)) THEN 1 ELSE 0 END + CASE WHEN (NOT (count(att-b) OVER (PARTITION BY att-b ORDER BY att-b ASC NULLS FIRST unspecifiedframe$()) = 1)) THEN 1 ELSE 0 END) + CASE WHEN (NOT (count(att-c) OVER (PARTITION BY att-c ORDER BY att-c ASC NULLS FIRST unspecifiedframe$()) = 1)) THEN 1 ELSE 0 END) + CASE WHEN (NOT (count(att-d) OVER (PARTITION BY att-d ORDER BY att-d ASC NULLS FIRST unspecifiedframe$()) = 1)) THEN 1 ELSE 0 END)

最终结果

scala> df.withColumn("similarity_score",conditions).show(false)
+-----+-----+-----+-----+----------+----------------+
|att-a|att-b|att-c|att-d|customer  |similarity_score|
+-----+-----+-----+-----+----------+----------------+
|9    |7    |12   |4    |customer-2|2               |
|7    |3    |1    |10   |customer-3|3               |
|7    |3    |10   |10   |customer-1|4               |
|9    |14   |10   |4    |customer-4|3               |
+-----+-----+-----+-----+----------+----------------+

【讨论】:

  • UDF 会是更好的选择吗?如果是,那么如何将行传递给 UDF。因为我看到的例子只对一个列进行操作,而这里的列可以不止几个。硬编码不会很好
  • 我认为没有。我的问题是你从方法中得到的结果你将如何映射相应的行或列?
  • 我也不知道。这就是我问这个问题的原因,因为我的想法是。您比较两行的列并返回一个 Int 并将 Int 作为值添加到传递给函数的该行的新列。但它在对所有数据进行计算后返回结果。所以我又来了,它没什么用
  • 坦率地说,我没有理解您的预期输出。如果可能,请获取 4 列的样本数据并显示所有可能的输出并包括所有列。抱歉再次询问。
  • 如果我只是运行val similarity_scores = flattenData.map(row => countSimilarColumns(row, referenceCustomerRow))。它给了我一个带有分数的数据集。我正在考虑将此(分数)添加到另一个数据框以获得所需的输出。也许这是更简单的方法。但我不确定,因为没有什么可以加入,或者如果 zip 两者(扁平化和得分)可能由于分区不同而导致结果不一样。
猜你喜欢
  • 2017-09-09
  • 1970-01-01
  • 2018-06-12
  • 2018-05-20
  • 1970-01-01
  • 1970-01-01
  • 2021-11-12
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多