В Scala, как я могу сделать эквивалент SQL SUM и GROUP BY? - PullRequest
18 голосов
/ 22 августа 2011

Например, предположим, у меня есть

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 ]

28 голосов
/ 22 августа 2011

Вот одна строка.Это не особенно читабельно, если только вы действительно не усваиваете типы этих функций более высокого порядка.

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

Может быть, что-то более хорошее можно сделать с помощью класса типов моноидов Скалаза для Map.1012 * и Seq[(K, V)] с использованием методов toSeq и toMap.


Обновление .Подумав еще немного, я думаю, что естественная абстракция будет «многокартовым» преобразованием типа

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

С соответствующим неявным расширением в личной библиотеке можно написать:

s.toMultimap.mapValues(_.sum)

Это самое чистое, на мой взгляд!

14 голосов
/ 24 августа 2011

Существует еще одна возможность использования Scalaz.

Ключевым моментом является то, что если M является Monoid, то Map[T, M] также является Monoid. Это означает, что если у меня есть 2 карты, m1 и m2, я могу добавить их так, чтобы для каждого подобного ключа элементы были добавлены вместе.

Например, Map[String, List[String]] - это моноид, потому что 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

Эти 2 определения не существуют в текущей библиотеке 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)

Если вы чувствуете, что приведенный выше код немного неясен (но действительно лаконичен, верно?), Я рекомендую вам проверить проект github для поста EIP и поиграть с ним. Один пример показывает решение вашего вопроса:

   "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)
   }
9 голосов
/ 22 августа 2011

Я использовал этот шаблон 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)

Я думаю, что тогда он будет более читабельным, чем fold или для версии.

3 голосов
/ 22 августа 2011
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)
0 голосов
/ 08 июня 2019

Начиная с 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 s кортежей по первой части (_._1) (групповая часть group MapReduce)

  • map s каждый сгруппированный кортеж для своей второй части (_._2) (часть карты группы Map Reduce)

  • reduce s значений в каждой группе (_ + _) путем их суммирования (уменьшить часть groupMap Reduce ).

Это однопроходная версия того, что можно перевести как:

l.groupBy(_._1).mapValues(_.map(_._2).reduce(_ + _)).toList
Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...