如何在Scala的Map List中按键值对值进行求和并对它们进行分组?

时间:2015-01-30 11:06:53

标签: scala

我有一张地图清单:

val list = List(
  Map("id" -> "A", "value" -> 20, "name" -> "a"),
  Map("id" -> "B", "value" -> 10, "name" -> "b"),
  Map("id" -> "A", "value" -> 5, "name" -> "a"),
  Map("id" -> "C", "value" -> 1, "name" -> "c"),
  Map("id" -> "D", "value" -> 60, "name" -> "d"),
  Map("id" -> "C", "value" -> 3, "name" -> "c")
)

我想对value求和,并以最有效的方式将它们按id值分组,以便它变为:

Map(A -> 25, B -> 10, C -> 4, D -> 60)

4 个答案:

答案 0 :(得分:7)

A)如果你有很多具有相同身份的项目,那么这个项目的可读性和性能最高:

scala> list.groupBy(_("id")).mapValues(_.map(_("value").asInstanceOf[Int]).sum)
res14: scala.collection.immutable.Map[Any,Int] = Map(D -> 60, A -> 25, C -> 4, B -> 10)

您也可以使用list.groupBy(_("id")).par...。只有当你有许多具有相同键的元素时,它才会更快地工作,否则它会过于缓慢。

否则,更改线程的上下文本身会使.par版本变慢,因为map(_"value").sum(您的嵌套map-reduce)可能比在线程之间切换更快。如果N =系统中的核心数量,那么map {reduce应该慢N倍,以便从par中受益,当然,大致可以说。

B)因此,如果并行化不能很好地工作(最好通过性能测试来检查),你可以用专门的方式“重新实现”groupBy

val m = scala.collection.mutable.Map[String, Int]() withDefaultValue(0)
for (e <- list; k = e("id").toString) m.update(k, m(k) + e("value").asInstanceOf[Int])

C)最平行的选项是:

val m = new scala.collection.concurrent.TrieMap[String, Int]()
for (e <- list.par; k = e("id").toString) {
    def replace = {           
       val v = m(k)
       m.replace(k, v, v + e("value").asInstanceOf[Int]) //atomic
    }
    m.putIfAbsent(k, 0) //atomic
    while(!replace){} //in case of conflict
}

scala> m
res42: scala.collection.concurrent.TrieMap[String,Int] = TrieMap(B -> 10, C -> 4, D -> 60, A -> 25)

D)功能样式中并行化程度最高(每次合并映射速度较慢,但​​对于没有共享内存的分布式map-reduce最佳),使用scalaz semigroups

import scalaz._; import Scalaz._
scala> list.map(x => Map(x("id").asInstanceOf[String] -> x("value").asInstanceOf[Int]))
    .par.reduce(_ |+| _)
res3: scala.collection.immutable.Map[String,Int] = Map(C -> 4, D -> 60, A -> 25, B -> 10)

但只有当你使用比“+”更复杂的聚合时,它才会更有效。


让我们做简单的性能测试:

def time[T](n: Int)(f: => T) = {
  val start = System.currentTimeMillis()
  for(i <- 1 to n) f
  (System.currentTimeMillis() - start).toDouble / n
}

在MacBook Pro 2.3 GHz Intel Core i7上使用JDK8在Scala 2.12 REPL中完成。每次测试都会启动两次 - 首先是为JVM预热。

1)对于您的输入集合和time(100000){...},从最慢到最快:

`par.groupBy.par.mapValues` = 0.13861 ms
`groupBy.par.mapValues` = 0.07667 ms
`most parallelized` = 0.06184 ms    
`scalaz par.reduce(_ |+| _)` = 0.04010 ms //same for other reduce-based implementations, mentioned here
`groupBy.mapValues` = 0.00212 ms
`for` + `update` with mutable map initialization time = 0.00201 ms
`scalaz suml` = 0.00171 ms      
`foldLeft` from another answer = 0.00114 ms
`for` + `update` without mutable map initialization time = 0.00105

所以,来自另一个答案的foldLeft似乎是您输入的最佳解决方案。

2)让它变得更大

 scala> val newlist = (1 to 1000).map(_ => list).reduce(_ ++ _)

现在输入newListtime(1000){...}

 `scalaz par.reduce(_ |+| _)` = 1.422 ms
 `foldLeft`/`for` = 0.418 ms
 `groupBy.par.mapValues` = 0.343 ms

最好在这里选择groupBy.par.mapValues

3)最后,让我们定义另一个聚合:

scala> implicit class RichInt(i: Int){ def ++ (i2: Int) = { Thread.sleep(1); i + i2}}
defined class RichInt

