Python: fusionner élégamment les dictionnaires avec sum() de valeurs [dupliquer]
Cette question a déjà une réponse ici:
J'essaie de fusionner les journaux de plusieurs serveurs. Chaque journal est une liste de tuples (date
, count
). date
peut apparaître plus d'une fois, et je veux que le dictionnaire résultant contienne la somme de tous compte de tous les serveurs.
Voici ma tentative, avec quelques données par exemple:
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)
Qui donne:
{'14.5': 100, '16.5': 100, '13.5': 100, '15.5': 200}
Comme prévu.
Je suis sur le point de devenir folle à cause d'un collègue qui a vu le code. Elle insiste sur le fait qu'il doit y avoir une façon plus pythonique et élégante de le faire, sans ces boucles imbriquées. Des idées?
4 réponses
Ne devient pas plus simple que cela, je pense:
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())
Notez que Counter
(Également connu sous le nom de multiset) est la structure de données la plus naturelle pour vos données (un type d'ensemble auquel les éléments peuvent appartenir plus d'une fois, ou de manière équivalente - une carte avec élément sémantique -> OccurrenceCount. Vous auriez pu l'utiliser en premier lieu, au lieu de listes de tuples.
Aussi possible:
from collections import Counter
from operator import add
print reduce(add, (Counter(dict(x)) for x in input))
Utiliser reduce(add, seq)
au lieu de sum(seq, initialValue)
est généralement plus souple et vous permet de passer passage de la valeur initiale redondante.
Notez que vous pouvez également utiliser operator.and_
pour trouver l'intersection des multisets au lieu de la somme.
La variante ci-dessus est terriblement lente, car un nouveau compteur est créé à chaque étape. Nous allons remédier à cela.
Nous savons que Counter+Counter
renvoie un nouveau Counter
avec des données fusionnées. C'est OK, mais nous voulons éviter la création supplémentaire. Utilisons Counter.update
à la place:
Mise à Jour(auto, itérable=None, **kwds) indépendant des collections.Compteur méthode
Comme dict.update () mais ajouter des comptes au lieu de les remplacer. Source peut être un itérable, un dictionnaire ou une autre instance de compteur.
C'est ce que nous voulons. Enveloppons-le avec une fonction compatible avec reduce
et voyons ce qui se passe.
def updateInPlace(a,b):
a.update(b)
return a
print reduce(updateInPlace, (Counter(dict(x)) for x in input))
Ce n'est que légèrement plus lent que la solution de L'OP.
Référence: http://ideone.com/7IzSx (mis à Jour avec encore une autre solution, grâce à Astynax)
(aussi: si vous voulez désespérément un one-liner, vous pouvez remplacer updateInPlace
par lambda x,y: x.update(y) or x
qui fonctionne de la même manière et s'avère même une fraction de seconde plus rapide, mais échoue à la lisibilité. Non :-))
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)
Sortie:
Counter({'15.5': 200, '14.5': 100, '16.5': 100, '13.5': 100})
Modifier: Comme duncan suggéré, vous pouvez remplacer ces 3 lignes avec une seule ligne:
count = Counter()
for y in inp:
count += Counter(y)
Remplacer par : count = sum((Counter(y) for y in inp), Counter())
Vous pouvez utiliser 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
L'utilisation de groupby
au lieu de deux boucles et d'un {[2] } rendra votre code plus clair.
Vous pouvez utiliser le Compteur ou defaultdict, ou vous pouvez essayer ma 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}
, Ou encore plus générique:
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']}
Vous pouvez également sous-classer le dict
et implémenter une méthode __add__
: