自学教程 学编程从这里开始

Pandas groupby 分组聚合进阶

说明:本文属于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. 性能对比 & 避坑清单(非常重要)

  1. 优先内置聚合函数(sum/mean/max/count),最慢是apply

    内置聚合是C实现;apply是Python循环,百万行以上差距巨大
  2. 分组字段尽量用category类型,分组速度大幅提升
    df["region"] = df["region"].astype("category")

  3. 分组后不要频繁链式索引,尽量一次agg完成
  4. transform 适合回填统计值,不要用apply实现transform功能
  5. filter 会复制数据,大数据慎用;优先先聚合再join回来
  6. 时间分组必须保证时间列是datetime64,字符串日期Grouper无效
  7. 多列分组,列顺序会影响结果,先粗粒度再细粒度

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 条笔记