窗口操作#

pandas 包含一套精简的 API,用于执行窗口操作 — 这是一种对滑动值分区执行聚合的操作。该 API 的工作方式类似于 groupby API,即 SeriesDataFrame 使用必要的参数调用窗口方法,然后依次调用聚合函数。

In [1]: s = pd.Series(range(5))

In [2]: s.rolling(window=2).sum()
Out[2]: 
0    NaN
1    1.0
2    3.0
3    5.0
4    7.0
dtype: float64

窗口是通过从当前观测值向前回溯窗口长度来构成的。可以通过对以下数据窗口化分区求和来推导出上面的结果

In [3]: for window in s.rolling(window=2):
   ...:     print(window)
   ...: 
0    0
dtype: int64
0    0
1    1
dtype: int64
1    1
2    2
dtype: int64
2    2
3    3
dtype: int64
3    3
4    4
dtype: int64

概述#

pandas 支持 4 种类型的窗口操作

  1. 滚动窗口:对值进行通用的固定或可变滑动窗口操作。

  2. 加权窗口:由 scipy.signal 库提供的加权、非矩形窗口。

  3. 展开窗口:对值进行累积窗口操作。

  4. 指数加权窗口:对值进行累积和指数加权窗口操作。

概念

方法

返回对象

支持基于时间的窗口

支持链式 groupby

支持表式方法

支持在线操作

滚动窗口

rolling

pandas.typing.api.Rolling

是(自 1.3 版本起)

加权窗口

rolling

pandas.typing.api.Window

展开窗口

expanding

pandas.typing.api.Expanding

是(自 1.3 版本起)

指数加权窗口

ewm

pandas.typing.api.ExponentialMovingWindow

是(自 1.2 版本起)

是(自 1.3 版本起)

如上所述,一些操作支持通过时间偏移量指定窗口

In [4]: s = pd.Series(range(5), index=pd.date_range('2020-01-01', periods=5, freq='1D'))

In [5]: s.rolling(window='2D').sum()
Out[5]: 
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
Freq: D, dtype: float64

此外,一些方法支持将 groupby 操作与窗口操作链式调用,这将首先按指定键对数据进行分组,然后对每个组执行窗口操作。

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

In [7]: df.groupby('A').expanding().sum()
Out[7]: 
       B
A       
a 0  0.0
  2  2.0
  4  6.0
b 1  1.0
  3  4.0

注意

窗口操作目前仅支持数值数据(整数和浮点数),并且始终返回 float64 值。

警告

某些窗口聚合操作(如 meansumvarstd)可能由于底层窗口算法累积求和而存在数值精度问题。当数值差异幅度达到 1/np.finfo(np.double).eps(约等于 \(4.5 \times 10^{15}\))时,会导致截断。必须注意的是,大数值可能会影响不包含这些数值的窗口。为了尽可能保留精度,Kahan 求和算法用于计算滚动求和。

一些窗口操作还在构造函数中支持 method='table' 选项,该选项对整个 DataFrame 执行窗口操作,而不是一次处理一个列。这可以为具有许多列的 DataFrame 提供有益的性能优势,或者能够在窗口操作过程中利用其他列。只有当在相应的函数调用中指定了 engine='numba' 时,才能使用 method='table' 选项。

例如,可以通过指定一个单独的权重列,使用 apply() 来计算加权平均值

In [8]: 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
   ...: 

In [9]: df = pd.DataFrame([[1, 2, 0.6], [2, 3, 0.4], [3, 4, 0.2], [4, 5, 0.7]])

In [10]: df.rolling(2, method="table", min_periods=0).apply(weighted_mean, raw=True, engine="numba")  # noqa: E501
Out[10]: 
          0         1    2
0  1.000000  2.000000  1.0
1  1.800000  2.000000  1.0
2  3.333333  2.333333  1.0
3  1.555556  7.000000  1.0

一些窗口操作还支持在构造窗口对象后调用 online 方法,该方法返回一个新对象,可以传入新的 DataFrameSeries 对象来继续窗口计算(即在线计算)。

这个新窗口对象上的方法必须先调用聚合方法来“预热”在线计算的初始状态。然后,新的 DataFrameSeries 对象可以传入 update 参数来继续窗口计算。

In [11]: df = pd.DataFrame([[1, 2, 0.6], [2, 3, 0.4], [3, 4, 0.2], [4, 5, 0.7]])

In [12]: df.ewm(0.5).mean()
Out[12]: 
          0         1         2
