[英]In Scala, how can I do the equivalent of an SQL SUM and GROUP BY?
例如,假设我有
val list: List[(String, Double)]
有价值的
"04-03-1985", 1.5
"05-03-1985", 2.4
"05-03-1985", 1.3
我怎么能产生一个新的清单
"04-03-1985", 1.5
"05-03-1985", 3.7
这是一个单行。 它不是特别易读,除非真正内化这些高阶函数的类型。
val s = Seq(("04-03-1985" -> 1.5),
("05-03-1985" -> 2.4),
("05-03-1985" -> 1.3))
s.groupBy(_._1).mapValues(_.map(_._2).sum)
// returns: Map(04-03-1985 -> 1.5, 05-03-1985 -> 3.7)
另一种方法是使用折叠逐个添加键值对,
s.foldLeft(Map[String, Double]()) { case (m, (k, v)) =>
m + (k -> (v + m.getOrElse(k, 0d)))
}
在我看来,相当于理解是最容易获得的,
var m = Map[String, Double]()
for ((k, v) <- s) {
m += k -> (v + m.getOrElse(k, 0d))
}
也许用Scalaz的Map的monoid类型类可以做得更好。
请注意,您可以使用toSeq
和toMap
方法在Map[K, V]
和Seq[(K, V)]
进行toMap
。
更新 。 在思考了一些之后,我认为自然抽象将是一种类型的“多图”转换,
def seqToMultimap[A, B](s: Seq[(A, B)]): Map[A, Seq[B]]
通过在个人库中进行适当的隐式扩展,可以编写:
s.toMultimap.mapValues(_.sum)
在我看来,这是最清楚的!
使用Scalaz还有另一种可能性。
关键是要注意,如果M
是Monoid
,那么Map[T, M]
也是Monoid
。 这意味着如果我有2个地图, m1
和m2
我可以添加它们,这样,对于每个相似的键,元素将被添加到一起。
例如, Map[String, List[String]]
是一个Monoid,因为List[String]
是一个Monoid
。 因此,在范围内给出适当的Monoid
实例,我应该能够做到:
val m1 = Map("a" -> List(1), "b" -> List(3))
val m2 = Map("a" -> List(2))
// |+| "adds" two elements of a Monoid together in Scalaz
m1 |+| m2 === Map("a" -> List(1, 2), "b" -> List(3))
对于您的问题,我们可以看到Map[String, Int]
是一个Monoid
因为Int
类型有一个Monoid
实例。 让我们导入它:
implicit val mapMonoid = MapMonoid[String, Int]
然后,我需要一个函数reduceMonoid
,它接受任何Traversable
并用Monoid
“添加”它的元素。 我只是在这里编写reduceMonoid
定义,为了完整实现,请参考我关于迭代器模式的本质的帖子:
// T is a "Traversable"
def reduce[A, M : Monoid](reducer: A => M): T[A] => M
这两个定义在当前的Scalaz库中不存在,但它们并不难添加(基于现有的Monoid
和Traverse
类型类)。 一旦我们拥有它们,您问题的解决方案就非常简单:
val s = Seq(("04-03-1985" -> 1.5),
("05-03-1985" -> 2.4),
("05-03-1985" -> 1.3))
// we just put each pair in its own map and we let the Monoid instance
// "add" the maps together
s.reduceMonoid(Map(_)) === Map("04-03-1985" -> 1.5,
"05-03-1985" -> 3.7)
如果你觉得上面的代码有点模糊(但是真的很简洁,对吧?),我建议你检查一下EIP帖子的github项目并使用它。 一个示例显示了您的问题的解决方案:
"I can build a map String->Int" >> {
val map1 = List("a" -> 1, "a" -> 2, "b" -> 3, "c" -> 4, "b" -> 5)
implicit val mapMonoid = MapMonoid[String, Int]
map1.reduceMonoid(Map(_)) must_== Map("a" -> 3, "b" -> 8, "c" -> 4)
}
我一直使用Kipton的答案中的那个模式s.groupBy(_._1).mapValues(_.map(_._2).sum)
。 它直接翻译我的思考过程,但遗憾的是并不总是很容易阅读。 我发现尽可能使用case类会让事情变得更好:
case class Data(date: String, amount: Double)
val t = s.map(t => (Data.apply _).tupled(t))
// List(Data(04-03-1985,1.5), Data(05-03-1985,2.4), Data(05-03-1985,1.3))
然后变成:
t.groupBy(_.date).mapValues{ group => group.map(_.amount).sum }
// Map(04-03-1985-> 1.5, 05-03-1985 -> 3.7)
我觉得它是那么比折 或版本更具可读性。
val s = List ( "04-03-1985" -> 1.5, "05-03-1985" -> 2.4, "05-03-1985" -> 1.3)
for { (key, xs) <- s.groupBy(_._1)
x = xs.map(_._2).sum
} yield (key, x)
启动Scala 2.13
,您可以使用groupMapReduce
方法(顾名思义)相当于groupBy
后跟mapValues
和reduce
步骤:
// val l = List(("04-03-1985", 1.5), ("05-03-1985", 2.4), ("05-03-1985", 1.3))
l.groupMapReduce(_._1)(_._2)(_ + _).toList
// List(("04-03-1985", 1.5), ("05-03-1985", 3.7))
这个:
group
小号元组可以通过第一部分( _._1
)( 组的MapReduce的基团部分)
map
每个分组的元组map
到它们的第二部分( _._2
)(映射部分Map Reduce)
reduce
每个组(内价值观_ + _
)通过对它们求和(减少groupMap的一部分缩小 )。
这是一个可以翻译的单程版本 :
l.groupBy(_._1).mapValues(_.map(_._2).reduce(_ + _)).toList
声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.