PySpark:键值对RDD及其常用算子

本文围绕Spark V3.2.1版本,介绍了键值对RDD。键值对RDD是特殊RDD,可减少通信开销,可通过map算子将普通RDD转化而来。还详细阐述了常用的转化算子如combineByKey、reduceByKey等,以及行动算子如countByKey、collectAsMap等的用法和作用。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

Spark版本:V3.2.1

1. 键值对RDD

1.1 键值对RDD的定义

键值对RDD是一种特殊的RDD,注意Spark中并没有这种RDD类型。普通RDD支持的算子都适用于键值对RDD。键值对RDD提供了并行操作各个键或跨节点重新进行数据分组的操作接口。用户可以通过控制键值对RDD在各个节点上的分布情况,大大减少应用的通信开销。

1.2 创建键值对RDD

普通RDD的数据元素一般为数值型、字符串型,键值对RDD中保存里面存储的数据为“键值对”,其形式为(key,value)。通常可以使用map()算子将普通RDD转化为键值对RDD。具体如下:

#直接定义pairRDD
rdd_1=sc.parallelize([(1,[2,5]),(3,[4,4]),(4,[7,8])])
#使用map将普通rdd转化为键值对RDD
rdd_2=sc.parallelize(range(1,10))
pairRDD=rdd_2.map(lambda x:[x,1])
print(rdd_1.collect())
print(pairRDD.collect())

其执行结果如下:

[(1, [2, 5]), (3, [4, 4]), (4, [7, 8])]
[[1, 1], [2, 1], [3, 1], [4, 1], [5, 1], [6, 1], [7, 1], [8, 1], [9, 1]]

这里要注意,键值对RDD的每个元素的类型为(key,value)形式,即在Python中这个元素的长度必须为2。若使用如下语句,其定义的是普通RDD而不是键值对RDD。

rdd_1=sc.parallelize([(1,2,5),(3,4,4),(4,7,8)])

另外,(key,value)的形式在Python中也可以使用[key,value]。因为在Python中这两者可以转化。

2.常用算子

2.1 转化算子

  • combineByKey()

combineByKey()算子可以将具有相同key的元素的value值组合成一个集合。主要包含三个参数:createCombiner, mergeValue, mergeCombiners。其具体作用如下:

(1)createCombiner: 当combineByKey()在遍历分区中的所有元素时,如果当前的元素的key从未出现过,则combineByKey()会调用一个叫作creatCombiner的函数创建该key对应的累加器的初始值。这一过程会在每个分区中各个key第一次出现时执行;

(2)mergeValue: 如果当前的key在该分区中已经出现过了,则会使用mergerValue方法将该键的累加器对应的当前值与这个新的value值合并;

(3)mergeCombiners: 将各个分区得到的结果进行合并;

(4)不是所有元素都会完整执行以上三个过程;

用法举例如下:

rdd_1=sc.parallelize([(1,[2,5]),(3,[4,4]),(4,[7,8]),(1,[9]),(3,[2,3]),(1,[2,3])]) 
rdd_2=rdd_1.combineByKey(createCombiner=lambda x:x, 
                         mergeValue=lambda x,y:x+y, 
                         mergeCombiners=lambda x,y:x+y)
print(rdd_1.glom().collect())
print(rdd_2.collect())

其具体结果如下:

[[(1, [2, 5]), (3, [4, 4]), (4, [7, 8])], [(1, [9]), (3, [2, 3]), (1, [2, 3])]]
[(4, [7, 8]), (1, [2, 5, 9, 2, 3]), (3, [4, 4, 2, 3])]

以key=1为例进行说明,从rdd_1的分区结果中可以看到元素(1,[2,5])在第1个分区中,(1,[9])和(1,[2,3])在第2个分区中。其具体执行过程如下:

(1) combineByKey()在遍历第1个分区时第1次遇到key=1的元素时,会将其value=[2,5]传入createCombiner中,其结果为[2,5]。之后继续遍历第1个分区,后续并没有key=1的元素了,所以combineByKey在第1个分区中key=1的结果为[2,5]。

