Python groupby()和reduce()

Python 使用groupby()和reduce(),数据分析中经常需要分组数据并总结分组情况。可以用defaultdict(list)进行分组,然后展开分析。

需要处理的数据实例如下所示:

>>> data = [('4', 6.1), ('1', 4.0), ('2', 8.3), ('2', 6.5),
... ('1', 4.6), ('2', 6.8), ('3', 9.3), ('2', 7.8),
... ('2', 9.2), ('4', 5.6), ('3', 10.5), ('1', 5.8),
... ('4', 3.8), ('3', 8.1), ('3', 8.0), ('1', 6.9),
... ('3', 6.9), ('4', 6.2), ('1', 5.4), ('4', 5.8)]

原始数据序列中每个元素包含一个键值以及对键值的度量。

创建分组的一个常用方法是将键值相同的元素放入同一个列表,如下所示:

from collections import defaultdict
from typing import (
    Iterable, Callable, Dict, List, TypeVar,
    Iterator, Tuple, cast)
D_ = TypeVar("D_")
K_ = TypeVar("K_")

def partition (
        source: Iterable[D_],
        key: Callable[[D_], K_] = lambda x: cast(K_, x)
    ) -> Iterable[Tuple[K_, Iterator[D_]]]:
    pd: Dict[K_, List[D_]] = defaultdict(list)
    for item in source:
        pd[key(item)].append(item)
    for k in sorted(pd):
        yield k, iter(pd[k])

上述代码按照键值将可迭代对象中的数据分到不同的组中,其中将可迭代对象中源数据的数据类型定义为D_,代表每个数据项。用key()函数从数据项中抽取键值,将返回结果的类型定义为K_,以与数据项的类型D_做区分。从示例数据可以看出,每个数据项是一个元组(类型标识符为tuple),每个键值是一个字符串(类型标识符为str),键值抽取参数作为可调用函数,将元组类型转换为字符串类型。

key()函数从数据项中抽取的字符串作为了pd字典的键值,将数据项追加到了对应的列表中。defaultdict对象将类型为K_的键值映射到了类型为List[D_]的数据列表上。

上述函数返回结果的数据结构与itertools.groupby()函数的一致,都是由(group key, iterator)元组组成的可迭代序列,其中分组键值的类型由键值函数的返回值的类型决定,迭代器中则包含一系列原始数据项。

基于itertools.groupby()实现的分组函数如下所示:

from itertools import groupby

def partition_s(
        source: Iterable[D_],
        key: Callable[[D_], K_] = lambda x: cast(K_, x)
    ) -> Iterable[Tuple[K_, Iterator[D_]]]:
    return groupby(sorted(source, key=key), key)

注意:上述两种实现的输入部分有个重要的差别:groupby()函数要求数据必须是按键值排序好的,defaultdict则不需要事先排序。对于大型数据集,不论从时间上还是存储空间上排序,成本都会非常高。

接下来使用上面定义的函数对数据进行分组,可以把该操作作为分组过滤的预处理或分组统计的预处理。

>>> for key, group_iter in partition(data, key=lambda x:x[0]):
... print(key, tuple(group_iter))
1 (('1', 4.0), ('1', 4.6), ('1', 5.8), ('1', 6.9), ('1', 5.4))
2 (('2', 8.3), ('2', 6.5), ('2', 6.8), ('2', 7.8), ('2', 9.2))
3 (('3', 9.3), ('3', 10.5), ('3', 8.1), ('3', 8.0), ('3', 6.9))
4 (('4', 6.1), ('4', 5.6), ('4', 3.8), ('4', 6.2), ('4', 5.8))

对分组数据的统计汇总如下所示:

mean = lambda seq: sum(seq)/len(seq)
var = lambda mean, seq: sum((x-mean)**2/mean for x in seq)

Item = Tuple[K_, float]
def summarize(
        key_iter: Tuple[K_, Iterable[Item]]
    ) -> Tuple[K_, float, float]:
    key, item_iter = key_iter
    values = tuple(v for k, v in item_iter)
    m = mean(values)
    return key, m, var(m, values)

partition()函数的返回结果是一系列(key, iterator)二元组,summarize()函数接收这类二元组,从输入数据项中抽取键值和数据迭代器。上面的实现将数据项的类型定义为Item,包含一个类型为K_的键值和一个可以转换为浮点数的数值。为了从item_iter迭代器中抽取出数值部分,使用生成器表达式得到一个仅包含值的元组。

还可以使用(snd, item_iter)实现从二元组中抽取第二项,并且snd = lambda x: x[1]snd这个名字是从元组中抽取第二项中“第二”(second)的简写。

summarize()函数应用于每个分组,如下所示:

>>> partition1 = partition(data, key=lambda x: x[0])
>>> groups1 = map(summarize, partition1)

或者使用另一个实现方案,如下所示:

>>> partition2 = partition_s(data, key=lambda x: x[0])
>>> groups2 = map(summarize, partition2)

两种方法都能计算出每个分组的统计汇总值,计算结果如下所示:

1 5.34 0.93
2 7.72 0.63
3 8.56 0.89
4 5.5 0.7

这里算出的方差可用于卡方检验,用于确定数据集的零假设是否成立。零假设指数据不包含有价值的信息:方差是完全随机的。还可以对数据做组间比较,确定各组平均值的变化是否符合零假设,或者存在某种统计意义上的显著变化。

Python教程

Java教程

Web教程

数据库教程

图形图像教程

大数据教程

开发工具教程

计算机教程