pandas 窗口操作(Windowing Operations)完全指南:Rolling / Expanding / EWM 窗口函数与自定义索引器 API 详解
2026/9/19 20:00:37 网站建设 项目流程

pandas 窗口操作(Windowing Operations)完全指南:Rolling / Expanding / EWM 窗口函数与自定义索引器 API 详解

【免费下载链接】pandasFlexible and powerful data analysis / manipulation library for Python, providing labeled data structures similar to R data.frame objects, statistical functions, and much more项目地址: https://gitcode.com/gh_mirrors/pa/pandas

导读

本文基于 pandas 官方 API 参考 doc/source/reference/window.rst 与配套用户指南 doc/source/user_guide/window.rst,系统梳理 pandas 四大类窗口操作(滚动窗口 Rolling、加权窗口 Window、扩展窗口 Expanding、指数加权窗口 EWM)的返回对象、聚合函数清单与核心参数语义,并结合pandas/core/window/pandas/core/indexers/的源码实现深入讲解底层原理。读完本文,你将掌握.rolling()/.expanding()/.ewm()三类 API 的完整方法矩阵、win_type加权窗口的用法、自定义窗口边界的BaseIndexer协议,以及min_periodscenterclosedmethod='table'、online 更新等高级特性的准确行为。

一、窗口操作总览:四种窗口类型与返回对象

pandas 的窗口操作(windowing operation)是一种"在滑动分片上执行聚合"的操作,API 风格与groupby一致:先由Series/DataFrame调用窗口方法并传入必要参数,得到窗口对象,再在窗口对象上调用聚合函数。

概念方法返回对象支持时间窗口支持 groupby 链式支持 table 模式支持 online 计算
滚动窗口 Rollingrollingpandas.api.typing.Rolling
加权窗口 Weightedrolling(带win_typepandas.api.typing.Window
扩展窗口 Expandingexpandingpandas.api.typing.Expanding
指数加权窗口 EWMewmpandas.api.typing.ExponentialMovingWindow

三种窗口对象在类型标注层面由 pandas/api/typing/init.py 导出,具体实现类位于 pandas/core/window/rolling.py(RollingWindow)、pandas/core/window/expanding.py(Expanding)和 pandas/core/window/ewm.py(ExponentialMovingWindow)。这些方法定义在Series/DataFrame的公共基类 pandas/core/generic.py(rollingL11915expandingL12214ewmL12287)。

最基础的用法:

import pandas as pd import numpy as np s = pd.Series(range(5)) s.rolling(window=2).sum() # 0 NaN # 1 1.0 # 2 3.0 # 3 5.0 # 4 7.0

窗口由"从当前观测点向前回看 window 长度"形成,也可通过迭代查看每个窗口的分区:

for window in s.rolling(window=2): print(window)

两个通用约束(来自用户指南的显式说明):

  • 窗口操作目前仅支持数值数据(整数与浮点),且结果恒为float64
  • meansumvarstd等聚合由于底层算法累加求和,当数值量级相差达到1/np.finfo(np.double).eps(约 4.5×10¹⁵)时可能出现数值截断;pandas 使用 Kahan 求和算法计算滚动求和以尽量保持精度。

二、Rolling 滚动窗口函数:完整方法清单与参数语义

pandas.api.typing.Rolling实例由DataFrame.rollingSeries.rolling调用返回。API 参考文档列出其全部聚合方法:countsummeanmedianvarstdminmaxfirstlastcorrcovskewkurtapplypipeaggregatequantilesemranknunique

这些方法定义在 pandas/core/window/rolling.py 的RollingAndExpandingMixinL1537,为RollingExpanding共享)及RollingL1955)中,其中countsummax等直接调用 Cython 扩展模块pandas._libs.window.aggregations中的roll_sumroll_max等底层例程。

2.1rolling()构造参数(以Series/DataFrame通用实现为准)

