boltthrower
boltthrower

Reputation: 1250

Combine dicts in a list if they have the same key

I have a dict that looks like this:

{'Item1': [{'Name1': 3}, {'Name2': 4}, {'Name1':7}],
 'Item2': [{'Name7': 44}, {'Name2': 3}, {'Name6':9}, {'Name6':2}]
}

I want to combine dictionaries in the list attached to the key such that if there are multiple dicts with the same key, I can combine them (sum) and leave the others as they are.

The output would look like:

{'Item1': [{'Name1': 10}, {'Name2': 4}],
 'Item2': [{'Name7': 44}, {'Name2': 3}, {'Name6': 11}]
}

I can't figure out how to do this in Python elegantly with a list/dict comprehension.

Upvotes: 0

Views: 695

Answers (4)

Transhuman
Transhuman

Reputation: 3547

Can also try with defaultdict

from itertools import chain
from collections import defaultdict
d_new = {}
for k, v in d.items():
    d_dict = defaultdict(int)
    for k1, v1 in chain(*[ i.items() for i in v ]) :
        d_dict[k1]+= v1
    d_new[k] = dict(d_dict)

print (d_new)

Output:

{'Item1': {'Name1': 10, 'Name2': 4}, 'Item2': {'Name7': 44, 'Name2': 3, 'Name6': 11}}

chain(*[ i.items() for i in v ]) will flatten the list of dicts into list of items

Converts

[{'Name1': 3}, {'Name2': 4}, {'Name1':7}]

to

[('Name1', 3), ('Name2', 4), ('Name1', 7)]

defaultdict(int) is used to add the values of dict with same keys

Upvotes: 0

AChampion
AChampion

Reputation: 30258

Assuming you do want to collapse this into a single dict vs list[dict] then you can do this without any additional modules with a couple of simple for loops:

In []:
r = {}
for k, ds in data.items():
    s = {}
    for d in ds:
        for v, c in d.items():
            s[v] = s.get(v, 0) + c
    r[k] = s
r

Out[]:
{'Item1': {'Name1': 10, 'Name2': 4}, 'Item2': {'Name2': 3, 'Name6': 11, 'Name7': 44}}

As some seem to be going for one liners:

In []:
import itertools as it
from collections import Counter

{k: dict(Counter(v for v, c in it.chain.from_iterable(d.items() for d in ds))
                 for _ in range(c)) for k, ds in data.items()}

Out[]:
{'Item1': {'Name1': 10, 'Name2': 4}, 'Item2': {'Name2': 3, 'Name6': 11, 'Name7': 44}}

Upvotes: 1

Izaak van Dongen
Izaak van Dongen

Reputation: 2545

This uses collections.Counter. It is about the most elegant I could come up with, because of the slightly convoluted structure of your input - a list of 1-length dictionaries would indeed be better implemented as a single dictionary, as the comments suggest. This is also what my code transforms it to, although I have provided some more possible tranformations if you really direly need the old data structure. If you do, I would recommend using tuples as your key-value pairs rather than just single-length dicts, as seen in tuple_output. I recommend you use output or dict_output.

from collections import Counter

d = {'Item1': [{'Name1': 3}, {'Name2': 4}, {'Name1':7}], 'Item2': [{'Name7': 44}, {'Name2': 3}, {'Name6':9}, {'Name6':2}] }

output = {}
for k, v in d.items():
    c = Counter()
    for sub_dict in v:
        c.update(sub_dict)
    output[k] = c

dict_output = {k: dict(v) for k, v in output.items()}
tuple_output = {k: v.most_common() for k, v in output.items()}
dict_list_output = {k: [{a: b} for a, b in v.most_common()] for k, v in output.items()}

print(output)
#{'Item1': Counter({'Name1': 10, 'Name2': 4}), 'Item2': Counter({'Name7': 44, 'Name6': 11, 'Name2': 3})}

print(dict_output)
#{'Item1': {'Name1': 10, 'Name2': 4}, 'Item2': {'Name7': 44, 'Name2': 3, 'Name6': 11}}

print(tuple_output)
#{'Item1': [('Name1', 10), ('Name2', 4)], 'Item2': [('Name7', 44), ('Name6', 11), ('Name2', 3)]}

print(dict_list_output)
#{'Item1': [{'Name1': 10}, {'Name2': 4}], 'Item2': [{'Name7': 44}, {'Name6': 11}, {'Name2': 3}]}

Of course, if you change the starting data structure altogether, it will become a lot easier to manage. If you use a dictionary from strings to Counters, you can use the Counter interface to easily update it (refer to the link)

Edit:

Just for fun, done in one line:

results = {item: reduce(lambda a, b: [a, a.update(b)][0], names, Counter()) for item, names in d.items()}

It was inspired by yours, except this only builds one Counter instance (given as the initial value for reduce) per list. Also, a little bit of a golfy trick is required to reduce properly, as Counter.update is in place. If you're reading this, you probably shouldn't use it, and instead build a data structure with Counters or dicts from the start, as mentioned earlier.

Upvotes: 1

boltthrower
boltthrower

Reputation: 1250

It occurred to me just a few minutes after posting the question.

This is what I did:

from operator import add
from collections import Counter
results = {}
for item, names in d.items():
        result[item] = (reduce(add, (Counter(name) for name in names)))

As the above comments and answers suggest, I am better off using a 1-length dictionary instead of having to combine several later. Still, leaving the answer out here for anyone that needs it.

Upvotes: 0

Related Questions