¿La mejor manera de fusionar dos mapas y sumar los valores de la misma clave?


179
val map1 = Map(1 -> 9 , 2 -> 20)
val map2 = Map(1 -> 100, 3 -> 300)

Quiero fusionarlos y sumar los valores de las mismas claves. Entonces el resultado será:

Map(2->20, 1->109, 3->300)

Ahora tengo 2 soluciones:

val list = map1.toList ++ map2.toList
val merged = list.groupBy ( _._1) .map { case (k,v) => k -> v.map(_._2).sum }

y

val merged = (map1 /: map2) { case (map, (k,v)) =>
    map + ( k -> (v + map.getOrElse(k, 0)) )
}

Pero quiero saber si hay mejores soluciones.


Lo más fácil esmap1 ++ map2
Seraf

3
@Seraf Eso simplemente "fusiona" los mapas, ignorando los duplicados en lugar de sumar sus valores.
Zeynep Akkalyoncu Yilmaz

@ZeynepAkkalyoncuYilmaz a la derecha debería haber leído mejor la pregunta, se va avergonzado
Seraf

Respuestas:


143

Scalaz tiene el concepto de un Semigrupo que captura lo que quiere hacer aquí y conduce a la solución más corta / más limpia:

scala> import scalaz._
import scalaz._

scala> import Scalaz._
import Scalaz._

scala> val map1 = Map(1 -> 9 , 2 -> 20)
map1: scala.collection.immutable.Map[Int,Int] = Map(1 -> 9, 2 -> 20)

scala> val map2 = Map(1 -> 100, 3 -> 300)
map2: scala.collection.immutable.Map[Int,Int] = Map(1 -> 100, 3 -> 300)

scala> map1 |+| map2
res2: scala.collection.immutable.Map[Int,Int] = Map(1 -> 109, 3 -> 300, 2 -> 20)

Específicamente, el operador binario para Map[K, V]combina las claves de los mapas, doblando Vel operador del semigrupo sobre cualquier valor duplicado. El semigrupo estándar para Intutiliza el operador de suma, por lo que obtiene la suma de valores para cada clave duplicada.

Editar : Un poco más de detalle, según la solicitud del usuario 482745.

Matemáticamente, un semigrupo es solo un conjunto de valores, junto con un operador que toma dos valores de ese conjunto y produce otro valor a partir de ese conjunto. Entonces, los enteros bajo suma son un semigrupo, por ejemplo: el +operador combina dos entradas para hacer otro int.

También puede definir un semigrupo sobre el conjunto de "todos los mapas con un tipo de clave y un tipo de valor dados", siempre que pueda encontrar alguna operación que combine dos mapas para producir uno nuevo que de alguna manera sea la combinación de los dos entradas

Si no hay claves que aparecen en ambos mapas, esto es trivial. Si existe la misma clave en ambos mapas, entonces necesitamos combinar los dos valores a los que se asigna la clave. Hmm, ¿no acabamos de describir un operador que combina dos entidades del mismo tipo? Es por eso que en Scalaz Map[K, V]existe un semigrupo para si y solo si Vexiste un semigrupo para - Vel semigrupo se usa para combinar los valores de dos mapas que se asignan a la misma clave.

Entonces, como Intes el tipo de valor aquí, la "colisión" en la 1clave se resuelve mediante la suma entera de los dos valores mapeados (ya que eso es lo que hace el operador de semigrupo de Int), por lo tanto 100 + 9. Si los valores hubieran sido Strings, una colisión hubiera resultado en la concatenación de los dos valores mapeados (de nuevo, porque eso es lo que hace el operador de semigrupo para String).

(Y curiosamente, debido a que la concatenación de cadenas no es conmutativa, es decir, "a" + "b" != "b" + "a"la operación de semigrupo resultante tampoco lo map1 |+| map2es . Por lo tanto, es diferente de map2 |+| map1en el caso de la cadena, pero no en el caso de Int.)


37
¡Brillante! Primer ejemplo práctico donde scalaztenía sentido.
soc

55
¡En serio! Si empiezas a buscarlo ... está por todas partes. Para citar al autor erric torrebone de especificaciones y especificaciones2: "Primero aprendes Opción y comienzas a verla en todas partes. Luego aprendes Aplicativo y es lo mismo. ¿Siguiente?" Los siguientes son conceptos aún más funcionales. Y eso te ayuda enormemente a estructurar tu código y resolver problemas de manera agradable.
AndreasScheinert

