如何显著加速 Pandas 中按组的时间窗口滚动均值计算?

12次阅读

如何显著加速 Pandas 中按组的时间窗口滚动均值计算?

本文介绍一种比 `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 倍以上,且代码更简洁、可维护性更强。

text=ZqhQzanResources