pandas的rolling/expanding自定义函数必须返回标量,返回Series/list会报错;需多输出时用apply+result_type='expand';expanding与rolling规则一致,仅窗口行为不同。
直接传 lambda x: x.mean() 没问题,但若函数返回 pd.Series 或 list,会报 ValueError: Must produce aggregated value。pandas 的 rolling 和 expanding 要求聚合函数最终输出单个值(标量),不是数组、Series 或 DataFrame。
常见踩坑点:
.describe()、.quantile([0.25, 0.75]) 这类返回多个值的函数,会直接失败lambda x: np.percentile(x, 90) 是 OK 的,因为返回 float;但 lambda x: np.percentile(x, [90, 95]) 就不行apply + result_type='expand' 拆开(见下一条)rolling(...).apply() 默认只接受标量返回,但加 result_type='expand'

示例:窗口内同时计算 90% 和 95% 分位数
df['x'].rolling(5).apply(
lambda x: np.quantile(x, [0.9, 0.95]),
result_type='expand'
).rename(columns={0: 'q90', 1: 'q95'})
注意:
result_type='expand' 仅在 apply 中有效,agg / aggregate 不支持expanding() 和 rolling() 共享同一套聚合逻辑,所有关于函数签名、返回值、result_type 的规则完全一样。区别只在窗口行为:rolling 是固定宽度滑动,expanding 是从首行累积增长。
所以以下写法是等价有效的:
# 两种写法效果相同(假设 df['x'] 有足够长度) df['x'].expanding(3).apply(lambda x: x.std(), engine='numba') # 可加 numba 加速 df['x'].rolling(3).apply(lambda x: x.std()) # 但 rolling 前 2 行是 NaN
关键点:
min_periods 对两者都适用:expanding(min_periods=3) 表示前 2 行返回 NaN,第 3 行开始计算x 始终是 pd.Series(即使原列是 int64,x.dtype 也是对应类型),可放心调用 .to_numpy() 或 np.array(x)
自定义函数慢的主因是 Python 循环 + pandas Series 开销。如果逻辑能转成 numpy 向量化操作,应尽量避开 apply。
例如计算滚动中位数偏移量:
# ❌ 慢:每窗口都调用 python median + sub s.rolling(10).apply(lambda x: np.median(x) - x.iloc[-1])✅ 快:先算好 rolling median(内置 C 实现),再减
s.rolling(10).median() - s
更进一步:
numba.jit 编译简单数值函数(如自定义分位数、winsorize),配合 engine='numba'
.corr()、.cov()、.kurt() 等都支持 rolling/expanding实际用的时候,多数情况卡在“以为要自己写函数”,结果发现 pandas 已经内置了;少数真要定制的,核心就两条:返回标量、别碰外部状态。其余都是优化层面的事。