跳转到内容

3.3.7 分组与聚合

Pandas GroupBy 分组聚合流程图

  • 理解 groupby 的”拆分—应用—合并”机制
  • 掌握常用聚合函数和 agg 方法
  • 学会分组转换(transform)和分组过滤(filter)
  • 掌握数据透视表(pivot_table)

groupby 更适合按“拆分 -> 应用 -> 合并”来理解:

flowchart LR
A["原始表"] --> B["按某个字段拆成多组"]
B --> C["每组单独做统计"]
C --> D["把结果重新合起来"]

所以这节真正想解决的是:

  • 为什么 groupby 能把很多“按部门 / 按类别 / 按月份统计”的题一把抓住
  • agg / transform / filter / pivot_table 各自到底在补什么

回想第 1 章——用纯 Python 做”按性别统计生存率”,需要手写字典和循环。用 Pandas 的 groupby 一行搞定

# 纯 Python:15 行代码
# Pandas:1 行
df.groupby("Sex")["Survived"].mean()

groupby 就像 SQL 的 GROUP BY——按某个字段分组,然后对每组分别计算。

你可以把 groupby 理解成:

  • 先把同类东西分成几堆,再分别数、算、比较

比如:

  • 按部门分
  • 按城市分
  • 按月份分

这会比你脑子里一直想着“Pandas 语法”更容易抓住本质。


flowchart LR
A["原始数据"] --> B["拆分<br/>Split"]
B --> C1["组1"]
B --> C2["组2"]
B --> C3["组3"]
C1 --> D["应用<br/>Apply"]
C2 --> D
C3 --> D
D --> E["合并<br/>Combine"]
E --> F["结果"]
import pandas as pd
import numpy as np
df = pd.DataFrame({
"部门": ["技术", "市场", "技术", "管理", "市场", "技术", "管理"],
"姓名": ["张三", "李四", "王五", "赵六", "钱七", "孙八", "周九"],
"薪资": [15000, 18000, 22000, 35000, 20000, 19000, 30000],
"年龄": [22, 28, 25, 35, 30, 24, 40]
})
# 按部门分组,计算平均薪资
result = df.groupby("部门")["薪资"].mean()
print(result)
# 部门
# 市场 19000.0
# 技术 18666.7
# 管理 32500.0
grouped = df.groupby("部门")
# 常用聚合函数
print(grouped["薪资"].sum()) # 总薪资
print(grouped["薪资"].mean()) # 平均薪资
print(grouped["薪资"].median()) # 中位数
print(grouped["薪资"].min()) # 最低薪资
print(grouped["薪资"].max()) # 最高薪资
print(grouped["薪资"].std()) # 标准差
print(grouped["薪资"].count()) # 人数
# 对多列聚合
print(df.groupby("部门")[["薪资", "年龄"]].mean())
# 薪资 年龄
# 部门
# 市场 19000.0 29.000000
# 技术 18666.7 23.666667
# 管理 32500.0 37.500000
df2 = pd.DataFrame({
"部门": ["技术", "技术", "市场", "市场", "技术", "市场"],
"级别": ["初级", "高级", "初级", "高级", "初级", "初级"],
"薪资": [15000, 25000, 12000, 22000, 18000, 14000]
})
# 按部门和级别分组
result = df2.groupby(["部门", "级别"])["薪资"].mean()
print(result)
# 部门 级别
# 市场 初级 13000.0
# 高级 22000.0
# 技术 初级 16500.0
# 高级 25000.0

第一次做分组题时,最稳的默认顺序

Section titled “第一次做分组题时,最稳的默认顺序”

更稳的顺序通常是:

  1. 先问自己按谁分
  2. 再问每组里要算什么
  3. 最后再决定返回的是汇总表,还是要写回原表

这一步很关键,因为它会直接决定你后面该用:

  • agg
  • transform
  • filter

哪一种。


agg 让你对同一列或不同列应用不同的聚合函数:

# 对薪资列同时计算多个统计量
result = df.groupby("部门")["薪资"].agg(["mean", "min", "max", "count"])
print(result)
# mean min max count
# 部门
# 市场 19000.0 18000 20000 2
# 技术 18666.7 15000 22000 3
# 管理 32500.0 30000 35000 2
# 对不同列用不同的聚合函数
result = df.groupby("部门").agg({
"薪资": ["mean", "max"],
"年龄": ["mean", "min"],
"姓名": "count" # 人数
})
print(result)
# 自定义聚合函数
result = df.groupby("部门")["薪资"].agg(
平均薪资="mean",
最高薪资="max",
薪资差距=lambda x: x.max() - x.min()
)
print(result)

当你脑子里的问题长得像:

  • “每个部门的平均值、最大值、人数分别是多少?”

这时通常就该先想到:

  • groupby(...).agg(...)

也就是说,agg 最适合:

  • 一次性做多种汇总统计

transform 对每组应用函数,但返回和原数据同样长度的结果——非常适合生成新列。

# 场景:给每个人标注"与部门平均薪资的差距"
df["部门平均薪资"] = df.groupby("部门")["薪资"].transform("mean")
df["薪资差距"] = df["薪资"] - df["部门平均薪资"]
print(df[["姓名", "部门", "薪资", "部门平均薪资", "薪资差距"]])
# 场景:组内标准化(每组减去均值除以标准差)
df["薪资_标准化"] = df.groupby("部门")["薪资"].transform(
lambda x: (x - x.mean()) / x.std() if x.std() > 0 else 0
)