在 pandas/core/generic.py#L11915 的签名中:

  • window:窗口间隔。整数表示固定观测数窗口;timedelta / 字符串 / offset表示时间跨度窗口(仅适用于 datetimelike 索引,且必须是固定频率,如'2D''1h''B'营业日、'ME'月末等非固定频率会抛ValueError);BaseIndexer 子类则按自定义get_window_bounds计算边界。
  • min_periods:窗口内非np.nan观测的最小数量,不足则结果为NaN默认值规则:按 offset 指定窗口时默认为1;按整数指定窗口时默认为window(即窗口大小)。min_periods=None等价于取窗口大小。
  • center:默认False(标签取窗口右缘);True时标签取窗口中心。
  • win_type:默认None(等权重);传入字符串则启用 scipy 加权窗口(见第三节)。
  • on:对DataFrame指定用于计算窗口的列标签或索引层级,而非 DataFrame 自身索引;且该列的值会成为raw=False时传给Rolling.applySeries的索引。
  • closed:窗口端点包含性,默认None(等价'right'):
    • 'right':(first, last] 包含最后一点;
    • 'left':[first, last) 包含第一点;
    • 'both':[first, last] 全部包含;
    • 'neither':(first, last) 两端都排除。
  • step:每隔step个结果计算一次(等价[::step]切片),window必须是整数,非None/1时结果形状与输入不同。
  • method'single'(默认,逐列/逐行执行)或'table'(整个对象上执行,仅在调用聚合方法时指定engine='numba'才可用)。

2.2 固定窗口与时间窗口

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 个观测的窗口 s.rolling(window=2).sum() # 覆盖 2 天观测的窗口(基于时间跨度) s.rolling(window='2D').sum()

使用时间 offset 时,对应的时间索引必须单调。

2.3 窗口居中对齐(centering)

s = pd.Series(range(10)) s.rolling(window=5).mean() # 标签对齐右缘 s.rolling(window=5, center=True).mean() # 标签对齐中心

center同样适用于 datetimelike 索引的时间窗口:

df = pd.DataFrame( {"A": [0, 1, 2, 3, 4]}, index=pd.date_range("2020", periods=5, freq="1D") ) df.rolling("2D", center=False).mean() df.rolling("2D", center=True).mean()

2.4 窗口端点(closed)的实战价值

closed常用于"避免当期信息污染历史信息"的场景——右端点开放意味着统计量只计算到该时刻之前、不含该时刻:

df = pd.DataFrame( {"x": 1}, index=[ pd.Timestamp("20130101 09:00:01"), pd.Timestamp("20130101 09:00:02"), pd.Timestamp("20130101 09:00:03"), pd.Timestamp("20130101 09:00:04"), pd.Timestamp("20130101 09:00:06"), ], ) df["right"] = df.rolling("2s", closed="right").x.sum() # 默认 df["both"] = df.rolling("2s", closed="both").x.sum() df["left"] = df.rolling("2s", closed="left").x.sum() df["neither"] = df.rolling("2s", closed="neither").x.sum()

2.5 min_periods 与 missing 值

s = pd.Series([np.nan, 1, 2, np.nan, np.nan, 3]) s.rolling(window=3, min_periods=1).sum() s.rolling(window=3, min_periods=2).sum() s.rolling(window=3, min_periods=None).sum() # 等价于 min_periods=3

2.6 二元窗口函数:cov 与 corr

Rolling.covRolling.corr支持三种组合:

  • 两个Series:计算配对统计量;
  • DataFrame/Series:对 DataFrame 每列与该 Series 计算,返回 DataFrame;
  • DataFrame/DataFrame:默认按列名匹配计算;传入pairwise=True时对每对列计算,返回以日期为一级索引的MultiIndexDataFrame(缺失值按"逐对完整观测"忽略)。
df = pd.DataFrame( np.random.randn(10, 4), index=pd.date_range("2020-01-01", periods=10), columns=["A", "B", "C", "D"], ).cumsum() df2 = df[:4] df2.rolling(window=2).corr(df2["B"]) covs = df[["B", "C", "D"]].rolling(window=4).cov( df[["A", "B", "C"]], pairwise=True )