0  1.000000  2.000000  0.600000
1  1.750000  2.750000  0.450000
2  2.615385  3.615385  0.276923
3  3.550000  4.550000  0.562500
In [13]: online_ewm = df.head(2).ewm(0.5).online()

In [14]: online_ewm.mean()
Out[14]: 
      0     1     2
0  1.00  2.00  0.60
1  1.75  2.75  0.45

In [15]: online_ewm.mean(update=df.tail(1))
Out[15]: 
          0         1         2
3  3.307692  4.307692  0.623077

所有窗口操作都支持 min_periods 参数,该参数决定了窗口中非 np.nan 值的最小数量;否则,结果值为 np.nan。对于基于时间的窗口,min_periods 的默认值为 1,对于固定窗口,默认值为 window

In [16]: s = pd.Series([np.nan, 1, 2, np.nan, np.nan, 3])

In [17]: s.rolling(window=3, min_periods=1).sum()
Out[17]: 
0    NaN
1    1.0
2    3.0
3    3.0
4    2.0
5    3.0
dtype: float64

In [18]: s.rolling(window=3, min_periods=2).sum()
Out[18]: 
0    NaN
1    NaN
2    3.0
3    3.0
4    NaN
5    NaN
dtype: float64

# Equivalent to min_periods=3
In [19]: s.rolling(window=3, min_periods=None).sum()
Out[19]: 
0   NaN
1   NaN
2   NaN
3   NaN
4   NaN
5   NaN
dtype: float64

此外,所有窗口操作都支持 aggregate 方法,用于返回对窗口应用多个聚合的结果。

In [20]: df = pd.DataFrame({"A": range(5), "B": range(10, 15)})

In [21]: df.expanding().agg(["sum", "mean", "std"])
Out[21]: 
      A                    B                
    sum mean       std   sum  mean       std
0   0.0  0.0       NaN  10.0  10.0       NaN
1   1.0  0.5  0.707107  21.0  10.5  0.707107
2   3.0  1.0  1.000000  33.0  11.0  1.000000
3   6.0  1.5  1.290994  46.0  11.5  1.290994
4  10.0  2.0  1.581139  60.0  12.0  1.581139

滚动窗口#

通用的滚动窗口支持将窗口指定为固定数量的观测值,或基于偏移量的可变数量的观测值。如果提供了基于时间的偏移量,则相应的基于时间的索引必须是单调的。

In [22]: times = ['2020-01-01', '2020-01-03', '2020-01-04', '2020-01-05', '2020-01-29']

In [23]: s = pd.Series(range(5), index=pd.DatetimeIndex(times))

In [24]: s
Out[24]: 
2020-01-01    0
2020-01-03    1
2020-01-04    2
2020-01-05    3
2020-01-29    4
dtype: int64

# Window with 2 observations
In [25]: s.rolling(window=2).sum()
Out[25]: 
2020-01-01    NaN
2020-01-03    1.0
2020-01-04    3.0
2020-01-05    5.0
2020-01-29    7.0
dtype: float64

# Window with 2 days worth of observations
In [26]: s.rolling(window='2D').sum()
Out[26]: 
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
dtype: float64

有关所有支持的聚合函数,请参阅 滚动窗口函数

窗口居中#

默认情况下,标签设置在窗口的右边缘,但提供了一个 center 关键字,以便可以将标签设置在中心。

In [27]: s = pd.Series(range(10))

In [28]: s.rolling(window=5).mean()
Out[28]: 
0    NaN
1    NaN
2    NaN
3    NaN
4    2.0
5    3.0
6    4.0
7    5.0
8    6.0
9    7.0
dtype: float64

In [29]: s.rolling(window=5, center=True).mean()
Out[29]: 
0    NaN
1    NaN
2    2.0
3    3.0
4    4.0
5    5.0
6    6.0
7    7.0
8    NaN
9    NaN
dtype: float64

这也可以应用于类似日期的索引。

In [30]: df = pd.DataFrame(
   ....:     {"A": [0, 1, 2, 3, 4]}, index=pd.date_range("2020", periods=5, freq="1D")
   ....: )
   ....: 

In [31]: df
Out[31]: 
            A
2020-01-01  0
2020-01-02  1
2020-01-03  2
2020-01-04  3
2020-01-05  4

In [32]: df.rolling("2D", center=False).mean()
Out[32]: 
              A
2020-01-01  0.0
2020-01-02  0.5
2020-01-03  1.5
2020-01-04  2.5
2020-01-05  3.5

In [33]: df.rolling("2D", center=True).mean()
Out[33]: 
              A
