spark学习之combineByKey函数

在数据分析中,处理Key,V alue的Pair数据是极为常见的场景,例如我们可以针对这样的数据进行分组、聚合或者将两个包含Pair数据的RDD根据key进行join。从函数的抽象层面看,这些操作具有共同的特征,都是将类型为RDD[(K,V)]的数据处理为RDD[(K,C)]。这里的V和C可以是相同类型,也可以是不同类型。这种数据处理操作并非单纯的对Pair的value进行map,而是针对不同的key值对原有的value进行联合(Combine)。因而,不仅类型可能不同,元素个数也可能不同。Spark为此提供了一个高度抽象的操作combineByKey。该方法的定义如下所示:

def combineByKey[C](createCombiner:V=>C,
mergeValue:(C,V)=>C,
mergeCombiners:(C,C)=>C,
partitioner:Partitioner,
mapSideCombine:Boolean=true,
serializer:Serializer=null):RDD[(K,C)]={
//实现略
}

  根据上述的定义,可以看到combineByKey函数主要接受了三个函数作为参数,分别为createCombiner、mergeValue、mergeCombiners。这三个函数足以说明它究竟做了什么。理解了这三个函数,就可以很好地理解combineByKey。

  combineByKey是将RDD[(K,V)]combine为RDD[(K,C)],因此,首先需要提供一个函数,能够完成从V到C的combine,称之为combiner。如果V和C类型一致,则函数为V => V。倘若C是一个集合,例如Iterable[V],则createCombiner为V => Iterable[V]。

  而mergeValue则是将原RDD中Pair的Value合并为操作后的C类型数据。合并操作的实现决定了结果的运算方式。所以,mergeValue更像是声明了一种合并方式,它是由整个combine运算的结果来导向的。函数的输入为原RDD中Pair的V,输出为结果RDD中Pair的C。

  最后的mergeCombiners则会根据每个Key所对应的多个C,进行归并。

  为了进一步形象理解,我们可以将combineByKey想象成是一个超级酷的果汁机。它能同时接受各种各样的水果,然后聪明地按照水果的种类分别榨出不同的果汁。苹果归苹果汁,橙子归橙汁,西瓜归西瓜汁。我们为水果定义类型为Fruit,果汁定义为Juice,那么combineByKey就是将RDD[(String,Fruit)]combine为RDD[(String, Juice)]。

  可以想象在榨果汁前,水果可能有很多,即使是相同类型的水果,也会作为不同的RDD元素:

  (“apple”, apple1), (“orange”, orange1), (“apple”, apple2)

而combine函数就行一个超级榨汁机一样,其作用就是使得每种水果只有一杯果汁(只是容量不同罢了):

  (“apple”, appleJuice), (“orange”, orangeJuice)

  那么现在我们思考一下 这个果汁机应该由什么元件构成呢?首先,它需要一个元件提供将各种水果榨为各种果汁的功能;其次,它需要提供将果汁进行混合的功能;最后,为了避免混合错误,还得提供能够根据水果类型进行混合的功能。注意第二个函数和第三个函数的区别,前者只提供混合功能,即能够将不同容器的果汁装到一个容器中,而后者的输入已有一个前提,那就是已经按照水果类型放到不同的区域,果汁机在混合果汁时,并不会混淆不同区域的果汁。

  果汁机的功能类似于groupByKey+foldByKey操作。它可以调用combineByKey函数:
“` scala
case class Fruit(kind:String,weight:Int){
 def makeJuice:Juice=Juice(weight*100)
}

case class Juice(volumn:Int){
 def add(j:Juice):Juice=Juice(volumn+j.volumn)
}

val apple1=Fruit(“apple”,5)
val apple2=Fruit(“apple”,8)
val orange =Fruit(“orange”,10)
val fruit=sc.parallelize(List((“apple”,apple1),(“orange”, orange1),(“apple”,apple2)))
val juice=fruit.combineByKey(
f=>f.makeJuice,
(j:Juice,f) =>j.add(f.makeJuice),
(j1:Juice,j2:Juice) =>j1.add(j2)
)


执行juice.collect,结果为: ``` scala Array[(String,Juice)]=Array((orange,Juice(1000)),(apple,Juice(1300)))

RDD中有许多针对Pair RDD的操作在内部实现都调用了combineByKey函数。例如 groupByKey:

class PairRDDFunctions[K,V](self:RDD[(K,V)])
 (implicit kt:ClassTag[K],vt:ClassTag[V],ord:Ordering[K]=null)
   extends Logging 
    with    SparkHadoopMapReduceUtil
    with Serializable{
   def groupByKey(partitioner:Partitioner):RDD[(K,Iterable[V])]={
val createCombiner=(v:V)=>CompactBuffer(v)
val mergeValue=(buf:CompactBuffer[V],v:V)=>buf+=v
val mergeCombiners=(c1:CompactBuffer[V],c2:CompactBuffer[V])=>c1++=c2
val bufs=combineByKey[CompactBuffer[V]](
createCombiner,mergeValue,mergeCombiners,partitioner,mapSideCombine=false
)
bufs.asInstanceOf[RDD[(K,Iterable[V])]]
}
}

groupByKey函数针对PairRddFunctions的RDD[(K, V)]按照key对value进行分组。它在内部调用了combineByKey函数,传入的三个函数分别承担了如下职责:
* createCombiner是将原RDD中的K类型转换为Iterable[V]类型,实现为CompactBuffer。
* mergeV alue实则就是将原RDD的元素追加到CompactBuffer中,即将追加操作(+=)视为合并操作。
* mergeCombiners则负责针对每个key值所对应的Iterable[V],提供合并功能。

再例如,我们要针对科目对成绩求平均值:

val scores=sc.parallelize(List(("chinese",88.0),("chinese",90.5),("math",60.0),("math",87.0))

  平均值并不能一次获得,而是需要求得各个科目的总分以及科目的数量。因此,我们需要针对scores进行combine,从(String,Float)combine为(String, (Float, Int))。调用combineByKey函数后,我们可以再通过map来获得平均值。代码如下:

scala
val avg=scores.combineByKey(
(v)=>(v,1),
(acc:(Float,Int),v)=>(acc._1+v,acc._2+1),
(acc1:(Float,Int),acc2:(Float,Int))=>(acc1._1+acc2._1,acc1._2+acc2._2)
).map{case(key,value)= (key,value._1/value._2.toFloat)}

  除了可以进行group、average之外,根据传入的函数实现不同,我们还可以利用combineByKey完成诸如aggregate、fold等操作。这是一个高度的抽象,但从声明的角度来看,却又不需要了解过多的实现细节。这正是函数式编程的魅力。


本文转自:rscala.com版权所有,本文spark学习之combineByKey函数转载请注明出处:http://rscala.com/index.php/378.html

该文章归档分类于 scala实践spark实践大数据基础

阅读更多

没有更多推荐了,返回首页