44
En realidad, había estado buscando Opción durante cinco años cuando finalmente encontré Scala. La diferencia entre una referencia de objeto Java que podría ser nula y una que no puede ser (es decir, entre Ay Option[A]) es tan grande que no podía creer que fueran realmente del mismo tipo. Yo simplemente empecé a buscar en Scalaz. No estoy seguro de ser lo suficientemente inteligente ...
Malvolio

1
También hay una opción para Java, consulte Java funcional. No tengas miedo, aprender es divertido. Y la programación funcional no le enseña cosas nuevas (solo), sino que le ofrece la ayuda del programador para proporcionar términos y vocabulario para abordar problemas. La pregunta OP es un ejemplo perfecto. El concepto de un Semigrupo es tan simple que lo utiliza todos los días, por ejemplo, para Strings. El poder real aparece si identificas esta abstracción, la nombras y finalmente la aplicas a otros tipos y luego a String.
AndreasScheinert

1
¿Cómo es posible que resulte en 1 -> (100 + 9)? ¿Me puede mostrar "rastro de pila"? Gracias. PD: estoy pidiendo aquí para que la respuesta sea más clara.
user482745

152

La respuesta más corta que conozco que usa solo la biblioteca estándar es

map1 ++ map2.map{ case (k,v) => k -> (v + map1.getOrElse(k,0)) }

34
Buena solución Me gusta agregar la pista, que ++reemplaza cualquier (k, v) del mapa en el lado izquierdo de ++(aquí mapa1) por (k, v) del mapa del lado derecho, si (k, _) ya existe en el lado izquierdo mapa lateral (aquí mapa1), por ejemploMap(1->1) ++ Map(1->2) results in Map(1->2)
Lutz

Un tipo de versión más ordenada: para ((k, v) <- (aa ++ bb)) rendir k -> (if ((aa contiene k) && (bb contiene k)) aa (k) + v else v)
dividebyzero

Hice algo diferente anteriormente, pero aquí hay una versión de lo que hiciste, reemplazando el mapa por un formapa1 ++ (para ((k, v) <- map2) produce k -> (v + map1.getOrElse (k, 0 )))
dividebyzero

1
@ Jus12 - No. .tiene mayor prioridad que ++; lees map1 ++ map2.map{...}como map1 ++ (map2 map {...}). Entonces, una forma de mapear map1los elementos y la otra no.
Rex Kerr

1
@matt: Scalaz ya lo hará, así que diría que "una biblioteca existente ya lo hace".
Rex Kerr

48

Solución rápida:

(map1.keySet ++ map2.keySet).map {i=> (i,map1.getOrElse(i,0) + map2.getOrElse(i,0))}.toMap

41

Bueno, ahora en la biblioteca scala (al menos en 2.10) hay algo que quería: la función fusionada . PERO solo se presenta en HashMap, no en Map. Es algo confuso. Además, la firma es engorrosa: no puedo imaginar por qué necesitaría una clave dos veces y cuándo necesitaría producir un par con otra clave. Sin embargo, funciona y es mucho más limpio que las soluciones "nativas" anteriores.

val map1 = collection.immutable.HashMap(1 -> 11 , 2 -> 12)
val map2 = collection.immutable.HashMap(1 -> 11 , 2 -> 12)
map1.merged(map2)({ case ((k,v1),(_,v2)) => (k,v1+v2) })

También en scaladoc mencionó que

El mergedmétodo es, en promedio, más eficaz que hacer un recorrido y reconstruir un nuevo mapa hash inmutable desde cero, o ++.


1
A partir de ahora, solo está en Hashmap inmutable, no en Hashmap mutable.
Kevin Wheeler

2
Esto es bastante molesto porque solo tienen eso para que HashMaps sea sincero.
Johan S

No puedo hacer que esto se compile, parece que el tipo que acepta es privado, por lo que no puedo pasar una función escrita que coincida.
Ryan The Leach

2
Parece que algo cambió en la versión 2.11. Echa un vistazo a 2.10 scaladoc - scala-lang.org/api/2.10.1/… Hay una función habitual. Pero en 2.11 es MergeFunction.
Mikhail Golubtsov

