第 3 篇:揭秘时间模式 - 时间序列分解
第 3 篇:揭秘时间模式 - 时间序列分解
在上一篇中,我们学会了如何加载、可视化时间序列数据,并了解了时间序列可能包含趋势 (Trend)、季节性 (Seasonality)、周期性 (Cyclicality) 和随机性 (Residual) 这些基本成分。但仅仅是“知道”还不够,我们希望能更深入地“看透”数据,把这些成分明确地分离出来。
这就是时间序列分解 (Time Series Decomposition) 的目标:将原始时间序列拆解成其内在的组成部分。这就像给时间序列做 CT 扫描,帮助我们:
- 更深入地理解数据驱动力: 长期增长是主要因素,还是季节性波动更显著?
- 为建模做准备: 某些模型(如 ARIMA 的一些变种)可能需要先去除趋势或季节性成分。分解后的成分本身也可以作为模型的特征。
- 改进预测: 分别预测趋势和季节性,然后组合起来,有时能得到更好的结果。
分解模型:加法 vs. 乘法
如何将这些成分组合起来得到原始序列?主要有两种基本模型:
-
加法模型 (Additive Model):
- 公式:
Y(t) = Trend(t) + Seasonal(t) + Residual(t)
- 含义: 假设季节性成分的幅度是相对恒定的,不随时间趋势的变化而变化。它只是在趋势的基础上进行加减。
- 适用场景: 季节性波动的大小不依赖于时间序列的水平。例如,某商店每天固定在午餐时间多卖出约 50 份快餐,无论当天总销量是 200 份还是 500 份。
- 图形特点: 在时间序列图上,季节性波动的“摆动幅度”看起来大致相同。
- 公式:
-
乘法模型 (Multiplicative Model):
- 公式:
Y(t) = Trend(t) * Seasonal(t) * Residual(t)
- 含义: 假设季节性成分的幅度与趋势成比例变化。季节性可以看作是趋势的一个百分比。
- 适用场景: 季节性波动的大小随着时间序列水平的升高而增大(或随降低而减小)。例如,航空乘客数量,在整体乘客量较低的年份,旺季和淡季的差距可能只有几千人;但在乘客量高的年份,这个差距可能达到数万人。
- 图形特点: 在时间序列图上,季节性波动的“摆动幅度”随着趋势的上升而变宽,随着趋势的下降而变窄。
- 小技巧: 对乘法模型取对数 (log),可以将其转换为加法模型:
log(Y(t)) ≈ log(Trend(t)) + log(Seasonal(t)) + log(Residual(t))
。这在某些分析中很有用。
- 公式:
如何选择?
- 观察图形: 这是最直观的方法。看季节性波动的幅度是恒定的(倾向于加法)还是随趋势变化的(倾向于乘法)。
- 经验判断: 某些类型的数据(如大多数经济指标、销售额)通常更符合乘法模型。
- 尝试与比较: 如果不确定,可以两种模型都尝试,看哪个分解结果的残差项更接近随机噪声。
如何进行分解? statsmodels
来帮忙
Python 的 statsmodels
库提供了一个非常方便的函数来进行经典的时间序列分解:seasonal_decompose
。
from statsmodels.tsa.seasonal import seasonal_decompose
import statsmodels.api as sm # 用于加载示例数据
import pandas as pd
import matplotlib.pyplot as plt
import seaborn as sns# 设置全局绘图参数 (可选)
plt.rcParams.update({'figure.figsize': (10, 8)})
sns.set_style("whitegrid")# 1. 加载示例数据 (经典的 CO2 浓度数据)
# 这个数据集有明显的趋势和季节性,适合演示
data = sm.datasets.co2.load_pandas().data# 2. 数据预处理
# CO2 数据是周度数据,可能有缺失值,且频率不完全固定
# 我们将其重采样为月度数据 (取均值),并填充缺失值
data['co2'].interpolate(inplace=True) # 线性插值填充 NaN
monthly_data = data.resample('M').mean() # 按月重采样print("月度 CO2 数据概览:")
print(monthly_data.head())# 3. 进行分解
# 观察 CO2 浓度图 (你可以先画一下 monthly_data['co2'].plot())
# 会发现季节性波动随着整体趋势上升而略有增大,因此选择乘法模型更合适
# 季节性周期为 12 (月度数据,年度季节性)
decomposition_result = seasonal_decompose(monthly_data['co2'], model='multiplicative', period=12)# 4. 可视化分解结果
print("\n正在绘制分解图...")
fig = decomposition_result.plot()
plt.suptitle('Time Series Decomposition of Monthly CO2 Data (Multiplicative Model)', y=1.02) # 添加总标题
plt.tight_layout()
plt.show()# 我们可以单独访问每个成分
trend_component = decomposition_result.trend
seasonal_component = decomposition_result.seasonal
residual_component = decomposition_result.residprint("\n分解得到的趋势成分 (前几行):")
print(trend_component.head())
print("\n分解得到的季节性成分 (前几行):")
print(seasonal_component.head())
print("\n分解得到的残差成分 (前几行):")
print(residual_component.head())
代码解释:
- 加载数据: 我们使用了
statsmodels
自带的datasets.co2
数据集。 - 预处理:
interpolate()
: 填充数据中的缺失值(NaN),这里使用简单的线性插值。resample('M').mean()
: 将数据从原始频率(可能是周度)转换为月度频率,计算每个月的平均 CO2 浓度。这使得我们有一个固定的频率,便于设定period
。
seasonal_decompose
:- 第一个参数是你要分解的时间序列(
monthly_data['co2']
)。 model='multiplicative'
: 指定使用乘法模型。如果你的数据更像加法,就用model='additive'
。period=12
: 这是非常重要的参数!它指定了季节性的周期长度。因为我们是月度数据,并且 CO2 浓度有明显的年度季节性,所以周期是 12 个月。如果是日数据且有周季节性,period
就应设为 7。
- 第一个参数是你要分解的时间序列(
decomposition_result.plot()
: 这个方法直接绘制出分解的四个部分。
解读分解结果图
seasonal_decompose
生成的图通常包含四个子图:
- Observed (原始数据): 就是我们输入的
monthly_data['co2']
。 - Trend (趋势成分): 显示了数据的长期平滑走向。注意,在序列的开始和结束部分,趋势线可能无法计算出来(显示为灰色或缺失),这是经典分解方法(基于移动平均)的一个局限性。对于 CO2 数据,我们可以看到一个明显的长期上升趋势。
- Seasonal (季节性成分): 显示了固定的季节性波动模式。对于乘法模型,这个值通常在 1 附近波动;对于加法模型,在 0 附近波动。你应该能看到一个每年重复的模式(对应
period=12
)。对于 CO2 数据,每年都有一个峰值和一个谷值。 - Residual (残差成分): 这是从原始数据中移除趋势和季节性成分后剩下的部分
Residual = Observed / (Trend * Seasonal)
(乘法) 或Residual = Observed - Trend - Seasonal
(加法)。理想情况下,残差应该看起来像随机噪声,没有明显的模式。如果残差中还存在明显的模式,可能意味着分解模型选择不当,或者数据中还包含其他未被捕捉的结构(如周期性成分或异常事件)。
讨论与局限性
seasonal_decompose
是一种比较基础的分解方法,它主要基于移动平均来估计趋势和季节性。- 端点问题: 如上所述,由于移动平均的计算方式,序列两端的数据点可能无法很好地估计出趋势和季节性成分(结果可能是 NaN)。
- 季节性假设: 它假设季节性模式是固定不变的。如果季节性模式随时间演变,这种方法可能效果不佳。
- 对异常值敏感: 移动平均对数据中的极端值(异常点)比较敏感。
尽管有这些局限性,seasonal_decompose
对于快速理解数据结构、检查趋势和季节性模式仍然是一个非常有用的入门工具。更高级的分解方法包括 STL (Seasonal and Trend decomposition using Loess)、X-13-ARIMA-SEATS 等,但它们超出了我们入门系列的范围。
小结
今天,我们深入了解了时间序列分解:
- 知道了分解的目标是将序列拆分为趋势、季节性和残差。
- 学习了加法和乘法两种分解模型,以及如何根据数据特点选择。
- 掌握了使用 Python
statsmodels.tsa.seasonal_decompose
函数进行实际分解操作,并理解了model
和period
参数的重要性。 - 学会了解读分解结果图,从趋势、季节性和残差图中获取信息。
- 了解了经典分解方法的一些局限性。
下一篇预告
通过分解,我们能清晰地看到序列中的趋势和季节性。然而,很多经典的时间序列模型(比如后面可能会接触的 ARIMA)都要求数据是平稳 (Stationary) 的——简单来说,就是数据的统计特性(如均值、方差)不随时间改变。而带有明显趋势或季节性的序列通常是非平稳的。
那么,什么是平稳性?为什么它如此重要?如何判断一个序列是否平稳?如果它不平稳,我们又该如何处理呢?下一篇,我们将探讨时间序列分析的基石——平稳性。
敬请期待!
(分解 CO2 数据时,你选择了乘法模型。如果尝试用加法模型,结果图会有什么不同?欢迎在评论区分享你的发现!)