注意:pairwise 方式下假设缺失数据完全随机时可得到无偏协方差估计,但估计出的协方差矩阵不保证半正定,可能导致相关性的绝对值大于 1 或协方差矩阵不可逆。

2.7 Rolling.apply:通用滚动计算

Rolling.apply接受额外func参数执行任意单值聚合。raw=False(默认)时窗口被包装为Series对象,raw=True时直接传入 ndarray:

def mad(x): return np.fabs(x - x.mean()).mean() s = pd.Series(range(10)) s.rolling(window=4).apply(mad, raw=True)

从源码 pandas/core/window/rolling.py#L1542 看,apply支持engine='cython'(默认,不接受engine_kwargs)与engine='numba'(要求raw=True);raw=False时窗口还会被包成以self._on(即on参数指定的索引)为索引的Series(见L1604-L1608)。

2.8 Numba 引擎与 table 模式

使用 Numba(可选依赖)时,engine='numba'+engine_kwargs(该字典会同时传给numba.jit装饰器、用户函数与窗口循环),且raw必须为True。Numba 会在两处被应用:一是对标准 Python 函数做 JIT(已 JIT 的函数不再重复 JIT),二是对"对每个窗口应用函数"的 for 循环做 JIT。meanmedianmaxminsumstdvar也支持engineengine_kwargs参数。

method='table'允许在整个DataFrame上执行窗口操作,而不是逐列执行,对多列 DataFrame 有性能收益,且可以在窗口函数中利用其他列——例如用Rolling.apply实现"加权均值"(权重来自独立列):

def weighted_mean(x): arr = np.ones((1, x.shape[1])) arr[:, :2] = (x[:, :2] * x[:, 2]).sum(axis=0) / x[:, 2].sum() return arr df = pd.DataFrame([[1, 2, 0.6], [2, 3, 0.4], [3, 4, 0.2], [4, 5, 0.7]]) df.rolling(2, method="table", min_periods=0).apply( weighted_mean, raw=True, engine="numba" )

table 模式仅当方法调用中指定engine='numba'时才可用(见 pandas/core/generic.py#L12016-L12022 的method参数说明)。

三、Weighted 加权窗口函数(win_type)

.rolling()中传入win_type参数即产生加权(非矩形)窗口,常用于滤波与谱估计。win_type必须是 scipy.signal 窗口函数 名称对应的字符串,因此需要安装 scipy;scipy 窗口方法的补充参数需在聚合函数调用中指定。

API 参考中Windowpandas.api.typing.Window)支持四个聚合方法:meansumvarstd。其实现类位于 pandas/core/window/rolling.py#L862。

s = pd.Series(range(10)) s.rolling(window=5).mean() # 等权重 s.rolling(window=5, win_type="triang").mean() # 三角窗 s.rolling(window=5, win_type="gaussian").mean(std=0.1) # 补充 scipy 参数

四、Expanding 扩展窗口函数

pandas.api.typing.ExpandingDataFrame.expanding/Series.expanding调用返回,窗口从序列起点累计到当前点,即"截止该时刻的全部可用数据"。它是滚动统计的特例,以下两种写法完全等价:

df = pd.DataFrame(range(5)) df.rolling(window=len(df), min_periods=1).mean() df.expanding(min_periods=1).mean()

API 参考为Expanding列出与Rolling相同的 21 个聚合方法:countsummeanmedianvarstdminmaxfirstlastcorrcovskewkurtapplypipeaggregatequantilesemranknunique。实现类为 pandas/core/window/expanding.py#L43 的Expanding,与Rolling共同继承RollingAndExpandingMixin,因此共享apply的 cython/numba 双引擎逻辑。

所有窗口操作都支持aggregate(别名agg)一次应用多种聚合:

df = pd.DataFrame({"A": range(5), "B": range(10, 15)}) df.expanding().agg(["sum", "mean", "std"])

五、Exponentially Weighted 指数加权窗口函数

