7.3 时间序列分解(Time Series Decomposition)
"Decomposition is a powerful tool for understanding complex time series.""分解是理解复杂时间序列的强大工具。"— Cleveland et al., STL Decomposition Authors (STL分解方法作者)
拆解时间序列的趋势、季节性和随机成分
本节目标
完成本节后,你将能够:
- 理解时间序列分解的基本原理和应用
- 掌握经典分解方法(加法模型、乘法模型)
- 使用 STL 分解方法(Seasonal-Trend decomposition using Loess)
- 进行季节性调整和趋势提取
- 应用 HP 滤波和其他去趋势方法
- 使用 Python 实现各种分解技术
时间序列分解的基本原理
为什么需要分解时间序列?
核心思想:许多时间序列数据是多个成分的组合
经典案例:零售额数据
- 趋势(Trend):长期增长趋势(经济发展、人口增长)
- 季节性(Seasonality):每年重复的模式(节假日、季度效应)
- 周期(Cycle):非固定长度的波动(经济周期)
- 随机性(Random/Irregular):不可预测的扰动
分解的好处:
- 理解数据结构:识别主要驱动因素
- 季节调整:消除季节性,看清真实趋势
- 预测改进:分别建模不同成分
- 异常检测:识别不寻常的波动
经典分解模型
1. 加法模型(Additive Model)
适用场景:季节性波动幅度不随时间变化
组成:
- :观测值
- :趋势成分(Trend)
- :季节成分(Seasonal)
- :随机成分(Random/Residual)
特征:
- 季节性波动是固定幅度(例如:每年12月销量增加100万)
- 各成分相互独立
2. 乘法模型(Multiplicative Model)
适用场景:季节性波动幅度随趋势增长
等价对数形式(转为加法):
特征:
- 季节性波动是比例变化(例如:每年12月销量增加20%)
- 适合指数增长数据
如何选择模型?
| 特征 | 加法模型 | 乘法模型 |
|---|---|---|
| 季节波动 | 固定幅度 | 比例变化 |
| 数据类型 | 线性增长 | 指数增长 |
| 季节图 | 波动幅度恒定 | 波动幅度递增 |
| 适用案例 | 温度、降水 | GDP、股价、销售额 |
️ Python 实现:经典分解
使用 statsmodels 进行分解
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
import seaborn as sns
from statsmodels.tsa.seasonal import seasonal_decompose
# 设置中文字体
plt.rcParams['font.sans-serif'] = ['Arial Unicode MS'] # macOS
plt.rcParams['axes.unicode_minus'] = False
sns.set_style("whitegrid")
# 1. 生成模拟数据:趋势 + 季节性 + 随机
np.random.seed(42)
n = 120 # 10年月度数据
dates = pd.date_range('2014-01', periods=n, freq='M')
# 趋势成分:线性增长
trend = 100 + 0.5 * np.arange(n)
# 季节成分:12个月周期
seasonal = 10 * np.sin(2 * np.pi * np.arange(n) / 12)
# 随机成分
random = np.random.normal(0, 3, n)
# 加法模型
y_additive = trend + seasonal + random
# 乘法模型
seasonal_mult = 1 + 0.1 * np.sin(2 * np.pi * np.arange(n) / 12)
y_multiplicative = trend * seasonal_mult * (1 + np.random.normal(0, 0.03, n))
# 创建时间序列
ts_add = pd.Series(y_additive, index=dates)
ts_mult = pd.Series(y_multiplicative, index=dates)
print("="*70)
print("时间序列数据预览")
print("="*70)
print(ts_add.head(12))加法分解
# 加法分解
decomposition_add = seasonal_decompose(ts_add, model='additive', period=12)
# 可视化
fig, axes = plt.subplots(4, 1, figsize=(14, 10))
# 原始序列
axes[0].plot(ts_add, linewidth=1.5, color='black')
axes[0].set_ylabel('观测值', fontsize=12, fontweight='bold')
axes[0].set_title('原始时间序列(加法模型)', fontsize=14, fontweight='bold')
axes[0].grid(True, alpha=0.3)
# 趋势成分
axes[1].plot(decomposition_add.trend, linewidth=2, color='blue')
axes[1].set_ylabel('趋势', fontsize=12, fontweight='bold')
axes[1].set_title('趋势成分(Trend)', fontsize=12, fontweight='bold')
axes[1].grid(True, alpha=0.3)
# 季节成分
axes[2].plot(decomposition_add.seasonal, linewidth=2, color='green')
axes[2].set_ylabel('季节性', fontsize=12, fontweight='bold')
axes[2].set_title('季节成分(Seasonal)', fontsize=12, fontweight='bold')
axes[2].axhline(y=0, color='black', linestyle='--', alpha=0.5)
axes[2].grid(True, alpha=0.3)
# 随机成分
axes[3].plot(decomposition_add.resid, linewidth=1, color='red', alpha=0.7)
axes[3].set_ylabel('残差', fontsize=12, fontweight='bold')
axes[3].set_title('随机成分(Residual)', fontsize=12, fontweight='bold')
axes[3].axhline(y=0, color='black', linestyle='--', alpha=0.5)
axes[3].set_xlabel('时间', fontsize=12, fontweight='bold')
axes[3].grid(True, alpha=0.3)
plt.tight_layout()
plt.savefig('time_series_decomposition_additive.png', dpi=300, bbox_inches='tight')
plt.show()
# 输出统计信息
print("\n" + "="*70)
print("分解结果统计")
print("="*70)
print(f"趋势成分范围: {decomposition_add.trend.min():.2f} - {decomposition_add.trend.max():.2f}")
print(f"季节成分范围: {decomposition_add.seasonal.min():.2f} - {decomposition_add.seasonal.max():.2f}")
print(f"随机成分标准差: {decomposition_add.resid.std():.2f}")乘法分解
# 乘法分解
decomposition_mult = seasonal_decompose(ts_mult, model='multiplicative', period=12)
# 可视化
fig, axes = plt.subplots(4, 1, figsize=(14, 10))
axes[0].plot(ts_mult, linewidth=1.5, color='black')
axes[0].set_ylabel('观测值', fontsize=12, fontweight='bold')
axes[0].set_title('原始时间序列(乘法模型)', fontsize=14, fontweight='bold')
axes[0].grid(True, alpha=0.3)
axes[1].plot(decomposition_mult.trend, linewidth=2, color='blue')
axes[1].set_ylabel('趋势', fontsize=12, fontweight='bold')
axes[1].set_title('趋势成分(Trend)', fontsize=12, fontweight='bold')
axes[1].grid(True, alpha=0.3)
axes[2].plot(decomposition_mult.seasonal, linewidth=2, color='green')
axes[2].set_ylabel('季节因子', fontsize=12, fontweight='bold')
axes[2].set_title('季节成分(Seasonal)- 注意是比例', fontsize=12, fontweight='bold')
axes[2].axhline(y=1, color='black', linestyle='--', alpha=0.5, label='基准线=1')
axes[2].legend()
axes[2].grid(True, alpha=0.3)
axes[3].plot(decomposition_mult.resid, linewidth=1, color='red', alpha=0.7)
axes[3].set_ylabel('残差因子', fontsize=12, fontweight='bold')
axes[3].set_title('随机成分(Residual)', fontsize=12, fontweight='bold')
axes[3].axhline(y=1, color='black', linestyle='--', alpha=0.5)
axes[3].set_xlabel('时间', fontsize=12, fontweight='bold')
axes[3].grid(True, alpha=0.3)
plt.tight_layout()
plt.savefig('time_series_decomposition_multiplicative.png', dpi=300, bbox_inches='tight')
plt.show()
print("\n" + "="*70)
print("乘法分解:季节因子解读")
print("="*70)
print("季节因子 > 1:该月高于趋势")
print("季节因子 < 1:该月低于趋势")
print(f"\n季节因子范围: {decomposition_mult.seasonal.min():.3f} - {decomposition_mult.seasonal.max():.3f}")STL 分解(Seasonal-Trend decomposition using Loess)
STL 的优势
经典分解的局限:
- 趋势估计在序列两端不稳定
- 假设季节成分不变
- 对异常值敏感
STL 的改进:
- 使用 LOESS(局部加权回归)平滑
- 允许季节性随时间变化
- 对异常值稳健
STL 数学原理
核心思想:迭代分离趋势和季节性
算法步骤:
- 初始去趋势:
- 提取季节性:对每个季节周期进行平滑
- 去季节性:
- 重新估计趋势:对去季节数据平滑
- 迭代直到收敛
Python 实现 STL
from statsmodels.tsa.seasonal import STL
# STL 分解
stl = STL(ts_add, seasonal=13, trend=51, robust=True)
result = stl.fit()
# 可视化
fig, axes = plt.subplots(4, 1, figsize=(14, 10))
axes[0].plot(ts_add, linewidth=1.5, color='black')
axes[0].set_ylabel('观测值', fontsize=12, fontweight='bold')
axes[0].set_title('STL 分解:原始序列', fontsize=14, fontweight='bold')
axes[0].grid(True, alpha=0.3)
axes[1].plot(result.trend, linewidth=2, color='blue')
axes[1].set_ylabel('趋势', fontsize=12, fontweight='bold')
axes[1].set_title('STL 趋势(更平滑)', fontsize=12, fontweight='bold')
axes[1].grid(True, alpha=0.3)
axes[2].plot(result.seasonal, linewidth=2, color='green')
axes[2].set_ylabel('季节性', fontsize=12, fontweight='bold')
axes[2].set_title('STL 季节成分(可随时间变化)', fontsize=12, fontweight='bold')
axes[2].axhline(y=0, color='black', linestyle='--', alpha=0.5)
axes[2].grid(True, alpha=0.3)
axes[3].plot(result.resid, linewidth=1, color='red', alpha=0.7)
axes[3].set_ylabel('残差', fontsize=12, fontweight='bold')
axes[3].set_title('STL 残差', fontsize=12, fontweight='bold')
axes[3].axhline(y=0, color='black', linestyle='--', alpha=0.5)
axes[3].set_xlabel('时间', fontsize=12, fontweight='bold')
axes[3].grid(True, alpha=0.3)
plt.tight_layout()
plt.savefig('stl_decomposition.png', dpi=300, bbox_inches='tight')
plt.show()
# 对比经典分解和 STL
print("\n" + "="*70)
print("经典分解 vs STL 对比")
print("="*70)
print(f"经典分解残差标准差: {decomposition_add.resid.std():.4f}")
print(f"STL 残差标准差: {result.resid.std():.4f}")
print(f"\nSTL 改善比例: {(1 - result.resid.std() / decomposition_add.resid.std()) * 100:.2f}%")STL 参数调优
# 不同参数对比
fig, axes = plt.subplots(3, 1, figsize=(14, 10))
# 参数配置
configs = [
{'seasonal': 7, 'trend': 15, 'label': '高频(seasonal=7, trend=15)'},
{'seasonal': 13, 'trend': 51, 'label': '中频(seasonal=13, trend=51)'},
{'seasonal': 25, 'trend': 101, 'label': '低频(seasonal=25, trend=101)'}
]
for i, config in enumerate(configs):
stl_temp = STL(ts_add, seasonal=config['seasonal'], trend=config['trend'], robust=True)
result_temp = stl_temp.fit()
axes[i].plot(result_temp.trend, linewidth=2, label='趋势')
axes[i].plot(ts_add, linewidth=0.5, alpha=0.3, color='gray', label='原始数据')
axes[i].set_title(config['label'], fontsize=12, fontweight='bold')
axes[i].set_ylabel('值', fontsize=11)
axes[i].legend(loc='upper left')
axes[i].grid(True, alpha=0.3)
axes[2].set_xlabel('时间', fontsize=12, fontweight='bold')
plt.tight_layout()
plt.show()
print("\n" + "="*70)
print("STL 参数选择指南")
print("="*70)
print("seasonal 参数(奇数):")
print(" - 越小:季节性捕捉越灵活(高频)")
print(" - 越大:季节性越平滑(低频)")
print("\ntrend 参数(奇数):")
print(" - 越小:趋势追踪越紧密")
print(" - 越大:趋势越平滑")
print("\n推荐:seasonal ≈ 1.5 × 周期长度")
print(" trend ≈ 1.5 × (n / 周期数)")季节性调整(Seasonal Adjustment)
为什么需要季节调整?
问题:季节性掩盖了真实的经济变化
案例:零售额数据
- 每年12月销量激增(圣诞节)
- 如何判断是正常季节效应还是经济好转?
解决:去除季节性,保留趋势+随机
季节调整的方法
# 季节调整:原始数据 - 季节成分
ts_sa_add = ts_add - decomposition_add.seasonal
ts_sa_stl = ts_add - result.seasonal
# 可视化对比
fig, axes = plt.subplots(3, 1, figsize=(14, 10))
# 原始数据
axes[0].plot(ts_add, linewidth=1.5, color='black', label='原始数据')
axes[0].set_title('原始时间序列', fontsize=14, fontweight='bold')
axes[0].set_ylabel('值', fontsize=11)
axes[0].legend()
axes[0].grid(True, alpha=0.3)
# 季节调整(经典分解)
axes[1].plot(ts_sa_add, linewidth=1.5, color='blue', label='经典分解季节调整')
axes[1].plot(decomposition_add.trend, linewidth=2, color='red', linestyle='--',
alpha=0.7, label='趋势')
axes[1].set_title('季节调整后(经典分解)', fontsize=12, fontweight='bold')
axes[1].set_ylabel('值', fontsize=11)
axes[1].legend()
axes[1].grid(True, alpha=0.3)
# 季节调整(STL)
axes[2].plot(ts_sa_stl, linewidth=1.5, color='green', label='STL 季节调整')
axes[2].plot(result.trend, linewidth=2, color='red', linestyle='--',
alpha=0.7, label='趋势')
axes[2].set_title('季节调整后(STL)', fontsize=12, fontweight='bold')
axes[2].set_xlabel('时间', fontsize=12, fontweight='bold')
axes[2].set_ylabel('值', fontsize=11)
axes[2].legend()
axes[2].grid(True, alpha=0.3)
plt.tight_layout()
plt.savefig('seasonal_adjustment.png', dpi=300, bbox_inches='tight')
plt.show()
print("\n" + "="*70)
print("季节调整效果评估")
print("="*70)
print(f"原始数据标准差: {ts_add.std():.4f}")
print(f"季节调整后标准差(经典): {ts_sa_add.std():.4f}")
print(f"季节调整后标准差(STL): {ts_sa_stl.std():.4f}")
print(f"\n季节性解释的波动比例: {(1 - ts_sa_add.std() / ts_add.std()) * 100:.2f}%")HP 滤波(Hodrick-Prescott Filter)
HP 滤波的原理
目标:从时间序列中提取平滑的趋势
优化问题:
解释:
- 第一项:趋势要拟合数据(最小化偏差)
- 第二项:趋势要平滑(惩罚二阶差分)
- :平滑参数(越大越平滑)
标准 λ 值:
- 年度数据:λ = 100
- 季度数据:λ = 1,600
- 月度数据:λ = 14,400
Python 实现 HP 滤波
from statsmodels.tsa.filters.hp_filter import hpfilter
# HP 滤波
cycle_add, trend_hp = hpfilter(ts_add, lamb=14400)
# 可视化
fig, axes = plt.subplots(3, 1, figsize=(14, 10))
# 原始数据 + HP 趋势
axes[0].plot(ts_add, linewidth=1.5, alpha=0.6, color='gray', label='原始数据')
axes[0].plot(trend_hp, linewidth=2.5, color='blue', label='HP 趋势 (λ=14400)')
axes[0].plot(decomposition_add.trend, linewidth=2, color='red', linestyle='--',
alpha=0.7, label='经典分解趋势')
axes[0].set_title('HP 滤波趋势提取', fontsize=14, fontweight='bold')
axes[0].set_ylabel('值', fontsize=11)
axes[0].legend()
axes[0].grid(True, alpha=0.3)
# HP 周期成分
axes[1].plot(cycle_add, linewidth=1.5, color='purple')
axes[1].axhline(y=0, color='black', linestyle='--', alpha=0.5)
axes[1].set_title('HP 周期成分(去趋势后)', fontsize=12, fontweight='bold')
axes[1].set_ylabel('偏离趋势', fontsize=11)
axes[1].grid(True, alpha=0.3)
# 不同 λ 的对比
lambdas = [1600, 14400, 100000]
for lamb in lambdas:
_, trend_temp = hpfilter(ts_add, lamb=lamb)
axes[2].plot(trend_temp, linewidth=2, label=f'λ={lamb}', alpha=0.7)
axes[2].plot(ts_add, linewidth=0.5, alpha=0.3, color='black', label='原始数据')
axes[2].set_title('不同 λ 参数的 HP 滤波对比', fontsize=12, fontweight='bold')
axes[2].set_xlabel('时间', fontsize=12, fontweight='bold')
axes[2].set_ylabel('值', fontsize=11)
axes[2].legend()
axes[2].grid(True, alpha=0.3)
plt.tight_layout()
plt.savefig('hp_filter.png', dpi=300, bbox_inches='tight')
plt.show()
print("\n" + "="*70)
print("HP 滤波结果统计")
print("="*70)
print(f"周期成分均值: {cycle_add.mean():.4f} (应接近0)")
print(f"周期成分标准差: {cycle_add.std():.4f}")
print(f"趋势成分标准差: {trend_hp.std():.4f}")实战案例:中国CPI季节性调整
案例背景
问题:中国CPI数据存在明显春节效应
- 春节前:食品价格上涨
- 春节后:价格回落
目标:去除季节性,识别真实通胀趋势
数据准备与分析
# 模拟中国CPI数据(2015-2024,月度)
np.random.seed(2024)
n_months = 120
dates_cpi = pd.date_range('2015-01', periods=n_months, freq='M')
# 趋势:缓慢上升
trend_cpi = 100 + 0.2 * np.arange(n_months) + 0.001 * np.arange(n_months)**2
# 季节性:春节效应(1-2月高峰)
seasonal_cpi = np.zeros(n_months)
for i in range(n_months):
month = (i % 12) + 1
if month in [1, 2]: # 春节
seasonal_cpi[i] = 3
elif month in [6, 7, 8]: # 夏季
seasonal_cpi[i] = 1
elif month in [9, 10]: # 国庆
seasonal_cpi[i] = 1.5
else:
seasonal_cpi[i] = 0
# 随机冲击
random_cpi = np.random.normal(0, 0.5, n_months)
# 合成数据
cpi = trend_cpi + seasonal_cpi + random_cpi
ts_cpi = pd.Series(cpi, index=dates_cpi, name='CPI')
# STL 分解
stl_cpi = STL(ts_cpi, seasonal=13, trend=25, robust=True)
result_cpi = stl_cpi.fit()
# 季节调整
cpi_sa = ts_cpi - result_cpi.seasonal
# 可视化
fig, axes = plt.subplots(5, 1, figsize=(14, 14))
# 原始CPI
axes[0].plot(ts_cpi, linewidth=1.5, color='black', marker='o', markersize=3)
axes[0].set_title('中国CPI指数(原始数据)', fontsize=14, fontweight='bold')
axes[0].set_ylabel('CPI', fontsize=11)
axes[0].grid(True, alpha=0.3)
# 趋势
axes[1].plot(result_cpi.trend, linewidth=2.5, color='blue')
axes[1].set_title('CPI 趋势成分', fontsize=12, fontweight='bold')
axes[1].set_ylabel('趋势', fontsize=11)
axes[1].grid(True, alpha=0.3)
# 季节性
axes[2].plot(result_cpi.seasonal, linewidth=1.5, color='green')
axes[2].axhline(y=0, color='black', linestyle='--', alpha=0.5)
axes[2].set_title('CPI 季节成分(春节效应明显)', fontsize=12, fontweight='bold')
axes[2].set_ylabel('季节偏差', fontsize=11)
axes[2].grid(True, alpha=0.3)
# 随机成分
axes[3].plot(result_cpi.resid, linewidth=1, color='red', alpha=0.7)
axes[3].axhline(y=0, color='black', linestyle='--', alpha=0.5)
axes[3].set_title('CPI 随机成分', fontsize=12, fontweight='bold')
axes[3].set_ylabel('残差', fontsize=11)
axes[3].grid(True, alpha=0.3)
# 季节调整对比
axes[4].plot(ts_cpi, linewidth=1, alpha=0.4, color='gray', label='原始CPI')
axes[4].plot(cpi_sa, linewidth=2, color='purple', label='季节调整后CPI')
axes[4].plot(result_cpi.trend, linewidth=2.5, color='blue', linestyle='--',
alpha=0.7, label='趋势')
axes[4].set_title('原始 vs 季节调整后CPI', fontsize=12, fontweight='bold')
axes[4].set_xlabel('时间', fontsize=12, fontweight='bold')
axes[4].set_ylabel('CPI', fontsize=11)
axes[4].legend()
axes[4].grid(True, alpha=0.3)
plt.tight_layout()
plt.savefig('cpi_seasonal_adjustment_china.png', dpi=300, bbox_inches='tight')
plt.show()
# 计算同比增长率
cpi_yoy = ts_cpi.pct_change(12) * 100 # 同比
cpi_sa_yoy = cpi_sa.pct_change(12) * 100
print("\n" + "="*70)
print("CPI 季节调整效果")
print("="*70)
print(f"原始CPI标准差: {ts_cpi.std():.4f}")
print(f"季节调整后标准差: {cpi_sa.std():.4f}")
print(f"波动性降低: {(1 - cpi_sa.std() / ts_cpi.std()) * 100:.2f}%")
print(f"\n近12个月平均同比增长率:")
print(f" 原始CPI: {cpi_yoy[-12:].mean():.2f}%")
print(f" 季节调整后: {cpi_sa_yoy[-12:].mean():.2f}%")
# 季节模式可视化
seasonal_pattern = result_cpi.seasonal[:12].values
months = ['1月', '2月', '3月', '4月', '5月', '6月',
'7月', '8月', '9月', '10月', '11月', '12月']
fig, ax = plt.subplots(figsize=(12, 6))
bars = ax.bar(months, seasonal_pattern, color=['red' if x > 1.5 else 'lightblue' for x in seasonal_pattern])
ax.axhline(y=0, color='black', linestyle='-', linewidth=0.8)
ax.set_title('CPI 月度季节性模式', fontsize=14, fontweight='bold')
ax.set_ylabel('季节偏差', fontsize=12)
ax.set_xlabel('月份', fontsize=12)
ax.grid(True, alpha=0.3, axis='y')
# 标注春节效应
ax.annotate('春节效应', xy=(1, seasonal_pattern[1]), xytext=(3, seasonal_pattern[1] + 1),
arrowprops=dict(arrowstyle='->', color='red', lw=2),
fontsize=12, color='red', fontweight='bold')
plt.tight_layout()
plt.savefig('cpi_seasonal_pattern.png', dpi=300, bbox_inches='tight')
plt.show()分解方法对比总结
各方法优缺点
| 方法 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|
| 经典分解 | 简单直观,计算快速 | 两端不稳定,对异常值敏感 | 初步探索 |
| STL | 灵活,对异常值稳健 | 计算复杂,参数调优 | 复杂季节性 |
| HP滤波 | 提取平滑趋势 | 不分离季节性 | 宏观经济周期 |
| X-13 | 官方标准,功能全面 | 复杂,需专门软件 | 政府统计 |
实践检查清单
分解时间序列时,确保完成以下步骤:
- [ ] 绘制原始数据,观察趋势和季节性
- [ ] 选择合适的分解模型(加法 vs 乘法)
- [ ] 检查残差是否为白噪声(无自相关)
- [ ] 验证分解质量:
- [ ] 绘制季节性模式图,检查是否合理
- [ ] 对季节调整后数据进行分析
- [ ] 记录分解参数,确保可重复性
常见问题与解决方案
问题 1:分解结果不稳定
症状:趋势在序列两端剧烈波动
原因:移动平均在端点缺少未来数据
解决:
- 使用 STL(更稳健)
- 对原始序列进行对称扩展
- 仅使用中间部分的分解结果
问题 2:季节性提取不完全
症状:残差仍有明显季节模式
原因:
- 季节周期识别错误
- 季节性随时间变化
- 存在多重季节性(周+年)
解决:
# 检查残差的自相关
from statsmodels.graphics.tsaplots import plot_acf
plot_acf(result.resid.dropna(), lags=40)
plt.title('残差自相关检验')
plt.show()
# 如果仍有季节性,调整 STL 参数
stl_refined = STL(ts_add, seasonal=25, trend=51, robust=True)
result_refined = stl_refined.fit()问题 3:趋势过于平滑
症状:趋势缺失重要结构性变化
原因:平滑参数(λ 或 trend window)过大
解决:减小平滑参数
# HP 滤波:使用更小的 λ
_, trend_sensitive = hpfilter(ts_add, lamb=1600)
# STL:使用更小的 trend window
stl_sensitive = STL(ts_add, seasonal=13, trend=21, robust=True)与其他方法的联系
与平稳性的关系
去趋势 = 达到平稳
- 原始序列非平稳 → 去趋势后平稳
- 分解帮助识别非平稳来源(趋势 vs 季节)
from statsmodels.tsa.stattools import adfuller
# 平稳性检验
def stationarity_check(series, name):
result = adfuller(series.dropna())
print(f"\n{name}:")
print(f" ADF统计量: {result[0]:.4f}")
print(f" p-value: {result[1]:.4f}")
print(f" 结论: {'平稳' if result[1] < 0.05 else '非平稳'}")
stationarity_check(ts_cpi, "原始CPI")
stationarity_check(result_cpi.trend, "CPI趋势")
stationarity_check(cpi_sa, "季节调整后CPI")
stationarity_check(result_cpi.resid, "CPI残差")与 ARIMA 的衔接
分解 → ARIMA 建模
- 分解识别季节周期(s)
- 去趋势/去季节后建立 ARIMA
- 或直接建立 SARIMA
本节小结
核心概念回顾
| 概念 | 定义 | 应用 |
|---|---|---|
| 分解 | 理解数据结构 | |
| 季节调整 | 识别真实趋势 | |
| STL | LOESS平滑的分解 | 复杂数据,稳健性 |
| HP滤波 | 平滑趋势提取 | 经济周期分析 |
实践要点
- 选择模型:加法(固定幅度)vs 乘法(比例变化)
- 方法选择:简单数据用经典分解,复杂数据用 STL
- 验证质量:检查残差是否为白噪声
- 参数调优:根据数据特征调整平滑参数
下节预告
在下一节中,我们将学习如何使用 ARIMA 模型进行时间序列预测。
延伸阅读
Cleveland, R. B., et al. (1990). "STL: A Seasonal-Trend Decomposition Procedure Based on Loess." Journal of Official Statistics, 6(1), 3-73.
Hodrick, R. J., & Prescott, E. C. (1997). "Postwar U.S. Business Cycles: An Empirical Investigation." Journal of Money, Credit and Banking, 29(1), 1-16.
Hyndman, R. J., & Athanasopoulos, G. (2021). Forecasting: Principles and Practice (3rd ed.). OTexts. Chapter 3.
Hamilton, J. D. (2018). "Why You Should Never Use the Hodrick-Prescott Filter." Review of Economics and Statistics, 100(5), 831-843.
理解数据,从分解开始!