解释Spark中的聚合功能

时间:2015-01-30 16:49:19

标签: python apache-spark lambda aggregate rdd

我正在寻找一些关于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个元素的总和。如果我将传递给聚合函数的初始值从(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)

9 个答案:

答案 0 :(得分:74)

我不能完全相信接受的答案,JohnKnight的答案也有帮助,所以这就是我的观点:

首先,让我用自己的话解释aggregate()

<强>原型

  

聚合(zeroValue,seqOp,combOp)

说明

aggregate()允许您获取RDD并生成与原始RDD中存储的类型不同的单个值。

<强>参数

  1. zeroValue:您想要的结果的初始化值 格式。
  2. seqOp:您要应用于RDD记录的操作。运行一次 分区中的每条记录。
  3. combOp:定义结果对象的方式(每个分区一个), 结合起来。
  4. 示例

      

    计算列表的总和和该列表的长度。将结果返回到一对(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初始化为我们提供zeroValue的{​​{1}}参数,即(0,0)和aggregate()是列表的第一个元素,即1.结果就是这样:

    list_element

    现在,本地结果是(1,1),这意味着,到目前为止,对于第一个分区,在仅处理第一个元素之后,总和为1,长度为1.注意,{{1}从(0,0)更新为(1,1)。

    0 + 1 = 1
    0 + 1 = 1
    

    现在本地结果是(3,2),这将是第一个分区的最终结果,因为它们不是第一个分区的子列表中的其他元素。

    为第二个分区做同样的事情,得到(7,2)。

    现在我们将combOp应用于每个本地结果,以便我们可以形成最终的全局结果,如下所示:local_result

    &#39>图&#39;中描述的示例:

    1 + 2 = 3
    1 + 1 = 2
    

    受到这位伟大的example的启发。

    所以现在如果(3,2) + (7,2) = (10, 4)不是(0,0),而是(1,0),那么人们会期望得到(8 + 4,2 + 2)=(12,4),这对于#39;解释你的体验。即使我们改变了我的例子的分区数量,我也无法再次获得它。

    这里的关键是JohnKnight的答案,其中指出 (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) 不仅类似于分区数量,而且可能应用的次数超出预期。

答案 1 :(得分:27)

Aggregate允许您随意转换和组合RDD的值。

它使用两个功能:

第一个转换并在本地聚合[U]中添加原始集合[T]的元素,并采用以下形式:(U,T)=&gt; U.您可以将其视为折叠,因此该操作也需要零。此操作本地应用于每个分区。

这里是问题的关键所在:这里应该使用的唯一值是还原操作的ZERO值。 此操作在每个分区上本地执行,因此,向该零值添加任何内容将添加到结果乘以RDD的分区数。

第二个操作采用前一个操作[U]的结果类型的2个值,并将其组合成一个值。此操作将减少每个分区的部分结果并生成实际总数。

例如: 鉴于字符串的RDD:

val rdd:RDD[String] = ???

假设您想要该RDD中字符串长度的总和,那么您可以这样做:

1)第一个操作将字符串转换为size(int)并累积size的值。

val stringSizeCummulator: (Int, String) => Int  = (total, string) => total + string.lenght`

2)为加法运算提供ZERO(0)

val ZERO = 0

3)将两个整数加在一起的操作:

val add: (Int, Int) => Int = _ + _

全部放在一起:

rdd.aggregate(ZERO, stringSizeCummulator, add)

那么,为什么需要ZERO? 当累加器函数应用于分区的第一个元素时,没有运行总计。 ZERO在这里使用。

EG。我的RDD是: - 分区1:[“跳转”,“结束”] - 分区2:[“the”,“wall”]

这将导致:

P1:

  1. stringSizeCummulator(ZERO,“Jump”)= 4
  2. stringSizeCummulator(4,“over”)= 8
  3. P2:

    1. stringSizeCummulator(ZERO,“the”)= 3
    2. stringSizeCummulator(3,“wall”)= 7
    3. 减少:添加(P1,P2)= 15

答案 2 :(得分:15)

我没有足够的声誉来评论马斯格先前的回答。 实际上零值应该是“中立的”。朝向seqop,意味着它不会干扰seqop结果,如0朝向添加,或1朝*;

您不应该尝试使用非中性值,因为它可能会被随意应用。 此行为不仅与分区数量相关联。

我尝试了问题中所述的相同实验。 使用1分区,零值应用3次。 有2个分区,6次。 有3个分区,9次,这将继续。

答案 3 :(得分:1)

很好的解释,它确实帮助我理解了聚合函数的底层工作。我玩了一段时间,发现如下。

  • 如果您使用acc为(0,0),则不会更改该函数的输出结果。

  • 如果更改了初始累加器,那么它将处理结果,如下所示

  

[RDD元素之和+ acc初始值* RDD分区数+   acc初始值]

对于这里的问题,我建议根据我的理解检查分区,因为分区数应该是8,因为每次我们处理RDD分区上的seq op时,它将以acc结果的初始总和开始并且当它要进行梳状操作时,它将再次使用acc初始值。

例如    清单(1,2,3,4)&amp; acc(1,0)

通过RDD.partitions.size

获取scala中的分区

如果分区是2&amp;元素数量是4然后=&gt; [10 + 1 * 2 + 1] =&gt; (13,4)

如果分区是4&amp;元素数量是4然后=&gt; [10 + 1 * 4 + 1] =&gt; (15,4)

希望这有帮助,您可以查看here以获取解释。感谢。

答案 4 :(得分:1)

您可以使用以下代码(在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)

然后,在shell中:

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, line 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
}

答案 5 :(得分:0)

对于寻找上述示例的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)

答案 6 :(得分:0)

我尝试了很多关于这个问题的实验。最好为聚合设置num分区。 seqOp将处理每个分区并应用初始值,而且,当组合所有分区时,combOp也将应用初始值。 所以,我提出了这个问题的格式:

final result = sum(list) + num_Of_Partitions * initial_Value + 1

答案 7 :(得分:0)

感谢gsamaras。

我的视线如下 enter image description here

答案 8 :(得分:0)

我将按以下方式解释Spark中的聚合操作的概念:

聚合函数的定义

**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分为以下四个分区:

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

谢谢