Вопрос по python, dictionary – Python: элегантно объединить словари с sum () значений [duplicate]

28

This question already has an answer here:

Is there any pythonic way to combine two dicts (adding values for keys that appear in both)? 18 answers

Я пытаюсь объединить журналы с нескольких серверов. Каждый журнал представляет собой список кортежей (date, count). date может появиться более одного раза, и я хочу, чтобы результирующий словарь содержал сумму всех подсчетов со всех серверов.

Вот моя попытка с некоторыми данными, например:

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)

Который дает:

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

Как и ожидалось.

I'm about to go bananas because of a colleague who saw the code. She insists that there must be a more Pythonic and elegant way to do it, without these nested for loops. Any ideas?

@AshwiniChaudhary:Counter() учитывает только случаи, и поскольку значения уже заполнены, он не будет работать для этого сценария. Christian Witts
@AshwiniChaudhary: Вы узнаете что-то новое каждый день :) Christian Witts
То, что мне кажется непитонным, это тратить время на беспокойство о том, чтобы сделать совершенно понятный код более питоническим. Pythonicness, который нуждается в часах мысли, не является истинной Pythonicness. DSM
@ChristianWitts смотрите мое решение ниже. Ashwini Chaudhary
использованиеCounter() Ashwini Chaudhary

Ваш Ответ

4   ответа
32

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

Обратите внимание, чтоCounter (также известный как мультимножество) - наиболее естественная структура данных для ваших данных (тип набора, к которому элементы могут принадлежать более одного раза, или, что то же самое, карта с семантикой Element -> OccurrenceCount. Вы могли использовать ее в первое место вместо списков кортежей.

Также возможно:

from collections import Counter
from operator import add

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

С помощьюreduce(add, seq) вместоsum(seq, initialValue) как правило, более гибкий и позволяет пропустить передачу избыточного начального значения.

Обратите внимание, что вы также можете использоватьoperator.and_ чтобы найти пересечение мультимножеств вместо суммы.

Вышеупомянутый вариант ужасно медленный, потому что новый счетчик создается на каждом шаге. Давайте исправим это.

Мы знаем этоCounter+Counter возвращает новыйCounter с объединенными данными. Это нормально, но мы хотим избежать дополнительного творчества. Давайте использоватьCounter.update вместо:

update(self, iterable=None, **kwds) unbound collections.Counter method

Like dict.update() but add counts instead of replacing them. Source can be an iterable, a dictionary, or another Counter instance.

Это то, что мы хотим. Обернем его функцией, совместимой сreduce и посмотрим, что получится.

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

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

Это лишь незначительно медленнее, чем решение OP.

Benchmark: http://ideone.com/7IzSx (Updated with yet another solution, thanks to astynax)

(Also: If you desperately want an one-liner, you can replace updateInPlace by lambda x,y: x.update(y) or x which works the same way and even proves to be a split second faster, but fails at readability. Don't :-))

А как насчет сложности времени? Является ли это более эффективным, чем код OP?
Этот подход действительно является самым медленным из представленных здесь, причем оригинальное решение OP является самым быстрым. Здесь нет сюрпризов.ideone.com/HAmvi
@ Кос, я немного изменилbenchmark, Самый быстрый способ:defaultdict + chain (и мой переписалmerge_with в середине) :)
+1 Мне очень нравится это решение.
Я так не думаю. Код OP не создает никаких непосредственных объектов, поэтому он, как правило, должен быть более эффективным.
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)

output:

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

Edit: КакDuncan Предполагается, что вы можете заменить эти 3 строки одной строкой:

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

заменить на :count = sum((Counter(y) for y in inp), Counter())

Вы могли бы даже удалитьfor цикл с помощьюsum: count = sum((Counter(y) for y in inp), Counter())
@ Дункан, спасибо, я никогда не знал, предложение реализовано.
1

или вы можете попробовать мой вариант:

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}

Или даже более общий:

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']}

Также вы можете подклассdict и реализовать__add__ метод:

Спасибо! Хотя это кажется немного менее понятным, чем оригинальный код. Adam Matan
7

Вы можете использовать itertools & apos;группа по:

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

Использованиеgroupby вместо двух петель иdefaultdict сделает ваш код более понятным.

@ Emmanuel Thanx за указание на это. Починил это.
вместо лямбды вы также можете оставитьoperator.itemgetter(0) :)
Неправильно:groupbyКак сказано в документе, который вы упомянули, сначала нужно выполнить сортировку! Вот это работает, потому чтоb[1] а такжеc[0] будет последовательно вchain(a,b,c) но если вы делаетеchain(a,c,b) вместо этого результат неверен (вы получаете 100 вместо 200 дляoutput['15.5'])...
Добро пожаловать, приятно подуматьgroupby тем не мение !
Я предполагаю его личный вкус, но я считаю, что это труднее читать, чем defaultdict, а также медленнее, чем подход OP

Похожие вопросы