Pyspark:如何编写复杂的 Dataframe 计算代码
Pyspark :How to code complicated Dataframe calculation
数据框已经按日期整理,
col1 ==1 值是唯一的,
传col1==1,increment加1(eg. 1,2,3,4,5,6,7...)
只有 -1 是重复的。
我有一个数据框看起来像这样称它为 df
TEST_schema = StructType([StructField("date", StringType(), True),\
StructField("col1", IntegerType(), True),\
StructField("col2", IntegerType(), True)])
TEST_data = [('2020-08-01',-1,-1),('2020-08-02',-1,-1),('2020-08-03',-1,3),('2020-08-04',-1,2),('2020-08-05',1,4),\
('2020-08-06',2,1),('2020-08-07',3,2),('2020-08-08',4,3),('2020-08-09',5,-1)]
rdd3 = sc.parallelize(TEST_data)
TEST_df = sqlContext.createDataFrame(TEST_data, TEST_schema)
TEST_df.show()
+--------+----+----+
date |col1|col2|
+--------+----+----+
2020-08-01| -1| -1|
2020-08-02| -1| -1|
2020-08-03| -1| 3|
2020-08-04| -1| 2|
2020-08-05| 1 | 4|
2020-08-06| 2 | 1|
2020-08-07| 3 | 2|
2020-08-08| 4 | 3|
2020-08-09| 5 | -1|
+--------+----+----+
条件是当col1 == 1,那么我们从col2 ==4开始向后添加,(eg. 4,5,6,7,8,...) 之后的col2 == 4 return 0 一路(例如 4,0,0,0,0...)
所以,我得到的 df 看起来像这样。
+--------+----+----+----+
date |col1|col2|want
+--------+----+----+----+
2020-08-01| -1| -1| 8 |
2020-08-02| -1| -1| 7 |
2020-08-03| -1| 3| 6 |
2020-08-04| -1| 2| 5 |
2020-08-05| 1 | 4| 4 |
2020-08-06| 2 | 1| 0 |
2020-08-07| 3 | 2| 0 |
2020-08-08| 4 | 3| 0 |
2020-08-09| 5 | -1| 0 |
+---------+----+----+----+
增强:我想添加附加条件 where col2 == -1 when
col1 == 1(在 2020 年 8 月 5 日),并且 col2 == -1 连续..然后我想计算连续的 -1,然后在连续中断 col2 == 的地方添加?价值。所以这里有一个例子要清楚。
+--------+----+----+----+
date |col1|col2|want
+--------+----+----+----+
2020-08-01| -1| -1| 11|
2020-08-02| -1| -1| 10|
2020-08-03| -1| 3| 9 |
2020-08-04| -1| 2| 8 |
2020-08-05| 1 | -1| 7*|
2020-08-06| 2 | -1| 0 |
2020-08-07| 3 | -1| 0 |
2020-08-08| 4 | 4*| 0 |
2020-08-09| 5 | -1| 0 |
+---------+----+----+----+
所以,我们看到 3 个连续的 -1,(从 2020-08-05 开始,我们只关心第一个连续的 -1),在连续之后我们有 4 个(在 2020-08-08 表示为 *) ,那么我们将在 col1 ==1 行有 4+ 3 =7。可能吗?
** 我的第一次尝试 **
TEST_df = TEST_df.withColumn('cumsum', sum(when( col('col1') < 1, col('col1') ) \
.otherwise( when( col('col1') == 1, 1).otherwise(0))).over(Window.partitionBy('col1').orderBy().rowsBetween(-sys.maxsize, 0)))
TEST_df.show()
+----------+----+----+------+
| date|col1|col2|cumsum|
+----------+----+----+------+
|2020-08-01| -1| -1| -1|
|2020-08-02| -1| -1| -2|
|2020-08-03| -1| 3| -3|
|2020-08-04| -1| 2| -4|
|2020-08-05| 1| 4| 1|
|2020-08-07| 3| 2| 0|
|2020-08-09| 5| -1| 0|
|2020-08-08| 4| 3| 0|
|2020-08-06| 2| 1| 0|
+----------+----+----+------+
w1 = Window.orderBy(desc('date'))
w2 =Window.partitionBy('case').orderBy(desc('cumsum'))
TEST_df.withColumn('case', sum(when( (col('cumsum') == 1) & (col('col2') != -1) , col('col2')) \
.otherwise(0)).over(w1)) \
.withColumn('rank', when(col('case') != 0, rank().over(w2)-1).otherwise(0)) \
.withColumn('want', col('case') + col('rank')) \
.orderBy('date') \
+----------+----+----+------+----+----+----+
|date |col1|col2|cumsum|case|rank|want|
+----------+----+----+------+----+----+----+
|2020-08-01|-1 |-1 |-1 |4 |1 |5 |
|2020-08-02|-1 |-1 |-2 |4 |2 |6 |
|2020-08-03|-1 |3 |-3 |4 |3 |7 |
|2020-08-04|-1 |2 |-4 |4 |4 |8 |
|2020-08-05|1 |4 |1 |4 |0 |4 |
|2020-08-06|2 |1 |0 |0 |0 |0 |
|2020-08-07|3 |2 |0 |0 |0 |0 |
|2020-08-08|4 |3 |0 |0 |0 |0 |
|2020-08-09|5 |-1 |0 |0 |0 |0 |
+----------+----+----+------+----+----+----+
你看到排名 1,2,3,4 如果我能把它变成 4,3,2,1 它将看起来像我的结果数据框....如何反转它?我尝试了 orderby asc 和 desc ...
当然这是在增强
之前
IIUC,你可以尝试以下方法:
groupby 并创建所有相关行的 collect_list(下面代码中的 vals
),按日期降序排列列表(注意: 将 groupby(lit(1))
更改为可用于将数据划分为独立子集的任何列。
找到具有col1 == 1
的数组索引idx
如果 col2==-1
在 idx
,则找到从 idx 到列表开头的偏移量,第一行有 col2 != -1
(注意:在当前代码中,如果idx
之前的所有col2都是-1,offset可能为NULL,你必须决定你想要什么。例如使用coalesce(IF(...),0)
)
在我们有了offset和idx之后,want
列可以通过以下方式计算:
IF(i<idx, 0, vals[idx-offset].col2 + offset + i - idx)
使用 SparkSQL 函数 inline 分解结构数组。
注意: 如果您的生产数据框中存在太多列,可以使用 Window 函数应用相同的逻辑。
代码如下:
from pyspark.sql.functions import sort_array, collect_list, struct, expr, lit
TEST_df = spark.createDataFrame([
('2020-08-01', -1, -1), ('2020-08-02', -1, -1), ('2020-08-03', -1, 3),
('2020-08-04', -1, 2), ('2020-08-05', 1, -1), ('2020-08-06', 2, -1),
('2020-08-07', 3, -1), ('2020-08-08', 4, 4), ('2020-08-09', 5, -1)
], ['date', 'col1', 'col2'])
# list of column used in calculation
cols = ["date", "col1", "col2"]
df_new = TEST_df \
.groupby(lit(1)) \
.agg(sort_array(collect_list(struct(*cols)),False).alias('vals')) \
.withColumn('idx', expr("filter(sequence(0,size(vals)-1), i -> vals[i].col1=1)[0]")) \
.withColumn('offset', expr("""
coalesce(IF(vals[idx].col2=-1, filter(sequence(1,idx), i -> vals[idx-i].col2 != -1)[0],0),0)
""")).selectExpr("""
inline(
transform(vals, (x,i) -> named_struct(
'dta', x,
'want', IF(i<idx, 0, vals[idx-offset].col2 + offset + i - idx)
)
)
)""").select('dta.*', 'want')
输出:
df_new.orderBy('date').show()
+----------+----+----+----+
| date|col1|col2|want|
+----------+----+----+----+
|2020-08-01| -1| -1| 11|
|2020-08-02| -1| -1| 10|
|2020-08-03| -1| 3| 9|
|2020-08-04| -1| 2| 8|
|2020-08-05| 1| -1| 7|
|2020-08-06| 2| -1| 0|
|2020-08-07| 3| -1| 0|
|2020-08-08| 4| 4| 0|
|2020-08-09| 5| -1| 0|
+----------+----+----+----+
编辑: 根据评论,添加了使用 Window 聚合函数而不是 groupby 的替代方法:
from pyspark.sql import Window
# WindowSpec to cover all related Rows in the same partition
w1 = Window.partitionBy().orderBy('date').rowsBetween(Window.unboundedPreceding,Window.unboundedFollowing)
cols = ["date", "col1", "col2"]
# below `cur_idx` is the index for the current Row in array `vals`
df_new = TEST_df.withColumn('vals', sort_array(collect_list(struct(*cols)).over(w1),False)) \
.withColumn('idx', expr("filter(sequence(0,size(vals)-1), i -> vals[i].col1=1)[0]")) \
.withColumn('offset', expr("IF(vals[idx].col2=-1, filter(sequence(1,idx), i -> vals[idx-i].col2 != -1)[0],0)")) \
.withColumn("cur_idx", expr("array_position(vals, struct(date,col1,col2))-1")) \
.selectExpr(*TEST_df.columns, "IF(cur_idx<idx, 0, vals[idx-offset].col2 + offset + cur_idx - idx) as want")
数据框已经按日期整理,
col1 ==1 值是唯一的,
传col1==1,increment加1(eg. 1,2,3,4,5,6,7...) 只有 -1 是重复的。
我有一个数据框看起来像这样称它为 df
TEST_schema = StructType([StructField("date", StringType(), True),\
StructField("col1", IntegerType(), True),\
StructField("col2", IntegerType(), True)])
TEST_data = [('2020-08-01',-1,-1),('2020-08-02',-1,-1),('2020-08-03',-1,3),('2020-08-04',-1,2),('2020-08-05',1,4),\
('2020-08-06',2,1),('2020-08-07',3,2),('2020-08-08',4,3),('2020-08-09',5,-1)]
rdd3 = sc.parallelize(TEST_data)
TEST_df = sqlContext.createDataFrame(TEST_data, TEST_schema)
TEST_df.show()
+--------+----+----+
date |col1|col2|
+--------+----+----+
2020-08-01| -1| -1|
2020-08-02| -1| -1|
2020-08-03| -1| 3|
2020-08-04| -1| 2|
2020-08-05| 1 | 4|
2020-08-06| 2 | 1|
2020-08-07| 3 | 2|
2020-08-08| 4 | 3|
2020-08-09| 5 | -1|
+--------+----+----+
条件是当col1 == 1,那么我们从col2 ==4开始向后添加,(eg. 4,5,6,7,8,...) 之后的col2 == 4 return 0 一路(例如 4,0,0,0,0...)
所以,我得到的 df 看起来像这样。
+--------+----+----+----+
date |col1|col2|want
+--------+----+----+----+
2020-08-01| -1| -1| 8 |
2020-08-02| -1| -1| 7 |
2020-08-03| -1| 3| 6 |
2020-08-04| -1| 2| 5 |
2020-08-05| 1 | 4| 4 |
2020-08-06| 2 | 1| 0 |
2020-08-07| 3 | 2| 0 |
2020-08-08| 4 | 3| 0 |
2020-08-09| 5 | -1| 0 |
+---------+----+----+----+
增强:我想添加附加条件 where col2 == -1 when col1 == 1(在 2020 年 8 月 5 日),并且 col2 == -1 连续..然后我想计算连续的 -1,然后在连续中断 col2 == 的地方添加?价值。所以这里有一个例子要清楚。
+--------+----+----+----+
date |col1|col2|want
+--------+----+----+----+
2020-08-01| -1| -1| 11|
2020-08-02| -1| -1| 10|
2020-08-03| -1| 3| 9 |
2020-08-04| -1| 2| 8 |
2020-08-05| 1 | -1| 7*|
2020-08-06| 2 | -1| 0 |
2020-08-07| 3 | -1| 0 |
2020-08-08| 4 | 4*| 0 |
2020-08-09| 5 | -1| 0 |
+---------+----+----+----+
所以,我们看到 3 个连续的 -1,(从 2020-08-05 开始,我们只关心第一个连续的 -1),在连续之后我们有 4 个(在 2020-08-08 表示为 *) ,那么我们将在 col1 ==1 行有 4+ 3 =7。可能吗?
** 我的第一次尝试 **
TEST_df = TEST_df.withColumn('cumsum', sum(when( col('col1') < 1, col('col1') ) \
.otherwise( when( col('col1') == 1, 1).otherwise(0))).over(Window.partitionBy('col1').orderBy().rowsBetween(-sys.maxsize, 0)))
TEST_df.show()
+----------+----+----+------+
| date|col1|col2|cumsum|
+----------+----+----+------+
|2020-08-01| -1| -1| -1|
|2020-08-02| -1| -1| -2|
|2020-08-03| -1| 3| -3|
|2020-08-04| -1| 2| -4|
|2020-08-05| 1| 4| 1|
|2020-08-07| 3| 2| 0|
|2020-08-09| 5| -1| 0|
|2020-08-08| 4| 3| 0|
|2020-08-06| 2| 1| 0|
+----------+----+----+------+
w1 = Window.orderBy(desc('date'))
w2 =Window.partitionBy('case').orderBy(desc('cumsum'))
TEST_df.withColumn('case', sum(when( (col('cumsum') == 1) & (col('col2') != -1) , col('col2')) \
.otherwise(0)).over(w1)) \
.withColumn('rank', when(col('case') != 0, rank().over(w2)-1).otherwise(0)) \
.withColumn('want', col('case') + col('rank')) \
.orderBy('date') \
+----------+----+----+------+----+----+----+
|date |col1|col2|cumsum|case|rank|want|
+----------+----+----+------+----+----+----+
|2020-08-01|-1 |-1 |-1 |4 |1 |5 |
|2020-08-02|-1 |-1 |-2 |4 |2 |6 |
|2020-08-03|-1 |3 |-3 |4 |3 |7 |
|2020-08-04|-1 |2 |-4 |4 |4 |8 |
|2020-08-05|1 |4 |1 |4 |0 |4 |
|2020-08-06|2 |1 |0 |0 |0 |0 |
|2020-08-07|3 |2 |0 |0 |0 |0 |
|2020-08-08|4 |3 |0 |0 |0 |0 |
|2020-08-09|5 |-1 |0 |0 |0 |0 |
+----------+----+----+------+----+----+----+
你看到排名 1,2,3,4 如果我能把它变成 4,3,2,1 它将看起来像我的结果数据框....如何反转它?我尝试了 orderby asc 和 desc ... 当然这是在增强
之前IIUC,你可以尝试以下方法:
groupby 并创建所有相关行的 collect_list(下面代码中的
vals
),按日期降序排列列表(注意: 将groupby(lit(1))
更改为可用于将数据划分为独立子集的任何列。找到具有
的数组索引col1 == 1
idx
如果
col2==-1
在idx
,则找到从 idx 到列表开头的偏移量,第一行有col2 != -1
(注意:在当前代码中,如果idx
之前的所有col2都是-1,offset可能为NULL,你必须决定你想要什么。例如使用coalesce(IF(...),0)
)在我们有了offset和idx之后,
want
列可以通过以下方式计算:IF(i<idx, 0, vals[idx-offset].col2 + offset + i - idx)
使用 SparkSQL 函数 inline 分解结构数组。
注意: 如果您的生产数据框中存在太多列,可以使用 Window 函数应用相同的逻辑。
代码如下:
from pyspark.sql.functions import sort_array, collect_list, struct, expr, lit
TEST_df = spark.createDataFrame([
('2020-08-01', -1, -1), ('2020-08-02', -1, -1), ('2020-08-03', -1, 3),
('2020-08-04', -1, 2), ('2020-08-05', 1, -1), ('2020-08-06', 2, -1),
('2020-08-07', 3, -1), ('2020-08-08', 4, 4), ('2020-08-09', 5, -1)
], ['date', 'col1', 'col2'])
# list of column used in calculation
cols = ["date", "col1", "col2"]
df_new = TEST_df \
.groupby(lit(1)) \
.agg(sort_array(collect_list(struct(*cols)),False).alias('vals')) \
.withColumn('idx', expr("filter(sequence(0,size(vals)-1), i -> vals[i].col1=1)[0]")) \
.withColumn('offset', expr("""
coalesce(IF(vals[idx].col2=-1, filter(sequence(1,idx), i -> vals[idx-i].col2 != -1)[0],0),0)
""")).selectExpr("""
inline(
transform(vals, (x,i) -> named_struct(
'dta', x,
'want', IF(i<idx, 0, vals[idx-offset].col2 + offset + i - idx)
)
)
)""").select('dta.*', 'want')
输出:
df_new.orderBy('date').show()
+----------+----+----+----+
| date|col1|col2|want|
+----------+----+----+----+
|2020-08-01| -1| -1| 11|
|2020-08-02| -1| -1| 10|
|2020-08-03| -1| 3| 9|
|2020-08-04| -1| 2| 8|
|2020-08-05| 1| -1| 7|
|2020-08-06| 2| -1| 0|
|2020-08-07| 3| -1| 0|
|2020-08-08| 4| 4| 0|
|2020-08-09| 5| -1| 0|
+----------+----+----+----+
编辑: 根据评论,添加了使用 Window 聚合函数而不是 groupby 的替代方法:
from pyspark.sql import Window
# WindowSpec to cover all related Rows in the same partition
w1 = Window.partitionBy().orderBy('date').rowsBetween(Window.unboundedPreceding,Window.unboundedFollowing)
cols = ["date", "col1", "col2"]
# below `cur_idx` is the index for the current Row in array `vals`
df_new = TEST_df.withColumn('vals', sort_array(collect_list(struct(*cols)).over(w1),False)) \
.withColumn('idx', expr("filter(sequence(0,size(vals)-1), i -> vals[i].col1=1)[0]")) \
.withColumn('offset', expr("IF(vals[idx].col2=-1, filter(sequence(1,idx), i -> vals[idx-i].col2 != -1)[0],0)")) \
.withColumn("cur_idx", expr("array_position(vals, struct(date,col1,col2))-1")) \
.selectExpr(*TEST_df.columns, "IF(cur_idx<idx, 0, vals[idx-offset].col2 + offset + cur_idx - idx) as want")