Todo lo que ha cambiado en 2.11 es la introducción de un alias de tipo para este tipo de función en particularprivate type MergeFunction[A1, B1] = ((A1, B1), (A1, B1)) => (A1, B1)
EthanP

14

Esto se puede implementar como un monoide con Scala simple. Aquí hay una implementación de muestra. Con este enfoque, podemos fusionar no solo 2, sino una lista de mapas.

// Monoid trait

trait Monoid[M] {
  def zero: M
  def op(a: M, b: M): M
}

La implementación basada en mapas del rasgo monoide que combina dos mapas.

val mapMonoid = new Monoid[Map[Int, Int]] {
  override def zero: Map[Int, Int] = Map()

  override def op(a: Map[Int, Int], b: Map[Int, Int]): Map[Int, Int] =
    (a.keySet ++ b.keySet) map { k => 
      (k, a.getOrElse(k, 0) + b.getOrElse(k, 0))
    } toMap
}

Ahora, si tiene una lista de mapas que necesita fusionar (en este caso, solo 2), puede hacerlo como a continuación.

val map1 = Map(1 -> 9 , 2 -> 20)
val map2 = Map(1 -> 100, 3 -> 300)

val maps = List(map1, map2) // The list can have more maps.

val merged = maps.foldLeft(mapMonoid.zero)(mapMonoid.op)

5
map1 ++ ( for ( (k,v) <- map2 ) yield ( k -> ( v + map1.getOrElse(k,0) ) ) )

5

Escribí una publicación de blog sobre esto, échale un vistazo:

http://www.nimrodstech.com/scala-map-merge/

Básicamente, usando Scalaz semi group puedes lograr esto fácilmente

se vería algo así como:

  import scalaz.Scalaz._
  map1 |+| map2

11
Necesita poner un poco más de detalle en su respuesta, preferiblemente algún código de implementación. Haga esto también para las otras respuestas similares que haya publicado y adapte cada respuesta a la pregunta específica que se le hizo. Regla general: el autor de la pregunta debería poder beneficiarse de su respuesta sin hacer clic en el enlace del blog.
Robert Harvey

5

También puedes hacer eso con los gatos .

import cats.implicits._

val map1 = Map(1 -> 9 , 2 -> 20)
val map2 = Map(1 -> 100, 3 -> 300)

map1 combine map2 // Map(2 -> 20, 1 -> 109, 3 -> 300)

Eek, import cats.implicits._. Importar import cats.instances.map._ import cats.instances.int._ import cats.syntax.semigroup._no mucho más detallado ...
St.Antario

@ St.Antario en realidad es una forma recomendada de tener soloimport cats.implicits._
Artsiom Miklushou

¿Recomendado por quién? Traer todas las instancias implícitas (la mayoría de las cuales no se utilizan) al alcance complica la vida del compilador. Y además, si no se necesita, por ejemplo, instancia aplicativa, ¿por qué lo traerían allí?
St.Antario

4

Comenzando Scala 2.13, otra solución basada únicamente en la biblioteca estándar consiste en reemplazar la groupByparte de su solución con la groupMapReduceque (como su nombre lo indica) es un equivalente de un paso groupByseguido por mapValuesun paso reducido:

// val map1 = Map(1 -> 9, 2 -> 20)
// val map2 = Map(1 -> 100, 3 -> 300)
(map1.toSeq ++ map2).groupMapReduce(_._1)(_._2)(_+_)
// Map[Int,Int] = Map(2 -> 20, 1 -> 109, 3 -> 300)

Esta:

  • Concatena los dos mapas como una secuencia de tuplas ( List((1,9), (2,20), (1,100), (3,300))). Para ser concisos, map2se convierte implícitamenteSeq para adaptarse al tipo de map1.toSeq, pero puede optar por hacerlo explícito utilizando map2.toSeq,

  • groups elementos basados ​​en su primera parte de tupla (parte del grupo del grupo MapReduce),

  • maps valores agrupados a su segunda parte de tupla (parte del mapa del grupo Map Map Reduce),

  • reduces valores asignados ( _+_) al sumarlos (reducir parte de groupMap Reduce ).


3

Esto es lo que terminé usando:

(a.toSeq ++ b.toSeq).groupBy(_._1).mapValues(_.map(_._2).sum)

