簡體   English   中英

使用 itertools 通過復合鍵對字典列表中的重復項求和

[英]Summing duplicates in a list of dictionaries by a compound key using itertools

我有一個這樣的字典排序列表:

dat = [
      {"id1": 1, "id2": 2, "value": 1},
      {"id1": 1, "id2": 2, "value": 2},
      {"id1": 2, "id2": 2, "value": 2},
      {"id1": 2, "id2": 3, "value": 1},
      {"id1": 3, "id2": 3, "value": 1},
      {"id1": 3, "id2": 4, "value": 1},
      {"id1": 3, "id2": 4, "value": 1},
      {"id1": 3, "id2": 4, "value": 1},
      {"id1": 3, "id2": 4, "value": 1},
      ]

這實際上是 (id1, id2, value) 元組,但存在重復。 我想通過對兩個 id 相等的值求和來刪除重復數據,留下唯一的 (id1, id2) 對,其中新值是重復項的總和。

也就是說,從上面來看,所需的 output 是:

dat =[
     {'id1': 1, 'id2': 2, 'value': 3},
     {'id1': 2, 'id2': 2, 'value': 2},
     {'id1': 2, 'id2': 3, 'value': 1},
     {'id1': 3, 'id2': 3, 'value': 1},
     {'id1': 3, 'id2': 4, 'value': 4}
     ]

假設列表有數百萬個,其中有很多重復項。 使用itertoolsfuncy (相對於使用 pandas)執行此操作的最有效方法是什么?

您可以從collections.Counter開始並使用+=運算符, Counter的方便部分是+=在不存在的鍵上假定為零。

dat = [
      {"id1": 1, "id2": 2, "value": 1},
      {"id1": 1, "id2": 2, "value": 2},
      {"id1": 2, "id2": 2, "value": 2},
      {"id1": 2, "id2": 3, "value": 1},
      {"id1": 3, "id2": 3, "value": 1},
      {"id1": 3, "id2": 4, "value": 1},
      {"id1": 3, "id2": 4, "value": 1},
      {"id1": 3, "id2": 4, "value": 1},
      {"id1": 3, "id2": 4, "value": 1},
      ]

from collections import Counter
cnt = Counter()
for item in dat:
  cnt[item["id1"], item["id2"]] += item["value"]

[{'id1':id1, 'id2': id2, 'value':v}for (id1, id2), v in cnt.items()]

給予

[{'id1': 1, 'id2': 2, 'value': 3},
 {'id1': 2, 'id2': 2, 'value': 2},
 {'id1': 2, 'id2': 3, 'value': 1},
 {'id1': 3, 'id2': 3, 'value': 1},
 {'id1': 3, 'id2': 4, 'value': 4}]

我們也可以使用collections.defaultdict

from collections import defaultdict
tmp = defaultdict(int)
for d in dat:
    tmp[d['id1'], d['id2']] += d['value']
out = [{'id1':id1, 'id2':id2, 'value':v} for (id1, id2), v in tmp.items()]

或者(假設 ID 已排序), itertools.groupby

from itertools import groupby
out = [{'id1': k1, 'id2': k2, 'value': sum(d['value'] for d in g)} for (k1,k2), g in groupby(dat, lambda x: (x['id1'], x['id2']))]

pandas中的groupby + sum + to_dict

out = pd.DataFrame(dat).groupby(['id1','id2'], as_index=False)['value'].sum().to_dict('records')

Output:

[{'id1': 1, 'id2': 2, 'value': 3},
 {'id1': 2, 'id2': 2, 'value': 2},
 {'id1': 2, 'id2': 3, 'value': 1},
 {'id1': 3, 'id2': 3, 'value': 1},
 {'id1': 3, 'id2': 4, 'value': 4}]


所提供數據的基本基准表明使用itemgettergroupby (如@ShadowRanger 所建議的那樣)是最快的:

  1. defaultdict: 6.57 µs ± 491 ns per loop (mean ± std. dev. of 7 runs, 100000 loops each)
  2. 計數器 ( Bob ): 9.56 µs ± 1.47 µs per loop (mean ± std. dev. of 7 runs, 100000 loops each)
  3. groupby + itemgetter ( ShadowRanger ): 6.01 µs ± 182 ns per loop (mean ± std. dev. of 7 runs, 100000 loops each)
  4. groupby + lambda: 9.02 µs ± 598 ns per loop (mean ± std. dev. of 7 runs, 100000 loops each)
  5. pandas: 3.81 ms ± 68.2 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)

現在如果我們復制dat 100 萬次,即 Do

dat = dat*1_000_000
dat.sort(key=itemgetter('id1', 'id2'))

並再次執行相同的基准測試,帶有itemgettergroupby是失控的贏家:

  1. 3.91 s ± 320 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)
  2. 5.38 s ± 251 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)
  3. 1.77 s ± 128 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)
  4. 3.53 s ± 199 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)
  5. 15.2 s ± 831 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)

在 Python 3.9.7(64 位)上運行

這個基准在某種程度上有利於groupby ,因為當我們復制現有的小型字典列表時,組很少。 如果創建隨機化“組”的大小, groupby + itemgetter仍然勝過所有,但差異並不那么明顯。

只是為了好玩,一個純粹的itertools解決方案(不使用collections或以其他方式使用任何中間容器,如果list已經按鍵順序,則必須逐步構建和更新,但如果你不能保證它已經存在,則需要預先排序排序以將唯一 ID 對組合在一起):

# At top of file
from itertools import groupby

# Also at top of file; not strictly necessary, but I find it's nicer to make cheap getters
# with self-documenting names
from operator import itemgetter
get_ids = itemgetter('id1', 'id2')
get_value = itemgetter('value')

# On each use:
dat.sort(key=get_ids)  # Not needed if data guaranteed grouped by unique id1/id2 pairs as in example

dat = [{'id1': id1, 'id2': id2, 'value': sum(map(get_value, group))}
       for (id1, id2), group in groupby(dat, key=get_ids)]

# If sorting needed, you can optionally one-line as the rather overly dense (I don't recommend it):
dat = [{'id1': id1, 'id2': id2, 'value': sum(map(get_value, group))}
       for (id1, id2), group in groupby(sorted(dat, key=get_ids), key=get_ids)]

就個人而言,我通常會使用其他答案中所示的Counterdefaultdict(int) ,因為即使使用未排序的數據它們也能獲得O(n)性能( groupbyO(n) ,但如果您需要先排序,則排序是O(n log n) )。 基本上,這甚至具有理論上優勢的唯一一次是當數據已經排序並且您重視使用單行(不包括導入和一次性設置成本來制作itemgetter s); 在實踐中, itertools.groupby有足夠的開銷,它通常仍然輸給collections.Counter / collections.defaultdict(int)中的一個或兩個,尤其是在其優化模式下使用collections.Counter來計算要計數的事物的迭代次數時(不' 在這里申請,但值得了解)。

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM