如果说 pandas 有一个「灵魂方法」,那一定是 groupby。第 10 章把它的机制拆到骨骼:拆分 → 应用 → 合并(split-apply-combine)。这一章读完,你处理「按 X 分组统计 Y」类问题的能力会有质的飞跃。
🪄 心智模型:拆分 → 应用 → 合并
任何 groupby 操作都经历三步:
- 拆分:按一个或多个键,把表切成若干小组;
- 应用:对每组独立地算一个函数(求和、均值、任意函数……);
- 合并:把每组结果拼成一张新表。
df.groupby("key1")["data1"].mean()
# 等价于:按 key1 拆组 → 每组对 data1 求 mean → 拼成一个 Series
这个抽象的威力在于:中间那步「应用」可以换成任何东西——一个聚合函数、一个变换、甚至一个建模过程,框架完全不变。
🔑 分组键:五种来源
# ① 列名(最常用)
df.groupby("key1")
# ② 列名列表(多层分组 → MultiIndex 结果)
df.groupby(["key1", "key2"]).mean()
# ③ 任意数组/列表:长度对得上就行
means = df["data1"].groupby([states, years]).mean()
# ④ 字典:把索引映射到组
mapping = {"a": "red", "b": "red", "c": "blue"}
by_column = people.groupby(mapping, axis=1).sum()
# ⑤ 函数:对索引逐个调用,返回值当组名
people.groupby(len).sum() # 按索引字符串长度分组
people.groupby(lambda x: x.year) # 按索引的年份分组(时序神器)
对 DataFrame 分组后,用 df.groupby("key")["col"] 选列、df.groupby("key")[["c1","c2"]] 选多列——选列先于计算能省大量内存。
📐 agg:一次算一堆
grouped.agg(["mean", "std", ("极差", lambda x: x.max()-x.min())])
# 一次出三列,还能顺手起名
# 每列不同函数
grouped.agg({"tip": "max", "size": "sum", "total_bill": "mean"})
# 返回不带分组索引的扁平表
grouped.agg(["mean", "std"], as_index=False)
🔄 transform 与 apply:两个超级形态
这是本章最重要的一节。三者区别一张表说清:
| 形态 | 输入 | 输出形状 | 典型用途 |
|---|---|---|---|
agg |
每组 → 一个数 | 行数变少 | 分组汇总 |
transform |
每组 → 同形状 | 行数不变 | 组内标准化、组内填充 |
apply |
每组 → 任意 | 任意 | 万能逃生舱 |
组内标准化(transform 的名场面):
zscore = lambda x: (x - x.mean()) / x.std()
df.groupby("key")["data"].transform(zscore) # 每个值减本组均值除本组std
组内填充缺失值(书里实战价值最高的例子之一):
filler = lambda g: g.fillna(g.mean())
states.groupby(group_key).apply(filler) # 每组用自己的均值填自己的洞
apply 万能:每组返回 Series/DataFrame/标量都行,pandas 负责拼回去。书里的「分组加权平均」「分组线性回归(每组跑一个 OLS 取斜率)」都是 apply 的表演时刻:
def regress(data, yvar, xvars):
Y = data[yvar]; X = data[xvars].copy()
X["intercept"] = 1.0
result = sm.OLS(Y, X).fit()
return result.params # 每组各自的回归系数
df.groupby("key").apply(regress, "y", ["x"])
其他亮点:quantile 分桶分析(qcut+groupby 组合拳)、groupby.apply(抽样) 做分层随机抽样、group_keys=False 不把组键变成外层索引。
📋 透视表与交叉表
pivot_table 就是「groupby + aggfunc + 分块小计」的语法糖:
tips.pivot_table(index=["day", "smoker"], # 行分组
values=["tip_pct", "size"], # 要算的列
aggfunc=["mean", "count"], # 算什么
margins=True) # 加 All 小计行/列
# 交叉表:分组频数的专用快捷方式
pd.crosstab([tips["time"], tips["day"]], tips["smoker"], margins=True)
📌 本章小结:groupby = 拆分 + 应用 + 合并;分组键可以是列、数组、字典、函数;agg 出汇总(行变少)、transform 保形状(组内标准化/填充)、apply 无所不能;
pivot_table加margins=True出小计;crosstab快速看两个类别的联合频数。