扩展窗口
使用 expanding().sum() 计算累计统计量,使每个位置都包含从开头到该位置的全部行。
扩展窗口 是 CoddyKit 上的免费 Pandas & NumPy Academy 课时。 这是第 2 节课,共 4 节。 你可以在下方免费阅读本课时的完整内容 — 然后在浏览器中使用内置代码编辑器和全天候 AI 导师进行实践。 这是 Pandas & NumPy Academy 学习路径的一部分,你的进度在网页和 CoddyKit 应用中同步。 Pandas & NumPy Academy 课程共包含 4 节课。
什么是扩展窗口
扩展窗口包含从 Series 最开始到当前行的所有行——窗口会随着每个新行而增长,而不是保持固定。在第 0 行,它只包含一个值;在第 5 行,它包含六个值;在第 100 行,它包含 101 个值。扩展窗口用于计算累积统计量,表示从数据集开头到当前时间点的累计总和、累计平均值或累计最大值。
import pandas as pd
import numpy as np
sales = pd.Series(
[100, 150, 120, 200, 180, 160, 220, 190],
index=pd.date_range('2024-01-01', periods=8)
)
# Expanding mean — grows with each row
df = pd.DataFrame({'sales': sales})
df['cumulative_mean'] = df['sales'].expanding().mean()
print(df)expanding() 方法
Series.expanding(min_periods=1) 会返回一个 Expanding 对象。min_periods 参数(默认值为 1)设置获得第一个非 NaN 结果所需的最小观测数量。与 rolling(n) 不同,expanding() 中的窗口不是固定的——它始终从开头开始。您可以链式调用任意聚合操作:.sum()、.mean()、.std()、.min()、.max()。
import pandas as pd
import numpy as np
np.random.seed(42)
monthly_revenue = pd.Series(
np.random.randint(500, 1500, 12),
index=pd.date_range('2024-01', periods=12, freq='ME'),
name='revenue'
)
df = pd.DataFrame({'revenue': monthly_revenue})
df['cumsum'] = df['revenue'].expanding().sum()
df['cummean'] = df['revenue'].expanding().mean()
df['cummax'] = df['revenue'].expanding().max()
df['cummin'] = df['revenue'].expanding().min()
print(df)Pandas 的 cumsum、cumprod、cummax、cummin
对于最常见的累积统计量,Pandas 为 Series 和 DataFrame 提供了无需使用 expanding() 的直接方法:cumsum()、cumprod()、cummax() 和 cummin()。这些方法经过优化,速度比对应的 expanding() 操作略快。对于年初至今的收入(cumsum)或历史最高价格(cummax)等常见业务指标,请使用这些快捷方法。
import pandas as pd
import numpy as np
prices = pd.Series(
[100, 95, 110, 105, 120, 115, 130, 125],
index=pd.date_range('2024-01-01', periods=8)
)
df = pd.DataFrame({'price': prices})
df['cummax'] = df['price'].cummax() # all-time high
df['cummin'] = df['price'].cummin() # all-time low
df['cumsum'] = df['price'].cumsum() # cumulative total
df['cumprod'] = (1 + df['price'].pct_change()).cumprod() # growth index
print(df.round(3))使用 Expanding 计算年初至今数据
扩展窗口的经典用途是计算年初至今(YTD)的收入。对于每一天,您都希望计算从当年 1 月 1 日到当天的所有收入总和。这实际上是在每个年份组内应用 cumsum()。请使用 groupby(year).cumsum() 实现——cumsum 会在每个日历年开始时重置,并在该年内持续累加。
import pandas as pd
import numpy as np
np.random.seed(0)
dates = pd.date_range('2023-01-01', '2024-06-30', freq='ME')
daily = pd.DataFrame({
'date': dates,
'revenue': np.random.randint(100, 500, len(dates))
})
# Year-to-date cumulative revenue
daily['year'] = daily['date'].dt.year
daily['ytd_revenue'] = daily.groupby('year')['revenue'].cumsum()
daily = daily.drop(columns='year')
print(daily.tail(10).to_string(index=False))扩展窗口的标准差
expanding().std() 使用从开头到当前行的所有数据,计算累计标准差。这对于监测某个指标的波动性是否随时间增加或减少非常有用,例如,跟踪随着业务发展,某个产品的日销量是否变得越来越可预测。与只反映近期波动性的滚动 std 不同,扩展 std 会涵盖完整的历史离散程度。
import pandas as pd
import numpy as np
np.random.seed(42)
sales = pd.Series(
np.concatenate([
np.random.normal(100, 5, 30), # stable period
np.random.normal(100, 25, 30) # volatile period
]),
index=pd.date_range('2024-01-01', periods=60)
)
df = pd.DataFrame({'sales': sales})
df['expanding_std'] = df['sales'].expanding().std()
df['rolling_30d_std'] = df['sales'].rolling(30).std()
print(df.tail(10).round(2))比较滚动窗口与扩展窗口
关键区别在于:滚动窗口反映近期表现(最近 n 天),不受很久以前的数据影响;而扩展窗口会纳入全部历史数据,因此会缓慢趋于稳定值。当您关注近期趋势时,请使用滚动窗口(7 天移动平均值比 90 天移动平均值更能及时反映近期变化)。当您需要历史以来的统计量或不能忘记早期数据的 YTD 指标时,请使用扩展窗口。
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
np.random.seed(10)
data = pd.Series(
np.random.randn(60).cumsum() + 50,
index=pd.date_range('2024-01-01', periods=60)
)
df = pd.DataFrame({'value': data})
df['rolling_10'] = data.rolling(10).mean()
df['expanding'] = data.expanding().mean()
df.plot(figsize=(12, 5), title='Rolling 10-day Mean vs. Expanding Mean')
plt.ylabel('Value')
plt.show()使用 Expanding Apply 计算自定义累积统计量
expanding().apply(func) 会像 rolling().apply() 一样,对从开头到当前行的所有数据应用自定义函数。这使您能够计算内置操作不支持的累积统计量,例如累计变异系数(std/mean)、累计中位数或累计夏普比率。该函数会接收一个包含目前为止所有值的 NumPy 数组,并且必须返回一个标量。
import pandas as pd
import numpy as np
np.random.seed(0)
returns = pd.Series(
np.random.normal(0.001, 0.02, 60),
index=pd.date_range('2024-01-01', periods=60)
)
# Cumulative Sharpe ratio (mean/std of returns)
def sharpe(arr):
if len(arr) < 2:
return float('nan')
return arr.mean() / arr.std() * (252 ** 0.5) # annualised
df = pd.DataFrame({'returns': returns})
df['cum_sharpe'] = df['returns'].expanding().apply(sharpe, raw=True)
print(df.tail(8).round(4))处理 NaN 的扩展窗口
扩展窗口默认会跳过 NaN 值——计算累积和或平均值时,Series 中间的 NaN 会被忽略。您可以使用 skipna=True 检查这一行为(这是大多数 Pandas 聚合操作的默认设置)。当时间序列中确实存在缺失值时(例如周末没有交易),扩展统计量只会根据目前为止遇到的非 NaN 值进行计算,这通常正是我们想要的行为。
import pandas as pd
import numpy as np
data = pd.Series([10, np.nan, 20, 30, np.nan, 40, 50])
# cumsum skips NaN by default
df = pd.DataFrame({'value': data})
df['cumsum'] = df['value'].cumsum() # NaN propagates in cumsum!
df['expanding_sum'] = df['value'].expanding().sum() # NaN skipped
print(df)
print('\nNote: cumsum propagates NaN; expanding().sum() skips it.')用于累计基准的扩展窗口
扩展窗口非常适合计算累计基准:历史最高值、历史最佳季度,或自发布以来的最低错误率。这些 KPI 需要记住所有历史值——滚动窗口会遗忘较早的记录。使用 cummax() 或 cummin() 会返回每个时间点的累计最佳值(或最差值),从而可以轻松跟踪新纪录何时出现。
import pandas as pd
import numpy as np
np.random.seed(5)
sales = pd.Series(
np.random.randint(100, 300, 20),
index=pd.date_range('2024-01-01', periods=20),
name='daily_sales'
)
df = pd.DataFrame({'sales': sales})
df['all_time_max'] = df['sales'].cummax()
df['new_record'] = df['sales'] == df['all_time_max']
print('Days where a new sales record was set:')
print(df[df['new_record']].drop(columns='new_record'))实践示例:累积收益
在金融领域,累积收益表示一项投资从开始日期到后续每个日期增长了多少。以每日百分比收益为基础,(1 + daily_return) 的累积乘积会给出总增长因子。在这里,扩展乘积比滚动乘积更自然——您想跟踪的是自投资开始以来的总增长,而不仅仅是最近 n 天的增长。
import pandas as pd
import numpy as np
np.random.seed(7)
start_price = 100
daily_returns = pd.Series(
np.random.normal(0.0005, 0.015, 252),
index=pd.date_range('2024-01-01', periods=252)
)
# Cumulative return = product of (1 + r_i)
cum_returns = (1 + daily_returns).cumprod()
portfolio_value = start_price * cum_returns
print(f'Start value: ${start_price:.2f}')
print(f'End value: ${portfolio_value.iloc[-1]:.2f}')
print(f'Total return: {(portfolio_value.iloc[-1]/start_price - 1)*100:.1f}%')何时使用 Expanding、Rolling 与 cumsum
选择合适方法的指南:
- 对于简单的累积聚合,请使用
cumsum() / cummax() / cummin()——它们是速度最快的选项。 - 当您需要累计平均值或方差时,请使用
expanding().mean() / std()。 - 对于内置操作不支持的复杂历史以来统计量,请使用
expanding().apply(custom_func)。 - 当只有近期历史数据应影响当前值时,请使用
rolling(n),而不是扩展窗口。
快速检查
测试您对本课扩展窗口的理解。
课程回顾
在本课中,您学到了:expanding() 会从 Series 开头开始,在不断增长的窗口上计算累积统计量;cumsum/cummax/cummin 是常见累积操作的优化快捷方法;扩展窗口非常适合计算历史纪录、YTD 指标和累积投资收益。接下来,我们将学习指数加权移动平均(EWMA)——为近期观测值赋予更高权重。
常见问题解答
「扩展窗口」课时是免费的吗?
是的 — 「扩展窗口」的完整文本可在网页上免费阅读。要进行交互式练习(内置代码编辑器和全天候 AI 导师)并解锁 Pandas & NumPy Academy 课程的其余内容,请升级到 CoddyKit PRO。 Pandas & NumPy Academy 课程共包含 4 节课。
「扩展窗口」这节课中我会学到什么?
使用 expanding().sum() 计算累计统计量,使每个位置都包含从开头到该位置的全部行。 你通过在浏览器中直接运行的动手代码来练习 Pandas & NumPy Academy,全天候 AI 导师会在你学习这节课的过程中回答你的问题。
学习 Pandas & NumPy Academy 需要有经验吗?
无需任何先前经验。CoddyKit 上的 Pandas & NumPy Academy 课程适合初学者到高级学习者,你可以从这里开始或从头开始,按照自己的节奏学习。 这是第 2 节课,共 4 节。
「扩展窗口」课时需要多长时间?
大多数 CoddyKit 课程大约需要 5–10 分钟。每节课都很精短且互动,所以你能稳步进步,并在网页和应用中从离开的地方继续。
我能在这节 Pandas & NumPy Academy 课中编写并运行代码吗?
能。每节 Pandas & NumPy Academy 课都包含内置代码编辑器,你可以在浏览器中直接编写并运行真实代码,并获得即时 AI 反馈 — 无需本地设置。