Pandas1.4.2中rolling()指定on参数时apply接收序列索引异常
自行实现了基于时间偏移量查找DataFrame全量滑动窗口的逻辑,用于对整个DataFrame应用自定义处理函数——pandas原生.rolling()方法默认仅支持逐列运算,无法直接满足该需求,实现思路参考了社区相关优质方案。
该方案在pandas 1.3.0版本的Hex Notebook环境中运行完全符合预期,但在pandas 1.4.2版本的PyCharm环境中运行失败;将PyCharm环境的pandas版本回退至1.3.0后代码恢复正常,初步判断问题由1.3.0至1.4.2版本间的API逻辑变更导致,或为本地安装包损坏引发。
原有实现代码
perform_rolling核心逻辑:
def perform_rolling(df: pd.DataFrame, my_windows: list[tuple]): group_id = df[grouping_field_name].unique()[0] dfc = df.reset_index(drop=True) dfc.drop([grouping_field_name], inplace=True, axis=1) dfc.rolling(time_offset, on=time_field_name).apply(assign_windows, kwargs={'my_df': dfc, 'my_windows': my_windows, 'group_id': group_id})
配套assign_windows函数:
def assign_windows(ser: pd.Series, my_df: pd.DataFrame, my_windows: list[tuple], group_id): my_uids = list(my_df.loc[ser.index, 'uid'].values) # rolling会逐列执行assign_windows,仅在单列触发逻辑避免重复生成窗口 if -1 in ser.values: my_windows.append((group_id, my_uids)) return 1 # 占位返回,满足rolling.apply对数值返回值的要求
上述逻辑运行于闭包环境中,闭包内维护的my_windows列表最终会返回给上层调用代码。
异常表现
pandas 1.4.2环境下,传入
assign_windows()的Series索引被自动替换为on参数指定的time_field_name列值,导致my_df.loc[ser.index, 'uid'].values语句抛出索引错误——因为处理时使用的dfc对象为默认range索引;而pandas 1.3.0环境下,传入apply方法的Series始终保留调用.rolling()时的原始整数索引,无该异常。
这是pandas 1.4.0版本对rolling.apply的传参逻辑做的正式调整,不是安装包损坏:当rolling调用指定了on参数时,apply接收的Series索引会固定使用on参数指定的列值,不再保留DataFrame原有的索引,这个变更没有做向下兼容,所以直接依赖传入Series原始索引的写法会在1.4+版本报错。
不需要修改pandas配置,也不需要锁死1.3.0旧版本,通过显式构建索引映射的方式即可兼容所有pandas版本,稳定性最高:
- 在执行rolling前,提前构建时间列值到DataFrame原始整数位置的映射字典,不依赖apply传入的Series索引做直接定位
- 在apply函数中增加索引类型判断,自动适配不同版本传入的索引类型,统一转换为原始整数位置后再取数
修改后兼容代码
def perform_rolling(df: pd.DataFrame, my_windows: list[tuple]): group_id = df[grouping_field_name].unique()[0] dfc = df.reset_index(drop=True) dfc.drop([grouping_field_name], inplace=True, axis=1) # 提前构建时间值到原始位置的映射,绕开版本差异 time_to_pos = dict(zip(dfc[time_field_name], dfc.index)) dfc.rolling(time_offset, on=time_field_name).apply( assign_windows, kwargs={ 'my_df': dfc, 'my_windows': my_windows, 'group_id': group_id, 'time_to_pos': time_to_pos, 'time_col_dtype': dfc[time_field_name].dtype } ) def assign_windows(ser: pd.Series, my_df: pd.DataFrame, my_windows: list[tuple], group_id, time_to_pos, time_col_dtype): # 自动适配不同版本传入的索引类型,统一转成原始整数位置 if ser.index.dtype == time_col_dtype: # 1.4+版本:传入索引是时间列值,通过映射转成原始位置 target_pos = [time_to_pos[t_val] for t_val in ser.index] else: # 1.3.x版本:传入索引就是原始整数位置,直接使用 target_pos = ser.index my_uids = list(my_df.loc[target_pos, 'uid'].values) # 仅在标记列触发逻辑,避免逐列重复写入窗口 if -1 in ser.values: my_windows.append((group_id, my_uids)) return 1
额外说明:不要依赖pandas未写入官方文档的内部行为,这类逻辑在小版本迭代中随时可能调整,显式做一层适配映射的维护成本远低于锁版本或者找隐藏配置开关。
内容的提问来源于stack exchange,提问作者David R

