![](/img/trans.png)
[英]Generic “reduceBy” or “groupBy + aggregate” functionality with Spark DataFrame
[英]Explain the aggregate functionality in Spark (with Python and Scala)
我正在尋找對通過 python 中的 spark 可用的聚合功能的更好解釋。
我的例子如下(使用來自 Spark 1.2.0 版本的 pyspark)
sc.parallelize([1,2,3,4]).aggregate(
(0, 0),
(lambda acc, value: (acc[0] + value, acc[1] + 1)),
(lambda acc1, acc2: (acc1[0] + acc2[0], acc1[1] + acc2[1])))
輸出:
(10, 4)
我得到了預期的結果(10,4)
,它是1+2+3+4
和 4 個元素的總和。 如果我將傳遞給聚合函數的初始值從(0,0)
更改為(1,0)
, (0,0)
得到以下結果
sc.parallelize([1,2,3,4]).aggregate(
(1, 0),
(lambda acc, value: (acc[0] + value, acc[1] + 1)),
(lambda acc1, acc2: (acc1[0] + acc2[0], acc1[1] + acc2[1])))
輸出:
(19, 4)
該值增加 9。如果我將其更改為(2,0)
,則該值將變為(28,4)
等等。
有人可以向我解釋這個值是如何計算的嗎? 我預計值會上升 1 而不是 9,預計會看到(11,4)
而我看到的是(19,4)
。
我對接受的答案並不完全信服,JohnKnight 的回答有所幫助,所以這是我的觀點:
首先,讓我們用我自己的話解釋aggregate() :
原型:
聚合(零值,seqOp,combOp)
說明:
aggregate()
允許您獲取一個 RDD 並生成一個與原始 RDD 中存儲的類型不同的單個值。
參數:
zeroValue
:結果的初始化值,采用所需格式。seqOp
:要應用於 RDD 記錄的操作。 為分區中的每條記錄運行一次。combOp
:定義結果對象(每個分區一個)如何組合。示例:
計算列表的總和和該列表的長度。 以
(sum, length)
對的形式返回結果。
在 Spark shell 中,我首先創建了一個包含 4 個元素和 2 個分區的列表:
listRDD = sc.parallelize([1,2,3,4], 2)
然后我定義了我的seqOp :
seqOp = (lambda local_result, list_element: (local_result[0] + list_element, local_result[1] + 1) )
和我的combOp :
combOp = (lambda some_local_result, another_local_result: (some_local_result[0] + another_local_result[0], some_local_result[1] + another_local_result[1]) )
然后我匯總:
listRDD.aggregate( (0, 0), seqOp, combOp)
Out[8]: (10, 4)
如您所見,我為變量指定了描述性名稱,但讓我進一步解釋一下:
第一個分區具有子列表 [1, 2]。 我們將 seqOp 應用於該列表的每個元素,這將產生一個本地結果,一對(sum, length)
,它將在本地反映結果,僅在第一個分區中。
所以,讓我們開始吧: local_result
被初始化為我們提供給aggregate()
的zeroValue
參數,即 (0, 0) 和list_element
是列表的第一個元素,即 1。結果是這樣的:
0 + 1 = 1
0 + 1 = 1
現在,本地結果是 (1, 1),這意味着,到目前為止,對於第一個分區,僅處理第一個元素后,總和為 1,長度為 1。注意, local_result
從 (0, 0),到 (1, 1)。
1 + 2 = 3
1 + 1 = 2
現在本地結果是 (3, 2),這將是第一個分區的最終結果,因為它們不是第一個分區的子列表中的其他元素。
對第二個分區做同樣的事情,我們得到 (7, 2)。
現在我們將 combOp 應用於每個局部結果,以便我們可以形成最終的全局結果,如下所示: (3,2) + (7,2) = (10, 4)
“圖”中描述的示例:
(0, 0) <-- zeroValue
[1, 2] [3, 4]
0 + 1 = 1 0 + 3 = 3
0 + 1 = 1 0 + 1 = 1
1 + 2 = 3 3 + 4 = 7
1 + 1 = 2 1 + 1 = 2
| |
v v
(3, 2) (7, 2)
\ /
\ /
\ /
\ /
\ /
\ /
------------
| combOp |
------------
|
v
(10, 4)
靈感來自這個偉大的例子。
所以現在如果zeroValue
不是 (0, 0),而是 (1, 0),人們會期望得到 (8 + 4, 2 + 2) = (12, 4),這並不能解釋你的經歷。 即使我們更改了示例的分區數,我也無法再次獲得它。
這里的關鍵是 JohnKnight 的回答,其中指出zeroValue
不僅類似於分區數,而且可能比您預期的應用更多次。
使用 Scala 進行解釋
Aggregate 允許您隨意轉換和組合 RDD 的值。
它使用兩個函數:
第一個將原始集合 [T] 的元素轉換並添加到本地聚合 [U] 中並采用以下形式:(U,T) => U。您可以將其視為折疊,因此它也需要一個零對於那個操作。 此操作在本地並行應用於每個分區。
這就是問題的關鍵所在:這里應該使用的唯一值是歸約運算的零值。 此操作在每個分區本地執行,因此,向該零值添加任何內容都將添加到結果乘以 RDD 的分區數。
第二個操作取前一個操作 [U] 的結果類型的 2 個值,並將其合並為一個值。 此操作將減少每個分區的部分結果並產生實際總數。
例如:給定一個字符串 RDD:
val rdd:RDD[String] = ???
假設您想要聚合該 RDD 中字符串的長度,因此您可以執行以下操作:
第一個操作將字符串轉換為 size (int) 並累積 size 的值。
val stringSizeCummulator: (Int, String) => Int = (total, string) => total + string.lenght`
為加法運算提供零 (0)
零值 = 0
將兩個整數相加的操作:
val add: (Int, Int) => Int = _ + _
把它們放在一起:
rdd.aggregate(ZERO, stringSizeCummulator, add)
使用 Spark 2.4 及更高版本
rdd.aggregate(ZERO)(stringAccumulator,add)
那么,為什么需要零呢? 當 cummulator 函數應用於分區的第一個元素時,沒有運行總計。 此處使用零。
例如。 我的RDD是:
這將導致:
P1:
P2:
減少:添加(P1,P2)= 15
我沒有足夠的聲望點來評論 Maasg 的先前回答。 實際上,零值對於 seqop 應該是“中性的”,這意味着它不會干擾 seqop 結果,例如 0 表示添加,或 1 表示 *;
您永遠不應該嘗試使用非中性值,因為它可能會應用於任意時間。 此行為不僅與分區數有關。
我嘗試了與問題中所述相同的實驗。 對於 1 個分區,零值應用了 3 次。 帶2個分區,6次。 有 3 個分區,9 次,這將繼續。
您可以使用以下代碼(在 Scala 中)來准確查看aggregate
正在做什么。 它構建了一個包含所有加法和合並操作的樹:
sealed trait Tree[+A]
case class Leaf[A](value: A) extends Tree[A]
case class Branch[A](left: Tree[A], right: Tree[A]) extends Tree[A]
val zero : Tree[Int] = Leaf(0)
val rdd = sc.parallelize(1 to 4).repartition(3)
然后,在外殼中:
scala> rdd.glom().collect()
res5: Array[Array[Int]] = Array(Array(4), Array(1, 2), Array(3))
所以,我們有這 3 個分區:[4]、[1,2] 和 [3]。
scala> rdd.aggregate(zero)((l,r)=>Branch(l, Leaf(r)), (l,r)=>Branch(l,r))
res11: Tree[Int] = Branch(Branch(Branch(Leaf(0),Branch(Leaf(0),Leaf(4))),Branch(Leaf(0),Leaf(3))),Branch(Branch(Leaf(0),Leaf(1)),Leaf(2)))
您可以將結果表示為一棵樹:
+
| \__________________
+ +
| \________ | \
+ + + 2
| \ | \ | \
0 + 0 3 0 1
| \
0 4
您可以看到在驅動程序節點(樹的左側)上創建了第一個零元素,然后將所有分區的結果一一合並。 您還會看到,如果您像在問題中一樣將 0 替換為 1,它將為每個分區上的每個結果加 1,並在驅動程序的初始值上加 1。 因此,您給出的零值被使用的總次數是:
number of partitions + 1
。
所以,在你的情況下,結果
aggregate(
(X, Y),
(lambda acc, value: (acc[0] + value, acc[1] + 1)),
(lambda acc1, acc2: (acc1[0] + acc2[0], acc1[1] + acc2[1])))
將:
(sum(elements) + (num_partitions + 1)*X, count(elements) + (num_partitions + 1)*Y)
aggregate
的實現非常簡單。 它在RDD.scala 第 1107 行中定義:
def aggregate[U: ClassTag](zeroValue: U)(seqOp: (U, T) => U, combOp: (U, U) => U): U = withScope {
// Clone the zero value since we will also be serializing it as part of tasks
var jobResult = Utils.clone(zeroValue, sc.env.serializer.newInstance())
val cleanSeqOp = sc.clean(seqOp)
val cleanCombOp = sc.clean(combOp)
val aggregatePartition = (it: Iterator[T]) => it.aggregate(zeroValue)(cleanSeqOp, cleanCombOp)
val mergeResult = (index: Int, taskResult: U) => jobResult = combOp(jobResult, taskResult)
sc.runJob(this, aggregatePartition, mergeResult)
jobResult
}
很好的解釋,它真的幫助我理解了聚合函數的底層工作。 我玩了一段時間,發現如下。
如果您使用 acc 作為 (0,0) 那么它不會改變函數的輸出結果。
如果初始累加器發生變化,那么它將處理結果如下
[RDD元素總和+acc初始值*RDD分區數+acc初始值]
對於這里的問題,我建議檢查分區,因為根據我的理解,分區數應為 8,因為每次我們在 RDD 的分區上處理 seq 操作時,它將以 acc 結果的初始總和開始,並且何時它將執行組合操作,它將再次使用 acc 初始值一次。
例如 List (1,2,3,4) & acc (1,0)
通過 RDD.partitions.size 獲取 scala 中的分區
如果分區是 2 & 元素數是 4 那么 => [ 10 + 1 * 2 + 1 ] => (13,4)
如果分區是 4 & 元素數是 4 那么 => [ 10 + 1 * 4 + 1 ] => (15,4)
希望這會有所幫助,您可以在此處查看解釋。 謝謝。
對於為上述示例尋找 Scala 等效代碼的人 - 在這里。 相同的邏輯,相同的輸入/結果。
scala> val listRDD = sc.parallelize(List(1,2,3,4), 2)
listRDD: org.apache.spark.rdd.RDD[Int] = ParallelCollectionRDD[2] at parallelize at <console>:21
scala> listRDD.collect()
res7: Array[Int] = Array(1, 2, 3, 4)
scala> listRDD.aggregate((0,0))((acc, value) => (acc._1+value,acc._2+1),(acc1,acc2) => (acc1._1+acc2._1,acc1._2+acc2._2))
res10: (Int, Int) = (10,4)
我嘗試了很多關於這個問題的實驗。 最好為聚合設置分區數。 seqOp 將處理每個分區並應用初始值,此外,當組合所有分區時,combOp 也會應用初始值。 所以,我提出這個問題的格式:
final result = sum(list) + num_Of_Partitions * initial_Value + 1
我將解釋Spark中Aggregate操作的概念如下:
聚合函數的定義
**def aggregate** (initial value)(an intra-partition sequence operation)(an inter-partition combination operation)
val flowers = sc.parallelize(List(11, 12, 13, 24, 25, 26, 35, 36, 37, 24, 25, 16), 4)
--> 4 表示我們的 Spark 集群中可用的分區數.
因此,rdd 被分配到 4 個分區中:
11, 12, 13
24, 25, 26
35, 36, 37
24, 25, 16
我們將問題陳述分為兩部分:第一部分是匯總每個象限采摘的花朵總數; 這就是分區內序列聚合
11+12+13 = 36
24+25+26 = 75
35+36+37 = 108
24+25 +16 = 65
問題的第二部分是對跨分區的這些單獨的聚合求和; 這就是分區間聚合。
36 + 75 + 108 + 65 = 284
存儲在 RDD 中的總和可以進一步用於任何類型的轉換或其他操作
所以代碼變成了這樣:
val sum = flowers.aggregate(0)((acc, value) => (acc + value), (x,y) => (x+y))
或val sum = flowers.aggregate(0)(_+_, _+_)
Answer: 284
解釋: (0) - 是累加器 第一個+是分區內和,將花園每個象限中每個采摘者采摘的花朵總數相加。 第二個+是分區間總和,它匯總了每個象限的總和。
情況1:
假設,如果我們需要在初始值之后減少函數。 如果初始值不為零會發生什么??。 如果是 4,例如:
該數字將添加到每個分區內聚合中,也會添加到分區間聚合中:
所以第一個計算是:
11+12+13 = 36 + 5 = 41
24+25+26 = 75 + 5 = 80
35+36+37 = 108 + 5 = 113
24+25 +16 = 65 + 5 = 70
下面是初始值為5的分區間聚合計算:
partition1 + partition2 + partition3+ partition4 + 5 = 41 + 80 + 113 + 70 = 309
因此,進入您的查詢:可以根據 rdd 數據分布的分區數計算總和。 我認為您的數據分布如下,這就是為什么結果為 (19, 4)。 因此,在進行聚合操作時,請特定於分區值的數量:
val list = sc.parallelize(List(1,2,3,4))
val list2 = list.glom().collect
val res12 = list.aggregate((1,0))(
(acc, value) => (acc._1 + value, acc._2 + 1),
(acc1, acc2) => (acc1._1 + acc2._1, acc1._2 + acc2._2)
)
結果:
list: org.apache.spark.rdd.RDD[Int] = ParallelCollectionRDD[19] at parallelize at command-472682101230301:1
list2: Array[Array[Int]] = Array(Array(), Array(1), Array(), Array(2), Array(), Array(3), Array(), Array(4))
res12: (Int, Int) = (19,4)
說明:由於您的數據分布在 8 個分區中,結果就像(通過使用上面解釋的邏輯)
分區內添加:
0+1=1
1+1=2
0+1=1
2+1=3
0+1=1
3+1=4
0+1=1
4+1=5
total=18
分區間計算:
18+1 (1+2+1+3+1+4+1+5+1) = 19
謝謝
聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.