【问题标题】:Python - group/merge dictionaries based on key/values identityPython - 基于键/值标识的分组/合并字典
【发布时间】:2020-03-28 10:04:39
【问题描述】:

我有一个列表,其中包含许多具有相同键但值不同的字典。

我想做的是根据某些键的值对字典进行分组/合并。 展示一个例子可能比试图解释更快:

[{'zone': 'A', 'weekday': 1, 'hour': 12,  'C1': 3, 'C2': 15},
 {'zone': 'B', 'weekday': 2, 'hour': 6,  'C1': 5, 'C2': 27},
 {'zone': 'A', 'weekday': 1, 'hour': 12,  'C1': 7, 'C2': 12},
 {'zone': 'C', 'weekday': 5, 'hour': 8,  'C1': 2, 'C2': 13}]

所以,我想要实现的是合并第一和第三个字典,因为它们具有相同的“区域”、“小时”和“工作日”,将 C1 和 C2 中的值相加:

[{'zone': 'A', 'weekday': 1, 'hour': 12,  'C1': 10, 'C2': 27},
 {'zone': 'B', 'weekday': 2, 'hour': 6,  'C1': 5, 'C2': 27},
 {'zone': 'C', 'weekday': 5, 'hour': 8,  'C1': 2, 'C2': 13}]

这里有什么帮助吗? :) 我已经为此苦苦挣扎了几天,我有一个糟糕的不可扩展的解决方案,但我敢肯定还有一些更 Pythonic 的东西我可以实施。

谢谢!

【问题讨论】:

  • 听起来您可能为此使用了错误的数据结构。你考虑过 Pandas 之类的东西吗?
  • @AlexanderCécile - 为什么字典不适合?
  • @wwii 不一定是字典不合适,而是在我看来,数据的形状看起来像表格。如果您正在执行任何更复杂的操作,那么像 Pandas 这样强大的东西会非常有用。
  • 例如,我认为这个特殊的操作可以在 Pandas 中这样完成:df.groupby(['zone', 'weekday', 'hour']).agg('sum')

标签: python list dictionary merge key


【解决方案1】:

通过使用defaultdict,您可以在线性时间内合并它们。

from collections import defaultdict

res = defaultdict(lambda : defaultdict(int))

for d in dictionaries:
        res[(d['zone'],d['weekday'],d['hour'])]['C1']+= d['C1']
        res[(d['zone'],d['weekday'],d['hour'])]['C2']+= d['C2']

缺点是您需要另一遍才能获得您定义的输出。