(2) combineByKey()算子在遍历第2个分区时第1次遇到key=1的元素时,将其value=[9]传入createCombiner,其结果为[9]。遇到第2个key=1的元素时,会将当前累加器的值[9]和当前value值[2,3]传入到mergeValue中,其结果为[9,2,3]。则当前分区的结果为[9,2,3]。

(3) 将以上两个分区的结果[2,5]和[9,2,3]传入到mergeCombiners中,得到最终结果[2,5,9,2,3]。

对上述代码进行如下修改:

rdd_1=sc.parallelize([(1,[2,5]),(3,[4,4]),(4,[7,8]),(1,[9]),(3,[2,3]),(1,[2,3])]) 
rdd_2=rdd_1.combineByKey(createCombiner=lambda x:x, 
                         mergeValue=lambda x,y:x+y, 
                         mergeCombiners=lambda x,y:[])
print(rdd_2.collect())

其结果如下:

[(4, [7, 8]), (1, []), (3, [])]

将mergeCombiners该为[]之后,key=4的结果仍然为[7,8],key=4的元素只出现在第1个分区中,可以推测出该元素没有执行mergeValue、mergeCombiners过程。 而当key=1或key=3时,则因为mergeCombiners方法的缘故,其最终值均为[]。

  • reduceByKey()算子、foldByKey()、aggregateByKey()算子

reduceByKey()算子可以将相同key的元素聚集到一起,最终把所有相同key的元素成一个元素。通过该操作,元素的key不变,value值可以聚合成一个列表或者进行求和等操作。其具体用法如下:

rdd_1=sc.parallelize([(1,[2,5]),(3,[4,4]),(4,[7,8]),(1,[9]),(3,[2,3]),(1,[2,3])]) 
rdd_2=rdd_1.reduceByKey(func=lambda x,y:[sum(x)+sum(y)])
print(rdd_2.collect())
rdd_3=sc.parallelize([('a',1),('b',2),('a',4),('b',4)])
rdd_4=rdd_3.reduceByKey(func=lambda x,y:x+y)
print(rdd_4.collect())

其结果如下:

[(4, [7, 8]), (1, [21]), (3, [13])]
[('b', 6), ('a', 5)]

从实验结果可以看到,key=4的所有value值并没有完成求和操作。这是因为reduceByKey(func)等价于combineByKey(lambda x:x,func,func)。而key=4只出现在一个分区中,所以始终没有机会执行func函数。若想对rdd_1的所有元素值进行累计求和,可以使用如下代码:

rdd_1=sc.parallelize([(1,[2,5]),(3,[4,4]),(4,[7,8]),(1,[9]),(3,[2,3]),(1,[2,3])]) 
rdd_2=rdd_1.combineByKey(createCombiner=lambda x:[sum(x)],
                         mergeValue=lambda x,y:[sum(x)+sum(y)], 
                         mergeCombiners=lambda x,y:[sum(x)+sum(y)])
rdd_3=rdd_2.map(lambda x:(x[0],x[1][0]))
print(rdd_3.collect())

  其结果如下:

[(4, 15), (1, 21), (3, 13)]

foldByKey()和aggregateByKey()算子与fold()、aggregate()算子效果相同,不过是按key进行聚合操作。其具体用法如下: 

rdd_1=sc.parallelize([('a',1),('b',2),('b',4),('c',5),('c',10)])
rdd_2=rdd_1.foldByKey(2, func=lambda x,y:x+y)
rdd_3=rdd_1.aggregateByKey(zeroValue=(0,0), 
                           seqFunc=lambda x,y:(x[0]+y,x[1]+1), 
                           combFunc=lambda x,y:(x[0]+y[0],x[1]+y[1]))
print(rdd_1.glom().collect())
print(rdd_2.collect())
print(rdd_3.collect())

 其结果如下:

[[('a', 1), ('b', 2)], [('b', 4), ('c', 5), ('c', 10)]]
[('b', 10), ('c', 17), ('a', 3)]
[('b', (6, 2)), ('c', (15, 2)), ('a', (1, 1))]

