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_periods、center、closed、method='table'、online 更新等高级特性的准确行为。
一、窗口操作总览:四种窗口类型与返回对象
pandas 的窗口操作(windowing operation)是一种"在滑动分片上执行聚合"的操作,API 风格与groupby一致:先由Series/DataFrame调用窗口方法并传入必要参数,得到窗口对象,再在窗口对象上调用聚合函数。
| 概念 | 方法 | 返回对象 | 支持时间窗口 | 支持 groupby 链式 | 支持 table 模式 | 支持 online 计算 |
|---|---|---|---|---|---|---|
| 滚动窗口 Rolling | rolling | pandas.api.typing.Rolling | 是 | 是 | 是 | 否 |
| 加权窗口 Weighted | rolling(带win_type) | pandas.api.typing.Window | 否 | 否 | 否 | 否 |
| 扩展窗口 Expanding | expanding | pandas.api.typing.Expanding | 否 | 是 | 是 | 否 |
| 指数加权窗口 EWM | ewm | pandas.api.typing.ExponentialMovingWindow | 否 | 是 | 否 | 是 |
三种窗口对象在类型标注层面由 pandas/api/typing/init.py 导出,具体实现类位于 pandas/core/window/rolling.py(Rolling、Window)、pandas/core/window/expanding.py(Expanding)和 pandas/core/window/ewm.py(ExponentialMovingWindow)。这些方法定义在Series/DataFrame的公共基类 pandas/core/generic.py(rolling在L11915、expanding在L12214、ewm在L12287)。
最基础的用法:
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; mean、sum、var、std等聚合由于底层算法累加求和,当数值量级相差达到1/np.finfo(np.double).eps(约 4.5×10¹⁵)时可能出现数值截断;pandas 使用 Kahan 求和算法计算滚动求和以尽量保持精度。
二、Rolling 滚动窗口函数:完整方法清单与参数语义
pandas.api.typing.Rolling实例由DataFrame.rolling与Series.rolling调用返回。API 参考文档列出其全部聚合方法:count、sum、mean、median、var、std、min、max、first、last、corr、cov、skew、kurt、apply、pipe、aggregate、quantile、sem、rank、nunique。
这些方法定义在 pandas/core/window/rolling.py 的RollingAndExpandingMixin(L1537,为Rolling与Expanding共享)及Rolling(L1955)中,其中count、sum、max等直接调用 Cython 扩展模块pandas._libs.window.aggregations中的roll_sum、roll_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.apply的Series的索引。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=32.6 二元窗口函数:cov 与 corr
Rolling.cov与Rolling.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。mean、median、max、min、sum、std、var也支持engine与engine_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 参考中Window(pandas.api.typing.Window)支持四个聚合方法:mean、sum、var、std。其实现类位于 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.Expanding由DataFrame.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 个聚合方法:count、sum、mean、median、var、std、min、max、first、last、corr、cov、skew、kurt、apply、pipe、aggregate、quantile、sem、rank、nunique。实现类为 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.ExponentialMovingWindow由DataFrame.ewm/Series.ewm调用返回,实现类在 pandas/core/window/ewm.py#L127。它类似 expanding 窗口,但每个历史点相对当前点按指数衰减。API 参考列出的方法:mean、sum、std、var、corr、cov。
一般加权移动平均公式:
$$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 的文档可见:若提供times且adjust=True,可同时提供halflife与com/span/alpha之一;若times且adjust=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.var、std、cov支持bias参数。bias=True时ewmvar(x) = ewma(x**2) - ewma(x)**2;bias=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 外,rolling的window参数还接受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_values、min_periods、center、closed、step由顶层 rolling API 自动传入,因此自定义方法必须始终接受这些参数。未实现时基类直接抛NotImplementedError(L105)。
6.2 自定义 indexer 示例
若希望在use_expanding为True处使用扩展窗口、否则用大小为 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 中提供FixedWindowIndexer、ExpandingIndexer(L390)、ExponentialMovingWindowIndexer(L637)等其他内建实现,可作为自定义 indexer 的参考范本。
七、与 groupby 链式组合
rolling、expanding、ewm均支持与groupby链式使用:先按指定键分组,再对每组执行窗口操作(对应实现RollingGroupby、ExpandingGroupby、ExponentialMovingWindowGroupby分别位于 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/DataFrame的rolling/expanding/ewm入口 | pandas/core/generic.py#L11915、L12214、L12287 |
Window(加权窗口)实现 | pandas/core/window/rolling.py#L862 |
Rolling/RollingAndExpandingMixin | pandas/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及内建 indexer | pandas/core/indexers/objects.py |
窗口边界计算的 Cython 底层(calculate_variable_window_bounds等) | pandas/_libs/window/indexers.pyx 及 pandas/_libs/window/aggregations.pyx |
从源码结构可以推断:Rolling与Expanding共享RollingAndExpandingMixin的聚合方法骨架,仅在窗口边界(固定/时间/自定义 vs 累计)上不同;Window是独立的BaseWindow子类,专门承接 scipy 加权窗口;而ExponentialMovingWindow的 mean/sum/std/var/corr/cov 各自实现递归或加权公式。聚合计算最终落到pandas._libs.window.aggregations的 Cython 例程(如roll_sum、roll_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),仅供参考