如何向Spark DataFrame添加持久的行ID列?

时间:2016-02-29 16:52:50

标签: apache-spark dataframe apache-spark-sql

这个问题并不新鲜,但我在Spark中发现了令人惊讶的行为。我需要向DataFrame添加一列行ID。我使用了DataFrame方法monotonically_increasing_id(),它确实给了我一个额外的uniques行ID(顺便说一句,它们不是连续的,但是是唯一的)。

我遇到的问题是,当我过滤DataFrame时,会重新分配生成的DataFrame中的行ID。两个DataFrame如下所示。

  • 第一个是添加了行ID的初始DataFrame,如下所示:

    df.withColumn("rowId", monotonically_increasing_id()) 
    
  • 第二个DataFrame是通过df.filter(col("P"))过滤col P后获得的数据帧。

问题由custId 169的rowId说明,在初始DataFrame中为5,但在过滤后,当custId 169被过滤掉时,rowId(5)被重新分配给custmId 773!我不知道为什么这是默认行为。

我希望rowIds“粘”;如果我从DataFrame中删除行,我不希望他们的ID“重新使用”,我希望它们与行一起消失。有可能吗?我没有看到任何标志从monotonically_increasing_id方法请求此行为。

+---------+--------------------+-------+
| custId  |    features|    P  |rowId|
+---------+--------------------+-------+
|806      |[50,5074,...|   true|    0|
|832      |[45,120,1...|   true|    1|
|216      |[6691,272...|   true|    2|
|926      |[120,1788...|   true|    3|
|875      |[54,120,1...|   true|    4|
|169      |[19406,21...|  false|    5|

after filtering on P:
+---------+--------------------+-------+
|   custId|    features|    P  |rowId|
+---------+--------------------+-------+
|      806|[50,5074,...|   true|    0|
|      832|[45,120,1...|   true|    1|
|      216|[6691,272...|   true|    2|
|      926|[120,1788...|   true|    3|
|      875|[54,120,1...|   true|    4|
|      773|[3136,317...|   true|    5|

8 个答案:

答案 0 :(得分:12)

Spark 2.0

  • 此问题已在使用SPARK-14241的Spark 2.0中得到解决。

  • 使用SPARK-14393

  • 在Spark 2.1中解决了另一个类似的问题

Spark 1.x

你遇到的问题相当微妙,但可以简化为一个简单的事实monotonically_increasing_id是一个非常难看的功能。它显然不是纯粹的,它的价值取决于完全无法控制的东西。

它没有采用任何参数,因此从优化器的角度来看,调用它并不重要,并且可以在所有其他操作之后推送。因此你看到的行为。

如果你看一下你发现的代码,就会通过MonotonicallyIncreasingID扩展Nondeterministic表达来明确标记。

我不认为有任何优雅的解决方案,但您可以采用的一种方法是在过滤后的值上添加一个人工依赖。例如,使用这样的UDF:

from pyspark.sql.types import LongType
from pyspark.sql.functions import udf

bound = udf(lambda _, v: v, LongType()) 

(df
  .withColumn("rn", monotonically_increasing_id())
  # Due to nondeterministic behavior it has to be a separate step
  .withColumn("rn", bound("P", "rn"))  
  .where("P"))

通常,使用zipWithIndex上的RDD添加索引,然后将其转换回DataFrame可能会更清晰。

*上面显示的解决方法不再是Spark 2.x中的有效解决方案(也不是必需的),其中Python UDF是执行计划优化的主题。

答案 1 :(得分:3)

我无法重现这一点。我虽然使用Spark 2.0,但行为可能已经改变,或者我没有做与你相同的事情。

val df = Seq(("one", 1,true),("two", 2,false),("three", 3,true),("four", 4,true))
.toDF("name", "value","flag")
.withColumn("rowd", monotonically_increasing_id())

df.show

val df2 = df.filter(col("flag")=== true)

df2.show

df: org.apache.spark.sql.DataFrame = [name: string, value: int ... 2 more fields]
+-----+-----+-----+----+
| name|value| flag|rowd|
+-----+-----+-----+----+
|  one|    1| true|   0|
|  two|    2|false|   1|
|three|    3| true|   2|
| four|    4| true|   3|
+-----+-----+-----+----+
df2: org.apache.spark.sql.Dataset[org.apache.spark.sql.Row] = [name: string, value: int ... 2 more fields]
+-----+-----+----+----+
| name|value|flag|rowd|
+-----+-----+----+----+
|  one|    1|true|   0|
|three|    3|true|   2|
| four|    4|true|   3|
+-----+-----+----+----+

答案 2 :(得分:3)

我最近正在研究类似的问题。尽管monotonically_increasing_id()非常快,但是它并不可靠,并且不会给您连续的行号,只会增加唯一的整数。

创建Windows分区然后使用row_number().over(some_windows_partition)非常耗时。

到目前为止,最好的解决方案是使用带索引的压缩文件,然后将压缩后的文件转换回原始数据框,并使用包含索引列的新架构。

尝试一下:

from pyspark.sql import Row
from pyspark.sql.types import StructType, StructField, LongType

new_schema = StructType(**original_dataframe**.schema.fields[:] + [StructField("index", LongType(), False)])
zipped_rdd = **original_dataframe**.rdd.zipWithIndex()
indexed = (zipped_rdd.map(lambda ri: row_with_index(*list(ri[0]) + [ri[1]])).toDF(new_schema))

original_dataframedataframe的地方,您必须在上面添加索引,而row_with_index是带有列索引的新模式,您可以将其写为

row_with_index = Row(
"calendar_date"
,"year_week_number"
,"year_period_number"
,"realization"
,"index"
)

这里calendar_dateyear_week_numberyear_period_numberrealization是我原始dataframe的列。您可以将名称替换为列的名称。索引是您必须为行号添加的新列名。

row_number().over(some_windows_partition)方法相比,此过程在很大程度上更高效,更流畅。

希望这会有所帮助。

答案 3 :(得分:1)

要绕过monotonically_increasing_id()的移位评估,您可以尝试将数据帧写入磁盘并重新读取。然后id列现在只是一个正在读取的数据字段,而不是在管道中的某个点上动态计算。虽然这是一个非常难看的解决方案,但是当我进行快速测试时它会起作用。

答案 4 :(得分:1)

这对我有用。创建了另一个标识列并使用了窗口函数row_number

import org.apache.spark.sql.functions.{row_number}
import org.apache.spark.sql.expressions.Window

val df1: DataFrame = df.withColumn("Id",lit(1))

df1
.select(
...,
row_number()
.over(Window
.partitionBy("Id"
.orderBy(col("...").desc))
)
.alias("Row_Nbr")
)

答案 5 :(得分:0)

为了在Chris T解决方案中获得更好的性能,您可以尝试写入apache点燃共享数据帧,而不是写入磁盘。 https://ignite.apache.org/use-cases/spark/shared-memory-layer.html

答案 6 :(得分:0)

最好的方法是使用唯一键的 concat 哈希。

例如:在python中:

from pyspark.sql.functions import concat, md5

unique_keys = ['event_datetime', 'ingesttime']
raw_df.withColumn('rowid', md5(concat(*unique_keys)))

原因:

答案 7 :(得分:-1)

Dataset<Row> dataset = sparkSession.read().option("header", "true")
                .csv("C:\\Users\\arun7.gupta\\Desktop\\Spark\\user.csv");
dataset.show();// show the csv data

WindowSpec orderBy = Window.orderBy(dataset.col("name"));
Dataset<Row> selectData = dataset.select(dataset.col("*"),functions.row_number().over(orderBy).alias("RowNumber"));
selectData.show();// show the rownuber

Output :
Csv data 
+-------+--------+
|   name| address|
+-------+--------+
|   Arun|  Indore|
|Shubham|  Indore|
| Mukesh|Hariyana|
|   Arun|  Bhopal|
|Shubham|Jabalpur|
| Mukesh|  Rohtak|
+-------+--------+

After adding rownumebr 

+-------+--------+---------+
|   name| address|RowNumber|
+-------+--------+---------+
|   Arun|  Indore|        1|
|   Arun|  Bhopal|        2|
| Mukesh|Hariyana|        3|
| Mukesh|  Rohtak|        4|
|Shubham|  Indore|        5|
|Shubham|Jabalpur|        6|
+-------+--------+---------+