通过查询源码可以发现这两个算子底层也是用combineByKey()来实现的。这里重点说一下foldByKey()的计算结果。由foldByKey()的源码可以知道zeroValue只作用在combineByKey()中的createCombiner中。 所以key=a的元素只执行了createCombiner操作,所以key=a的value=2+1=3; key=b的元素分布在两个分区中,所以createCombiner执行2次,combFunc执行一次,所以key=b的value=(2+2)+(2+4)=10;key=c只在一个分区中,但有两个元素,所以createCombiner执行一次,seqFunc执行一次,所以key=c的value=(2+5)+10=17。

  • groupByKey()算子

groupByKey()算子可以把所有相同key的元素合并成一个元素,即改元素的key不变,value则聚集到一个集合中。其具体用法如下:

rdd_1=sc.parallelize([(1,[2,5]),(3,[4,4]),(4,[7,8]),(1,[9]),(3,[2,3]),(1,[2,3])]) 
rdd_2=rdd_1.groupByKey().map(lambda x:(x[0],list(x[1])))
print(rdd_2.collect())

其结果如下:

[(4, [[7, 8]]), (1, [[2, 5], [9], [2, 3]]), (3, [[4, 4], [2, 3]])]

这里使用map()是为了把groupByKey()的结果清晰明了的展示出来。

  • sortByKey()算子

sortByKey()算子可以对键值对RDD按key进行排序,使用参ascending指定升序或降序。其具体用法如下:

rdd_1=sc.parallelize([(1,[2,5]),(3,[4,4]),(4,[7,8]),(1,[9]),(3,[2,3]),(1,[2,3])]) 
res_1=rdd_1.sortByKey(ascending=False)
print(res_1.collect())

其具体用法如下:

[(4, [7, 8]), (3, [4, 4]), (3, [2, 3]), (1, [2, 5]), (1, [9]), (1, [2, 3])] 

  • sampleByKey()算子

sampleByKey()与sample()类似,不同的地方在于sampleyByKey()中fractions的参数类型为dic类型,并且键值对RDD中每个key都必须在dict中出现。其用法如下:

rdd_1=sc.parallelize([('a',1),('b',2),('b',4),('a',3),('a',10),('b',11)])
rdd_2=rdd_1.sampleByKey(withReplacement=False,fractions={'a':0.5,'b':0.5})
print(rdd_2.collect())

其结果如下:

[('a', 3), ('b', 11)] 

  •  subtractByKey()算子

 subtractByKey()算子可以对两个键值对RDD进行差集操作。这个算子只比较key,并不会对value进行比较。其具体用法如下:

rdd_1=sc.parallelize([('a',1),('b',2),('b',4)])
rdd_2=sc.parallelize([('a',4)])
rdd_3=rdd_1.subtractByKey(rdd_2)
print(rdd_3.collect())

其结果如下:

[('b', 2), ('b', 4)]

  •  join类算子

Spark中常用的join类算子主要由:join()算子、leftOuterJoin()算子、rightOuterJoin()算子、fullOuterJoin()算子。这些算子与数据库中的内连接、左外连接、右外连接、全外连接作用相似。其用法如下:

rdd_1=sc.parallelize([('a',1),('b',2),('b',4),('c',5)])
rdd_2=sc.parallelize([('a',2),('b',4),('d',7)])
rdd_3=rdd_1.join(rdd_2)
rdd_4=rdd_1.leftOuterJoin(rdd_2)
rdd_5=rdd_1.rightOuterJoin(rdd_2)
rdd_6=rdd_1.fullOuterJoin(rdd_2)
print(rdd_3.collect())
print(rdd_4.collect())
print(rdd_5.collect())
print(rdd_6.collect())

其结果如下:

