pandas rolling exponential 滚动计算指数加权标准差会受起点影响¶
在使用 rolling(win_type="exponential").std()
计算标准差时,结果可能会受数据起点的影响。
本文将通过几个简单的代码示例,探究数据具有不同数据起点(通过 iloc[1:]
手动构造不同的数据起点进行模拟)对 Pandas rolling(win_type="exponential").std()
计算结果的影响,并提供了一个自定义函数实现严格控制滚动回看窗口,以获得一致的结果。
构造模拟数据¶
我们首先构造一个包含 20 个随机数的简单 Series 作为模拟数据:
import pandas as pd
import numpy as np
np.random.seed(0)
df_raw = pd.DataFrame(np.random.randn(20), columns=["values"])
print(df_raw)
values
0 1.764052
1 0.400157
2 0.978738
3 2.240893
4 1.867558
5 -0.977278
6 0.950088
7 -0.151357
8 -0.103219
9 0.410599
10 0.144044
11 1.454274
12 0.761038
13 0.121675
14 0.443863
15 0.333674
16 1.494079
17 -0.205158
18 0.313068
19 -0.854096
滚动等权计算标准差¶
我们首先直接使用 rolling(window=10, min_periods=10).std()
方法,分别在原始数据 df_raw
和将其具有不同数据起点(即 df_raw.iloc[1:]
)后的数据上进行计算。
# 代码 1
df = df_raw.copy()
res_1 = df["values"].rolling(window=10, min_periods=10).std().rename("res_1")
# 代码 2
df = df_raw.copy()
df = df.iloc[1:]
res_2 = df["values"].rolling(window=10, min_periods=10).std().rename("res_2")
将两个结果合并查看:
res_1 res_2
0 NaN NaN
1 NaN NaN
2 NaN NaN
3 NaN NaN
4 NaN NaN
5 NaN NaN
6 NaN NaN
7 NaN NaN
8 NaN NaN
9 1.019391 NaN
10 0.965520 0.965520
11 1.001074 1.001074
12 0.996245 0.996245
13 0.834837 0.834837
14 0.671154 0.671154
15 0.498640 0.498640
16 0.583373 0.583373
17 0.590163 0.590163
18 0.557819 0.557819
19 0.709826 0.709826
观察:
- 由于设置了
min_periods=10
,前 9 个结果均为NaN
. - 原始数据
res_1
在索引9
处得到了第一个有效值1.019391
. - 具有不同数据起点后的数据
res_2
由于数据整体向后移动了一位,在索引9
处仍为NaN
. - 从索引
10
开始,res_1
和res_2
的结果完全一致。
滚动指数加权计算标准差¶
接下来,我们使用指数加权窗口 (win_type="exponential"
) 进行滚动标准差计算,同样对比原始数据和具有不同数据起点后的数据。我们设置 window=10
和 min_periods=10
,并使用 center=0
, tau=-5 / np.log(2), sym=False
参数来模拟回看窗口为 10、半衰期为 5 的指数加权。
# 代码 3
df = df_raw.copy()
res_3 = (
df["values"]
.rolling(window=10, min_periods=10, win_type="exponential")
.std(center=0, tau=-5 / np.log(2), sym=False)
).rename("res_3")
# 代码 4
df = df_raw.copy()
df = df.iloc[1:]
res_4 = (
df["values"]
.rolling(window=10, min_periods=10, win_type="exponential")
.std(center=0, tau=-5 / np.log(2), sym=False)
).rename("res_4")
将两个结果合并查看:
res_3 res_4
0 NaN NaN
1 NaN NaN
2 NaN NaN
3 NaN NaN
4 NaN NaN
5 NaN NaN
6 NaN NaN
7 NaN NaN
8 NaN NaN
9 0.961620 NaN
10 0.919546 0.865700
11 0.949999 0.896859
12 0.944208 0.890761
13 0.792478 0.743234
14 0.627014 0.585890
15 0.462449 0.438443
16 0.572467 0.543364
17 0.580613 0.550393
18 0.537701 0.515704
19 0.759793 0.710164
观察:
- 同样地,
res_3
在索引9
处有第一个有效值,而res_4
为NaN
. - 重要差异: 从索引
10
开始,res_3
和res_4
的结果并不一致!尽管这两个位置的窗口似乎应该包含了相同的数据序列(从索引1
到10
),但计算出的指数加权标准差却不同,分别为0.919546
和0.865700
。
这似乎表明,在使用 rolling().std()
并指定 win_type="exponential"
时,其内部计算可能不仅仅依赖于窗口内的原始数据序列,还可能隐式地依赖于原始 Series 的起始位置。
使用 .apply()
自定义函数实现严格控制回看窗口¶
为了更严格地控制滚动窗口的计算结果,我们可以使用 .apply()
方法,将窗口数据传递给一个自定义函数。我们定义一个函数 std_exponential
,它在接收到的 固定窗口数据(一个 Series) 上调用指数加权滚动计算,并取最后一个结果。
def std_exponential(x: np.array) -> float:
"""Exponentially weighted standard deviation."""
return (
pd.Series(x)
.rolling(window=10, min_periods=10, win_type="exponential")
.std(center=0, tau=-5 / np.log(2), sym=False)
.iloc[-1]
)
我们再次在原始数据和具有不同数据起点的数据上,使用 .apply(std_exponential, raw=True)
进行计算。
代码 5 在原始数据上使用 .apply()
:
# 代码 5
df = df_raw.copy()
res_5 = (
df["values"]
.rolling(window=10, min_periods=10)
.apply(std_exponential, raw=True)
.rename("res_5")
)
代码 6 在具有不同数据起点一位后的数据上使用 .apply()
:
# 代码 6
df = df_raw.copy()
df = df.iloc[1:]
res_6 = (
df["values"]
.rolling(window=10, min_periods=10)
.apply(std_exponential, raw=True)
.rename("res_6")
)
将两个结果合并查看:
res_5 res_6
0 NaN NaN
1 NaN NaN
2 NaN NaN
3 NaN NaN
4 NaN NaN
5 NaN NaN
6 NaN NaN
7 NaN NaN
8 NaN NaN
9 0.961620 NaN
10 0.865700 0.865700
11 0.904988 0.904988
12 0.843088 0.843088
13 0.715911 0.715911
14 0.589018 0.589018
15 0.457798 0.457798
16 0.593950 0.593950
17 0.626757 0.626757
18 0.575467 0.575467
19 0.759793 0.759793
观察:
- 第一个有效值同样出现在不同位置。
- 关键发现: 从索引
10
开始,res_5
和res_6
的结果又变得一致了。
通过 .apply()
将窗口数据传递给自定义函数进行计算,每次传入的窗口数据都形成了一个新的、独立的 Series,其总是相对于这个固定的窗口数据进行指数加权计算。因此,只要窗口内的数据内容相同,结果就相同。