承接前面数据合并、拼接章节,本篇系统讲解 Pandas groupby 分组全套用法,包含分组原理、分组对象、聚合、转换、过滤、应用、分组迭代、多层分组、分组排序、实战场景与坑点,和之前的 groupby 进阶篇互为补充,保持系列教程统一风格。
前置导入与示例数据
import pandas as pd
import numpy as np
df = pd.DataFrame({
"region": ["华东","华东","华南","华南","华北","华北","华东","华南"],
"cate": ["手机","电脑","手机","电脑","手机","电脑","手机","电脑"],
"sales": [1200, 800, 1500, 950, 1100, 780, 1350, 1020],
"profit": [180, 120, 220, 140, 160, 110, 195, 150]
})
print(df)
一、groupby 核心思想:Split-Apply-Combine
- Split(拆分):按照指定字段把 DataFrame 切分成多组
- Apply(应用):对每一组执行函数(聚合、转换、过滤、自定义逻辑)
- Combine(合并):把各组计算结果合并成新的 DataFrame / Series
# 基础分组,返回分组对象(不是直接结果!)
g = df.groupby("region")
print(g)
groupby 不会立刻计算,是惰性求值,访问结果时才执行。
二、基础聚合 agg
2.1 单个聚合函数
# 按地区分组,销售额求和
g["sales"].sum()
2.2 多个聚合函数
# 同时求总和、均值、最大值
g["sales"].agg(["sum", "mean", "max"])
2.3 不同字段使用不同聚合(业务高频)
res = g.agg(
sales_sum=("sales", "sum"),
profit_avg=("profit", "mean")
)
print(res)
2.4 自定义聚合函数
def range_val(x):
return x.max() - x.min()
res = g["sales"].agg(["sum", range_val])
print(res)
三、多字段分组(多层分组)
同时按 region + cate 两层分组,生成 MultiIndex
g2 = df.groupby(["region", "cate"])
res2 = g2["sales"].sum()
print(res2)
# 重置索引,转为普通表格,导出Excel常用
res2 = res2.reset_index()
四、分组三大操作:agg / transform / filter
4.1 agg:一组输出一行(聚合)
用于汇总统计,行数 = 分组数量
df.groupby("region")["sales"].agg("sum")
4.2 transform:分组计算,返回和原表行数一样(组内特征)
非常适合组内标准化、组内占比、组内均值回填到每一行
# 每个地区的销售均值回填到本行
df["region_sales_mean"] = df.groupby("region")["sales"].transform("mean")
# 组内占比
df["sales_pct"] = df["sales"] / df.groupby("region")["sales"].transform("sum")
print(df)
4.3 filter:按组条件过滤,保留符合条件的整组
对整个分组做判断,满足条件才保留该分组全部数据
# 只保留分组销售总和大于 2000 的地区
df_filter = df.groupby("region").filter(lambda x: x["sales"].sum() > 2000)
print(df_filter)
区分:布尔索引是筛选行;filter 是筛选整组
五、apply 分组自定义函数
apply 传入自定义函数,函数入参是该分组的子DataFrame
def group_top1(sub_df):
# 每组取销售额最高一行
return sub_df.sort_values("sales", ascending=False).head(1)
res_apply = df.groupby("region").apply(group_top1).reset_index(drop=True)
print(res_apply)
⚠️ apply 性能偏弱,优先用 agg / transform / 内置函数,大量数据避免滥用。
六、分组迭代遍历
循环取出每一组的名称和子表,适合自定义复杂处理
for name, sub in df.groupby("region"):
print(f"分组名称:{name}")
print(sub)
多字段分组时 name 是元组:
for key, sub in df.groupby(["region","cate"]):
print(key)
七、groupby 参数详解
df.groupby(
by,
axis=0,
level=None,
as_index=True,
sort=True,
group_keys=True,
observed=False,
dropna=True
)
as_index=True(默认):分组字段作为结果索引;as_index=False分组字段保留为普通列,不用反复 reset_index
# as_index=False,直接返回普通DataFrame
res = df.groupby("region", as_index=False)["sales"].sum()
sort=True:对分组key排序;不需要排序时设为False,提升性能dropna=True:默认把分组key为NaN的行丢弃;dropna=False 会单独分出NaN分组group_keys:搭配apply时,控制是否添加分组键到索引
八、分组内排序
# 每个地区内部,按销售额降序
df["sales_rank_in_region"] = df.groupby("region")["sales"].rank(ascending=False, method="min")
# 分组内排序
df_sort = df.groupby("region", group_keys=False).apply(lambda x: x.sort_values("sales", ascending=False))
print(df_sort)
九、分组和窗口函数的区别
- groupby + agg:每组输出一条汇总结果
- groupby + rolling:分组内滚动窗口(每个用户独立3日移动平均)
# 分组内rolling,时序高频
df["ma3"] = df.groupby("region")["sales"].rolling(window=3, min_periods=1).mean().reset_index(drop=True)
十、高频坑清单
- groupby 后忘记
as_index=False,每次都要 reset_index - transform 和 agg 混淆:transform 行数和原表一致,agg行数等于分组数
- filter 返回的是原始行子集,不是汇总统计
- apply 速度慢,大数据量优先替换为内置聚合函数
- 分组key存在NaN,默认被dropna=True丢弃,需要单独统计NaN分组时设置 dropna=False
- 多字段分组后索引是MultiIndex,取值需要元组
- 分组内rolling一定要注意数据顺序,提前sort_values
十一、综合实战模板
def group_summary(df, group_cols, agg_map):
"""分组汇总模板
agg_map: {"新列名": ("原列", "聚合函数")}
"""
return df.groupby(group_cols, as_index=False).agg(**agg_map)
def add_group_ratio(df, group_cols, val_col):
"""增加组内占比"""
group_sum = df.groupby(group_cols)[val_col].transform("sum")
df[f"{val_col}_ratio"] = df[val_col] / group_sum
return df
summary = group_summary(df, ["region","cate"], {
"sales_total": ("sales", "sum"),
"profit_avg": ("profit", "mean")
})
df = add_group_ratio(df, ["region"], "sales")
print(summary)
十二、速查表
| 需求 | 写法 |
|---|---|
| 分组汇总求和、均值 | groupby + agg |
| 把分组统计值回填到每一行 | groupby + transform |
| 满足条件保留整组 | groupby + filter |
| 每组自定义复杂逻辑 | groupby + apply |
| 分组字段不变成索引 | groupby(…, as_index=False) |
| 分组内滚动窗口 | groupby + rolling |
0 条笔记