1
Eso realmente no es significativamente diferente de la primera solución propuesta por el OP.
jwvh

2

La respuesta de Andrzej Doyle contiene una gran explicación de los semigrupos que le permite utilizar el |+|operador para unir dos mapas y sumar los valores de las claves coincidentes.

Hay muchas formas en que algo puede definirse como una instancia de una clase de tipos, y a diferencia del OP, es posible que no desee sumar sus claves específicamente. O bien, es posible que desee operar en una unión en lugar de una intersección. Scalaz también agrega funciones adicionales Mappara este propósito:

https://oss.sonatype.org/service/local/repositories/snapshots/archive/org/scalaz/scalaz_2.11/7.3.0-SNAPSHOT/scalaz_2.11-7.3.0-SNAPSHOT-javadoc.jar/!/ index.html # scalaz.std.MapFunctions

Tu puedes hacer

import scalaz.Scalaz._

map1 |+| map2 // As per other answers
map1.intersectWith(map2)(_ + _) // Do things other than sum the values

2

La forma más rápida y sencilla:

val m1 = Map(1 -> 1.0, 3 -> 3.0, 5 -> 5.2)
val m2 = Map(0 -> 10.0, 3 -> 3.0)
val merged = (m2 foldLeft m1) (
  (acc, v) => acc + (v._1 -> (v._2 + acc.getOrElse(v._1, 0.0)))
)

De esta manera, cada uno de los elementos se agrega inmediatamente al mapa.

La segunda ++forma es:

map1 ++ map2.map { case (k,v) => k -> (v + map1.getOrElse(k,0)) }

A diferencia de la primera forma, en una segunda forma para cada elemento en un segundo mapa, se creará una nueva Lista y se concatenará con el mapa anterior.

La caseexpresión crea implícitamente una nueva Lista usando el unapplymétodo.


1

Esto es lo que se me ocurrió ...

def mergeMap(m1: Map[Char, Int],  m2: Map[Char, Int]): Map[Char, Int] = {
   var map : Map[Char, Int] = Map[Char, Int]() ++ m1
   for(p <- m2) {
      map = map + (p._1 -> (p._2 + map.getOrElse(p._1,0)))
   }
   map
}

1

Usando el patrón typeclass, podemos fusionar cualquier tipo numérico:

object MapSyntax {
  implicit class MapOps[A, B](a: Map[A, B]) {
    def plus(b: Map[A, B])(implicit num: Numeric[B]): Map[A, B] = {
      b ++ a.map { case (key, value) => key -> num.plus(value, b.getOrElse(key, num.zero)) }
    }
  }
}

Uso:

import MapSyntax.MapOps

map1 plus map2

Fusionar una secuencia de mapas:

maps.reduce(_ plus _)

0

Tengo una pequeña función para hacer el trabajo, está en mi pequeña biblioteca para algunas funciones de uso frecuente que no están en la biblioteca estándar. Debería funcionar para todo tipo de mapas, mutables e inmutables, no solo HashMaps

Aquí está el uso

scala> import com.daodecode.scalax.collection.extensions._
scala> val merged = Map("1" -> 1, "2" -> 2).mergedWith(Map("1" -> 1, "2" -> 2))(_ + _)
merged: scala.collection.immutable.Map[String,Int] = Map(1 -> 2, 2 -> 4)

https://github.com/jozic/scalax-collection/blob/master/README.md#mergedwith

Y aquí está el cuerpo.

def mergedWith(another: Map[K, V])(f: (V, V) => V): Repr =
  if (another.isEmpty) mapLike.asInstanceOf[Repr]
  else {
    val mapBuilder = new mutable.MapBuilder[K, V, Repr](mapLike.asInstanceOf[Repr])
    another.foreach { case (k, v) =>
      mapLike.get(k) match {
        case Some(ev) => mapBuilder += k -> f(ev, v)
        case _ => mapBuilder += k -> v
      }
    }
    mapBuilder.result()
  }

https://github.com/jozic/scalax-collection/blob/master/src%2Fmain%2Fscala%2Fcom%2Fdaodecode%2Fscalax%2Fcollection%2Fextensions%2Fpackage.scala#L190

Al usar nuestro sitio, usted reconoce que ha leído y comprende nuestra Política de Cookies y Política de Privacidad.
Licensed under cc by-sa 3.0 with attribution required.