使用行值作为分隔符将spark数据帧划分为块

时间:2017-10-23 18:55:25

标签: apache-spark pyspark spark-dataframe

在我的PySpark代码中,我有一个DataFrame填充了来自传感器的数据,每一行都有时间戳,event_description和event_value。 每个传感器事件由id和值定义的测量组成。我唯一的保证是所有的"阶段"与单个事件相关的行包含在两个EV_SEP行之间(未排序)。 每个活动内部"阻止"有一个事件标签,它是与EV_CODE相关联的值。

+-------------------------+------------+-------------+
| timestamp               | event_id   | event_value |
+-------------------------+------------+-------------+
| 2017-01-01 00:00:12.540 | EV_SEP     | -----       |
+-------------------------+------------+-------------+
| 2017-01-01 00:00:14.201 | EV_2       | 10          |
+-------------------------+------------+-------------+
| 2017-01-01 00:00:13.331 | EV_1       | 11          |
+-------------------------+------------+-------------+
| 2017-01-01 00:00:15.203 | EV_CODE    | ABC         |
+-------------------------+------------+-------------+
| 2017-01-01 00:00:16.670 | EV_SEP     | -----       |
+-------------------------+------------+-------------+

我想创建一个包含该标签的新列,以便我知道所有事件都与该标签相关联:

+-------------------------+----------+-------------+------------+
| timestamp               | event_id | event_value | event_code |
+-------------------------+----------+-------------+------------+
| 2017-01-01 00:00:12.540 | EV_SEP   | -----       | ABC        |
+-------------------------+----------+-------------+------------+
| 2017-01-01 00:00:14.201 | EV_2     | 10          | ABC        |
+-------------------------+----------+-------------+------------+
| 2017-01-01 00:00:13.331 | EV_1     | 11          | ABC        |
+-------------------------+----------+-------------+------------+
| 2017-01-01 00:00:15.203 | EV_CODE  | ABC         | ABC        |
+-------------------------+----------+-------------+------------+
| 2017-01-01 00:00:16.670 | EV_SEP   | -----       | ABC        |
+-------------------------+----------+-------------+------------+

使用pandas,我可以轻松获取EV_SEP行的索引,将表拆分为块,从每个块中取出EV_CODE并创建具有此值的event_code列。 / p>

可能的解决方案是:

  • 根据时间戳
  • 对DataFrame进行排序
  • 将数据帧转换为RDD并调用zipWithIndex
  • 获取包含EV_SEP
  • 的索引
  • 计算块范围(start_index,end_index)
  • 处理单个"块" (过滤索引)以提取EV_CODE
  • 最后创建想要的列

有没有更好的方法来解决这个问题?

2 个答案:

答案 0 :(得分:2)

from pyspark.sql import functions as f

示例数据:

df.show()

+-----------------------+--------+-----------+
|timestamp              |event_id|event_value|
+-----------------------+--------+-----------+
|2017-01-01 00:00:12.540|EV_SEP  |null       |
|2017-01-01 00:00:14.201|EV_2    |10         |
|2017-01-01 00:00:13.331|EV_1    |11         |
|2017-01-01 00:00:15.203|EV_CODE |ABC        |
|2017-01-01 00:00:16.670|EV_SEP  |null       |
|2017-01-01 00:00:20.201|EV_2    |10         |
|2017-01-01 00:00:24.203|EV_CODE |DEF        |
|2017-01-01 00:00:31.670|EV_SEP  |null       |
+-----------------------+--------+-----------+

添加索引:

df_idx = df.filter(df['event_id'] == 'EV_SEP') \
    .withColumn('idx', f.row_number().over(Window.partitionBy().orderBy(df['timestamp'])))
df_block = df.filter(df['event_id'] != 'EV_SEP').withColumn('idx', f.lit(0))

'点差'指数:

df = df_idx.union(df_block).withColumn('idx', f.max('idx').over(
    Window.partitionBy().orderBy('timestamp').rowsBetween(Window.unboundedPreceding, Window.currentRow))).cache()

添加EV_CODE

df_code = df.filter(df['event_id'] == 'EV_CODE').withColumnRenamed('event_value', 'event_code')
df = df.join(df_code, on=[df['idx'] == df_code['idx']]) \
    .select(df['timestamp'], df['event_id'], df['event_value'], df_code['event_code'])

最后:

+-----------------------+--------+-----------+----------+
|timestamp              |event_id|event_value|event_code|
+-----------------------+--------+-----------+----------+
|2017-01-01 00:00:12.540|EV_SEP  |null       |ABC       |
|2017-01-01 00:00:13.331|EV_1    |11         |ABC       |
|2017-01-01 00:00:14.201|EV_2    |10         |ABC       |
|2017-01-01 00:00:15.203|EV_CODE |ABC        |ABC       |
|2017-01-01 00:00:16.670|EV_SEP  |null       |DEF       |
|2017-01-01 00:00:20.201|EV_2    |10         |DEF       |
|2017-01-01 00:00:24.203|EV_CODE |DEF        |DEF       |
+-----------------------+--------+-----------+----------+

答案 1 :(得分:0)

创建一个新的Hadoop InputFormat将是一种在计算上更有效的方法来实现你的目标(虽然在代码方面可以说是相同或更多的体操)。您可以使用the Python API中的sc.hadoopFile指定备用Hadoop输入格式,但必须注意从Java格式到Python的转换。然后,您可以指定格式。 PySpark中可用的转换器相对较少,但this reference建议使用Avro converter as an example。您可能还会发现让自定义Hadoop输入格式输出文本然后在Python中另外解析以避免实现转换器的问题。

一旦你有了这个,你就会创建一个特殊的输入格式(使用Hadoop API在Java或Scala中)来处理具有EV_SEP作为记录分隔符而不是换行符的行的特殊序列。您可以通过在累加器中读取行来简单地收集行(只需要一个简单的ArrayList可以作为概念证明)然后在找到两个{{1时发出累积的记录列表行中的行。

我想指出,使用EV_SEP作为此类设计的基础可能很诱人,但输入格式会在换行符处任意分割此类文件,您需要实现自定义逻辑以正确支持拆分文件。或者,您可以通过简单地不实现文件拆分来避免此问题。这是对分区程序的简单修改。

如果确实需要拆分文件,基本思路是:

  • 通过将文件均分为部分
  • 来选择分割偏移
  • 寻求偏移
  • 从偏移量到找到分隔符序列的位置逐个字符地向后搜索(在这种情况下,行中有两行,类型为TextInputFormat

针对文件拆分的边缘情况检测这些序列将是一个挑战。我建议建立最大字节宽度的行,并从起始点向后读取适当宽度(基本上是行大小的2倍)的滑动窗口块,然后使用预编译的Java正则表达式和匹配器匹配这些窗口。这类似于Sequence Files find their sync marks,但使用正则表达式来检测序列而不是严格的相等。

作为旁注,我会关注你提到的其他一些背景,即按时间戳排序DataFrame可能会改变在不同文件中同一时间段内发生的事件的内容。