扩展窗口与指数加权:expanding 与 ewm
本教程共 54 篇 · 第 43 篇 · 更新于 2026-08-11 · 约 8 分钟阅读
本节目标:搞懂 expanding(累积窗口)和 ewm(指数加权窗口)的差别,知道什么时候用哪个,能算出累积统计量和指数加权平均。
expanding:窗口只增不减
上一节的 rolling 窗口大小固定,滑动前进。expanding(扩展窗口)不一样:窗口从第一个数据开始,每次都往后多包含一条,只增不减。第 i 个位置的结果,用的是从起点到 i 的全部数据。
用一句话记:expanding 算的是”到目前为止”的统计量。
import pandas as pd
import numpy as np
s = pd.Series([1, 2, 3, 4, 5])
# 累积均值:到每个位置为止的平均
print(s.expanding().mean())
# 0 1.0
# 1 1.5
# 2 2.0
# 3 2.5
# 4 3.0
# 累积最大值:到每个位置为止见过的最大值
print(s.expanding().max())
# 0 1.0
# 1 2.0
# 2 3.0
# 3 4.0
# 4 5.0
看累积均值:索引 1 是 (1+2)/2=1.5,索引 2 是 (1+2+3)/3=2.0。窗口越长,数据越多,但起点永远是第一个。
实际上 expanding 就是滚动窗口的特例:窗口大小等于整个数据长度,再用 min_periods=1 让第一个点就能出结果。
import pandas as pd
import numpy as np
s = pd.Series([1, 2, 3, 4, 5])
# 下面两种写法结果完全一样
print(s.rolling(window=len(s), min_periods=1).mean())
print(s.expanding(min_periods=1).mean())
expanding 的用法与场景
expanding 支持的聚合方法和 rolling 一样:sum、mean、max、min、std、var、median、count,也能用 agg 一次算多个:
import pandas as pd
import numpy as np
df = pd.DataFrame({"A": range(5), "B": range(10, 15)})
print(df.expanding().agg(["sum", "mean", "std"]))
# 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
min_periods 在这里同样有效:指定至少累积多少个值才出结果。
expanding 的典型场景:
- 累计求和,比如”今年累计销售额”——其实用
cumsum()也能做,expanding().sum() 是更通用的写法 - 累计最大值/最小值,比如股价上市以来的最高价
- 累积均值、累积标准差,比如”到目前为止的平均成绩”
Noteexpanding 没有时间窗口的写法,窗口永远从起点开始。如果只想算”最近 3 个月的累积值”,那属于 rolling 加时间窗口的范畴。
ewm:越近的数据越重要
expanding 把历史上所有数据一视同仁。但很多场景里,昨天的数据比去年的数据更能说明问题。指数加权窗口(exponentially weighted window,ewm)就为此而生:每条数据都有权重,越靠近当前,权重越大,权重按指数递减。
import pandas as pd
import numpy as np
s = pd.Series([1, 2, 3, 4, 5, 6, 7, 8, 9, 10])
# alpha=0.5:每条数据的权重是前一条的 (1-0.5)=0.5 倍
print(s.ewm(alpha=0.5).mean().round(3))
# 0 1.000
# 1 1.667
# 2 2.429
# 3 3.267
# 4 4.161
# ...
拿索引 3 来说,结果 3.267 不是简单平均,而是当前值 4 的权重最大,越早的数据权重越小。alpha 是平滑系数,范围是 0 到 1:
- alpha 越大,越看重近期数据,曲线跟得越紧
- alpha 越小,历史数据的影响越久,曲线越平滑
把结果和 rolling 的普通平均对比,感觉更直观:同样 10 个递增的数,5 条平均在索引 4 才等于 2.0,而 ewm 从第二个点开始就”追”着最新值走。
参数怎么选:alpha、span 还是 halflife
直接记 alpha 不太符合直觉,pandas 提供了另外三个等价的参数,只能四选一:
| 参数 | 含义 | 与 alpha 的关系 |
|---|---|---|
alpha | 平滑系数 | 直接指定 |
span | 跨度,类似”N 日加权平均” | alpha = 2 / (span + 1) |
halflife | 半衰期:权重衰减到一半所需的时间 | alpha = 1 - 0.5^(1/halflife) |
com | 质心,物理含义较强 | alpha = 1 / (com + 1) |
日常最常用的是 span。股票软件里的 EMA(指数移动平均线)就常用 span=12、span=26 这种”日”的说法:
import pandas as pd
import numpy as np
s = pd.Series([1, 2, 3, 4, 5, 6, 7, 8, 9, 10])
# span=3 等价于 alpha=2/(3+1)=0.5
print(s.ewm(span=3).mean().round(3))
# 0 1.000
# 1 1.667
# 2 2.429
# 3 3.267
# ...
两种计算方式的区别
ewm 的默认参数 adjust=True,结果按”加权平均”公式算:分子是各数据乘权重后求和,分母是权重之和。这样前几个点因为权重总和还很小,结果会明显偏向当前值。
改成 adjust=False,就变成递推公式:y_t = alpha * x_t + (1 - alpha) * y_{t-1},新的结果由”当前值”和”上一个结果”直接算出来,第一个点等于原始值。数据无限长时两种方式结果一致;数据短时略有差别。
import pandas as pd
import numpy as np
s = pd.Series([1, 2, 3, 4, 5])
print(s.ewm(alpha=0.5).mean().round(3))
# 0 1.000
# 1 1.667
# 2 2.429
# 3 3.267
# 4 4.161
print(s.ewm(alpha=0.5, adjust=False).mean().round(3))
# 0 1.000
# 1 1.500
# 2 2.250
# 3 3.125
# 4 4.062
看到差别了吗?adjust=False 时索引 1 是 0.52 + 0.51 = 1.5,完全由递推得到。默认的 adjust=True 足够日常使用,不用刻意改。
ewm 的其他方法
除了 mean,ewm 还支持 std、var、cov、corr。比如算”指数加权波动率”,对收益率序列做 ewm 标准差,比 rolling 标准差反应更快:
import pandas as pd
import numpy as np
s = pd.Series([0.01, -0.02, 0.03, 0.005, -0.01])
print(s.ewm(span=10).std().round(4))
小例子:平滑一条带噪声的曲线
把三种窗口放在同一份数据上对比,用途一目了然。造一串带噪声的数据,分别用 rolling 和 ewm 平滑:
import pandas as pd
import numpy as np
np.random.seed(42)
s = pd.Series(100 + np.random.randn(20).cumsum())
# 普通 5 条移动平均:对每个点都一视同仁
ma = s.rolling(5).mean()
# 指数加权:近期数据权重更大,拐弯更快
ema = s.ewm(span=5).mean()
print(pd.DataFrame({"原始": s, "MA5": ma, "EMA5": ema}).round(2).head(10))
# 原始 MA5 EMA5
# 0 100.50 NaN 100.50
# 1 100.36 NaN 100.41
# 2 101.01 NaN 100.69
# 3 102.53 NaN 101.46
# 4 102.30 101.34 101.78
# ...
注意看 EMA5 从第 0 个点就有值,而且每一步都贴着原始数据走;MA5 要凑满 5 条才开始输出,拐弯也更”钝”。这就是指数加权的意义:敏感、及时。
Tipewm 对缺失值默认按位置计算权重(
ignore_na=False),中间有 NaN 会影响结果。数据缺失较多时,可以设ignore_na=True,让权重跳过缺失值。
分组后的 expanding 与 ewm
窗口函数同样可以接在 groupby(§37)后面,每组各自累积。比如每个城市的销售额,各自算”开业以来累计销售额”和”指数加权均值”:
import pandas as pd
import numpy as np
df = pd.DataFrame(
{"城市": ["北京"] * 3 + ["上海"] * 3,
"销售额": [100, 200, 150, 300, 200, 250]}
)
df["累计销售额"] = df.groupby("城市")["销售额"].expanding().sum().reset_index(drop=True)
print(df)
# 城市 销售额 累计销售额
# 0 北京 100 100.0
# 1 北京 200 300.0
# 2 北京 150 450.0
# 3 上海 300 300.0
# 4 上海 200 500.0
# 5 上海 250 750.0
北京自己的累计是 100、300、450,上海的累计从 300 重新开始,两组互不干扰。
三种窗口怎么选
| 窗口 | 窗口大小 | 权重 | 典型场景 |
|---|---|---|---|
| rolling | 固定 n 条(或固定时长) | 平均 | 移动平均线、短期波动率 |
| expanding | 从起点到当前 | 平均 | 累计统计、历史极值 |
| ewm | 从起点到当前 | 近期更大 | 技术指标 EMA、快速平滑 |
一句话总结:想算”最近 n 条”,用 rolling;想算”到目前为止”,用 expanding;想算”到目前为止但更看重近期”,用 ewm。
Warningexpanding 和 ewm 都要求数据按时间顺序排列。数据乱序时,累积窗口的含义就乱了,结果没有意义。