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