使用listtime(1000)进行测试:

`foldLeft` = 7.742 ms
`most parallelized` = 3.315 ms

所以最好在这里使用大多数并行版本。


为什么减速是如此缓慢:

让我们采取8个要素。它从叶子[1] + ... + [1]到根[1 + ... + 1]生成一个计算树:

time(([1] + [1]) + ([1] + [1]) + ([1] + [1]) + ([1] + [1]) 
   => ([1 +1] + [1 +1]) + ([1 + 1] + [1 + 1]) 
   => [1 + 1 + 1 + 1] + [1 + 1 + 1 + 1]) 
 = (1 + 1 + 1 + 1) +  (2 + 2) + 4 = 12

时间(N = 8)= 8/2 + 2 * 8/4 + 4 * 8/8 = 8 *(1/2 + 2/4 + 4/8)= 8 * log2(8)/ 2 = 12

或者只是:

time(N) = N * log2(N)/2

当然,此公式仅适用于实际上为2的幂。无论如何,复杂度为O(NlogN),慢于foldLeft的{​​{1}}。即使在并行化之后,它也只是O(N)所以这个实现只能用于Big Data的分布式Map-Reduce,或者简单地说当你没有足够的内存并将Map存储在某个缓存中时。

您可能会注意到它的并行化程度比输入的其他选项更好 - 这只是因为对于6个元素来说它不是那么慢(这里几乎是O(N)) - 并且当只有其他选项进行分组时,您只进行一次减少调用数据之前或只是创建更多线程,这会导致更多的“线程切换”开销。简单地说,O(1)在这里创建的线程更少。但是如果你有更多的数据 - 它当然不起作用(参见实验2)。

答案 1 :(得分:5)

还使用foldLeft

list.foldLeft(Map[String, Int]().withDefaultValue(0))((res, v) => {
  val key = v("id").toString
  res + (key -> (res(key) + v("value").asInstanceOf[Int]))
})

更新:reduceLeft

(Map[String, Any]().withDefaultValue(0) :: list).reduceLeft((res, v) => {
  val key = v("id").toString
  res + (key -> (res(key).asInstanceOf[Int] + v("value").asInstanceOf[Int]))
})

顺便说一下,如果你看一下reduceLeft定义,你就会发现它使用相同的foldLeft

  def reduceLeft[B >: A](f: (B, A) => B): B =
    if (isEmpty) throw new UnsupportedOperationException("empty.reduceLeft")
    else tail.foldLeft[B](head)(f)

更新2:parreduce: 这里的问题是将结果Map值与初始Map值区分开来。我选择了contains("id")

list.par.reduce((a, b) => {
  def toResultMap(m: Map[String, Any]) =
    if (m.contains("id"))
      Map(m("id").toString -> m("value")).withDefaultValue(0)
    else m
  val aM = toResultMap(a)
  val bM = toResultMap(b)
  aM.foldLeft(bM)((res, v) =>
    res + (v._1 -> (res(v._1).asInstanceOf[Int] + v._2.asInstanceOf[Int])))
})

答案 2 :(得分:3)

我不知道&#34;效率最高&#34;,但我能想到的最好的方法是使用scalaz suml,它使用Monoid; Monoid的{​​{1}}完全符合您的要求。唯一令人遗憾的部分是将Map转换为更好的类型并表示我们想要的结构(例如Map[String, Any])。

Map("A" → 20)

答案 3 :(得分:0)

Scala 2.13开始,您可以使用groupMapReduce方法,该方法(如其名称所示)等效于groupBy后跟mapValues和{{1} }步骤:

reduce

此:

  • // val list = List(Map("id" -> "A", "value" -> 20, "name" -> "a"), Map("id" -> "B", "value" -> 10, "name" -> "b"), Map("id" -> "A", "value" -> 5, "name" -> "a"), Map("id" -> "C", "value" -> 1, "name" -> "c"), Map("id" -> "D", "value" -> 60, "name" -> "d"), Map("id" -> "C", "value" -> 3, "name" -> "c")) list.groupMapReduce(_("id"))(_("value").asInstanceOf[Int])(_ + _) // Map("A" -> 25, "B" -> 10, "C" -> 4, "D" -> 60) 的“ id”字段(group)的地图( MapReduce的组部分)

  • _("id")将每个分组的Map映射到其“值”字段,然后键入Int(map)( Map Reduce组的映射部分)

  • 通过对每个组(_("value").asInstanceOf[Int])中的值
  • reduce进行求和(减少groupMap Reduce 的一部分)。

这是one-pass version可以翻译的内容:

_ + _