2020-01-01  0.5
2020-01-02  1.5
2020-01-03  2.5
2020-01-04  3.5
2020-01-05  4.0

滚动窗口端点#

可以使用 closed 参数指定区间端点在滚动窗口计算中的包含情况

行为

'right'

闭合右端点

'left'

闭合左端点

'both'

闭合两个端点

'neither'

开放端点

例如,右端点开放在许多需要避免当前信息污染过去信息的问题中非常有用。这使得滚动窗口可以在“直到某个时间点”计算统计量,但不包括该时间点。

In [34]: 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"),
   ....:     ],
   ....: )
   ....: 

In [35]: df["right"] = df.rolling("2s", closed="right").x.sum()  # default

In [36]: df["both"] = df.rolling("2s", closed="both").x.sum()

In [37]: df["left"] = df.rolling("2s", closed="left").x.sum()

In [38]: df["neither"] = df.rolling("2s", closed="neither").x.sum()

In [39]: df
Out[39]: 
                     x  right  both  left  neither
2013-01-01 09:00:01  1    1.0   1.0   NaN      NaN
2013-01-01 09:00:02  1    2.0   2.0   1.0      1.0
2013-01-01 09:00:03  1    2.0   3.0   2.0      1.0
2013-01-01 09:00:04  1    2.0   3.0   2.0      1.0
2013-01-01 09:00:06  1    1.0   2.0   1.0      NaN

自定义窗口滚动#

除了接受整数或偏移量作为 window 参数外,rolling 还接受 BaseIndexer 的子类,允许用户定义计算窗口边界的自定义方法。 BaseIndexer 子类需要定义一个 get_window_bounds 方法,该方法返回一个包含两个数组的元组,第一个数组是窗口的起始索引,第二个数组是窗口的结束索引。此外,num_valuesmin_periodscenterclosedstep 将自动传递给 get_window_bounds,并且定义的函数必须始终接受这些参数。

例如,如果我们有以下 DataFrame

In [40]: use_expanding = [True, False, True, False, True]

In [41]: use_expanding
Out[41]: [True, False, True, False, True]

In [42]: df = pd.DataFrame({"values": range(5)})

In [43]: df
Out[43]: 
   values
0       0
1       1
2       2
3       3
4       4

并且我们想使用展开窗口,其中 use_expandingTrue,否则为大小为 1 的窗口,我们可以创建以下 BaseIndexer 子类

In [44]: from pandas.api.indexers import BaseIndexer

In [45]: 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
   ....: 

In [46]: indexer = CustomIndexer(window_size=1, use_expanding=use_expanding)

In [47]: df.rolling(indexer).sum()
Out[47]: 
   values
0     0.0
1     1.0
2     3.0
3     3.0
4    10.0

您可以在 此处 查看 BaseIndexer 子类的其他示例。

其中一个值得注意的子类是 VariableOffsetWindowIndexer,它允许在非固定偏移量(如 BusinessDay)上进行滚动操作。

In [48]: from pandas.api.indexers import VariableOffsetWindowIndexer

In [49]: df = pd.DataFrame(range(10), index=pd.date_range("2020", periods=10))

In [50]: offset = pd.offsets.BDay(1)

In [51]: indexer = VariableOffsetWindowIndexer(index=df.index, offset=offset)

In [52]: df
Out[52]: 
            0
2020-01-01  0
2020-01-02  1
2020-01-03  2
2020-01-04  3
2020-01-05  4
2020-01-06  5
2020-01-07  6
2020-01-08  7
2020-01-09  8
2020-01-10  9

In [53]: df.rolling(indexer).sum()
Out[53]: 
               0
2020-01-01   0.0
2020-01-02   1.0
2020-01-03   2.0
2020-01-04   3.0
2020-01-05   7.0
2020-01-06  12.0
2020-01-07   6.0
2020-01-08   7.0
2020-01-09   8.0
2020-01-10   9.0

对于某些问题,对未来的知识是可用的,可以用于分析。例如,当每个数据点都是从实验中读取的完整时间序列,并且任务是提取底层条件时,就会出现这种情况。在这些情况下,执行前瞻性滚动窗口计算可能很有用。FixedForwardWindowIndexer 类可用于此目的。这个 BaseIndexer 子类实现了一个闭合的固定宽度前瞻性滚动窗口,我们可以如下使用它

In [54]: from pandas.api.indexers import FixedForwardWindowIndexer

In [55]: indexer = FixedForwardWindowIndexer(window_size=2)

