第10章封面

如果说 pandas 有一个「灵魂方法」,那一定是 groupby。第 10 章把它的机制拆到骨骼:拆分 → 应用 → 合并(split-apply-combine)。这一章读完,你处理「按 X 分组统计 Y」类问题的能力会有质的飞跃。

🪄 心智模型:拆分 → 应用 → 合并

任何 groupby 操作都经历三步:

  1. 拆分:按一个或多个键,把表切成若干小组;
  2. 应用:对每组独立地算一个函数(求和、均值、任意函数……);
  3. 合并:把每组结果拼成一张新表。

split-apply-combine

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_tablemargins=True 出小计;crosstab 快速看两个类别的联合频数。