Pandas按组对含缺失日期的滚动窗口高效应用函数
我之前碰到这个Pandas滚动求和的难题,在Stack Overflow翻了一圈没找到合适的解法,折腾了好半天才搞定,所以把经验分享出来,也欢迎大家提出其他更巧妙的思路!
我有一个包含三列的Pandas DataFrame:日期列(dates)、分组列(id_col)、数值列(value_col)。需求是按组对日期列计算滚动求和,但有个关键要求:分组内缺失的日期要视为值为0的观测值,同时我不想用交叉连接这类方法补全所有缺失日期(数据量大的时候太费资源)。
先构造一个测试用的DataFrame:
import pandas as pd import numpy as np df = pd.DataFrame({ 'id_col': np.array([1,1,1,1,1,1,2,2,2,2,2,3,3,3]), 'value_col': np.random.randint(0,5,size=14), 'dates': pd.to_datetime(np.array([ '2018-01-01', '2018-02-01', '2018-03-01', '2018-05-01', '2018-06-01', '2018-09-01', '2018-01-01', '2018-02-01', '2018-05-01', '2018-06-01', '2018-07-01', '2018-01-01', '2018-02-01', '2018-03-01' ])) })
生成的数据样例大概是这样:
| id_col | value_col | dates | |
|---|---|---|---|
| 0 | 1 | 0 | 2018-01-01 |
| 1 | 1 | 1 | 2018-02-01 |
| 2 | 1 | 4 | 2018-03-01 |
| 3 | 1 | 0 | 2018-05-01 |
| 4 | 1 | 3 | 2018-06-01 |
| 5 | 1 | 3 | 2018-09-01 |
| 6 | 2 | 4 | 2018-01-01 |
| 7 | 2 | 3 | 2018-02-01 |
| 8 | 2 | 2 | 2018-05-01 |
| 9 | 2 | 0 | 2018-06-01 |
| 10 | 2 | 2 | 2018-07-01 |
| 11 | 3 | 4 | 2018-01-01 |
| 12 | 3 | 2 | 2018-02-01 |
| 13 | 3 | 3 | 2018-03-01 |
方案1:直接忽略缺失日期
代码:df.groupby(['id_col']).rolling(2)['value_col'].sum().reset_index()问题:这种方式完全跳过了缺失日期,比如id=1的2018-04-01没有数据,滚动窗口不会把它当作0计算,导致结果不符合需求。
方案2:尝试时间偏移窗口
参考Pandas滚动文档,想用时间偏移设置窗口宽度,代码:df.groupby(['id_col']).rolling('60d')['value_col'].sum().reset_index()问题:直接报错
ValueError: window must be an integer。后来尝试把日期设为DataFrame索引,但因为不同id_col可能有相同日期,普通索引行不通;创建MultiIndex后也会报同样的错误,这条路走不通。
方案1:交叉连接补全所有缺失日期
思路:先生成每个分组的完整日期序列,再和原数据交叉连接,把缺失的value_col填充为0,之后再做滚动求和。
问题:数据量大的时候,生成的全量DataFrame会非常庞大,内存和计算成本都很高。方案2:笛卡尔积构建MultiIndex补全日期
思路:通过生成分组+日期的笛卡尔积MultiIndex来补全缺失日期,本质和方案1一致,同样存在数据膨胀的问题,不适合大规模数据。
核心思路是:对每个分组,先临时补全窗口内的缺失日期为0,计算滚动求和后,只保留原始数据存在的日期结果,避免数据量爆炸。具体代码如下:
def calculate_rolling_sum(group, window='60d', freq='D'): # 按日期排序并设置为索引 group_sorted = group.set_index('dates').sort_index() # 重采样补全缺失日期,填充0 resampled_group = group_sorted.resample(freq).fillna(0) # 计算滚动求和 resampled_group['rolling_sum'] = resampled_group['value_col'].rolling(window).sum() # 只保留原始数据中存在的日期的结果,再重置索引 return resampled_group.loc[group_sorted.index].reset_index() # 应用到每个分组 final_result = df.groupby('id_col').apply(calculate_rolling_sum).reset_index(drop=True)
关键说明:
- 窗口与频率匹配:
window参数可以设为时间偏移(比如'60d'代表60天,'2M'代表两个月),freq要和你的原始日期粒度匹配(比如原始是月度数据就用'M',日度用'D')。 - 避免数据膨胀:重采样只是临时补全日期,最后会筛选出原始存在的日期,不会保留所有补全的行,兼顾了需求和性能。
比如对id=1的2018-05-01,滚动窗口如果设为60天,会包含2018-03-01(值4)、2018-04-01(值0)、2018-05-01(值0),求和结果是4+0+0=4,符合缺失日期算0的要求。
内容的提问来源于stack exchange,提问作者ira

