Я хочу суммировать две разные переменные в одной функции, но я хочу, чтобы они были суммированы на основе нескольких других элементов.
Если у меня есть следующий список dicts x:
x=[{'id':1, 'var1':'a', 'var2':'left', 'var3':0.1, 'var4':1},
{'id':2, 'var1':'a', 'var2':'right', 'var3':0.1, 'var4':1},
{'id':2, 'var1':'a', 'var2':'right', 'var3':0.2, 'var4':3},
{'id':4, 'var1':'b', 'var2':'left', 'var3':0.4, 'var4':4},
{'id':5, 'var1':'b', 'var2':'right', 'var3':0.1, 'var4':5},
{'id':5, 'var1':'b', 'var2':'right', 'var3':0.4, 'var4':2}]
Затем я могу использовать следующую функцию для суммирования отдельной переменной ("var3") на основе двух других переменных ("var1" и "var2"):
from operator import itemgetter
from itertools import groupby
def aggregate_var3_by_var1_and_var2(data):
my_data= []
grouper = itemgetter("id", "var1", "var2")
for key, grp in groupby(sorted(data, key = grouper), grouper):
temp_dict = dict(zip(["id", "var1", "var2"], key))
temp_dict["var3"] = sum(item["var3"] for item in grp)
my_data.append(temp_dict)
return my_data
my_output = aggregate_var3_by_var1_and_var2(x)
Тем не менее, я хочу суммировать несколько переменных ("var3" и "var4") по нескольким категориям ("var1" и "var2"), чтобы результат выглядел следующим образом:
y=[{'id': 1, 'var1': 'a', 'var2': 'left', 'var3': 0.1, 'var4': 1},
{'id': 2, 'var1': 'a', 'var2': 'right', 'var3': 0.3, 'var4':4},
{'id': 4, 'var1': 'b', 'var2': 'left', 'var3': 0.4, 'var4':4},
{'id': 5, 'var1': 'b', 'var2': 'right', 'var3': 0.5, 'var4':7}]
Как суммировать сразу несколько переменных с помощью этого метода?
Вы можете использовать collections.defaultdict
для решения O (n). В отличие от itertools.groupby
, это не требует предварительной сортировки.
Идея состоит в том, чтобы группировать предварительно определенные group_keys
. Затем используйте понимание списка для объединения ключей и значений вашего defaultdict
. Синтаксис {**d1, **d2}
используется для объединения двух словарей.
from collections import defaultdict
from operator import itemgetter
d = defaultdict(lambda: defaultdict(int))
group_keys = ['id', 'var1', 'var2']
sum_keys = ['var3', 'var4']
for item in x:
for key in sum_keys:
d[itemgetter(*group_keys)(item)][key] += item[key]
res = [{**dict(zip(group_keys, k)), **v} for k, v in d.items()]
print(res)
[{'id': 1, 'var1': 'a', 'var2': 'left', 'var3': 0.1, 'var4': 1},
{'id': 2, 'var1': 'a', 'var2': 'right', 'var3': 0.3, 'var4': 4},
{'id': 4, 'var1': 'b', 'var2': 'left', 'var3': 0.4, 'var4': 4},
{'id': 5, 'var1': 'b', 'var2': 'right', 'var3': 0.5, 'var4': 7}]
Простое расширение вашего подхода, предоставление ключей группового вызова и ключей значений в качестве аргументов:
from operator import itemgetter
from itertools import groupby
from itertools import chain
def reducer(ts):
return map(sum, zip(*ts))
def agg(data, keys, aggfields):
my_data = []
getter = itemgetter(*aggfields)
grouper = itemgetter(*keys)
for ks, grp in groupby(sorted(data, key=grouper), grouper):
vs = map(getter, grp)
kvs = chain(zip(keys,ks), zip(aggfields, reducer(vs)))
my_data.append(dict(kvs))
return my_data
В реплике:
In [9]: x=[{'id':1, 'var1':'a', 'var2':'left', 'var3':0.1, 'var4':1},
...: {'id':2, 'var1':'a', 'var2':'right', 'var3':0.1, 'var4':1},
...: {'id':2, 'var1':'a', 'var2':'right', 'var3':0.2, 'var4':3},
...: {'id':4, 'var1':'b', 'var2':'left', 'var3':0.4, 'var4':4},
...: {'id':5, 'var1':'b', 'var2':'right', 'var3':0.1, 'var4':5},
...: {'id':5, 'var1':'b', 'var2':'right', 'var3':0.4, 'var4':2}]
In [10]: agg(x, ['var1','var2'], ['var3','var4'])
Out[10]:
[{'var1': 'a', 'var2': 'left', 'var3': 0.1, 'var4': 1},
{'var1': 'a', 'var2': 'right', 'var3': 0.30000000000000004, 'var4': 4},
{'var1': 'b', 'var2': 'left', 'var3': 0.4, 'var4': 4},
{'var1': 'b', 'var2': 'right', 'var3': 0.5, 'var4': 7}]
Вот альтернативный подход, в котором группы используют словарь (по умолчанию dict counter dicts...)
from collections import Counter, defaultdict
from itertools import chain
from operator import itemgetter
def agg(data, keys, aggfields):
grouper = defaultdict(Counter)
pluck_keys = itemgetter(*keys)
pluck_vals = itemgetter(*aggfields)
for d in data:
ctr = grouper[pluck_keys(d)]
for k, v in zip(aggfields, pluck_vals(d)):
ctr[k] += v
return [
{k:v for k,v in chain(zip(keys, ks), ctr.items())}
for ks, ctr in grouper.items()
]
from itertools import groupby
x=[{'id':1, 'var1':'a', 'var2':'left', 'var3':0.1, 'var4':1},
{'id':2, 'var1':'a', 'var2':'right', 'var3':0.1, 'var4':1},
{'id':2, 'var1':'a', 'var2':'right', 'var3':0.2, 'var4':3},
{'id':4, 'var1':'b', 'var2':'left', 'var3':0.4, 'var4':4},
{'id':5, 'var1':'b', 'var2':'right', 'var3':0.1, 'var4':5},
{'id':5, 'var1':'b', 'var2':'right', 'var3':0.4, 'var4':2}]
res = []
for key, value in groupby(x, lambda x: x["id"]):
d = None
for i in value:
if not d:
d = i
else:
d["var3"] += i["var3"]
d["var4"] += i["var4"]
res.append(d)
print(res)
Выход:
[{'id': 1, 'var1': 'a', 'var2': 'left', 'var3': 0.1, 'var4': 1},
{'id': 2,
'var1': 'a',
'var2': 'right',
'var3': 0.30000000000000004,
'var4': 4},
{'id': 4, 'var1': 'b', 'var2': 'left', 'var3': 0.4, 'var4': 4},
{'id': 5, 'var1': 'b', 'var2': 'right', 'var3': 0.5, 'var4': 7}]
Вы можете использовать Pandas для эффективного, векторизованного решения.
Недостатки itertools.groupby
здесь - это необходимость сортировки [дополнительной сложности] и не реализует векторизованные вычисления [неэффективное суммирование].
Если вы хотите спуститься по замкнутому маршруту, я рекомендую collections.defaultdict
чтобы убедиться, что у вас все еще есть сложность O (n).
import pandas as pd
df = pd.DataFrame(x)
res = df.groupby(['id', 'var1', 'var2']).agg({'var3': 'sum', 'var4': 'sum'}).reset_index()
print(res.to_dict('records'))
[{'id': 1, 'var1': 'a', 'var2': 'left', 'var3': 0.1, 'var4': 1},
{'id': 2, 'var1': 'a', 'var2': 'right', 'var3': 0.3, 'var4': 4},
{'id': 4, 'var1': 'b', 'var2': 'left', 'var3': 0.4, 'var4': 4},
{'id': 5, 'var1': 'b', 'var2': 'right', 'var3': 0.5, 'var4': 7}]