【问题标题】:Group by and aggregate the values of a list of dictionaries in Python在 Python 中对字典列表的值进行分组和聚合
【发布时间】:2013-08-06 15:19:40
【问题描述】:

我正在尝试以一种优雅的方式编写一个函数,它将对字典列表进行分组并聚合(汇总)like-keys 的值。

示例:

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
    }
]
"""

我已经尝试使用 itertools 对 groupby 进行此操作,并对每个 like-key 值对求和,但在这里遗漏了一些东西。这是我的函数目前的样子:

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

【问题讨论】:

    标签: python dictionary itertools


    【解决方案1】:

    您可以使用collections.Countercollections.defaultdict

    使用字典可以在O(N) 中完成,而排序需要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})
    })
    

    Counter 的优点是它会自动对相似键的值求和。:

    示例:

    >>> c = Counter(**{'value1': 10, 'value2': 5})
    >>> c.update({'value1': 7, 'value2': 3})
    >>> c
    Counter({'value1': 17, 'value2': 8})
    

    【讨论】:

    • 这太棒了!您对按 2 个字段分组有什么想法吗?就像在那个例子中说你想按 id 和 date 分组?现在我的想法是把这两个字段连接成一个,但是看起来不是很优雅。
    【解决方案2】:

    谢谢,我忘了计数器。我仍然想保持返回数据集的输出格式和排序,所以我的最终函数如下所示:

    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
    

    【讨论】:

      【解决方案3】:

      这是一种使用more_itertools 的方法,您只需关注如何构建输出。

      给定

      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}
      ]
      

      代码

      # 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
      

      输出

      defaultdict(None,
                  {datetime.date(2013, 1, 1): Counter({'value1': 20, 'value2': 20}),
                   datetime.date(2013, 1, 2): Counter({'value1': 10, 'value2': 10})})
      

      项目按日期分组,相关值减少为Counters


      详情

      步骤

      1. 构建辅助函数以自定义最终defaultdictkeysvaluesreduced 值的构造。在这里,我们想要:
        • 按日期分组 (kfunc)
        • 构建的字典保留“value*”参数 (vfunc)
        • 通过转换为 collections.Counterssumming them 来聚合字典 (rfunc)。请参阅下面的等效rfunc+
      2. 将辅助函数传递给more_itertools.map_reduce

      简单分组

      ...说在那个例子中你想按 id 和 date 分组?

      没问题。

      >>> 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})})
      

      自定义输出

      虽然生成的数据结构清晰简洁地呈现了结果,但 OP 的预期输出可以重建为一个简单的 dicts 列表:

      >>> [{**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}]
      

      有关map_reduce 的更多信息,请参阅the docs。通过&gt; pip install more_itertools安装。

      +等价的约简函数:

      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
      

      【讨论】:

        猜你喜欢
        • 2014-08-03
        • 2019-06-12
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        相关资源
        最近更新 更多