按多个键进行分组,并对字典列表中的值进行汇总/平均。

29

请问在Python中,最符合Python风格的方式是什么,可以根据多个键对字典列表进行分组,并汇总或平均值?假设我有如下字典列表:

input = [
{'dept': '001', 'sku': 'foo', 'transId': 'uniqueId1', 'qty': 100},
{'dept': '001', 'sku': 'bar', 'transId': 'uniqueId2', 'qty': 200},
{'dept': '001', 'sku': 'foo', 'transId': 'uniqueId3', 'qty': 300},
{'dept': '002', 'sku': 'baz', 'transId': 'uniqueId4', 'qty': 400},
{'dept': '002', 'sku': 'baz', 'transId': 'uniqueId5', 'qty': 500},
{'dept': '002', 'sku': 'qux', 'transId': 'uniqueId6', 'qty': 600},
{'dept': '003', 'sku': 'foo', 'transId': 'uniqueId7', 'qty': 700}
]

聚合操作的期望输出:

output=[
{'dept': '001', 'sku': 'foo', 'qty': 400},
{'dept': '001', 'sku': 'bar', 'qty': 200},
{'dept': '002', 'sku': 'baz', 'qty': 900},
{'dept': '002', 'sku': 'qux', 'qty': 600},
{'dept': '003', 'sku': 'foo', 'qty': 700}
]

或平均值:

output=[
{'dept': '001', 'sku': 'foo', 'avg': 200},
{'dept': '001', 'sku': 'bar', 'avg': 200},
{'dept': '002', 'sku': 'baz', 'avg': 450},
{'dept': '002', 'sku': 'qux', 'avg': 600},
{'dept': '003', 'sku': 'foo', 'avg': 700}
]

我找到了这个:Python中按字典列表分组并聚合值,但它似乎不能给我想要的结果。

8个回答

48

获取聚合结果

from itertools import groupby
from operator import itemgetter

grouper = itemgetter("dept", "sku")
result = []
for key, grp in groupby(sorted(input_data, key = grouper), grouper):
    temp_dict = dict(zip(["dept", "sku"], key))
    temp_dict["qty"] = sum(item["qty"] for item in grp)
    result.append(temp_dict)

from pprint import pprint
pprint(result)

输出

[{'dept': '001', 'qty': 200, 'sku': 'bar'},
 {'dept': '001', 'qty': 400, 'sku': 'foo'},
 {'dept': '002', 'qty': 900, 'sku': 'baz'},
 {'dept': '002', 'qty': 600, 'sku': 'qux'},
 {'dept': '003', 'qty': 700, 'sku': 'foo'}]

而要获取平均值,您可以简单地更改for循环中的内容,就像这样

temp_dict = dict(zip(["dept", "sku"], key))
temp_list = [item["qty"] for item in grp]
temp_dict["avg"] = sum(temp_list) / len(temp_list)
result.append(temp_dict)

输出

[{'avg': 200, 'dept': '001', 'sku': 'bar'},
 {'avg': 200, 'dept': '001', 'sku': 'foo'},
 {'avg': 450, 'dept': '002', 'sku': 'baz'},
 {'avg': 600, 'dept': '002', 'sku': 'qux'},
 {'avg': 700, 'dept': '003', 'sku': 'foo'}]

建议:无论如何,我都会像这样在同一个dict中添加qtyavg

Translated text:

建议:无论如何,我都会像这样在同一个dict中添加qtyavg

temp_dict = dict(zip(["dept", "sku"], key))
temp_list = [item["qty"] for item in grp]
temp_dict["qty"] = sum(temp_list)
temp_dict["avg"] = temp_dict["qty"] / len(temp_list)
result.append(temp_dict)

输出

[{'avg': 200, 'dept': '001', 'qty': 200, 'sku': 'bar'},
 {'avg': 200, 'dept': '001', 'qty': 400, 'sku': 'foo'},
 {'avg': 450, 'dept': '002', 'qty': 900, 'sku': 'baz'},
 {'avg': 600, 'dept': '002', 'qty': 600, 'sku': 'qux'},
 {'avg': 700, 'dept': '003', 'qty': 700, 'sku': 'foo'}]

1
我只想说,这个解决方案有多么的简洁! - nexla

6

受Eelco Hoogendoorn答案的启发,这里介绍使用Pandas包解决此问题的另一种方法。代码更易读。

import numpy as np
import pandas as pd

def sum_by_cusip_and_dept(data):
    df = pd.DataFrame(data)
    grouped = df.groupby(['sku', 'dept'])    
    sum = grouped.sum()
    return [{'sku': r[0], 'dept': r[1], 'qty': kv.to_dict().get('qty')} for r, kv in sum.iterrows()]     

2

使用numpy EP,您可以在这里找到,您可以编写:

inputs = dict( (k, [i[k] for i in input ]) for k in input[0].keys())
print group_by((inputs['dept'], inputs['sku'])).mean(inputs['qty'])

