reducByKey總結
在進行Spark開發算法時,最有用的一個函數就是reduceByKey。
reduceByKey的作用對像是(key, value)形式的rdd,而reduce有減少、壓縮之意,reduceByKey的作用就是對相同key的數據進行處理,最終每個key只保留一條記錄。
保留一條記錄通常有兩種結果。一種是隻保留我們希望的信息,比如每個key出現的次數。第二種是把value聚合在一起形成列表,這樣後續可以對value做進一步的操作,比如排序。
常用方式舉例
比如現在我們有數據goodsSale:RDD[(String, String)],兩個字段分別是goodsid、單個訂單中的銷售額,現在我們需要統計每個goodsid的銷售額。
我們只需要保留每個goodsid的累記銷售額,可以使用如下語句來實現:
val goodsSaleSum = goodsSale.reduceByKey((x,y) => x+y)
熟悉之後你可以使用更簡潔的方式:
val goodsSaleSum = goodsSale.reduceByKey(_+_)
reduceByKey會尋找相同key的數據,當找到這樣的兩條記錄時會對其value(分別記爲x,y)做(x,y) => x+y
的處理,即只保留求和之後的數據作爲value。反覆執行這個操作直至每個key只留下一條記錄。
現在假設goodsSaleSum還有一個字段類目id,即 RDD[(String, String, String)] 形式,三個字段分別是類目id、goodsid、總銷量,現在我們要獲得第個類目id下銷量最高的一個商品。
上一步聚是保留value求和之後的數據,而這裏其實我們只需要保留銷量更高的那條記錄。不過我們不能直接對RDD[(String, String, String)]類型的數據使用reduceByKey方法,因爲這並不是一個(key, value)形式的數據,所以需要使用map方法轉化一下類型。
val catGmvTopGoods = goodsSaleSum.map(x => (x._1, (x._2, x._3)))
.reduceByKey((x, y) => if (x._2.toDouble > y._2.toDouble) x else y)
.map(x => (x._1, x._2._1, x._2._2)
再進一步,假設現在我們有一個任務:推薦5個銷售額最高的類目,併爲每個類目推薦一個銷售額最高的商品,而我們的數據就是上述RDD[(String, String, String)類型的goodsSaleSum。
這需要兩步,一是計算每個類目的銷售額,這和舉的第一個例子一樣。二是找出每個類目下銷量最高的商品,這和第二個例子一樣。實際上,我們可以只實用一個reduceByKey就達到上面的目的。
val catIdGmvTopGoods = goodsSaleSum.map(x => (x._1, (x._2, x._3, x._3)))
.reduceByKey((x, y) => if (x._2 > y._2) (x._1, x._2, x._3+y._3) else (y._1, y._2, x._3+y._3))
.map( x => (x._1, x._2._1, x._2._2, x._2._3)
.sortBy(_._3, false)
.take(5)
由於我們需要計算每個類目的總銷售額,同時需要保留商品的銷售額,所以先使用map增加一個字段用來記錄類目的總銷售額。這樣一來,我們就可以使用reduceByKey同時完成前兩個例子的操作。
剩下的就是進行排序並獲取前5條記錄。
聚合方式舉例
上述的三個例子都是隻保留需要的信息,但有時我們需要將value聚合在一起進行排序操作,比如對每個類目下的商品按銷售額進行排序。
假設我們的數據是 RDD[(String, String, String)],三個字段分別是類目id、goodsid、銷售額。
若是使用sql,那我們直接用row_number函數就可以很簡單的使用分類目排序這個任務。
但由於spark-sql佔用的資源會比RDD多不少,在開發任務時並不建議使用spark-sql。
我們的方法是通過reduceByKey把商品聚合成一個List,然後對這個List進行排序,再使用flatMapValues攤平數據。
我們在使用reduceyByKey時會注意到,兩個value聚合後的數據類型必須和之前一致。
所以在聚合商品時我們也需要保證這點,通常有兩種方法,一是使用ListBuffer,即可變長度的List。二是使用String,以分隔符來區分商品和銷售額。下面我們使用第一種方式完成這個任務。
val catIdGoodsIdSorted = goodsGmvSum.map(x => (x._1, ListBuffer(x._2, x._3.toDouble)))
.reduceByKey((x, y) => x++y)
.flatMapValues( x => x.toList.sortBy(_._2).reverse.zipWithIndex)
上述zipWithIndex給列表增加一個字段,用來記錄元素的位置信息。而flatMapValues可以把List的每個元素單獨拆成一條記錄,詳細的說明可以參考我寫的另一篇文章Spark入門-常用函數彙總
小結
我在本文中介紹了reduceByKey的三種作用:
- 求和彙總
- 獲得每個key下value最大的記錄
- 聚合value形成一個List之後進行排序