面板数据分组滚动窗口计算的两类技术疑问及原因探究
问题1:为什么groupby('customer').rolling(3, on='month').sales.sum()会报错?
这个报错的核心原因和rolling参数on的查找逻辑直接相关:
当你对groupby('customer')后的对象调用rolling时,rolling是在每个分组的子DataFrame上独立工作的。此时每个子DataFrame的索引是原MultiIndex中的month,但on参数的规则是:如果传入字符串名称,它会优先去子DataFrame的列中查找匹配项,而不是索引。
你的month是原DataFrame的索引层级,在分组后变成了子DataFrame的索引,并不是列,所以rolling找不到名为month的列,就会抛出ValueError: invalid on specified as month, must be a column (of DataFrame), an Index or None这个错误。
解决思路其实很简单:
- 如果你想基于
month索引滚动,完全不需要指定on参数,rolling默认会用当前子DataFrame的索引做滚动依据,直接写groupby('customer').rolling(3).sales.sum()即可(前提是每个分组的month是按时间排序的,你的示例数据满足这个条件)。 - 如果你一定要用
on='month',那需要把month从索引转为列,也就是用reset_index('month')处理原DataFrame,让month成为子DataFrame的列。
问题2:为什么groupby('customer').rolling(3).sales.sum()会新增索引层级?
首先要澄清:在pandas 1.0+的版本中,这个代码的结果索引应该和原DataFrame的索引结构完全一致——都是(customer, month)的多层索引,并没有新增层级。你感觉有额外层级,大概率是两种情况:
- 旧版本pandas的行为差异:在pandas 1.0之前的旧版本中,
RollingGroupby的结果会把分组键和滚动后的索引合并成一个三层索引(比如customer+month+month),看起来像是多了一层。 - 显示误解:当你查看结果时,pandas的索引显示方式可能让你误以为多了层级,但实际结构和原索引是匹配的。
如果确实遇到了索引不匹配的情况,不要用.values这种忽略索引的粗暴方式,更好的做法是用reset_index去掉重复的分组键层级:
# 正确赋值方式,保持索引匹配 example_df['rolling_3m_sum'] = example_df.groupby('customer')\ .rolling(3).sales.sum()\ .reset_index(level=0, drop=True)
这样既保留了正确的索引关联,又避免了忽略索引带来的潜在问题。
额外提示:性能优化
你提到要避免apply这类性能低下的函数,groupby.rolling本身就是pandas提供的矢量化滚动计算接口,内部用C实现,性能远高于apply自定义滚动逻辑,完全符合你的性能要求。
内容的提问来源于stack exchange,提问作者Raisin