In [56]: df.rolling(indexer, min_periods=1).sum()
Out[56]: 
               0
2020-01-01   1.0
2020-01-02   3.0
2020-01-03   5.0
2020-01-04   7.0
2020-01-05   9.0
2020-01-06  11.0
2020-01-07  13.0
2020-01-08  15.0
2020-01-09  17.0
2020-01-10   9.0

我们也可以通过使用切片、应用滚动聚合,然后翻转结果来达到此目的,如下面的示例所示。

In [57]: 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")
   ....: 

In [58]: df
Out[58]: 
                     value
time                      
2018-01-01 00:00:00    100
2018-01-01 00:00:01    101
2018-01-01 00:00:03    103
2018-01-01 00:00:04    111

In [59]: reversed_df = df[::-1].rolling("2s").sum()[::-1]

In [60]: reversed_df
Out[60]: 
                     value
time                      
2018-01-01 00:00:00  201.0
2018-01-01 00:00:01  101.0
2018-01-01 00:00:03  214.0
2018-01-01 00:00:04  111.0

滚动 apply#

apply() 函数接受一个额外的 func 参数,并执行通用的滚动计算。func 参数应该是一个能够从 ndarray 输入产生单个值的函数。raw 指定窗口是否被转换为 Series 对象(raw=False)或 ndarray 对象(raw=True)。

In [61]: def mad(x):
   ....:     return np.fabs(x - x.mean()).mean()
   ....: 

In [62]: s = pd.Series(range(10))

In [63]: s.rolling(window=4).apply(mad, raw=True)
Out[63]: 
0    NaN
1    NaN
2    NaN
3    1.0
4    1.0
5    1.0
6    1.0
7    1.0
8    1.0
9    1.0
dtype: float64

Numba 引擎#

此外,如果安装了 Numba 作为可选依赖项,apply() 可以利用 Numba。可以通过指定 engine='numba'engine_kwargs 参数(raw 也必须设置为 True)来使用 Numba 执行 apply 聚合。有关参数的一般用法和性能考虑,请参阅 使用 Numba 增强性能

Numba 将可能在两个例程中应用

  1. 如果 func 是一个标准的 Python 函数,引擎将对传入的函数进行JIT 编译func 也可以是一个已 JIT 编译的函数,在这种情况下,引擎将不再重新 JIT 编译该函数。

  2. 引擎将对 apply 函数应用于每个窗口的 for 循环进行 JIT 编译。

engine_kwargs 参数是一个关键字参数字典,它将被传递给 numba.jit 装饰器。这些关键字参数将应用于*已传入的函数*(如果它是标准 Python 函数)以及应用于每个窗口的 for 循环。

meanmedianmaxminsum 也支持 engineengine_kwargs 参数。

二元窗口函数#

cov()corr() 可以计算两个 Series 或任意 DataFrame/SeriesDataFrame/DataFrame 的组合的移动窗口统计量。以下是每种情况下的行为:

  • 两个 Series:计算配对的统计量。

  • DataFrame/Series:计算 DataFrame 的每一列与传入的 Series 的统计量,从而返回一个 DataFrame。

  • DataFrame/DataFrame:默认情况下,计算匹配列名的统计量,返回一个 DataFrame。如果传递了关键字参数 pairwise=True,则计算每一列对的统计量,返回一个具有 MultiIndexDataFrame,其值为相关日期(请参阅 下一节)。

例如:

In [64]: df = pd.DataFrame(
   ....:     np.random.randn(10, 4),
   ....:     index=pd.date_range("2020-01-01", periods=10),
   ....:     columns=["A", "B", "C", "D"],
   ....: )
   ....: 

In [65]: df = df.cumsum()

In [66]: df2 = df[:4]

In [67]: df2.rolling(window=2).corr(df2["B"])
Out[67]: 
              A    B    C    D
2020-01-01  NaN  NaN  NaN  NaN
2020-01-02 -1.0  1.0 -1.0  1.0
2020-01-03  1.0  1.0  1.0 -1.0
2020-01-04 -1.0  1.0  1.0 -1.0

计算滚动成对协方差和相关性#

在金融数据分析和其他领域,计算时间序列集合的协方差和相关性矩阵是很常见的。通常,人们也对移动窗口的协方差和相关性矩阵感兴趣。这可以通过传递 pairwise 关键字参数来实现,在 DataFrame 输入的情况下,这将产生一个 MultiIndexed DataFrame,其 index 是相关的日期。对于单个 DataFrame 输入,甚至可以省略 pairwise 参数。

