Pandas: 时间序列进阶
最后更新:2026-08-26
第 16 课学会了时间类型,本课进入时间序列分析的实战——重采样把日数据变月数据,shift/diff 构造滞后特征,rolling/expanding/ewm 三大窗口让趋势和波动一目了然。这是量化分析和 ML 特征工程的核心基础。
⚠️ 注意: 以下代码需在本地 Python 环境中运行。
1. 你将学到
- ❶ resample 重采样
- ❷ shift / diff / pct_change
- ❸ rolling 移动窗口
- ❹ expanding 累计窗口
- ❺ ewm 指数加权
2. Alice 的咖啡店月度报表
(1) 痛点:日数据太碎,需要月度汇总
Alice 有 90 天的日销售额,老板要看月报:
PYTHON
import pandas as pd
import numpy as np
np.random.seed(42)
df = pd.DataFrame({
'sales': np.random.poisson(50, 90) * 10,
'customers': np.random.poisson(30, 90)
}, index=pd.date_range('2024-01-01', periods=90))
# Daily data → need monthly summary
TEXT
📖 仅展示
> **输出:** 在本地 Python 环境(pandas 2.x)运行。Piston 服务器未预装 pandas,请在本机安装(`pip install pandas`)后实操对照。实际数值会因 pandas 版本略有差异。
(2) 解法:resample 一行聚合
▶ 示例
TEXT
📖 仅展示
> **输出:** 在本地 Python 环境(pandas 2.x)运行。Piston 服务器未预装 pandas,请在本机安装(`pip install pandas`)后实操对照。实际数值会因 pandas 版本略有差异。
:resample 月度重采样(难度⭐)
PYTHON
import pandas as pd
import numpy as np
np.random.seed(42)
df = pd.DataFrame({
'sales': np.random.poisson(50, 90) * 10,
'customers': np.random.poisson(30, 90)
}, index=pd.date_range('2024-01-01', periods=90))
# Resample daily → monthly
monthly = df.resample('M').agg({
'sales': 'sum',
'customers': 'mean'
})
print(monthly)
# sales customers
# 2024-01-31 14680 29.55
# 2024-02-29 13520 31.10
# 2024-03-31 15430 30.45
TEXT
📖 仅展示
> **输出:** 在本地 Python 环境(pandas 2.x)运行。Piston 服务器未预装 pandas,请在本机安装(`pip install pandas`)后实操对照。实际数值会因 pandas 版本略有差异。
3. resample 重采样
(1) 降采样与升采样
▶ 示例
TEXT
📖 仅展示
> **输出:** 在本地 Python 环境(pandas 2.x)运行。Piston 服务器未预装 pandas,请在本机安装(`pip install pandas`)后实操对照。实际数值会因 pandas 版本略有差异。
:resample 降采样/升采样(难度⭐⭐)
PYTHON
import pandas as pd
import numpy as np
np.random.seed(42)
df = pd.DataFrame({
'price': np.random.uniform(100, 200, 60).round(2)
}, index=pd.date_range('2024-01-01', periods=60))
# Downsample: daily → weekly
weekly = df.resample('W').agg({
'price': ['first', 'max', 'min', 'last'] # OHLC
})
print("Weekly OHLC:")
print(weekly.head())
# Downsample: daily → quarterly
quarterly = df.resample('Q').mean()
# Upsample: daily → hourly (need fill method)
hourly = df.resample('h').ffill() # forward fill
# Warning: creates many rows!
TEXT
📖 仅展示
> **输出:** 在本地 Python 环境(pandas 2.x)运行。Piston 服务器未预装 pandas,请在本机安装(`pip install pandas`)后实操对照。实际数值会因 pandas 版本略有差异。
(2) resample vs groupby
| 特性 | resample | groupby |
|---|---|---|
| 分组依据 | 时间频率 | 任意列 |
| 适用 | DatetimeIndex | 任意 DataFrame |
| 升采样 | ✅ 填充缺失 | ❌ |
| 频率别名 | D/W/M/Q/H | — |
| 本质 | 时间维度的 groupby | 通用分组 |
4. shift / diff / pct_change
(1) 滞后与差分
▶ 示例
TEXT
📖 仅展示
> **输出:** 在本地 Python 环境(pandas 2.x)运行。Piston 服务器未预装 pandas,请在本机安装(`pip install pandas`)后实操对照。实际数值会因 pandas 版本略有差异。
:shift/diff/pct_change(难度⭐⭐)
PYTHON
import pandas as pd
import numpy as np
np.random.seed(42)
df = pd.DataFrame({
'sales': np.random.poisson(50, 10) * 10
}, index=pd.date_range('2024-01-01', periods=10))
# shift: move data forward/backward
df['sales_prev'] = df['sales'].shift(1) # yesterday's sales
df['sales_next'] = df['sales'].shift(-1) # tomorrow's sales
# diff: difference with previous value
df['sales_diff'] = df['sales'].diff() # sales - prev_sales
# pct_change: percentage change
df['sales_pct'] = df['sales'].pct_change() * 100 # % change
print(df.round(1))
TEXT
📖 仅展示
> **输出:** 在本地 Python 环境(pandas 2.x)运行。Piston 服务器未预装 pandas,请在本机安装(`pip install pandas`)后实操对照。实际数值会因 pandas 版本略有差异。
💡 提示: shift 是 ML 时间序列特征工程的核心——
df['lag_1'] = df['value'].shift(1) 用昨天的值预测今天。diff 提取趋势变化,pct_change 提取增长率。
5. rolling 移动窗口
(1) 移动平均
▶ 示例
TEXT
📖 仅展示
> **输出:** 在本地 Python 环境(pandas 2.x)运行。Piston 服务器未预装 pandas,请在本机安装(`pip install pandas`)后实操对照。实际数值会因 pandas 版本略有差异。
:rolling 移动平均(难度⭐⭐)
PYTHON
import pandas as pd
import numpy as np
np.random.seed(42)
df = pd.DataFrame({
'temperature': np.round(np.random.normal(20, 5, 30), 1)
}, index=pd.date_range('2024-01-01', periods=30))
# 7-day moving average
df['ma_7'] = df['temperature'].rolling(window=7).mean()
# With min_periods (allow partial windows)
df['ma_7_flex'] = df['temperature'].rolling(window=7, min_periods=3).mean()
# Multiple aggregations
# Pandas 2.x:rolling.agg() 返回多列 DataFrame,不能直接赋值给单列;改用 join
rolling_agg = df['temperature'].rolling(7).agg(['mean', 'std', 'min', 'max'])
rolling_agg.columns = [f'temp_{stat}' for stat in ['mean', 'std', 'min', 'max']]
df = df.join(rolling_agg)
# Centered rolling (include future values — for smoothing, not forecasting)
df['ma_7_center'] = df['temperature'].rolling(7, center=True).mean()
print(df[['temperature', 'ma_7', 'ma_7_center']].head(10))
TEXT
📖 仅展示
> **输出:** 在本地 Python 环境(pandas 2.x)运行。Piston 服务器未预装 pandas,请在本机安装(`pip install pandas`)后实操对照。实际数值会因 pandas 版本略有差异。
(2) 时间窗口
▶ 示例
TEXT
📖 仅展示
> **输出:** 在本地 Python 环境(pandas 2.x)运行。Piston 服务器未预装 pandas,请在本机安装(`pip install pandas`)后实操对照。实际数值会因 pandas 版本略有差异。
:时间窗口 rolling(难度⭐⭐)
PYTHON
import pandas as pd
import numpy as np
# Irregular time series — row-based window is wrong
df = pd.DataFrame({
'value': [10, 20, 15, 30, 25, 40, 35, 50, 45, 60]
}, index=pd.to_datetime([
'2024-01-01', '2024-01-02', '2024-01-05', # gap on 03-04
'2024-01-06', '2024-01-07', '2024-01-15', # gap on 08-14
'2024-01-16', '2024-01-20', '2024-01-21', '2024-01-25'
]))
# Row-based window (wrong for irregular data)
print("Row-based 3:")
print(df['value'].rolling(3).mean())
# Time-based window (correct — 7 calendar days)
print("\nTime-based 7D:")
print(df['value'].rolling('7D').mean())
TEXT
📖 仅展示
> **输出:** 在本地 Python 环境(pandas 2.x)运行。Piston 服务器未预装 pandas,请在本机安装(`pip install pandas`)后实操对照。实际数值会因 pandas 版本略有差异。
6. expanding 累计窗口
▶ 示例
TEXT
📖 仅展示
> **输出:** 在本地 Python 环境(pandas 2.x)运行。Piston 服务器未预装 pandas,请在本机安装(`pip install pandas`)后实操对照。实际数值会因 pandas 版本略有差异。
:expanding 累计统计(难度⭐)
PYTHON
import pandas as pd
import numpy as np
np.random.seed(42)
df = pd.DataFrame({
'sales': np.random.poisson(50, 10) * 10
})
# Expanding: all values up to current position
df['cumsum'] = df['sales'].expanding().sum()
df['cummean'] = df['sales'].expanding().mean()
df['cummax'] = df['sales'].expanding().max()
# With min_periods
df['cummean_3'] = df['sales'].expanding(min_periods=3).mean()
print(df)
TEXT
📖 仅展示
> **输出:** 在本地 Python 环境(pandas 2.x)运行。Piston 服务器未预装 pandas,请在本机安装(`pip install pandas`)后实操对照。实际数值会因 pandas 版本略有差异。
(1) rolling vs expanding vs ewm 对比
| 特性 | rolling | expanding | ewm |
|---|---|---|---|
| 窗口 | 固定大小 | 不断增长 | 无限(权重衰减) |
| 权重 | 等权 | 等权 | 近期权重更大 |
| 适用 | 局部趋势 | 累计统计 | 平滑趋势 |
| 参数 | window | min_periods | span/com/halflife |
7. ewm 指数加权
▶ 示例
TEXT
📖 仅展示
> **输出:** 在本地 Python 环境(pandas 2.x)运行。Piston 服务器未预装 pandas,请在本机安装(`pip install pandas`)后实操对照。实际数值会因 pandas 版本略有差异。
:ewm 指数平滑(难度⭐⭐)
PYTHON
import pandas as pd
import numpy as np
np.random.seed(42)
df = pd.DataFrame({
'price': np.cumsum(np.random.randn(30)) + 100
}, index=pd.date_range('2024-01-01', periods=30))
# Exponentially weighted moving average
df['ewm_5'] = df['price'].ewm(span=5).mean()
df['ewm_10'] = df['price'].ewm(span=10).mean()
df['ewm_20'] = df['price'].ewm(span=20).mean()
# Compare with simple moving average
df['sma_5'] = df['price'].rolling(5).mean()
print(df[['price', 'sma_5', 'ewm_5']].head(10).round(2))
# ewm parameters (only specify one):
# span=5 → similar to 5-period SMA but recent-heavy
# com=4 → center of mass (com = span-1)
# halflife=3 → weight halves every 3 periods
# alpha=0.3 → smoothing factor (0<α≤1)
TEXT
📖 仅展示
> **输出:** 在本地 Python 环境(pandas 2.x)运行。Piston 服务器未预装 pandas,请在本机安装(`pip install pandas`)后实操对照。实际数值会因 pandas 版本略有差异。
8. 完整示例:股票日K线分析
(5) ▶ rolling 窗口滑过时间轴
graph TB
A[时间序列数据] --> B[rolling 窗口]
B --> C[窗口沿时间轴滑动]
C --> D[窗口 1: t1 到 tN]
C --> E[窗口 2: t2 到 t(N+1)]
C --> F[窗口 3: t3 到 t(N+2)]
D --> G[mean / std / max / min]
E --> G
F --> G
G --> H[平滑趋势 / 波动率]
TEXT
📖 仅展示
> **输出:** 在本地 Python 环境(pandas 2.x)运行。Piston 服务器未预装 pandas,请在本机安装(`pip install pandas`)后实操对照。实际数值会因 pandas 版本略有差异。
▶ 示例
TEXT
📖 仅展示
> **输出:** 在本地 Python 环境(pandas 2.x)运行。Piston 服务器未预装 pandas,请在本机安装(`pip install pandas`)后实操对照。实际数值会因 pandas 版本略有差异。
:时间序列分析全流程(难度⭐⭐⭐)
PYTHON
import pandas as pd
import numpy as np
# ============================================
# Comprehensive example: Stock daily data
# resample → rolling → diff → ewm
# ============================================
# 1. Generate daily stock data (60 trading days)
np.random.seed(42)
dates = pd.bdate_range('2024-01-01', periods=60)
price = 100 + np.cumsum(np.random.randn(60) * 2)
df = pd.DataFrame({
'open': price + np.random.randn(60) * 0.5,
'high': price + np.abs(np.random.randn(60)) * 1.5,
'low': price - np.abs(np.random.randn(60)) * 1.5,
'close': price + np.random.randn(60) * 0.5,
'volume': np.random.randint(100000, 500000, 60)
}, index=dates)
# 2. Resample: daily → weekly
weekly = df.resample('W-FRI').agg({
'open': 'first', 'high': 'max', 'low': 'min',
'close': 'last', 'volume': 'sum'
})
print("=== Weekly OHLCV ===")
print(weekly.head(4).round(2))
# 3. Rolling: 20-day moving average
df['ma_20'] = df['close'].rolling(20).mean()
df['vol_20'] = df['close'].rolling(20).std()
# 4. Daily returns
df['returns'] = df['close'].pct_change()
df['log_returns'] = np.log(df['close'] / df['close'].shift(1))
# 5. EWM: exponential smoothing
df['ewm_12'] = df['close'].ewm(span=12).mean()
df['ewm_26'] = df['close'].ewm(span=26).mean()
df['macd'] = df['ewm_12'] - df['ewm_26']
# 6. Summary
print(f"\n=== Statistics ===")
print(f"Period: {df.index[0].date()} to {df.index[-1].date()}")
print(f"Total return: {((df['close'].iloc[-1] / df['close'].iloc[0] - 1) * 100):.1f}%")
print(f"Volatility (20d): {df['vol_20'].iloc[-1]:.2f}")
print(f"Max drawdown: {((df['close'] / df['close'].cummax() - 1).min() * 100):.1f}%")
TEXT
📖 仅展示
> **输出:** 在本地 Python 环境(pandas 2.x)运行。Piston 服务器未预装 pandas,请在本机安装(`pip install pandas`)后实操对照。实际数值会因 pandas 版本略有差异。
❓ 常见问题
Q resample 和 groupby 区别?
A resample 是时间维度的 groupby——按频率(日/周/月/季)分组,只支持 DatetimeIndex/PeriodIndex。groupby 按任意列分组,不限于时间。resample 独有升采样(低频→高频需填充),groupby 没有。日常:时间聚合用 resample,非时间聚合用 groupby。
Q rolling 窗口大小怎么选?
A 取决于场景。短期趋势用 5-10 窗口,中期用 20-30(月度),长期用 50-200(季度/年度)。金融常用 20/50/200 日均线。窗口太小→噪声多,窗口太大→滞后严重。建议:先看数据频率,窗口覆盖 1-3 个"自然周期"。
Q shift 和 diff 区别?
A shift 平移数据(不计算),diff 计算差值。
s.diff() = s - s.shift(1)。shift 用于构造滞后特征(ML),diff 用于提取变化量(趋势分析)。pct_change 计算变化率:s.pct_change() = (s - s.shift(1)) / s.shift(1)。Q expanding 和 cumsum 区别?
A expanding 是 cumsum 的泛化版本——支持任意聚合函数(mean/std/max/min),cumsum 只算累计和。
s.expanding().sum() 等价于 s.cumsum()。expanding 更灵活但稍慢,日常用 cumsum 更简洁。Q ewm 是什么?
A EWM(Exponentially Weighted Moving)是指数加权移动平均——近期数据权重大,远期权重指数衰减。span=5 类似 5 周期 SMA 但近期的贡献更大。比 SMA 更快响应变化,是技术分析(MACD)和在线学习的基础。
Q resample 升采样怎么填?
A 升采样(低频→高频)会产生大量 NaN。填充方式:ffill()(前向填充,用前一个值)、bfill()(后向填充)、interpolate()(插值)、asfreq()(保留 NaN)。推荐 ffill(简单且保守),时间序列分析用 interpolate(更平滑)。
Q center=True 能用于预测吗?
A 不能!center=True 让窗口"向未来看"(包含后续值),只用于历史数据平滑,不能用于预测场景。实时/预测场景必须 center=False(默认),只看过去值。这个错误在量化分析中会导致严重的数据泄漏。
📖 小节
- resample 按时间频率聚合,是"时间维度的 groupby"
- shift 平移数据构造滞后特征,diff/pct_change 计算差分和变化率
- rolling 固定窗口移动计算(移动平均/标准差/极值)
- 时间窗口 rolling('7D') 适合非均匀时间序列
- expanding 累计窗口(从开头到当前位置),ewm 指数加权(近期权重大)
- 选择:局部趋势→rolling,累计统计→expanding,平滑→ewm
📝 作业
- 基础题(难度⭐):创建 30 天日销售 DataFrame,用 resample('W') 聚合为周数据,用 rolling(7) 计算 7 日移动平均。
- 进阶题(难度⭐⭐):创建 60 天股票收盘价,计算:shift(1) 滞后 → diff() 日差 → pct_change() 收益率 → rolling(20) 波动率 → ewm(span=12) 平滑。
- 挑战题(难度⭐⭐⭐):模拟 90 天日销售,完成:resample 月度/季度汇总 → rolling 7/14/30 日均线对比 → expanding 累计均值 → ewm 平滑 → 最大回撤计算。