[('b', (2, 4)), ('b', (4, 4)), ('a', (1, 2))]
[('b', (2, 4)), ('b', (4, 4)), ('c', (5, None)), ('a', (1, 2))]
[('b', (2, 4)), ('b', (4, 4)), ('a', (1, 2)), ('d', (None, 7))]
[('b', (2, 4)), ('b', (4, 4)), ('c', (5, None)), ('a', (1, 2)), ('d', (None, 7))]

  • mapValues()算子、flatMapValues()算子

这两个算子与map()算子、flatMap()算子的区别在于这两个算子的操作对象为value值。其用法如下:

rdd_1=sc.parallelize([(1,[2,5]),(3,[4,4]),(4,[7,8]),(1,[9]),(3,[2,3]),(1,[2,3])]) 
rdd_2=rdd_1.mapValues(lambda x:[i**2 for i in x])
rdd_3=rdd_1.flatMapValues(lambda x:[i**2 for i in x])
print(rdd_2.collect())
print(rdd_3.collect())

其具体结果如下:

[(1, [4, 25]), (3, [16, 16]), (4, [49, 64]), (1, [81]), (3, [4, 9]), (1, [4, 9])]
[(1, 4), (1, 25), (3, 16), (3, 16), (4, 49), (4, 64), (1, 81), (3, 4), (3, 9), (1, 4), (1, 9)]

  • keys()算子和values()算子

keys()算子和values()算子与Python中dict中的对应方法相同。具体用法如下:

rdd_1=sc.parallelize([(1,[2,5]),(3,[4,4]),(4,[7,8]),(1,[9]),(3,[2,3]),(1,[2,3])]) 
rdd_2=rdd_1.keys()
rdd_3=rdd_1.values()
print(rdd_2.collect())
print(rdd_3.collect())

其结果如下;

[1, 3, 4, 1, 3, 1]
[[2, 5], [4, 4], [7, 8], [9], [2, 3], [2, 3]]

这里要注意,keys()算子得到的结果并不会去重。 

  • cogroup()算子

cogroup()算子对两个(key,value)形式的RDD根据key进行组合,相当于根据key进行并集操作。其具体用法如下:

rdd_1=sc.parallelize([('a',1),('b',4),('c',19)])
rdd_2=sc.parallelize([('a',1),('b',6),('d',15)])
rdd_3=rdd_1.cogroup(rdd_2).mapValues(lambda x:[list(i) for i in x])
rdd_4=rdd_2.cogroup(rdd_1).mapValues(lambda x:[list(i) for i in x])
print(rdd_3.collect())
print(rdd_4.collect())

 其具体结果如下:

[('b', [[4], ['6']]), ('c', [[19], []]), ('a', [[1], [1]]), ('d', [[], [15]])]
[('b', [['6'], [4]]), ('c', [[], [19]]), ('a', [[1], [1]]), ('d', [[15], []])] 

2.2 行动算子

  • countByKey()算子

countByKey()算子可以按key统计元素出现的次数。其用法如下:

rdd_1=sc.parallelize([(1,[2,5]),(3,[4,4]),(4,[7,8]),(1,[9]),(3,[2,3]),(1,[2,3])]) 
res_1=rdd_1.countByKey()
print(res_1)

其用法如下:

defaultdict(int, {1: 3, 3: 2, 4: 1})

  • collectAsMap()算子

collectAsMap()算子将所有(key,value)转化为类似Python中的dict的结果。其用法如下:

rdd_1=sc.parallelize([('a',1),('b',4),('c',19),('a',[1,2])])
res_1=rdd_1.collectAsMap()
print(res_1)

其具体如下 :

{'a': [1, 2], 'b': 4, 'c': 19} 

这里要注意, Python中的dict中key只能出现一次,所以collectAsMap()中的结果中每一个key也能出现一次,其对应的value为该key最后一次出现时对应的value值。

  • lookup()算子

lookup()算子可以查找对应key的value值。其用法如下:

rdd_1=sc.parallelize([('a',1),('b',4),('c',19),('a',[1,2])])
res_1=rdd_1.lookup('a')
print(res_1)

其结果如下:

[1, [1, 2]]

其他类型的RDD算子:Pyspark: RDD及其常用算子_Sun_Sherry的博客-优快云博客

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值