pandas.api.typing.ExponentialMovingWindowDataFrame.ewm/Series.ewm调用返回,实现类在 pandas/core/window/ewm.py#L127。它类似 expanding 窗口,但每个历史点相对当前点按指数衰减。API 参考列出的方法:meansumstdvarcorrcov

一般加权移动平均公式:

$$y_t = \frac{\sum_{i=0}^t w_i x_{t-i}}{\sum_{i=0}^t w_i}$$

5.1 衰减参数:com / span / halflife / alpha(四选一)

必须恰好指定其中一个(除非配合times),三者与平滑因子 α 的关系:

$$\alpha = \begin{cases} \frac{2}{s + 1}, & \text{span } s \geq 1\[4pt] \frac{1}{1 + c}, & \text{com } c \geq 0\[4pt] 1 - e^{\frac{\log 0.5}{h}}, & \text{halflife } h > 0 \end{cases}$$

  • span:对应俗称的"N 日 EW 移动平均";
  • com(center of mass,质心):物理意义更直观,与 span 的关系为 $c = (s-1)/2$;
  • halflife:权重衰减到一半所需周期;
  • alpha:直接指定平滑因子,须满足 $0 < \alpha \leq 1$。

从 pandas/core/window/ewm.py#L127-L163 的文档可见:若提供timesadjust=True,可同时提供halflifecom/span/alpha之一;若timesadjust=False,则halflife必须是唯一的衰减参数。

5.2 adjust=True 与 adjust=False 两种权重变体

  • adjust=True(默认)使用权重 $w_i = (1-\alpha)^i$:

$$y_t = \frac{x_t + (1-\alpha)x_{t-1} + (1-\alpha)^2 x_{t-2} + \cdots + (1-\alpha)^t x_0}{1 + (1-\alpha) + (1-\alpha)^2 + \cdots + (1-\alpha)^t}$$

  • adjust=False使用递推式 $y_0 = x_0$,$y_t = (1-\alpha)y_{t-1} + \alpha x_t$,等价于权重 $w_i = \alpha(1-\alpha)^i$($i<t$)且 $w_t=(1-\alpha)^t$。对无限历史序列,两种变体数学上等价;adjust=False隐式假设 $x_0$ 是截至该点的无限序列的指数加权矩。

5.3 基于 times 的 halflife

当传入时间戳序列times时,halflife可写成 timedelta 可换算单位,表示观测值衰减到一半所需的时间:

df = pd.DataFrame({"B": [0, 1, 2, np.nan, 4]}) times = ["2020-01-01", "2020-01-03", "2020-01-10", "2020-01-15", "2020-01-17"] df.ewm(halflife="4 days", times=pd.DatetimeIndex(times)).mean()

对应公式为:

$$y_t = \frac{\sum_{i=0}^t 0.5^{\frac{t_t - t_i}{\lambda}} x_i}{\sum_{i=0}^t 0.5^{\frac{t_t - t_i}{\lambda}}}$$

其中 $\lambda$ 即 halflife。

5.4 ignore_na:中间缺失值的权重处理

ignore_na=False(默认)时按绝对位置计算权重,中间的空值会影响结果;ignore_na=True时计算权重时忽略中间空值。例如adjust=True下对序列3, NaN, 5

  • ignore_na=False:$\frac{(1-\alpha)^2 \cdot 3 + 1 \cdot 5}{(1-\alpha)^2 + 1}$
  • ignore_na=True:$\frac{(1-\alpha) \cdot 3 + 1 \cdot 5}{(1-\alpha) + 1}$

adjust=False下同理:ignore_na=False为 $\frac{(1-\alpha)^2 \cdot 3 + \alpha \cdot 5}{(1-\alpha)^2 + \alpha}$;ignore_na=True为 $\frac{(1-\alpha) \cdot 3 + \alpha \cdot 5}{(1-\alpha) + \alpha}$。

用户指南特别提示:adjust=False的递推式 $y_t = (1-\alpha)y_{t-1} + \alpha x_t$ 仅在无缺失值时成立(此时权重和恰为 1);出现缺失值后剩余观测的权重和不再为 1,会被重新归一化,因此不能直接对该递推式两侧的值做替换。可实测验证:

