resueltos - Agrupe por y agregue los valores de una lista de diccionarios en Python
lista de diccionarios python (3)
Estoy tratando de escribir una función, de una manera elegante, que agrupará una lista de diccionarios y agregará (suma) los valores de las claves similares.
Ejemplo:
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
}
]
"""
He intentado hacer esto usando itertools para el groupby y sumando cada par de valor-clave similar, pero me falta algo aquí. Así es como se ve mi función actualmente:
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
Este es un enfoque que utiliza more_itertools
donde simplemente se enfoca en cómo construir resultados.
Dado
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}
]
Código
# 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
Salida
defaultdict(None,
{datetime.date(2013, 1, 1): Counter({''value1'': 20, ''value2'': 20}),
datetime.date(2013, 1, 2): Counter({''value1'': 10, ''value2'': 10})})
Los elementos se agrupan por fecha y los valores pertinentes se reducen como Counters
.
Detalles
Pasos
- Construye funciones de ayuda para personalizar la construcción de claves , valores y valores reducidos en el último
defaultdict
. Aquí queremos:- grupo por fecha (
kfunc
) - Dicts construidos manteniendo los parámetros de "valor *" (
vfunc
) - agregue los dictados (
rfunc
) convirtiéndolos encollections.Counters
. Contadores yrfunc
. Ver un funcionamiento equivalente a continuación + .
- grupo por fecha (
- pasar las funciones de ayuda a
more_itertools.map_reduce
.
Groupby simple
... diga en ese ejemplo que quería agrupar por id y fecha?
No hay problema.
>>> 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})})
Salida personalizada
Si bien la estructura de datos resultante presenta el resultado de manera clara y concisa, la salida esperada del OP se puede reconstruir como una simple lista de dictados:
>>> [{**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}]
Para más información sobre map_reduce
, vea more_itertools.map_reduce . Instalar via > pip install more_itertools
.
+ Una función reductora equivalente:
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
Gracias, me olvidé de Counter. Todavía quería mantener el formato de salida y la clasificación de mi conjunto de datos devuelto, así que aquí está mi función final:
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
Puede utilizar collections.Counter
y collections.defaultdict
.
Usando un dict, esto se puede hacer en O(N)
, mientras que la clasificación requiere tiempo 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})
})
La ventaja de Counter
es que sumará automáticamente los valores de claves similares:
Ejemplo:
>>> c = Counter(**{''value1'': 10, ''value2'': 5})
>>> c.update({''value1'': 7, ''value2'': 3})
>>> c
Counter({''value1'': 17, ''value2'': 8})