然而,如果你需要进行大量的这种关系操作,建议使用pandas包。


1
你可以将数量和出现次数放在一个大的默认字典中:
from collections import defaultdict

counts = defaultdict(lambda: [0, 0])
for line in input_data:
    entry = counts[(line['dept'], line['sku'])]
    entry[0] += line['qty']
    entry[1] += 1

现在的问题只是将数字转换为字典列表:
sums_dict = [{'dept': k[0], 'sku': k[1], 'qty': v[0]} 
              for k, v in counts.items()]
avg_dict = [{'dept': k[0], 'sku': k[1], 'avg': float(v[0]) / v[1]} for 
             k, v in counts.items()]

求和的结果:

sums_dict

[{'dept': '002', 'qty': 600, 'sku': 'qux'},
 {'dept': '001', 'qty': 400, 'sku': 'foo'},
 {'dept': '003', 'qty': 700, 'sku': 'foo'},
 {'dept': '002', 'qty': 900, 'sku': 'baz'},
 {'dept': '001', 'qty': 200, 'sku': 'bar'}]

而对于平均值:
avg_dict

[{'avg': 600.0, 'dept': '002', 'sku': 'qux'},
 {'avg': 200.0, 'dept': '001', 'sku': 'foo'},
 {'avg': 700.0, 'dept': '003', 'sku': 'foo'},
 {'avg': 450.0, 'dept': '002', 'sku': 'baz'},
 {'avg': 200.0, 'dept': '001', 'sku': 'bar'}]

一种不使用默认字典的替代版本:

counts = {}
for line in input_data:
    entry = counts.setdefault((line['dept'], line['sku']), [0, 0])
    entry[0] += line['qty']
    entry[1] += 1

剩下的部分相同。
sums_dict = [{'dept': k[0], 'sku': k[1], 'qty': v[0]} 
              for k, v in counts.items()]
avg_dict = [{'dept': k[0], 'sku': k[1], 'avg': float(v[0]) / v[1]} for 
             k, v in counts.items()]

0
使用pandas和duckdb,您可以轻松地使用SQL查询数据集:
import pandas as pd
import duckdb

data = [
    {'dept': '001', 'sku': 'foo', 'transId': 'uniqueId1', 'qty': 100},
    {'dept': '001', 'sku': 'bar', 'transId': 'uniqueId2', 'qty': 200},
    {'dept': '001', 'sku': 'foo', 'transId': 'uniqueId3', 'qty': 300},
    {'dept': '002', 'sku': 'baz', 'transId': 'uniqueId4', 'qty': 400},
    {'dept': '002', 'sku': 'baz', 'transId': 'uniqueId5', 'qty': 500},
    {'dept': '002', 'sku': 'qux', 'transId': 'uniqueId6', 'qty': 600},
    {'dept': '003', 'sku': 'foo', 'transId': 'uniqueId7', 'qty': 700}
]

df = pd.DataFrame(data)
result = duckdb.query("""
    SELECT dept, sku, SUM(qty), AVG(qty)
    FROM df
    GROUP BY dept, sku;
""").to_df()
print(result)

输出:

  dept  sku  sum(qty)  avg(qty)
0  001  foo     400.0     200.0
1  001  bar     200.0     200.0
2  002  baz     900.0     450.0
3  002  qux     600.0     600.0
4  003  foo     700.0     700.0

0

除了原始问题之外,我还有一些额外的要求。我想传递分组器,而不必传递字段的原始顺序,如果需要将分组键重构为字典。

namedtuple() 在这方面表现得非常好,因为它允许您进行排序并使用._asdict()

from collections import namedtuple

def get_grouper(fields):

    key = namedtuple('GroupingKey', fields)

    def get_key(row):
        return key(**{field: row[field] for field in fields})

    return get_key

rows = [
    {'a': 1, 'b': 1, 'c': 1},
    {'a': 1, 'b': 2, 'c': 3},
    {'a': 1, 'b': 1, 'c': 2},
    {'a': 1, 'b': 0},
    {'a': 1, 'b': 2, 'c': 4}
]

grouper = get_grouper(['a','b'])

rows = sorted(rows, key=grouper)

for k, g in groupby(rows, key=grouper):
    print(k, list(g))

0

像往常一样,有许多有效的解决方案,我喜欢defaultdict,因为我觉得它更容易理解。

from collections import defaultdict as df
food = df(lambda:df(lambda:df(int)))
for dct in input:  food[dct['transId']][dct['sku']][dct['dept']]=dct['qty']
output_tupl=[(d1,d2,sum(food[d1][d2][d3] for d3 in food[d1][d2]) )for d1 in food for d2 in food[d1]]

0

@thefourtheye 如果我们只使用groupby一个键,那么在分组后应该检查键的类型,如果不是元组,则返回列表。

for key, grp in groupby(sorted(input_data, key = grouper), grouper):
  if not isinstance(key, tuple):
    key = [key]

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接