注意

缺失值被忽略,并且每个条目都是使用成对完整观测值计算的。

假设缺失数据是随机缺失的,这将导致无偏的协方差矩阵估计。然而,对于许多应用来说,这种估计可能不可接受,因为估计的协方差矩阵不保证是半正定的。这可能导致估计的相关性绝对值大于 1,以及/或协方差矩阵不可逆。有关更多详细信息,请参阅 协方差矩阵估计

In [68]: covs = (
   ....:     df[["B", "C", "D"]]
   ....:     .rolling(window=4)
   ....:     .cov(df[["A", "B", "C"]], pairwise=True)
   ....: )
   ....: 

In [69]: covs
Out[69]: 
                     B         C         D
2020-01-01 A       NaN       NaN       NaN
           B       NaN       NaN       NaN
           C       NaN       NaN       NaN
2020-01-02 A       NaN       NaN       NaN
           B       NaN       NaN       NaN
...                ...       ...       ...
2020-01-09 B  0.342006  0.230190  0.052849
           C  0.230190  1.575251  0.082901
2020-01-10 A -0.333945  0.006871 -0.655514
           B  0.649711  0.430860  0.469271
           C  0.430860  0.829721  0.055300

[30 rows x 3 columns]

加权窗口#

`.rolling` 中的 `win_type` 参数生成加权窗口,这些窗口通常用于滤波和谱估计。win_type 必须是字符串,对应于一个 scipy.signal 窗口函数。为了使用这些窗口,必须安装 Scipy,并且 Scipy 窗口方法接受的补充参数必须在聚合函数中指定。

In [70]: s = pd.Series(range(10))

In [71]: s.rolling(window=5).mean()
Out[71]: 
0    NaN
1    NaN
2    NaN
3    NaN
4    2.0
5    3.0
6    4.0
7    5.0
8    6.0
9    7.0
dtype: float64

In [72]: s.rolling(window=5, win_type="triang").mean()
Out[72]: 
0    NaN
1    NaN
2    NaN
3    NaN
4    2.0
5    3.0
6    4.0
7    5.0
8    6.0
9    7.0
dtype: float64

# Supplementary Scipy arguments passed in the aggregation function
In [73]: s.rolling(window=5, win_type="gaussian").mean(std=0.1)
Out[73]: 
0    NaN
1    NaN
2    NaN
3    NaN
4    2.0
5    3.0
6    4.0
7    5.0
8    6.0
9    7.0
dtype: float64

有关所有支持的聚合函数,请参阅 加权窗口函数

展开窗口#

展开窗口产生一个聚合统计量的数值,该统计量包含截至当前时间点的所有可用数据。由于这些计算是滚动统计量的特例,因此在 pandas 中它们的实现方式使得以下两个调用是等效的:

In [74]: df = pd.DataFrame(range(5))

In [75]: df.rolling(window=len(df), min_periods=1).mean()
Out[75]: 
     0
0  0.0
1  0.5
2  1.0
3  1.5
4  2.0

In [76]: df.expanding(min_periods=1).mean()
Out[76]: 
     0
0  0.0
1  0.5
2  1.0
3  1.5
4  2.0

有关所有支持的聚合函数,请参阅 展开窗口函数

指数加权窗口#

指数加权窗口类似于展开窗口,但每个先前点相对于当前点都呈指数级加权下降。

通常,加权移动平均计算如下:

\[y_t = \frac{\sum_{i=0}^t w_i x_{t-i}}{\sum_{i=0}^t w_i},\]

其中 \(x_t\) 是输入,\(y_t\) 是结果,\(w_i\) 是权重。

有关所有支持的聚合函数,请参阅 指数加权窗口函数

EW 函数支持两种指数权重变体。默认的 adjust=True 使用权重 \(w_i = (1 - \alpha)^i\),这给出了:

\[y_t = \frac{x_t + (1 - \alpha)x_{t-1} + (1 - \alpha)^2 x_{t-2} + ... + (1 - \alpha)^t x_{0}}{1 + (1 - \alpha) + (1 - \alpha)^2 + ... + (1 - \alpha)^t}\]

当指定 adjust=False 时,移动平均计算如下:

\[\begin{split}y_0 &= x_0 \\ y_t &= (1 - \alpha) y_{t-1} + \alpha x_t,\end{split}\]

这等同于使用权重:

\[\begin{split}w_i = \begin{cases} \alpha (1 - \alpha)^i & \text{if } i < t \\ (1 - \alpha)^i & \text{if } i = t. \end{cases}\end{split}\]

