分组进阶
本教程共 54 篇 · 第 40 篇 · 更新于 2026-08-11 · 约 6 分钟阅读
本节目标:掌握多级分组、时间分组、分组与排序和窗口的组合用法,学会让分组代码跑得更快。
前三章的分组基础足够日常使用。这一章解决更复杂的分组场景:多级索引、时间频率分组、组内排序、分组加窗口,以及性能注意事项。
多级分组:按索引层级分组
数据带 MultiIndex 时,可以直接按层级分组。先造一个多级索引的 Series:
import pandas as pd
import numpy as np
index = pd.MultiIndex.from_arrays(
[["bar", "bar", "baz", "baz", "foo", "foo"],
["one", "two", "one", "two", "one", "two"]],
names=["first", "second"],
)
s = pd.Series([1, 2, 3, 4, 5, 6], index=index)
按第一层分组:
s.groupby(level=0).sum()
也可以按层级名分组:
s.groupby(level="second").sum()
一次按多个层级:
s.groupby(level=["first", "second"]).sum()
Notegroupby 的键可以是列名,也可以是索引层级名。列名和层级名重名时会报 ValueError,用
pd.Grouper(level=...)显式指定层级即可。
时间分组:pd.Grouper
想把”日期”按周、按月归组,用 pd.Grouper。它和 resample 类似,但能和其他分组键组合:
df = pd.DataFrame({
"日期": pd.date_range("2024-01-01", periods=6, freq="D"),
"产品": ["A", "B", "A", "B", "A", "B"],
"销量": [10, 8, 15, 9, 20, 12],
})
df.groupby([pd.Grouper(key="日期", freq="W"), "产品"])["销量"].sum()
按周(W)把日期切成桶,再叠加产品分组。freq 常用取值:“D”(天)、“W”(周)、“ME”(月)、“QE”(季)、“YE”(年)。
Tipresample 只能按时间索引分组;Grouper 的时间字段可以是一列(key),还能和其他列组合。时间列 + 其他键的场景,Grouper 更顺手。
如果日期本身是索引,用 level 代替 key:
df2 = df.set_index("日期")
df2.groupby([pd.Grouper(freq="ME"), "产品"])["销量"].sum()
组内编号:cumcount 与 ngroup
想给每行标上”这是组里第几个”,用 cumcount;想给每组一个全局编号,用 ngroup:
df2.groupby("产品").cumcount() # 组内第几个,从 0 数起
df2.groupby("产品").ngroup() # 组的编号,从 0 数起
cumcount 常用来给重复行做区分,比如同一产品的多次记录编号后做透视;ngroup 则适合把分组结果喂给只认整数编号的模型或算法。
分组与排序
分组键默认排序(sort=True)。传 sort=False 让组按出现顺序排列,组多时能省一点时间:
df.groupby("产品", sort=False)["销量"].sum()
组内行的顺序不会被改变:每组里的行保持原表的相对顺序。
需要”组内按某列排序后再取”时,先 sort_values 再分组:
df.sort_values("销量", ascending=False).groupby("产品").head(2)
每组销量最高的两行就取出来了。这个”先排序再分组取前 N”的组合,是取每组 Top N 的标准套路。
分组 + 窗口
groupby 可以和 rolling、expanding、resample 组合,实现”每个组内做滚动计算”:
df2.groupby("产品")["销量"].rolling(2).mean()
按产品分组,组内取 2 期滚动均值。结果索引变成(产品, 原索引)两层。expanding 同理,做组内累计:
df2.groupby("产品")["销量"].expanding().sum()
分组后重采样也能做,先分组再按日补齐:
ts = pd.DataFrame({
"日期": pd.date_range("2024-01-01", periods=4, freq="W"),
"组": [1, 1, 2, 2],
"值": [5, 6, 7, 8],
}).set_index("日期")
ts.groupby("组").resample("1D").ffill()
窗口计算的细节在 §42、§43、§46 展开,这里先记住”能组合”这个能力。
性能提示
分组操作的性能差异可以很大,几条实用原则:
- 先选列再聚合:
df.groupby("产品")["销量"].sum()比df.groupby("产品").sum()["销量"]快 - 内置方法优先于 apply/lambda:内置方法有专门优化,UDF 逐组调 Python 函数慢得多
- 组多且顺序无所谓时,
sort=False可以提速 - 分组键是 Categorical 时,3.0 起默认
observed=True,只返回实际出现的组;想保留所有类别,显式传observed=False - 组内计算优先用 transform 组合内置方法(如
transform("mean")再相减),而不是写自定义函数
Warning分组 + 自定义函数是性能陷阱:函数每调用一次都重新解释执行,百万行数据下可能慢百倍。先想能不能拆成内置方法链,实在不行再写自定义函数。
分组链式操作
分组结果可以继续接方法链,比如聚合后整理:
(df.groupby(["产品", "日期"])["销量"].sum()
.reset_index()
.sort_values("销量", ascending=False))
聚合、展开、排序一次完成。这类写法在后面的实战里很常见。
小结
多级分组用 level、时间分组用 Grouper、组内编号用 cumcount/ngroup、组内取 Top N 先排序、窗口计算直接组合。性能上记住”内置方法优先、先选列再聚合”,分组分析就够用了。