groupby是 pandas 最强大的统计分析武器:按一个或多个键把数据分组,然后对每组做聚合、变换、过滤。它对应 SQL 的GROUP BY,也是"拆分-应用-合并"(split-apply-combine)思想的典范。
目录
1. groupby 思想:拆分-应用-合并
2. 基本用法
3. 聚合:agg
4. 遍历分组
5. 变换:transform
6. 过滤:filter
7. 多键分组
8. apply 与 groupby 的组合
9. 分组排序与 TopN
10. 分组实战案例
11. 常见坑与注意事项
12. 本章小结与练习
1. groupby 思想:拆分-应用-合并
groupby 的经典三步骤:
1. 拆分(split): 按分组键把行分成若干组
2. 应用(apply): 对每组执行函数(聚合/变换/过滤)
3. 合并(combine): 把结果组合起来
import pandas as pd
df = pd.DataFrame({
"部门": ["技术", "技术", "销售", "销售", "市场"],
"姓名": ["张三", "李四", "王五", "赵六", "孙七"],
"工资": [12000, 15000, 9000, 11000, 8000],
})
# 按部门分组,求每组平均工资
result = df.groupby("部门")["工资"].mean()
print(result)
# 部门
# 市场 8000.0
# 技术 13500.0
# 销售 10000.0
# Name: 工资, dtype: float64
2. 基本用法
2.1 选择分组键的方式
# 方式 1:按列名
df.groupby("部门")
# 方式 2:按多个列名
df.groupby(["部门", "城市"])
# 方式 3:按 Series / 数组(长度须等于行数)
df.groupby(df["部门"])
# 方式 4:按自定义规则
df.groupby(lambda idx: df.loc[idx, "工资"] > 10000)
# 方式 5:按索引层级
# df.groupby(level=0)
2.2 查看分组对象
g = df.groupby("部门")
print(type(g)) # <class 'pandas.core.groupby.generic.DataFrameGroupBy'>
print(g.groups) # 每个组的行索引字典
print(g.size()) # 每组行数
print(g.ngroups) # 组数
{'市场': [4], '技术': [0, 1], '销售': [2, 3]}
部门
市场 1
技术 2
销售 2
dtype: int64
2.3 选择要聚合的列
# 对整个分组的数值列聚合
print(df.groupby("部门").mean())
# 只对指定列聚合
print(df.groupby("部门")["工资"].mean())
# 多列
print(df.groupby("部门")[["工资"]].mean())
# 一行内完成:mean/sum/count...
print(df.groupby("部门").sum())
print(df.groupby("部门").count()) # 非空计数
print(df.groupby("部门").size()) # 行数(含 NaN)
print(df.groupby("部门").first()) # 每组第一行
print(df.groupby("部门").last()) # 每组最后一行
print(df.groupby("部门").max())
print(df.groupby("部门").min())
print(df.groupby("部门").std())
count()统计非空值,size()统计行数,两者在有缺失值时结果不同。
3. 聚合:agg
agg(aggregate)是聚合的"瑞士军刀",支持多种聚合方式组合、自定义函数、重命名结果列。
3.1 多个聚合函数
# 对一列做多种聚合
print(df.groupby("部门")["工资"].agg(["sum", "mean", "max", "min", "count"]))
# 对不同列做不同聚合
print(df.groupby("部门").agg({
"工资": ["sum", "mean"],
"姓名": "count",
}))
工资 姓名
sum mean count
部门
市场 8000.0 8000.000000 1
技术 27000.0 13500.000000 2
销售 20000.0 10000.000000 2
3.2 自定义聚合函数
# lambda 或自定义函数
result = df.groupby("部门")["工资"].agg(
total=lambda x: x.sum(),
avg=lambda x: x.mean(),
spread=lambda x: x.max() - x.min(),
)
print(result)
total avg spread
部门
市场 8000.0 8000.0 0.0
技术 27000.0 13500.0 3000.0
销售 20000.0 10000.0 2000.0
3.3 named agg(新式命名聚合)
# pandas 0.25+ 的命名聚合语法
result = df.groupby("部门")["工资"].agg(
平均工资="mean",
最高工资="max",
工资中位数="median",
)
print(result)
平均工资 最高工资 工资中位数
部门
市场 8000.0 8000.0 8000.0
技术 13500.0 15000.0 13500.0
销售 10000.0 11000.0 10000.0
3.4 agg 后重置索引
# groupby 结果默认以分组键为索引
result = df.groupby("部门")["工资"].mean()
print(result.index) # Index(['市场', '技术', '销售'], ...)
# 转回普通列
result2 = result.reset_index()
print(result2.columns) # Index(['部门', '工资'], ...)
4. 遍历分组
g = df.groupby("部门")
for name, group in g:
print(f"===== 部门: {name} =====")
print(group)
print()
===== 部门: 市场 =====
部门 姓名 工资
4 市场 孙七 8000
===== 部门: 技术 =====
部门 姓名 工资
0 技术 张三 12000
1 技术 李四 15000
===== 部门: 销售 =====
部门 姓名 工资
2 销售 王五 9000
3 销售 赵六 11000
遍历常用于:对每组画图、导出每个组到单独文件、复杂的逐组处理。
# 按组分块导出
for name, group in df.groupby("部门"):
group.to_csv(f"{name}.csv", index=False)
5. 变换:transform
transform 与 agg 的关键区别:agg 压缩行数(每组一行),transform 保持行数不变(每组内做广播)。
# 给每个人算"工资 - 部门平均工资"
df["部门平均"] = df.groupby("部门")["工资"].transform("mean")
df["工资差"] = df["工资"] - df["部门平均"]
print(df)
部门 姓名 工资 部门平均 工资差
0 技术 张三 12000 13500.0 -1500.0
1 技术 李四 15000 13500.0 1500.0
2 销售 王五 9000 10000.0 -1000.0
3 销售 赵六 11000 10000.0 1000.0
4 市场 孙七 8000 8000.0 0.0
transform 常见应用:
# 1. 组内归一化
df["工资占比"] = df["工资"] / df.groupby("部门")["工资"].transform("sum")
# 2. 组内排名
df["组内排名"] = df.groupby("部门")["工资"].rank(ascending=False)
# 3. 组内填充缺失值
df["缺失列"] = df.groupby("部门")["工资"].transform(lambda x: x.fillna(x.mean()))
# 4. 组内累计和
df["累计工资"] = df.groupby("部门")["工资"].transform("cumsum")
transform 的灵魂:结果行数与原始数据一一对应,可以直接作为新列加入原表。
6. 过滤:filter
filter 根据整组的条件筛选组(不是筛选行)。
# 只保留"总工资 > 15000"的部门
result = df.groupby("部门").filter(lambda g: g["工资"].sum() > 15000)
print(result)
部门 姓名 工资
0 技术 张三 12000
1 技术 李四 15000
2 销售 王五 9000
3 销售 赵六 11000
(市场组总和 8000 < 15000,整组被过滤掉了)
# 只保留行数 >= 2 的组
df.groupby("部门").filter(lambda g: len(g) >= 2)
# 保留均值 > 10000 的组
df.groupby("部门").filter(lambda g: g["工资"].mean() > 10000)
filter 与布尔索引的区别:布尔索引是"按行条件筛选",filter 是"按组条件筛选整组"。7. 多键分组
df2 = pd.DataFrame({
"部门": ["技术", "技术", "技术", "销售", "销售", "销售"],
"城市": ["北京", "北京", "上海", "北京", "上海", "上海"],
"工资": [12000, 13000, 15000, 9000, 11000, 10000],
})
# 两个键分组
g2 = df2.groupby(["部门", "城市"])
print(g2["工资"].mean())
部门 城市
技术 北京 12500.0
上海 15000.0
销售 北京 9000.0
上海 10500.0
Name: 工资, dtype: Float64
# as_index=False:分组键作为普通列
print(df2.groupby(["部门", "城市"], as_index=False)["工资"].mean())
# reset_index 等价效果
print(df2.groupby(["部门", "城市"])["工资"].mean().reset_index())
# 多种聚合
print(df2.groupby(["部门", "城市"])["工资"].agg(["sum", "mean", "count"]))
多键分组的索引访问
g2 = df2.groupby(["部门", "城市"])["工资"].mean()
# 按第一层
print(g2.loc["技术"])
# 按多层
print(g2.loc[("技术", "北京")])
8. apply 与 groupby 的组合
groupby.apply 可以做任意复杂的"按组处理",但要注意性能与行为差异。
# 每组求最赚钱的员工
def top_earner(group):
return group.nlargest(1, "工资")
print(df.groupby("部门").apply(top_earner))
部门 姓名 工资
部门
市场 4 市场 孙七 8000
技术 1 技术 李四 15000
销售 3 销售 赵六 11000
# apply 返回多行
def summary(group):
return pd.DataFrame({
"组人数": [len(group)],
"总工资": [group["工资"].sum()],
})
print(df.groupby("部门").apply(summary))
注意:apply 是"万能但慢"的方案,能分别用 agg/transform/filter 解决的场景,优先用专用方法。9. 分组排序与 TopN
9.1 按组内某列排序
# 按组排序(先按部门,组内按工资降序)
df_sorted = df.sort_values(["部门", "工资"], ascending=[True, False])
print(df_sorted)
9.2 每组 TopN
# 每组工资最高的 1 人
print(df.groupby("部门").apply(lambda g: g.nlargest(1, "工资")).reset_index(drop=True))
# 每组工资最低的 2 人
print(df.groupby("部门").apply(lambda g: g.nsmallest(2, "工资")).reset_index(drop=True))
# 每组排名列
df["组内排名"] = df.groupby("部门")["工资"].rank(ascending=False)
9.3 分组后整体排序(重新排序组)
# 按组均值的降序排列组
g_mean = df.groupby("部门")["工资"].mean().sort_values(ascending=False)
print(g_mean)
# 技术 13500.0
# 销售 10000.0
# 市场 8000.0
10. 分组实战案例
import pandas as pd
import numpy as np
np.random.seed(1)
sales = pd.DataFrame({
"区域": np.random.choice(["华东", "华南", "华北", "西南"], 200),
"品类": np.random.choice(["数码", "服装", "食品", "家居"], 200),
"金额": np.random.uniform(10, 1000, 200).round(2),
"月份": np.random.choice(["1月", "2月", "3月"], 200),
})
# 1. 每个区域的总销售额(降序)
print(sales.groupby("区域")["金额"].sum().sort_values(ascending=False))
# 2. 区域 × 品类的销售额透视
pt = sales.groupby(["区域", "品类"])["金额"].sum().unstack()
print(pt)
# 3. 每月各区域平均客单价
print(sales.groupby(["月份", "区域"])["金额"].mean().unstack())
# 4. 每个区域金额最高的品类
top_cat = (
sales.groupby(["区域", "品类"])["金额"]
.sum()
.groupby(level=0)
.nlargest(1)
)
print(top_cat)
# 5. 每个区域当月占整体比例
sales["区域月占比"] = sales["金额"] / sales.groupby("月份")["金额"].transform("sum")
print(sales.head())
# 6. 保留订单数 >= 50 的区域
big_regions = sales.groupby("区域").filter(lambda g: len(g) >= 50)
print(big_regions["区域"].value_counts())
11. 常见坑与注意事项
| 坑 | 现象 | 解决办法 |
|----|------|----------|
| 分组后索引多层 | 后续操作困惑 | as_index=False 或 reset_index() |
| 忘记选列 | 所有数值列都被聚合 | 明确 ["工资"] |
| count vs size | 结果不一致 | 确认需求:非空计数用 count,行数用 size |
| agg 后列名混乱 | 多层列名 | 用 named agg 或 reset_index |
| transform 忘赋值 | 结果没保存 | df["新列"] = df.groupby(...).transform(...) |
| apply 太慢 | 大数据卡顿 | 优先 agg/transform,仅复杂场景用 apply |
| 分组键有 NaN | NaN 组被单独分出或丢弃 | 先 dropna() 或 fillna |
12. 本章小结与练习
小结
- 核心流程:拆分 → 应用 → 合并;
agg:多种聚合组合、命名聚合、自定义函数;transform:保持行数的组内变换(归一化、排名、填充、累计);filter:按组条件过滤整组;apply:万能但慢,复杂逻辑兜底;- 多键分组 +
unstack可以生成透视表。
练习题
1. 对销售数据按"区域"分组,求金额的 sum、mean、max。
2. 用 transform 给每个员工计算"工资占部门总工资比例"。
3. 用 filter 保留订单数大于 30 的区域。
4. 按"区域×品类"分组,找出每个区域销售额最高的品类。
5. 用命名聚合(named agg)生成包含 count、sum、mean 的分组结果表。
下一篇预告:第 14 章 时间序列分析 —— 日期处理、重采样、滚动窗口,玩转时间维度数据。
文章回复
0 条公开回复