注意

这些方程有时也用 \(\alpha' = 1 - \alpha\) 来表示,例如:

\[y_t = \alpha' y_{t-1} + (1 - \alpha') x_t.\]

上述两种变体之间的差异是由于我们处理的是具有有限历史的序列。考虑一个无限历史的序列,当 adjust=True 时:

\[y_t = \frac{x_t + (1 - \alpha)x_{t-1} + (1 - \alpha)^2 x_{t-2} + ...} {1 + (1 - \alpha) + (1 - \alpha)^2 + ...}\]

注意到分母是一个公比为 \(1 - \alpha\) 的等比数列,首项为 1,我们得到:

\[\begin{split}y_t &= \frac{x_t + (1 - \alpha)x_{t-1} + (1 - \alpha)^2 x_{t-2} + ...} {\frac{1}{1 - (1 - \alpha)}}\\ &= [x_t + (1 - \alpha)x_{t-1} + (1 - \alpha)^2 x_{t-2} + ...] \alpha \\ &= \alpha x_t + [(1-\alpha)x_{t-1} + (1 - \alpha)^2 x_{t-2} + ...]\alpha \\ &= \alpha x_t + (1 - \alpha)[x_{t-1} + (1 - \alpha) x_{t-2} + ...]\alpha\\ &= \alpha x_t + (1 - \alpha) y_{t-1}\end{split}\]

这与上面 adjust=False 的表达式相同,因此表明两种变体对于无限序列是等效的。当 adjust=False 时,我们有 \(y_0 = x_0\)\(y_t = \alpha x_t + (1 - \alpha) y_{t-1}\)。因此,假设 \(x_0\) 不是一个普通值,而是无限序列截至该点的指数加权矩。

必须有 \(0 < \alpha \leq 1\),虽然可以直接传入 \(\alpha\),但通常更容易考虑 EW 矩的跨度(span)质心(center of mass)半衰期(half-life)

\[\begin{split}\alpha = \begin{cases} \frac{2}{s + 1}, & \text{对于跨度}\ s \geq 1\\ \frac{1}{1 + c}, & \text{对于质心}\ c \geq 0\\ 1 - e^{\frac{\log 0.5}{h}}, & \text{对于半衰期}\ h > 0 \end{cases}\end{split}\]

必须仅指定 spancenter of masshalf-lifealpha 中的一个选项给 EW 函数。

  • Span 对应于通常所说的“N 日 EW 移动平均”。

  • Center of mass 具有更物理的解释,可以从跨度方面考虑:\(c = (s - 1) / 2\)

  • Half-life 是指指数权重衰减到一半所需的时间周期。

  • Alpha 直接指定平滑因子。

您还可以通过在指定一组 times 的同时,将 halflife 指定为 timedelta 可转换单位,来指定观测值衰减到其值一半所需的时间。

In [77]: df = pd.DataFrame({"B": [0, 1, 2, np.nan, 4]})

In [78]: df
Out[78]: 
     B
0  0.0
1  1.0
2  2.0
3  NaN
4  4.0

In [79]: times = ["2020-01-01", "2020-01-03", "2020-01-10", "2020-01-15", "2020-01-17"]

In [80]: df.ewm(halflife="4 days", times=pd.DatetimeIndex(times)).mean()
Out[80]: 
          B
0  0.000000
1  0.585786
2  1.523889
3  1.523889
4  3.233686

以下公式用于计算带时间输入向量的指数加权平均值:

\[y_t = \frac{\sum_{i=0}^t 0.5^\frac{t_{t} - t_{i}}{\lambda} x_{t-i}}{\sum_{i=0}^t 0.5^\frac{t_{t} - t_{i}}{\lambda}},\]

ExponentialMovingWindow 还有一个 `ignore_na` 参数,它决定了中间的 null 值如何影响权重的计算。当 `ignore_na=False`(默认值)时,权重是基于绝对位置计算的,因此中间的 null 值会影响结果。当 `ignore_na=True` 时,权重是通过忽略中间的 null 值来计算的。例如,假设 `adjust=True`,如果 `ignore_na=False`,则 `3, NaN, 5` 的加权平均值计算如下:

\[\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}.\]

`.var()`、`.std()` 和 `.cov()` 函数有一个 `bias` 参数,指定结果是否应包含有偏或无偏统计量。例如,如果 `bias=True`,则 `ewmvar(x)` 计算为 `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\)。)有关详细信息,请参阅维基百科上的加权样本方差