说明:本文属于Pandas系列进阶教程,前置基础:groupby基础分组、agg、apply。本篇重点讲解多聚合、命名聚合、分组过滤、分组转换、滚动分组、多级索引分组、时间分组、自定义聚合函数、性能避坑,偏向生产数据分析场景。
测试数据统一使用下面示例DataFrame,方便直接复制运行。
1. 环境准备 & 测试数据集
pip install pandas
import pandas as pd
import numpy as np
df = pd.DataFrame({
"region": ["华东","华东","华南","华南","华东","华南","华东","华南"],
"product": ["A","A","A","B","B","B","A","B"],
"sales": [120, 200, 90, 150, 180, 220, 130, 170],
"cost": [80, 110, 50, 90, 100, 130, 75, 95],
"sale_date": pd.to_datetime([
"2026-01-01","2026-01-02","2026-01-01","2026-01-01",
"2026-01-02","2026-01-02","2026-01-03","2026-01-03"
])
})
print(df)
2. 基础回顾
# 按region分组,sales求和
df.groupby("region")["sales"].sum()
groupby 核心流程:拆分(split) → 应用(apply) → 合并(combine)
3. 多列分组 + 多聚合(重点)
3.1 普通agg,多指标聚合
# 按区域+产品分组,销售额求和、均值、数量
res = df.groupby(["region","product"])["sales"].agg([
"sum", "mean", "count", "max"
])
print(res)
缺点:生成多级列名,后续重命名麻烦
3.2 命名聚合(推荐,Pandas 0.25+ 新语法 ✅)
可以直接指定输出列名,生产最常用:
res = df.groupby(["region","product"]).agg(
sales_total = ("sales", "sum"),
sales_avg = ("sales", "mean"),
sales_cnt = ("sales", "count"),
cost_total = ("cost", "sum")
)
print(res)
3.3 不同字段使用不同聚合函数
res = df.groupby("region").agg({
"sales": ["sum", "max"],
"cost": ["min", "sum"]
})
print(res)
注意:这种写法会产生复合列,推荐配合
.columns扁平化
res.columns = ["_".join(col) for col in res.columns]
res = res.reset_index()
print(res)
4. 分组过滤 filter(分组级筛选,不是行筛选)
filter:保留满足条件的整个分组,返回原始行数据
区别:普通df[]是筛选行;filter是按分组整体条件过滤
示例:只保留分组内销售额总和大于300的区域所有明细行
# 保留分组sum(sales) > 300 的分组全部行
res = df.groupby("region").filter(lambda g: g["sales"].sum() > 300)
print(res)
业务场景:只保留“总销量达标”的地区全部明细
5. 分组转换 transform(同分组内计算,返回和原df等长序列)
非常高频!计算分组统计值,回填到原表每一行,不减少行数
- agg:一组输出一行
- transform:一组内每一行都输出同一个统计值
示例:增加一列,每个区域的区域总销售额
df["region_sales_sum"] = df.groupby("region")["sales"].transform("sum")
df["region_sales_avg"] = df.groupby("region")["sales"].transform("mean")
# 计算单品占区域销售额比重
df["ratio"] = df["sales"] / df["region_sales_sum"]
print(df)
经典场景:分组占比、分组排名、分组归一化
6. 分组 apply 自定义函数(灵活,注意性能)
apply 对每个分组的子DataFrame执行自定义函数
# 自定义函数:计算毛利
def calc_profit(g):
total_sales = g["sales"].sum()
total_cost = g["cost"].sum()
profit = total_sales - total_cost
return pd.Series({
"sales_total": total_sales,
"cost_total": total_cost,
"profit": profit
})
res = df.groupby(["region","product"]).apply(calc_profit, include_groups=False)
print(res)
⚠️ 坑:apply 速度慢,大数据优先用 agg / transform 内置函数,不要无脑apply
7. 分组窗口:分组内排序、分组内排名
# 分组内按销售额降序排名
df["rank_in_group"] = df.groupby("region")["sales"].rank(method="min", ascending=False)
print(df)
rank method说明:
min:同分同名次,下一名跳跃dense:同分同名次,名次连续first:按出现先后区分排名
8. 时间分组(Grouper,时间维度聚合)
pd.Grouper 专门用于时间频率分组,不用手动构造年月列
# 按天分组
res_day = df.groupby(pd.Grouper(key="sale_date", freq="D"))["sales"].sum()
# 按月分组 freq="M",按周W,按季度Q
res_month = df.groupby(pd.Grouper(key="sale_date", freq="M"))["sales"].sum()
print(res_day)
freq常用:D天、W周、M月末、MS月初、Q季度、Y年
搭配多维度:区域+日期分组
res = df.groupby(["region", pd.Grouper(key="sale_date", freq="D")])["sales"].sum()
9. 多级索引分组、重置索引
groupby默认会把分组字段作为索引,很多时候需要转成普通列:
res = df.groupby(["region","product"]).agg(sales_sum=("sales","sum")).reset_index()
print(res)
reset_index():把分组索引变回普通列as_index=False等价写法:
res = df.groupby(["region","product"], as_index=False).agg(sales_sum=("sales","sum"))
10. 分组滚动统计(groupby + rolling)
分组内滚动窗口,比如每个区域内,最近2条记录滚动求和
# 先排序,再分组滚动
df = df.sort_values(["region","sale_date"])
df["rolling_2"] = df.groupby("region")["sales"].rolling(window=2, min_periods=1).sum().reset_index(drop=True)
print(df)
注意:rolling 一定要先对分组内时间排序,否则结果错乱
11. 自定义聚合函数传入 agg
可以直接在agg里写简单自定义函数,比apply更快
def p90(x):
return np.percentile(x, 90)
res = df.groupby("region")["sales"].agg(
sum_sales="sum",
p90_sales=p90
)
print(res)
12. 分组对象常用方法
g = df.groupby("region")
g.groups # 返回字典:分组key -> 行索引列表
g.get_group("华东") # 取出指定分组的子DataFrame
g.size() # 每个分组行数(计数,包含NaN)
g.count() # 每个分组非空值数量
g.nunique() # 分组内唯一值数量
13. 性能对比 & 避坑清单(非常重要)
- 优先内置聚合函数(sum/mean/max/count),最慢是apply
内置聚合是C实现;apply是Python循环,百万行以上差距巨大 - 分组字段尽量用category类型,分组速度大幅提升
df["region"] = df["region"].astype("category") - 分组后不要频繁链式索引,尽量一次agg完成
- transform 适合回填统计值,不要用apply实现transform功能
- filter 会复制数据,大数据慎用;优先先聚合再join回来
- 时间分组必须保证时间列是datetime64,字符串日期Grouper无效
- 多列分组,列顺序会影响结果,先粗粒度再细粒度
14. 综合实战:分组统计 + 占比 + 排名 + 导出
import pandas as pd
df = pd.DataFrame({
"region": ["华东","华东","华南","华南","华东","华南","华东","华南"],
"product": ["A","A","A","B","B","B","A","B"],
"sales": [120, 200, 90, 150, 180, 220, 130, 170],
"cost": [80, 110, 50, 90, 100, 130, 75, 95],
"sale_date": pd.to_datetime([
"2026-01-01","2026-01-02","2026-01-01","2026-01-01",
"2026-01-02","2026-01-02","2026-01-03","2026-01-03"
])
})
# 1. 分组汇总
group_summary = df.groupby(["region","product"], as_index=False).agg(
sales_total=("sales","sum"),
cost_total=("cost","sum")
)
group_summary["profit"] = group_summary["sales_total"] - group_summary["cost_total"]
# 2. 计算区域总销售额,join回汇总表
region_sum = df.groupby("region", as_index=False)["sales"].sum().rename(columns={"sales":"region_total"})
group_summary = group_summary.merge(region_sum, on="region")
group_summary["ratio"] = group_summary["sales_total"] / group_summary["region_total"]
# 3. 区域内按利润排名
group_summary["profit_rank"] = group_summary.groupby("region")["profit"].rank(ascending=False, method="dense")
print(group_summary)
group_summary.to_excel("group_result.xlsx", index=False)
15. 选型速记
- 分组汇总指标 →
agg(命名聚合优先) - 分组内每行回填统计值、占比 →
transform - 保留符合条件分组的全部明细行 →
filter - 复杂自定义逻辑、多列联动计算 →
apply - 按年月日季度聚合 →
pd.Grouper - 分组内排序、打分 →
rank - 分组内滑动窗口指标 →
groupby + rolling
0 条笔记