一个很适合初学者先记的对比表

Section titled “一个很适合初学者先记的对比表”
方法最值得先记住的返回结果
agg每组一条汇总结果
transform行数不变,只是补一列“组内统计”
filter把整组留下或删掉
pivot_table把结果整理成交叉表

这个表特别适合新人,因为它能把最容易混的几种方式重新拉开。


filter 根据条件保留或排除整个组:

# 只保留平均薪资 > 20000 的部门
result = df.groupby("部门").filter(lambda x: x["薪资"].mean() > 20000)
print(result)
# 只有"管理"部门的平均薪资 > 20000,所以只保留管理部门的人
# 只保留人数 >= 3 的部门
result = df.groupby("部门").filter(lambda x: len(x) >= 3)
print(result)

透视表是 Excel 用户最熟悉的功能——Pandas 也完美支持。

# 准备销售数据
sales = pd.DataFrame({
"日期": ["1月", "1月", "2月", "2月", "1月", "2月"],
"商品": ["苹果", "牛奶", "苹果", "牛奶", "面包", "面包"],
"销量": [50, 30, 60, 25, 40, 45],
"金额": [250, 240, 300, 200, 120, 135]
})
# 透视表:每月每种商品的总销量
pivot = pd.pivot_table(
sales,
values="销量", # 聚合的值
index="商品", # 行
columns="日期", # 列
aggfunc="sum" # 聚合方式
)
print(pivot)
# 日期 1月 2月
# 商品
# 牛奶 30 25
# 苹果 50 60
# 面包 40 45
# 多个聚合
pivot2 = pd.pivot_table(
sales,
values="金额",
index="商品",
columns="日期",
aggfunc=["sum", "mean"],
margins=True # 添加合计行和列
)
print(pivot2)
# 统计部门和级别的人数分布
ct = pd.crosstab(df2["部门"], df2["级别"])
print(ct)
# 级别 初级 高级
# 部门
# 市场 2 1
# 技术 2 1
# 加上合计和占比
ct2 = pd.crosstab(df2["部门"], df2["级别"], margins=True, normalize="index")
print(ct2) # 每行占比(各部门中初级/高级的比例)

import pandas as pd
import numpy as np
rng = np.random.default_rng(seed=42)
n = 200
orders = pd.DataFrame({
"月份": rng.choice(["1月", "2月", "3月", "4月"], n),
"区域": rng.choice(["华东", "华南", "华北", "西南"], n),
"商品": rng.choice(["手机", "电脑", "耳机", "平板"], n),
"销量": rng.integers(1, 50, n),
"单价": rng.choice([99, 299, 999, 2999, 5999], n)
})
orders["金额"] = orders["销量"] * orders["单价"]
# 1. 每个区域的总销售额
print(orders.groupby("区域")["金额"].sum().sort_values(ascending=False))
# 2. 每种商品的平均销量和总金额
print(orders.groupby("商品").agg(
平均销量=("销量", "mean"),
总金额=("金额", "sum"),
订单数=("金额", "count")
))
# 3. 透视表:区域 × 商品的总金额
print(pd.pivot_table(orders, values="金额", index="区域", columns="商品", aggfunc="sum"))
# 4. 每个月销售额最高的区域
monthly_top = orders.groupby(["月份", "区域"])["金额"].sum().reset_index()
idx = monthly_top.groupby("月份")["金额"].idxmax()
print(monthly_top.loc[idx])

学完这一页,至少保留这张证据卡:

数据框状态
列、数据类型、行数、缺失值和样本行
操作
读/写、select/filter、清洗、转换、groupby、merge,或时间序列步骤
输出
结果表、保存的文件、聚合、连接结果,或时间索引视图
失败检查
dtype 不匹配、缺失数据、重复键、链式赋值或时间频率错误
期望产出
前后对比表格样本,以及转换原因
操作方法返回行数用途
基本聚合groupby().mean()组数汇总统计
多重聚合groupby().agg()组数多种统计量
分组转换groupby().transform()原始行数生成新列
分组过滤groupby().filter()≤ 原始行数按条件保留组
透视表pivot_table()行值种数交叉统计
交叉表crosstab()行值种数频次统计

# 用上面的 orders 数据
# 1. 按月份统计平均客单价(金额/销量)
# 2. 哪个月份哪种商品的销量最高?
# 3. 每个区域卖得最好的商品是什么?
# 1. 给每个订单添加"区域平均金额"列
# 2. 标记每个订单的金额是否高于其所在区域的平均水平
# 3. 计算每个订单金额占其区域总金额的百分比
# 1. 创建一个透视表:行=区域,列=月份,值=总金额,带合计
# 2. 哪个区域在哪个月份的销售额最高?
参考实现与讲解
  • 客单价是总金额除以订单数,所以要同时计算分子和分母,不要对已经平均过的行再求平均。
  • 查每月或每地区最佳产品时,先聚合到正确粒度,再排序或用 idxmax。直接取原始行最大值,在同一产品出现多次时会出错。
  • 当每条原始记录都需要附上组级信息时用 transform,例如地区均值或月销售占比;当输出应为每组一行时用 agg