web-dev-qa-db-fra.com

Grouper et agréger les valeurs d'une liste de dictionnaires en Python

J'essaie d'écrire une fonction, de manière élégante, qui regroupera une liste de dictionnaires et agrégera (somme) les valeurs de like-keys.

Exemple:

my_dataset = [  
    {
        'date': datetime.date(2013, 1, 1),
        'id': 99,
        'value1': 10,
        'value2': 10
    },
    {
        'date': datetime.date(2013, 1, 1),
        'id': 98,
        'value1': 10,
        'value2': 10
    },
    {
        'date': datetime.date(2013, 1, 2),
        'id' 99,
        'value1': 10,
        'value2': 10
    }
]

group_and_sum_dataset(my_dataset, 'date', ['value1', 'value2'])

"""
Should return:
[
    {
        'date': datetime.date(2013, 1, 1),
        'value1': 20,
        'value2': 20
    },
    {
        'date': datetime.date(2013, 1, 2),
        'value1': 10,
        'value2': 10
    }
]
"""

J'ai essayé de faire cela en utilisant itertools pour groupby et en sommant chaque paire de valeurs de clé similaire, mais il me manque quelque chose ici. Voici à quoi ma fonction ressemble actuellement:

def group_and_sum_dataset(dataset, group_by_key, sum_value_keys):
    keyfunc = operator.itemgetter(group_by_key)
    dataset.sort(key=keyfunc)
    new_dataset = []
    for key, index in itertools.groupby(dataset, keyfunc):
        d = {group_by_key: key}
        d.update({k:sum([item[k] for item in index]) for k in sum_value_keys})
        new_dataset.append(d)
    return new_dataset
16
Kyle Getrost

Vous pouvez utiliser collections.Counter et collections.defaultdict.

En utilisant un dict, cela peut être fait dans O(N), alors que le tri nécessite O(NlogN).

from collections import defaultdict, Counter
def solve(dataset, group_by_key, sum_value_keys):
    dic = defaultdict(Counter)
    for item in dataset:
        key = item[group_by_key]
        vals = {k:item[k] for k in sum_value_keys}
        dic[key].update(vals)
    return dic
... 
>>> d = solve(my_dataset, 'date', ['value1', 'value2'])
>>> d
defaultdict(<class 'collections.Counter'>,
{
 datetime.date(2013, 1, 2): Counter({'value2': 10, 'value1': 10}),
 datetime.date(2013, 1, 1): Counter({'value2': 20, 'value1': 20})
})

L’avantage de Counter est qu’il additionnera automatiquement les valeurs de clés similaires:

Exemple:

>>> c = Counter(**{'value1': 10, 'value2': 5})
>>> c.update({'value1': 7, 'value2': 3})
>>> c
Counter({'value1': 17, 'value2': 8})
20
Ashwini Chaudhary

Merci, j'ai oublié Counter. Je souhaitais toujours conserver le format de sortie et le tri de mon ensemble de données renvoyé. Voici donc à quoi ressemble ma fonction finale:

def group_and_sum_dataset(dataset, group_by_key, sum_value_keys):

    container = defaultdict(Counter)

    for item in dataset:
        key = item[group_by_key]
        values = {k:item[k] for k in sum_value_keys}
        container[key].update(values)

    new_dataset = [
        dict([(group_by_key, item[0])] + item[1].items())
            for item in container.items()
    ]
    new_dataset.sort(key=lambda item: item[group_by_key])

    return new_dataset
4
Kyle Getrost

Voici une approche utilisant more_itertools où vous vous concentrez simplement sur la manière de construire la sortie.

Donné

import datetime
import collections as ct

import more_itertools as mit


dataset = [
    {"date": datetime.date(2013, 1, 1), "id": 99, "value1": 10, "value2": 10},
    {"date": datetime.date(2013, 1, 1), "id": 98, "value1": 10, "value2": 10},
    {"date": datetime.date(2013, 1, 2), "id": 99, "value1": 10, "value2": 10}
]

Code  

# Step 1: Build helper functions    
kfunc = lambda d: d["date"]
vfunc = lambda d: {k:v for k, v in d.items() if k.startswith("val")}
rfunc = lambda lst: sum((ct.Counter(d) for d in lst), ct.Counter())

# Step 2: Build a dict    
reduced = mit.map_reduce(dataset, keyfunc=kfunc, valuefunc=vfunc, reducefunc=rfunc)
reduced

Sortie

defaultdict(None,
            {datetime.date(2013, 1, 1): Counter({'value1': 20, 'value2': 20}),
             datetime.date(2013, 1, 2): Counter({'value1': 10, 'value2': 10})})

Les articles sont regroupés par date et les valeurs pertinentes sont réduites à Counters


Détails

Pas

  1. construit des fonctions d'assistance pour personnaliser la construction des clés, valeurs et réduites valeurs dans la valeur finale defaultdict . Ici nous voulons:
    • groupe par date (kfunc)
    • dict construits en conservant les paramètres "valeur *" (vfunc)
    • agrégez les dessins (rfunc) en convertissant en collections.Counters et en les additionnant . Voir l'équivalent rfunc ci-dessous+.
  2. transmettre les fonctions d'assistance à more_itertools.map_reduce

Simple Groupby

... dire dans cet exemple que vous vouliez regrouper par id et date?

Aucun problème.

>>> kfunc2 = lambda d: (d["date"], d["id"])
>>> mit.map_reduce(dataset, keyfunc=kfunc2, valuefunc=vfunc, reducefunc=rfunc)
defaultdict(None,
            {(datetime.date(2013, 1, 1),
              99): Counter({'value1': 10, 'value2': 10}),
             (datetime.date(2013, 1, 1),
              98): Counter({'value1': 10, 'value2': 10}),
             (datetime.date(2013, 1, 2),
              99): Counter({'value1': 10, 'value2': 10})})

Sortie personnalisée

Alors que la structure de données résultante présente le résultat de manière claire et concise, le résultat attendu du PO peut être reconstruit sous la forme d'une simple liste de bases de données:

>>> [{**dict(date=k), **v} for k, v in reduced.items()]
[{'date': datetime.date(2013, 1, 1), 'value1': 20, 'value2': 20},
 {'date': datetime.date(2013, 1, 2), 'value1': 10, 'value2': 10}]

Pour plus d'informations sur map_reduce, voir the docs . Installer via > pip install more_itertools.

+Une fonction réductrice équivalente:

def rfunc(lst: typing.List[dict]) -> ct.Counter:
    """Return reduced mappings from map-reduce values."""
    c = ct.Counter()
    for d in lst:
        c += ct.Counter(d)
    return c
1
pylang