2012-07-02 9 views
26

Estoy intentando fusionar registros de varios servidores. Cada registro es una lista de tuplas (date, count). date puede aparecer más de una vez, y quiero que el diccionario resultante contenga la suma de todos los recuentos de todos los servidores.Python: combine elegantemente diccionarios con suma() de valores

Aquí está mi intento, con algunos datos como por ejemplo:

from collections import defaultdict 

a=[("13.5",100)] 
b=[("14.5",100), ("15.5", 100)] 
c=[("15.5",100), ("16.5", 100)] 
input=[a,b,c] 

output=defaultdict(int) 
for d in input: 
     for item in d: 
      output[item[0]]+=item[1] 
print dict(output) 

Lo que da:

{'14.5': 100, '16.5': 100, '13.5': 100, '15.5': 200} 

como se esperaba.

Estoy a punto de ir a los plátanos debido a un colega que vio el código. Ella insiste en que debe haber una forma más elegante y pitonítica de hacerlo, sin estos bucles anidados. ¿Algunas ideas?

+4

uso 'Contador()' –

+2

@AshwiniChaudhary: 'Contador()' solamente cuenta las ocurrencias, y como los valores ya están precompuestos, no funcionará para este escenario. –

+0

@ChristianWitts vea mi solución a continuación. –

Respuesta

30

no recibe más sencillo que esto, pienso:

a=[("13.5",100)] 
b=[("14.5",100), ("15.5", 100)] 
c=[("15.5",100), ("16.5", 100)] 
input=[a,b,c] 

from collections import Counter 

print sum(
    (Counter(dict(x)) for x in input), 
    Counter()) 

Tenga en cuenta que Counter (también conocido como un conjunto múltiple) es la estructura de datos más natural para sus datos (un tipo de conjunto de los elementos que pueden pertenecer más de una vez, o equivalentemente, un mapa con semántica Elemento -> OccurrenceCount. Podrías haberlo usado en primer lugar, en lugar de listas de tuplas.


También es posible:

from collections import Counter 
from operator import add 

print reduce(add, (Counter(dict(x)) for x in input)) 

usando reduce(add, seq) en lugar de sum(seq, initialValue) es generalmente más flexible y permite que se salte pasando el valor inicial redundante.

Tenga en cuenta que también puede usar operator.and_ para encontrar la intersección de los multisectos en lugar de la suma.


La variante anterior es terriblemente lenta, porque se crea un nuevo contador en cada paso. Arreglemos eso.

Sabemos que Counter+Counter devuelve un nuevo Counter con datos combinados.Esto está bien, pero queremos evitar la creación adicional. Vamos a usar en lugar Counter.update:

actualización (uno mismo, iterables = ninguno, ** kwds) Método collections.Counter no unido

Como dict.update() además de añadir los recuentos en lugar de sustituirlos. La fuente puede ser un iterable, un diccionario u otra instancia de contador.

Eso es lo que queremos. Vamos a envolverlo con una función compatible con reduce y ver qué pasa.

def updateInPlace(a,b): 
    a.update(b) 
    return a 

print reduce(updateInPlace, (Counter(dict(x)) for x in input)) 

Esto es solo un poco más lento que la solución del OP.

Benchmark: http://ideone.com/7IzSx(Actualizado con otra solución, gracias a astynax)

(también: Si desea desesperadamente una sola línea, puede reemplazar updateInPlace por lambda x,y: x.update(y) or x que trabaja el De la misma manera, e incluso resulta ser una fracción de segundo más rápido, pero no puede leerse. No :-))

+2

+1 Me gusta mucho esta solución. – sloth

+1

¿Qué pasa con la complejidad del tiempo? Es más eficiente que el código OP – jerrymouse

+0

No lo creo. El código OP no crea ningún objeto inmediato, por lo que generalmente debería ser más eficiente. – Kos

7

Usted podría utilizar itertools' groupby:

from itertools import groupby, chain 

a=[("13.5",100)] 
b=[("14.5",100), ("15.5", 100)] 
c=[("15.5",100), ("16.5", 100)] 
input = sorted(chain(a,b,c), key=lambda x: x[0]) 

output = {} 
for k, g in groupby(input, key=lambda x: x[0]): 
    output[k] = sum(x[1] for x in g) 

print output 

El uso de groupby en lugar de dos bucles y una defaultdict hará que su código más claro.

+2

en lugar de la lambda, también puede colocar un' operator.itemgetter (0) ':) – Kos

+1

Incorrecto:' groupby', como se dijo en ¡El documento que mencionas necesita ordenar primero! Aquí esto funciona porque 'b [1]' y 'c [0]' serán consecutivos en 'chain (a, b, c)' pero si lo haces 'chain (a, c, b)' en su lugar, el resultado es no correcto (obtiene 100 en lugar de 200 para 'salida ['15 .5 ']') ... – Emmanuel

+1

Supongo que es un gusto personal, pero me parece más difícil de leer que el default, y también es más lento que el enfoque OP – fraxel

8
from collections import Counter 


a = [("13.5",100)] 
b = [("14.5",100), ("15.5", 100)] 
c = [("15.5",100), ("16.5", 100)] 

inp = [dict(x) for x in (a,b,c)] 
count = Counter() 
for y in inp: 
    count += Counter(y) 
print(count) 

de salida:

Counter({'15.5': 200, '14.5': 100, '16.5': 100, '13.5': 100}) 

Editar: Como duncan sugirieron puede reemplazar estas 3 líneas con una sola línea:

count = Counter() 
    for y in inp: 
     count += Counter(y) 

sustituir por: count = sum((Counter(y) for y in inp), Counter())

+2

Incluso podría eliminar el bucle 'for' usando' suma': 'count = sum ((Contador (y) para y en inp), Contador())' – Duncan

+0

@Duncan gracias, nunca lo supe, sugerencia implementada . –

1

Puede usar Contador o default, o puede probar mi variante:

def merge_with(d1, d2, fn=lambda x, y: x + y): 
    res = d1.copy() # "= dict(d1)" for lists of tuples 
    for key, val in d2.iteritems(): # ".. in d2" for lists of tuples 
     try: 
      res[key] = fn(res[key], val) 
     except KeyError: 
      res[key] = val 
    return res 

>>> merge_with({'a':1, 'b':2}, {'a':3, 'c':4}) 
{'a': 4, 'c': 4, 'b': 2} 

O incluso más genérico:

def make_merger(fappend=lambda x, y: x + y, fempty=lambda x: x): 
    def inner(*dicts): 
     res = dict((k, fempty(v)) for k, v 
      in dicts[0].iteritems()) # ".. in dicts[0]" for lists of tuples 
     for dic in dicts[1:]: 
      for key, val in dic.iteritems(): # ".. in dic" for lists of tuples 
       try: 
        res[key] = fappend(res[key], val) 
       except KeyError: 
        res[key] = fempty(val) 
     return res 
    return inner 

>>> make_merger()({'a':1, 'b':2}, {'a':3, 'c':4}) 
{'a': 4, 'c': 4, 'b': 2} 

>>> appender = make_merger(lambda x, y: x + [y], lambda x: [x]) 
>>> appender({'a':1, 'b':2}, {'a':3, 'c':4}, {'b':'BBB', 'c':'CCC'}) 
{'a': [1, 3], 'c': [4, 'CCC'], 'b': [2, 'BBB']} 

también le puede subclase el dict y poner en práctica un método __add__:

+1

¡Gracias! Sin embargo, parece un poco menos claro que el código original. –

Cuestiones relacionadas