Pyspark数据帧的GroupBy替代方案?

Pyspark数据帧的GroupBy替代方案?,pyspark,group-by,pyspark-sql,Pyspark,Group By,Pyspark Sql,我有这样一个数据集: timestamp vars 2 [1,2] 2 [1,2] 3 [1,2,3] 3 [1,2] 我想要一个这样的数据帧。基本上,上述数据帧中的每个值都是一个索引,该值的频率就是该索引处的值。这个计算是在每个唯一的时间戳上完成的 timestamp vars 2 [0, 2, 2] 3 [0,2,2,1] 现

我有这样一个数据集:

timestamp     vars 
2             [1,2]
2             [1,2]
3             [1,2,3]
3             [1,2]
我想要一个这样的数据帧。基本上,上述数据帧中的每个值都是一个索引,该值的频率就是该索引处的值。这个计算是在每个唯一的时间戳上完成的

timestamp     vars 
2             [0, 2, 2]
3             [0,2,2,1]
现在,我正在按时间戳分组,并对变量进行聚合/展平(得到类似(1,2,1,2表示时间戳2或1,2,3,1,2表示时间戳3)的内容,然后我有一个使用集合的udf。Counter to获得键->值dict。然后我将此dict转换为我想要的格式

groupBy/agg可以任意大(数组大小可以达到数百万),这对于窗口函数来说似乎是一个很好的用例,但我不确定如何将其组合在一起


认为还值得一提的是,我尝试过重新分区、转换为RDD和使用groupByKey。两者都非常慢(>24小时)在大型数据集上。

编辑:如评论中所述,原始方法的问题可能来自使用过滤器或聚合函数的
count
,这会触发不必要的数据扫描。下面我们分解数组并在创建最终数组列之前进行聚合(计数):

from pyspark.sql.functions import collect_list, struct  

df = spark.createDataFrame([(2,[1,2]), (2,[1,2]), (3,[1,2,3]), (3,[1,2])],['timestamp', 'vars'])

df.selectExpr("timestamp", "explode(vars) as var") \
    .groupby('timestamp','var') \
    .count() \
    .groupby("timestamp") \
    .agg(collect_list(struct("var","count")).alias("data")) \
    .selectExpr(
        "timestamp",
        "transform(data, x -> x.var) as indices",
        "transform(data, x -> x.count) as values"
    ).selectExpr(
        "timestamp",
        "transform(sequence(0, array_max(indices)), i -> IFNULL(values[array_position(indices,i)-1],0)) as new_vars"
    ).show(truncate=False)
+---------+------------+
|timestamp|new_vars    |
+---------+------------+
|3        |[0, 2, 2, 1]|
|2        |[0, 2, 2]   |
+---------+------------+
其中:

(1) 我们分解数组并对每个
时间戳
+
var

(2) groupby
timestamp
并创建一个包含两个字段的结构数组
var
count

(3) 将结构数组转换为两个数组:索引和值(类似于我们定义的SparseVector)

(4) 转换序列
序列(0,数组_max(索引))
,对于序列中的每个i,使用在
索引
数组中查找
i
的索引,然后从相同位置的
值数组中检索值,如下所示:

IFNULL(values[array_position(indices,i)-1],0)
请注意函数array_position使用基于1的索引,而数组索引使用基于0的索引,因此在上面的表达式中有一个
-1

旧方法: (1) 使用变换+过滤器/大小

from pyspark.sql.functions import flatten, collect_list

df.groupby('timestamp').agg(flatten(collect_list('vars')).alias('data')) \
  .selectExpr(
    "timestamp", 
    "transform(sequence(0, array_max(data)), x -> size(filter(data, y -> y = x))) as vars"
  ).show(truncate=False)
+---------+------------+
|timestamp|vars        |
+---------+------------+
|3        |[0, 2, 2, 1]|
|2        |[0, 2, 2]   |
+---------+------------+
(2) 使用功能:

df.groupby('timestamp').agg(flatten(collect_list('vars')).alias('data')) \
   .selectExpr("timestamp", """ 

     aggregate(   
       data,         
       /* use an array as zero_value, size = array_max(data))+1 and all values are zero */
       array_repeat(0, int(array_max(data))+1),       
       /* increment the ith value of the Array by 1 if i == y */
       (acc, y) -> transform(acc, (x,i) -> IF(i=y, x+1, x))       
     ) as vars   

""").show(truncate=False)

编辑:如评论中所述,原始方法的问题可能来自使用过滤器或聚合函数(触发不必要的数据扫描)的
count
。下面我们分解数组并在创建最终数组列之前进行聚合(计数):

from pyspark.sql.functions import collect_list, struct  

df = spark.createDataFrame([(2,[1,2]), (2,[1,2]), (3,[1,2,3]), (3,[1,2])],['timestamp', 'vars'])

df.selectExpr("timestamp", "explode(vars) as var") \
    .groupby('timestamp','var') \
    .count() \
    .groupby("timestamp") \
    .agg(collect_list(struct("var","count")).alias("data")) \
    .selectExpr(
        "timestamp",
        "transform(data, x -> x.var) as indices",
        "transform(data, x -> x.count) as values"
    ).selectExpr(
        "timestamp",
        "transform(sequence(0, array_max(indices)), i -> IFNULL(values[array_position(indices,i)-1],0)) as new_vars"
    ).show(truncate=False)
+---------+------------+
|timestamp|new_vars    |
+---------+------------+
|3        |[0, 2, 2, 1]|
|2        |[0, 2, 2]   |
+---------+------------+
其中:

(1) 我们分解数组并对每个
时间戳
+
var

(2) groupby
timestamp
并创建一个包含两个字段的结构数组
var
count

(3) 将结构数组转换为两个数组:索引和值(类似于我们定义的SparseVector)

(4) 转换序列
序列(0,数组_max(索引))
,对于序列中的每个i,使用在
索引
数组中查找
i
的索引,然后从相同位置的
值数组中检索值,如下所示:

IFNULL(values[array_position(indices,i)-1],0)
请注意函数array_position使用基于1的索引,而数组索引使用基于0的索引,因此在上面的表达式中有一个
-1

旧方法: (1) 使用变换+过滤器/大小

from pyspark.sql.functions import flatten, collect_list

df.groupby('timestamp').agg(flatten(collect_list('vars')).alias('data')) \
  .selectExpr(
    "timestamp", 
    "transform(sequence(0, array_max(data)), x -> size(filter(data, y -> y = x))) as vars"
  ).show(truncate=False)
+---------+------------+
|timestamp|vars        |
+---------+------------+
|3        |[0, 2, 2, 1]|
|2        |[0, 2, 2]   |
+---------+------------+
(2) 使用功能:

df.groupby('timestamp').agg(flatten(collect_list('vars')).alias('data')) \
   .selectExpr("timestamp", """ 

     aggregate(   
       data,         
       /* use an array as zero_value, size = array_max(data))+1 and all values are zero */
       array_repeat(0, int(array_max(data))+1),       
       /* increment the ith value of the Array by 1 if i == y */
       (acc, y) -> transform(acc, (x,i) -> IF(i=y, x+1, x))       
     ) as vars   

""").show(truncate=False)

对于索引2,它是如何从1,2,1,2发展到[0,2,2]?带有partitionby子句的窗口应该比groupby执行得更好,如果您不使用udf,而是使用spark内置函数来实现我们的目标,[1,2,1,2]中有21和22。因此在索引1中,我将2(频率)放在在索引2,我把2放进去。因为没有0,索引0仍然是这样。因此,[0,2,2]。我不知道如何使用partitionBy和window从[1,2]和[1,2]到[1,2,1,2]。尝试过,但它只对和有效。对于索引2,它如何从1,2,1,2到[0,2,2]?带有partitionby子句的窗口应该比groupby执行得更好,如果您不使用udf,而是使用spark内置函数来实现您的目标,那么性能可能会更好[1,2,1,2]中有2个1和2个2。因此在索引1中,我放2(频率),在索引2中,我放2。因为没有0,索引0仍然是这样。因此,[0,2,2].我不知道如何从[1,2]和[1,2]到[1,2,1,2]使用partitionBy和window。尝试过,但它只适用于求和。这是一个让我大开眼界的方法,让我知道如何在转换中使用过滤器。非常好的解决方案谢谢你!它有帮助,但仍然会在聚合中爆炸,因为列表的长度可以让我以百万计。有没有办法通过时间戳引入窗口函数/分区?顺便说一句。数组_max(数据)的最大值是多少?您提到的数组中的数百万项是在聚合之前还是聚合之后?数组_max(数据)的值可以达到一百万。转换表达式确实是大型数据集的瓶颈。需要尝试并优化它。@tanyabrown,我看到了现有方法的问题,如果M是数组中所有项的数量,N是数组的最大值(数据),那么每个时间戳要扫描/比较的数据将是O(M*N),这对于大M和N来说效率很低。最好先分解数组进行聚合,然后创建数组。这可能是O(N)对于每一行。我回家后会检查此方法。顺便说一句。是否可以为您的任务创建一个固定大小的SparseVector列,而不是可变大小的ArrayType列?这让我大开眼界,了解如何在转换中使用过滤器。很好的解决方案谢谢您!这很有帮助,但仍然会在聚合时爆炸因为该列表的长度可以以百万计。有没有办法通过时间戳引入窗口函数/分区?顺便问一下。数组_max(数据)的最大值是多少?您提到的数组中的数百万项是在聚合之前还是聚合之后?数组_max(数据)的值转换表达式是lar上的瓶颈