滚动窗口:rolling
本教程共 54 篇 · 第 42 篇 · 更新于 2026-08-11 · 约 8 分钟阅读
本节目标:搞懂滚动窗口(rolling)是什么、怎么用,能自己算移动平均、滚动求和,还会用 apply 写自定义窗口计算。
滚动窗口是什么
想象一扇固定大小的窗户,在数据上从左往右滑动。每停在一个位置,就只”看”窗户里的那几条数据,算出一个结果。窗户每滑动一步,丢掉最左边一条,纳入最右边一条。这就是滚动窗口(rolling window)。
它解决什么问题?你有一串按天记录的销售数字,想知道”最近 3 天的平均值”是多少。直接用全部数据算平均,抹平了时间先后;只看某一天,又太抖。滚动窗口就是中间路线:每个位置都算一次”最近 3 天”的统计量。
先看最基础的用法:
import pandas as pd
import numpy as np
s = pd.Series(range(5))
print(s)
# 0 0
# 1 1
# 2 2
# 3 3
# 4 4
# 窗口大小为 2:每个位置取当前值和前一个值求和
print(s.rolling(window=2).sum())
# 0 NaN
# 1 1.0
# 2 3.0
# 3 5.0
# 4 7.0
窗口大小为 2,表示”当前位置 + 前面 1 个”,一共 2 个数据。索引 0 前面没有数据,凑不满窗口,结果是 NaN。索引 1 算的是 0+1=1,索引 2 算的是 1+2=3,以此类推。
Note窗口是”向前看”的:第 i 个位置的结果,只用到 i 和它之前的 n-1 条数据,不会偷看未来。这是滚动窗口在时序分析里好用的根本原因。
常用聚合方法
.rolling() 本身只负责”切窗口”,真正干活的是后面跟的聚合方法。支持的聚合和普通 Series 差不多:
import pandas as pd
import numpy as np
s = pd.Series(range(1, 11))
# 移动平均:每 3 个算一次均值
print(s.rolling(window=3).mean())
# 0 NaN
# 1 NaN
# 2 2.0
# 3 3.0
# ...
# 滚动标准差(波动率分析常用)
print(s.rolling(window=3).std())
# 滚动最大、最小、中位数
print(s.rolling(window=3).max())
print(s.rolling(window=3).min())
print(s.rolling(window=3).median())
sum、mean、std、var、max、min、median、count 都是现成的。想一次算多个指标,用 agg,和分组聚合(§37)的写法一致:
import pandas as pd
import numpy as np
df = pd.DataFrame({"A": range(5), "B": range(10, 15)})
print(df.rolling(window=3).agg(["sum", "mean"]))
# A B
# sum mean sum mean
# 0 NaN NaN NaN NaN
# 1 NaN NaN NaN NaN
# 2 3.0 1.0 33.0 11.0
# 3 6.0 2.0 36.0 12.0
# 4 9.0 3.0 39.0 13.0
DataFrame 上直接调用 .rolling(),会逐列各算各的,互不干扰。想让每列的窗口大小不一样,就分别对列调用。
min_periods:窗口不满怎么办
前面说过,窗口凑不满时结果是 NaN。但有些场景你不想等窗口完全填满。比如数据里混着缺失值,窗口大小 3,里面只有 1 个有效数字,你希望照算不误。这时用 min_periods:窗口里至少要有多少个非缺失值,才输出结果。
import pandas as pd
import numpy as np
s = pd.Series([np.nan, 1, 2, np.nan, np.nan, 3])
# 窗口 3,只要至少有 1 个有效值就算
print(s.rolling(window=3, min_periods=1).sum())
# 0 NaN
# 1 1.0
# 2 3.0
# 3 3.0
# 4 2.0
# 5 3.0
# 窗口 3,要求至少有 2 个有效值
print(s.rolling(window=3, min_periods=2).sum())
# 0 NaN
# 1 NaN
# 2 3.0
# 3 3.0
# 4 NaN
# 5 NaN
固定大小窗口的 min_periods 默认等于 window,也就是必须凑满才算。把它调小,是处理稀疏数据时最常用的手法。
按时间滑动的窗口
前面窗口大小是”几条数据”。如果数据不是均匀的——比如有的日子没记录——按条数算就不合理。这时可以把窗口指定为时间长度,比如”2 天”:
import pandas as pd
import numpy as np
s = pd.Series(range(5), index=pd.date_range("2020-01-01", periods=5, freq="1D"))
print(s.rolling(window="2D").sum())
# 2020-01-01 0.0
# 2020-01-02 1.0
# 2020-01-03 3.0
# 2020-01-04 5.0
# 2020-01-05 7.0
“2D” 就是 2 天。第一个位置没有更早的数据,结果本该是 NaN,但时间窗口的 min_periods 默认是 1,所以第一个点也算出来了。
再看一个数据不连续的例子,更能体会”按时间”和”按条数”的区别:
import pandas as pd
import numpy as np
times = ["2020-01-01", "2020-01-03", "2020-01-04", "2020-01-05", "2020-01-29"]
s = pd.Series(range(5), index=pd.DatetimeIndex(times))
# 按 2 条数据滑动:不管隔了多久
print(s.rolling(window=2).sum())
# 2020-01-01 NaN
# 2020-01-03 1.0
# 2020-01-04 3.0
# 2020-01-05 5.0
# 2020-01-29 7.0
# 按 2 天滑动:1 月 29 日离前一条太远,窗口里只有它自己
print(s.rolling(window="2D").sum())
# 2020-01-01 0.0
# 2020-01-03 1.0
# 2020-01-04 3.0
# 2020-01-05 5.0
# 2020-01-29 4.0
注意最后一行:1 月 29 日距离 1 月 5 日隔了 24 天,“2 天”窗口里只有它自己,所以结果就是 4。
Tip使用时间窗口时,索引必须是 DatetimeIndex,而且最好按时间排好序(单调递增),否则结果可能不符合预期。
窗口端点 closed
时间窗口默认 closed="right":含右端点、不含左端点。需要调整端点时,用 closed 参数:"right"(默认,含右不含左)、"left"(含左不含右)、"both"(两端都含)、"neither"(两端都不含)。
import pandas as pd
import numpy as np
s = pd.Series([1, 1, 1, 1, 1], index=pd.date_range("2024-01-01 09:00:00", periods=5, freq="2s"))
# 默认 closed="right":窗口 (前一个时间点, 当前时间点]
print(s.rolling("4s").sum())
# 2024-01-01 09:00:00 1.0
# 2024-01-01 09:00:02 2.0
# 2024-01-01 09:00:04 2.0
# ...
closed 最常用的场景是算”截至此刻但不含此刻”的统计,避免用当前信息预测当前,防止数据泄漏。
中心窗口
默认结果落在窗口的右端:窗口 [0,1,2,3,4] 算出的均值挂在索引 4 上。想让结果挂在窗口正中间,用 center=True:
import pandas as pd
import numpy as np
s = pd.Series(range(10))
# 默认:结果在右端
print(s.rolling(window=5).mean())
# 0 NaN
# 1 NaN
# 2 NaN
# 3 NaN
# 4 2.0
# 5 3.0
# ...
# 居中:结果在窗口中间
print(s.rolling(window=5, center=True).mean())
# 0 NaN
# 1 NaN
# 2 2.0
# 3 3.0
# 4 4.0
# 5 5.0
# ...
# 8 NaN
# 9 NaN
居中之后,头尾各多出几个 NaN。center 适合做平滑曲线,让每个点对应窗口中心,视觉上更对称。
自定义函数:apply
内置聚合不够用时,用 apply 传自己的函数。函数接收一个窗口的数据,返回一个数。raw=True 表示窗口以 NumPy 数组传入(更快),raw=False(默认)以 Series 传入。
import pandas as pd
import numpy as np
s = pd.Series(range(10))
# 平均绝对偏差 MAD:每个窗口里,数据偏离均值的平均距离
def mad(x):
return np.fabs(x - x.mean()).mean()
print(s.rolling(window=4).apply(mad, raw=True))
# 0 NaN
# 1 NaN
# 2 NaN
# 3 1.0
# 4 1.0
# 5 1.0
# ...
range(10) 里任意连续 4 个数的 MAD 都是 1.0,所以后面全是 1.0。apply 是滚动窗口的”万能出口”,任何你想对窗口做的计算都能塞进去。数据量大时,apply 会比内置聚合慢不少,能不用尽量不用。
分组滚动
滚动窗口还能和分组(§37)链式使用:先按某个键分组,再在每组内部做滚动计算。比如按商品分组,算每种商品最近 2 条销量的和:
import pandas as pd
import numpy as np
df = pd.DataFrame({"A": ["a", "b", "a", "b", "a"], "B": range(5)})
print(df.groupby("A").rolling(window=2).sum())
# B
# A
# a 0 NaN
# 2 2.0
# 4 6.0
# b 1 NaN
# 3 4.0
组 a 的数据是 0、2、4,组 b 是 1、3,各自独立滚动求和。结果带了 MultiIndex,第一层是组名,第二层是原索引。
小结
滚动窗口的套路很固定:数据.rolling(窗口).聚合()。窗口可以是条数,也可以是时间长度;min_periods 控制最少有效值;closed 控制端点;center 控制结果位置;apply 支持自定义逻辑。下一节要讲的 expanding 和 ewm,是滚动窗口的两个变体:一个窗口越来越大,一个给数据加权重。
Warning滚动窗口目前只支持数值列,结果一律是 float64。另外 sum、mean、std 这类聚合在数据量级相差极大时可能有精度损失,日常数据不必担心。