ser = pd.Series([3, np.nan, 5]) ser.ewm(alpha=2 / 3, adjust=False, ignore_na=False).mean() ser.ewm(alpha=2 / 3, adjust=False, ignore_na=True).mean()

5.5 bias:方差/标准差/协方差的有偏与无偏

ExponentialMovingWindow.varstdcov支持bias参数。bias=Trueewmvar(x) = ewma(x**2) - ewma(x)**2bias=False(默认)时对有偏方差乘上去偏因子:

$$\frac{\left(\sum_{i=0}^t w_i\right)^2}{\left(\sum_{i=0}^t w_i\right)^2 - \sum_{i=0}^t w_i^2}$$

当 $w_i = 1$ 时该因子退化为常见的 $N/(N-1)$($N=t+1$)。

5.6 online 在线计算

EWM 支持online()方法:先以聚合方法调用"预热"初始状态,之后传入update参数继续计算:

df = pd.DataFrame([[1, 2, 0.6], [2, 3, 0.4], [3, 4, 0.2], [4, 5, 0.7]]) df.ewm(0.5).mean() online_ewm = df.head(2).ewm(0.5).online() online_ewm.mean() # 先预热 online_ewm.mean(update=df.tail(1)) # 传入新数据继续计算

六、Window Indexer:自定义窗口边界的三种内建类

除整数与时间 offset 外,rollingwindow参数还接受BaseIndexer子类(API 参考"Window indexer"一节),定义于 pandas/core/indexers/objects.py,通过pandas.api.indexers命名空间对外暴露。

6.1 BaseIndexer 协议

BaseIndexer(pandas/core/indexers/objects.py#L21)是"自定义窗口边界"的基类:构造参数为index_array(默认None,可用于不规则时间戳场景)与window_size(默认0),其余**kwargs会被设置为实例属性供get_window_bounds使用。子类必须实现:

def get_window_bounds(self, num_values, min_periods, center, closed, step): ...

该方法返回(start, end)两个 int64 ndarray,分别表示每个窗口的起始与结束下标;num_valuesmin_periodscenterclosedstep由顶层 rolling API 自动传入,因此自定义方法必须始终接受这些参数。未实现时基类直接抛NotImplementedErrorL105)。

6.2 自定义 indexer 示例

若希望在use_expandingTrue处使用扩展窗口、否则用大小为 1 的窗口:

from pandas.api.indexers import BaseIndexer use_expanding = [True, False, True, False, True] df = pd.DataFrame({"values": range(5)}) class CustomIndexer(BaseIndexer): def get_window_bounds(self, num_values, min_periods, center, closed, step): start = np.empty(num_values, dtype=np.int64) end = np.empty(num_values, dtype=np.int64) for i in range(num_values): if self.use_expanding[i]: start[i] = 0 end[i] = i + 1 else: start[i] = i end[i] = i + self.window_size return start, end indexer = CustomIndexer(window_size=1, use_expanding=use_expanding) df.rolling(indexer).sum()

6.3 VariableOffsetWindowIndexer:非固定频率 offset

VariableOffsetWindowIndexer(pandas/core/indexers/objects.py#L211)允许对BusinessDay这类非固定频率 offset 做滚动操作(rolling直接传入'B'等非固定频率会抛ValueError,见 pandas/core/generic.py#L11944-L11946):

from pandas.api.indexers import VariableOffsetWindowIndexer df = pd.DataFrame(range(10), index=pd.date_range("2020", periods=10)) offset = pd.offsets.BDay(1) indexer = VariableOffsetWindowIndexer(index=df.index, offset=offset) df.rolling(indexer).sum()

6.4 FixedForwardWindowIndexer:前向滚动窗口

当"未来信息可用"(例如每个数据点本身是一条完整实验时间序列)时,可用FixedForwardWindowIndexer(pandas/core/indexers/objects.py#L429)实现闭式固定宽度的前向窗口:

from pandas.api.indexers import FixedForwardWindowIndexer indexer = FixedForwardWindowIndexer(window_size=2) df.rolling(indexer, min_periods=1).sum()

等效做法是"切片 → 滚动聚合 → 翻转":

df = pd.DataFrame( data=[ [pd.Timestamp("2018-01-01 00:00:00"), 100], [pd.Timestamp("2018-01-01 00:00:01"), 101], [pd.Timestamp("2018-01-01 00:00:03"), 103], [pd.Timestamp("2018-01-01 00:00:04"), 111], ], columns=["time", "value"], ).set_index("time") reversed_df = df[::-1].rolling("2s").sum()[::-1]

用户指南还提到 pandas/core/indexers/objects.py 中提供FixedWindowIndexerExpandingIndexerL390)、ExponentialMovingWindowIndexerL637)等其他内建实现,可作为自定义 indexer 的参考范本。

