Skip to content

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. 理解数据结构:识别主要驱动因素
  2. 季节调整:消除季节性,看清真实趋势
  3. 预测改进:分别建模不同成分
  4. 异常检测:识别不寻常的波动

经典分解模型

1. 加法模型(Additive Model)

适用场景:季节性波动幅度不随时间变化

组成

  • :观测值
  • :趋势成分(Trend)
  • :季节成分(Seasonal)
  • :随机成分(Random/Residual)

特征

  • 季节性波动是固定幅度(例如:每年12月销量增加100万)
  • 各成分相互独立

2. 乘法模型(Multiplicative Model)

适用场景:季节性波动幅度随趋势增长

等价对数形式(转为加法):

特征

  • 季节性波动是比例变化(例如:每年12月销量增加20%)
  • 适合指数增长数据

如何选择模型?

特征加法模型乘法模型
季节波动固定幅度比例变化
数据类型线性增长指数增长
季节图波动幅度恒定波动幅度递增
适用案例温度、降水GDP、股价、销售额

️ Python 实现:经典分解

使用 statsmodels 进行分解

python
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))

加法分解

python
# 加法分解
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}")

乘法分解

python
# 乘法分解
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 数学原理

核心思想:迭代分离趋势和季节性

算法步骤

  1. 初始去趋势:
  2. 提取季节性:对每个季节周期进行平滑
  3. 去季节性:
  4. 重新估计趋势:对去季节数据平滑
  5. 迭代直到收敛

Python 实现 STL

python
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 参数调优

python
# 不同参数对比
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月销量激增(圣诞节)
  • 如何判断是正常季节效应还是经济好转?

解决:去除季节性,保留趋势+随机

季节调整的方法

python
# 季节调整:原始数据 - 季节成分
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 滤波

python
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数据存在明显春节效应

  • 春节前:食品价格上涨
  • 春节后:价格回落

目标:去除季节性,识别真实通胀趋势

数据准备与分析

python
# 模拟中国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:季节性提取不完全

症状:残差仍有明显季节模式

原因

  • 季节周期识别错误
  • 季节性随时间变化
  • 存在多重季节性(周+年)

解决

python
# 检查残差的自相关
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)过大

解决:减小平滑参数

python
# HP 滤波:使用更小的 λ
_, trend_sensitive = hpfilter(ts_add, lamb=1600)

# STL:使用更小的 trend window
stl_sensitive = STL(ts_add, seasonal=13, trend=21, robust=True)

与其他方法的联系

与平稳性的关系

去趋势 = 达到平稳

  • 原始序列非平稳 → 去趋势后平稳
  • 分解帮助识别非平稳来源(趋势 vs 季节)
python
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 建模

  1. 分解识别季节周期(s)
  2. 去趋势/去季节后建立 ARIMA
  3. 或直接建立 SARIMA

本节小结

核心概念回顾

概念定义应用
分解理解数据结构
季节调整识别真实趋势
STLLOESS平滑的分解复杂数据,稳健性
HP滤波平滑趋势提取经济周期分析

实践要点

  1. 选择模型:加法(固定幅度)vs 乘法(比例变化)
  2. 方法选择:简单数据用经典分解,复杂数据用 STL
  3. 验证质量:检查残差是否为白噪声
  4. 参数调优:根据数据特征调整平滑参数

下节预告

下一节中,我们将学习如何使用 ARIMA 模型进行时间序列预测。


延伸阅读

  1. Cleveland, R. B., et al. (1990). "STL: A Seasonal-Trend Decomposition Procedure Based on Loess." Journal of Official Statistics, 6(1), 3-73.

  2. 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.

  3. Hyndman, R. J., & Athanasopoulos, G. (2021). Forecasting: Principles and Practice (3rd ed.). OTexts. Chapter 3.

  4. Hamilton, J. D. (2018). "Why You Should Never Use the Hodrick-Prescott Filter." Review of Economics and Statistics, 100(5), 831-843.

理解数据,从分解开始!

基于 MIT 许可证发布。内容版权归作者所有。