Подтвердить что ты не робот

В Scala, как я могу сделать эквивалент SQL SUM и 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
4b9b3361

Ответ 1

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

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

Может быть, что-то более приятное можно сделать с помощью Scolaz monoid typeclass для Map.

Обратите внимание, что вы можете конвертировать между Map[K, V] и Seq[(K, V)] с помощью методов toSeq и toMap.


Update. Поразмыслив над этим, я думаю, что естественная абстракция будет "многомассовым" преобразованием типа

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

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

s.toMultimap.mapValues(_.sum)

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

Ответ 2

Существует еще одна возможность использования 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 здесь, для полной реализации, см. Мой пост в Суть шаблона итератора:

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

Ответ 3

Я использовал этот шаблон s.groupBy(_._1).mapValues(_.map(_._2).sum) от Kipton все время. Это очень хорошо отражает мой мыслительный процесс, но, к сожалению, не всегда легко читать. Я обнаружил, что использование класса 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)

Я думаю, что тогда это более читаемо, чем складка или версия.

Ответ 4

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)

Ответ 5

Начиная с Scala 2.13, вы можете использовать метод K)(f:A=>B)(reduce:(B,B)=>B):scala.collection.immutable.Map[K,B] rel="nofollow noreferrer"> 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 каждый сгруппированный кортеж со своей второй частью (_._2) (отобразить часть группы Map Reduce)

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

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

l.groupBy(_._1).mapValues(_.map(_._2).reduce(_ + _)).toList