七、与 groupby 链式组合

rollingexpandingewm均支持与groupby链式使用:先按指定键分组,再对每组执行窗口操作(对应实现RollingGroupbyExpandingGroupbyExponentialMovingWindowGroupby分别位于 pandas/core/window/rolling.py#L3540、pandas/core/window/expanding.py#L1456、pandas/core/window/ewm.py#L1016):

df = pd.DataFrame({'A': ['a', 'b', 'a', 'b', 'a'], 'B': range(5)}) df.groupby('A').expanding().sum()

八、源码实现速览:从 API 到底层 Cython

关注点仓库位置
Series/DataFramerolling/expanding/ewm入口pandas/core/generic.py#L11915、L12214、L12287
Window(加权窗口)实现pandas/core/window/rolling.py#L862
Rolling/RollingAndExpandingMixinpandas/core/window/rolling.py#L1955 / L1537
Expanding实现pandas/core/window/expanding.py#L43
ExponentialMovingWindow实现pandas/core/window/ewm.py#L127
Numba 引擎辅助(generate_numba_apply_func、table 函数等)pandas/core/window/numba_.py
EWM online 状态对象pandas/core/window/online.py
BaseIndexer及内建 indexerpandas/core/indexers/objects.py
窗口边界计算的 Cython 底层(calculate_variable_window_bounds等)pandas/_libs/window/indexers.pyx 及 pandas/_libs/window/aggregations.pyx

从源码结构可以推断:RollingExpanding共享RollingAndExpandingMixin的聚合方法骨架,仅在窗口边界(固定/时间/自定义 vs 累计)上不同;Window是独立的BaseWindow子类,专门承接 scipy 加权窗口;而ExponentialMovingWindow的 mean/sum/std/var/corr/cov 各自实现递归或加权公式。聚合计算最终落到pandas._libs.window.aggregations的 Cython 例程(如roll_sumroll_max),这也是 Kahan 求和等数值精度措施的实施位置。

九、小结与速查

  • 选型速查:固定窗口用.rolling(window=int);时间跨度窗口用.rolling(window='2D')(索引需单调);累计统计用.expanding();衰减加权用.ewm();滤波/谱估计用.rolling(win_type=...);前向窗口用FixedForwardWindowIndexer;非固定频率 offset 用VariableOffsetWindowIndexer;全表多列加速用method='table'+engine='numba'
  • 通用参数min_periods(时间窗口默认 1、整数窗口默认window)、center(标签居中)、closed(端点包含性)、step(结果降采样)、aggregate/agg(多聚合)。
  • 返回对象.rolling()返回Rolling(带win_type时返回Window),.expanding()返回Expanding.ewm()返回ExponentialMovingWindow,三者均为pandas.api.typing下的类型,支持 IDE 类型提示。

如需查看每个方法的完整文档字符串与示例,可继续阅读 doc/source/reference/window.rst 对应的api/目录自动生成页,以及 doc/source/user_guide/window.rst 的完整用户指南。

【免费下载链接】pandasFlexible and powerful data analysis / manipulation library for Python, providing labeled data structures similar to R data.frame objects, statistical functions, and much more项目地址: https://gitcode.com/gh_mirrors/pa/pandas

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询