【讨论】:

    【解决方案2】:

    按相关键排序然后分组;遍历组并创建具有总和值的新字典。

    import operator
    import itertools
    
    keys = operator.itemgetter('zone','weekday','hour')
    c1_c2 = operator.itemgetter('C1','C2')
    
    # data is your list of dicts
    data.sort(key=keys)
    grouped = itertools.groupby(data,keys)
    
    new_data = []
    for (zone,weekday,hour),g in grouped:
        c1,c2 = 0,0
        for d in g:
            c1 += d['C1']
            c2 += d['C2']
        new_data.append({'zone':zone,'weekday':weekday,
                         'hour':hour,'C1':c1,'C2':c2})
    

    最后一个循环也可以写成:

    for (zone,weekday,hour),g in grouped:
        cees = map(c1_c2,g)
        c1,c2 = map(sum,zip(*cees))
        new_data.append({'zone':zone,'weekday':weekday,
                         'hour':hour,'C1':c1,'C2':c2})
    

    【讨论】:

    • 比我的蹩脚尝试干净得多,也比其他人更快。
    • 在这里选择正确答案是一个艰难的选择,因为这 3 个解决方案都是完全有效的。尽管这个可能是最慢的(正如@RishiG 所展示的那样),但我认为它比其他的更适合我的特定需求,所以我要投票给这个!但是感谢大家的贡献!
    • @ffede - 只是我的版本最慢。这个清理得比其他的快。这个问题出现后我没有更新我的答案。
    • 我在我的答案中添加了一个更新,反映了这个解决方案。
    【解决方案3】:

    我已经写了一个稍微长一点的解决方案,使用名称作为字典的键:

    from collections import namedtuple
    
    zones = [{'zone': 'A', 'weekday': 1, 'hour': 12,  'C1': 3, 'C2': 15},
     {'zone': 'B', 'weekday': 2, 'hour': 6,  'C1': 5, 'C2': 27},
     {'zone': 'A', 'weekday': 1, 'hour': 12,  'C1': 7, 'C2': 12},
     {'zone': 'C', 'weekday': 5, 'hour': 8,  'C1': 2, 'C2': 13}]
    
    ZoneTime = namedtuple("ZoneTime", ["zone", "weekday", "hour"])
    results = dict()
    
    for zone in zones:
        zone_time = ZoneTime(zone['zone'], zone['weekday'], zone['hour'])
        if zone_time in results:
            results[zone_time]['C1'] += zone['C1']
            results[zone_time]['C2'] += zone['C2']
        else:
            results[zone_time] = {'C1': zone['C1'], 'C2': zone['C2']}
    
    
    print(results)
    

    这使用 (zone, weekday, hour) 的命名元组作为每个字典的键。如果它已经存在于results 中,那么添加它或者在字典中创建一个新条目是相当简单的。

    你当然可以让它更短更“聪明”,但它可能会变得不那么可读。

    【讨论】:

      【解决方案4】:

      编辑:运行时间比较

      我的原始答案(见下文)不是一个好的答案,但我认为通过对其他答案进行一些运行时分析,我做出了有用的贡献,因此我编辑了该部分并将其放在顶部.在这里,我包括了其他三个解决方案,以及产生所需输出所需的转换。为了完整起见,我还包括一个使用pandas 的版本,它假定用户正在使用DataFrame(从字典列表转换为数据框并返回甚至不值得)。比较时间根据生成的随机数据略有不同,但这些比较具有代表性:

      >>> run_timer(100)
      Times with 100 values
          ...with defaultdict: 0.1496697600000516
          ...with namedtuple: 0.14976404899994122
          ...with groupby: 0.0690777249999428
          ...with pandas: 3.3165711250001095
      >>> run_timer(1000)
      Times with 1000 values
          ...with defaultdict: 1.267153091999944
          ...with namedtuple: 0.9605341750000207
          ...with groupby: 0.6634409229998255
          ...with pandas: 3.5146895360001054
      >>> run_timer(10000)
      Times with 10000 values
          ...with defaultdict: 9.194478484000001
          ...with namedtuple: 9.157486462000179
          ...with groupby: 5.18553969300001
          ...with pandas: 4.704001281000046
      >>> run_timer(100000)
      Times with 100000 values
          ...with defaultdict: 59.644778522000024
          ...with namedtuple: 89.26688319799996
          ...with groupby: 93.3517027989999
          ...with pandas: 14.495209061999958
      

      外卖:

      • 使用 pandas 数据框可以为大型数据集节省大量时间

        • 注意:我确实包括字典列表和数据框之间的转换,这绝对是重要的
      • 否则,可接受的解决方案(二战)在中小型数据集上胜出,但对于非常大的数据集,它可能是最慢的

      • 更改组的大小(例如,通过减少区域的数量)会产生巨大的影响,此处未进行检查

      这是我用来生成上述内容的脚本。

      import random
      import pandas
      
      from timeit import timeit
      
      from functools import partial
      
      from itertools import groupby
      from operator import itemgetter
      
      from collections import namedtuple, defaultdict
      
      
      def with_pandas(df):
          return df.groupby(['zone', 'weekday', 'hour']).agg(sum).reset_index()
      
      
      def with_groupby(data):
          keys = itemgetter('zone', 'weekday', 'hour')
      
          # data is your list of dicts
          data.sort(key=keys)
          grouped = groupby(data, keys)
      
          new_data = []
          for (zone, weekday, hour), g in grouped:
              c1, c2 = 0, 0
              for d in g:
                  c1 += d['C1']
                  c2 += d['C2']
              new_data.append({'zone': zone, 'weekday': weekday,
                               'hour': hour, 'C1': c1, 'C2': c2})
      
          return new_data
      
      
      def with_namedtuple(zones):
          ZoneTime = namedtuple("ZoneTime", ["zone", "weekday", "hour"])
          results = dict()
          for zone in zones:
              zone_time = ZoneTime(zone['zone'], zone['weekday'], zone['hour'])
              if zone_time in results:
                  results[zone_time]['C1'] += zone['C1']
                  results[zone_time]['C2'] += zone['C2']
              else:
                  results[zone_time] = {'C1': zone['C1'], 'C2': zone['C2']}
          return [
              {
                  'zone': key[0],
                  'weekday': key[1],
                  'hour': key[2],
                  **val
              }
              for key, val in results.items()
          ]
      
      
      def with_defaultdict(dictionaries):
          res = defaultdict(lambda: defaultdict(int))
          for d in dictionaries:
              res[(d['zone'], d['weekday'], d['hour'])]['C1'] += d['C1']
              res[(d['zone'], d['weekday'], d['hour'])]['C2'] += d['C2']
          return [
              {
                  'zone': key[0],
                  'weekday': key[1],
                  'hour': key[2],
                  **val
              }
              for key, val in res.items()
          ]
      
      
      def gen_random_vals(num):
          return [
              {
                  'zone': random.choice('ABCDEFGHIJKLMNOPQRSTUVWXYZ'),
                  'weekday': random.randint(1, 7),
                  'hour': random.randint(0, 23),
                  'C1': random.randint(1, 50),
                  'C2': random.randint(1, 50),
              }
              for idx in range(num)
          ]
      
      
      def run_timer(num_vals=1000, timeit_num=1000):
          vals = gen_random_vals(num_vals)
          df = pandas.DataFrame(vals)
          p_fmt = "\t...with %s: %s"
          times = {
              'defaultdict': timeit(stmt=partial(with_defaultdict, vals), number=timeit_num),
              'namedtuple': timeit(stmt=partial(with_namedtuple, vals), number=timeit_num),
              'groupby': timeit(stmt=partial(with_groupby, vals), number=timeit_num),
              'pandas': timeit(stmt=partial(with_pandas, df), number=timeit_num),
          }
          print("Times with %d values" % num_vals)
          for key, val in times.items():
              print(p_fmt % (key, val))
      

      在哪里

      原答案:

      只是为了好玩,这是使用groupby 的完全不同的方法。当然,这不是最漂亮的,但应该很快。

      from itertools import groupby
      from operator import itemgetter
      from pprint import pprint
      
      vals = [
          {'zone': 'A', 'weekday': 1, 'hour': 12,  'C1': 3, 'C2': 15},
          {'zone': 'B', 'weekday': 2, 'hour': 6,  'C1': 5, 'C2': 27},
          {'zone': 'A', 'weekday': 1, 'hour': 12,  'C1': 7, 'C2': 12},
          {'zone': 'C', 'weekday': 5, 'hour': 8,  'C1': 2, 'C2': 13}
      ]
      ordered = sorted(
          [
              (
                  (row['zone'], row['weekday'], row['hour']),
                  row['C1'], row['C2']
              )
              for row in vals
          ]
      )
      
      
      def invert_columns(grp):
          return zip(*[g_row[1:] for g_row in grp])
      
      
      merged = [
          {
              'zone': key[0],
              'weekday': key[1],
              'hour': key[2],
              **dict(
                  zip(["C1", "C2"], [sum(col) for col in invert_columns(grp)])
              )
          }
          for key, grp in groupby(ordered, itemgetter(0))
      ]
      
      pprint(merged)
      

      产生

      [{'C1': 10, 'C2': 27, 'hour': 12, 'weekday': 1, 'zone': 'A'},
       {'C1': 5, 'C2': 27, 'hour': 6, 'weekday': 2, 'zone': 'B'},
       {'C1': 2, 'C2': 13, 'hour': 8, 'weekday': 5, 'zone': 'C'}]
      

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 1970-01-01
        • 2016-04-07
        • 2019-09-17
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        相关资源
        最近更新 更多