在Scala中,我如何才能完成SQL SUM和GROUP BY的等效操作?

时间:2011-08-22 02:06:46

标签: scala

例如,假设我有

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

5 个答案:

答案 0 :(得分:28)

这是一个单行。它不是特别易读,除非真正内化这些高阶函数的类型。

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)

另一种方法是使用fold,

逐个添加键值对
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类型类可能可以做得更好。

请注意,您可以使用Map[K, V]Seq[(K, V)]方法在toSeqtoMap之间进行转换。


更新。在思考了一些之后,我认为自然抽象将是一种类型的“多图”转换,

def seqToMultimap[A, B](s: Seq[(A, B)]): Map[A, Seq[B]]

在个人图书馆中使用适当的隐式扩展,然后可以写:

s.toMultimap.mapValues(_.sum)

在我看来,这是最清楚的!

答案 1 :(得分:14)

使用Scalaz还有另一种可能性。

关键是要注意,如果MMonoid,那么Map[T, M]也是Monoid。这意味着如果我有2个地图,m1m2我可以添加它们,以便对于每个相似的键,元素将被添加到一起。

例如,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,因为Monoid类型有Int个实例。让我们导入它:

  implicit val mapMonoid = MapMonoid[String, Int]

然后,我需要一个函数reduceMonoid,它将Traversable和“{”添加“其元素”Monoid。我只是在这里写下reduceMonoid定义,为了完整实施,请参阅Essence of the Iterator Pattern上的帖子:

  // T is a "Traversable"
  def reduce[A, M : Monoid](reducer: A => M): T[A] => M

这两个定义在当前的Scalaz库中不存在,但它们并不难添加(基于现有的MonoidTraverse类型类)。一旦我们拥有它们,您问题的解决方案就非常简单:

  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)

如果你觉得上面的代码有点模糊(但真的很简洁,对吧?),我建议你查看github project for the EIP post并使用它。 One example显示了您问题的解决方案:

   "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)
   }

答案 2 :(得分:9)

我一直使用Kipton的答案中的那种模式s.groupBy(_._1).mapValues(_.map(_._2).sum)。它直接翻译我的思考过程,但遗憾的是并不总是很容易阅读。我发现尽可能使用案例类会让事情变得更好:

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)

我认为它比 fold 版本更具可读性。

答案 3 :(得分:3)

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)

答案 4 :(得分:0)

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

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)( MapReduce的组部分)

  • _._1将每个分组的元组复制到第二部分(map)(映射组 Map Reduce)

  • 通过对每个组(_._2)中的值
  • reduce进行求和(减少groupMap Reduce 的一部分)。

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

_ + _