
本文介绍一种比 `groupby().apply(rolling())` 快约 15 倍的替代方案:利用 `set_index + groupby().rolling()` 链式操作后合并回原表,大幅提升大规模时序分组滚动计算性能。
在处理高频、多维度的时序数据(如赛马历史战绩)时,常需对“ horse–trainer 组合”等复合键,在时间窗口(如过去 180 天)内计算滚动统计量(如平均得分)。原始写法虽语义清晰,但性能瓶颈明显:
# ❌ 原始低效写法(约 18.5 秒 / 10 万行)
df['HorseRaceCount90d'] = (
df.groupby(['Horse', 'Trainer'], group_keys=False)
.apply(lambda x: x.rolling(window='180D', on='RaceDate', min_periods=1)['Points'].mean())
)该方式对每个分组单独执行 rolling(..., on='RaceDate'),触发大量重复索引对齐与时间窗口判定,且 apply 无法被 Pandas 内部优化器有效向量化。
✅ 更优解:先设时间列为索引,再分组滚动,最后精准回填
# ✅ 优化写法(约 1.18 秒 / 10 万行,提速 15×)
rolling_result = (
df.set_index('RaceDate')
.groupby(['Horse', 'Trainer'])['Points']
.rolling('180D', min_periods=1)
.mean()
.rename('HorseRaceCount90d')
)
df = df.merge(
rolling_result,
left_on=['Horse', 'Trainer', 'RaceDate'],
right_index=True,
how='left'
)关键优化原理:
- set_index('RaceDate') 将时间列转为索引后,rolling('180D') 可直接基于 DatetimeIndex 进行高效滑动(底层使用 libwindow 加速),避免每次 apply 中重复解析时间窗口;
- groupby([...])['Points'].rolling(...) 是 Pandas 原生支持的“分组+时间滚动”组合操作,全程在 C 层完成,无 Python 循环开销;
- merge 比 assign 或 map 更鲁棒,尤其当存在重复 (Horse, Trainer, RaceDate) 组合时仍能正确对齐。
注意事项:
- 确保 RaceDate 列已转换为 datetime64[ns] 类型(可用 pd.to_datetime(df['RaceDate'], errors='coerce') 安全转换);
- 若原始数据中 RaceDate 存在时区信息,建议统一转为 UTC 或无时区(.dt.tz_localize(None)),避免滚动计算异常;
- min_periods=1 保证首条记录也有值(即从该点起始的窗口内均值),若需严格满窗才计算,可设为 min_periods=2 或更高;
- 对于超大数据集(千万级+),可考虑结合 dask.dataframe 或 polars 进一步扩展,但本方法在百万行内已接近 Pandas 性能上限。
总结:
避免在 groupby().apply() 中嵌套 rolling(..., on=...) —— 改用“索引化→分组滚动→合并回表”三步范式,是提升 Pandas 时间窗口分组计算速度最简单、最有效的方式之一。实测性能提升达 15 倍以上,且代码更简洁、可维护性更强。










