3

I have a dictionary that looks something like this:

letters_by_number = {
  1: ['a', 'b', 'c', 'd'],
  2: ['b', 'd'],
  3: ['a', 'c'],
  4: ['a', 'd'],
  5: ['b', 'c']
}

I want to reverse it to look something like this:

numbers_by_letter = {
  'a': [1, 3, 4],
  'b': [1, 2, 5],
  'c': [1, 3, 5],
  'd': [1, 2, 4]
}

I know that I could do this by looping through (key, value) through letters_by_number, looping through value (which is a list), and adding (val, key) to a list in the dictionary. This is cumbersome and I feel like there must be a more "pythonic" way to do this. Any suggestions?

jxmorris12
  • 1,262
  • 4
  • 15
  • 27
  • Does this answer your question? [Inverting a dictionary with list values](https://stackoverflow.com/questions/35491223/inverting-a-dictionary-with-list-values) – Georgy Mar 27 '20 at 11:04
  • Does this answer your question? [How to reverse a dictionary (whose values are lists) in Python?](https://stackoverflow.com/questions/35945473/how-to-reverse-a-dictionary-whose-values-are-lists-in-python) – mkrieger1 Feb 13 '23 at 14:29

4 Answers4

7

This is well-suited for collections.defaultdict:

>>> from collections import defaultdict
>>> numbers_by_letter = defaultdict(list)
>>> for k, seq in letters_by_number.items():
...     for letter in seq:
...         numbers_by_letter[letter].append(k)
... 
>>> dict(numbers_by_letter)
{'a': [1, 3, 4], 'b': [1, 2, 5], 'c': [1, 3, 5], 'd': [1, 2, 4]}

Note that you don't really need the final dict() call (a defaultdict will already give you the behavior you probably want), but I included it here because the result from your question is type dict.

Brad Solomon
  • 38,521
  • 31
  • 149
  • 235
  • The result is actually: `defaultdict(, {'a': [1, 3, 4], 'c': [1, 3, 5], 'b': [1, 2, 5], 'd': [1, 2, 4]})` since `numbers_by_letter` is a `defaultdict`. – martineau Nov 14 '19 at 18:41
1

Use setdefault:

letters_by_number = {
    1: ['a', 'b', 'c', 'd'],
    2: ['b', 'd'],
    3: ['a', 'c'],
    4: ['a', 'd'],
    5: ['b', 'c']
}

inv = {}
for k, vs in letters_by_number.items():
    for v in vs:
        inv.setdefault(v, []).append(k)

print(inv)

Output

{'a': [1, 3, 4], 'b': [1, 2, 5], 'c': [1, 3, 5], 'd': [1, 2, 4]}
Dani Mesejo
  • 61,499
  • 6
  • 49
  • 76
1

A (trivial) subclass of dict would make this very easy:

class ListDict(dict):
    def __missing__(self, key):
        value = self[key] = []
        return value


letters_by_number = {
  1: ['a', 'b', 'c', 'd'],
  2: ['b', 'd'],
  3: ['a', 'c'],
  4: ['a', 'd'],
  5: ['b', 'c']
}


numbers_by_letter = ListDict()
for key, values in letters_by_number.items():
    for value in values:
        numbers_by_letter[value].append(key)

from pprint import pprint
pprint(numbers_by_letter, width=40)

Output:

{'a': [1, 3, 4],
 'b': [1, 2, 5],
 'c': [1, 3, 5],
 'd': [1, 2, 4]}
martineau
  • 119,623
  • 25
  • 170
  • 301
  • 2
    Seems like this is exactly what the `collections.defaultdict` class does! The more you know! – jxmorris12 Nov 14 '19 at 20:49
  • 1
    @jxmorris12: To a large degree it accomplishes the same thing, but one difference is that it does not define its own `__repr__()` or `__str__()` methods, so instances typically look exactly like regular dictionaries when displayed. Another possible advantage is that, depending on what you plan on doing with the result, the `__missing__()` method can be made to do other, non-trivial things. – martineau Nov 14 '19 at 20:55
  • 1
    @martineau Shouldn't it have a different `__repr__` though since it's a subclass? Something like `ListDict({'a': [1, 3, 4], ...})` – wjandrea Nov 15 '19 at 19:23
  • 1
    @wjandrea: Yes, technically — according to the docs — it _should_ so the returned string could be used to recreate the object. However if you define one then it will also be used as the `__str__()` method unless you also define that. This quickly makes the class not-so-trivial which sorta defeats its primary purpose (expediency). – martineau Nov 15 '19 at 19:48
0

Here's a solution using a dict comprehension, without adding list elements in a loop. Build a set of keys by joining all the lists together, then build each list using a list comprehension. To be more efficient, I've first built another dictionary containing sets instead of lists, so that k in v is an O(1) operation.

from itertools import chain

def invert_dict_of_lists(d):
    d = { i: set(v) for i, v in d.items() }
    return {
        k: [ i for i, v in d.items() if k in v ]
        for k in set(chain.from_iterable(d.values()))
    }

Strictly, dictionaries in modern versions of Python 3 retain the order that keys are inserted in. This produces a result where the keys are in the order they appear in the lists; not alphabetical order like in your example. If you do want the keys in sorted order, change for k in set(...) to for k in sorted(set(...)).

kaya3
  • 47,440
  • 4
  • 68
  • 97
  • Personally, I think there is nothing un-Pythonic about using a loop instead of a comprehension. Brad's answer is cleaner and easier to understand, and should be at least as efficient too; his way is how I would solve the problem myself. I've posted this answer anyway, because the question implies that you want to do it without a loop. – kaya3 Nov 14 '19 at 18:24