当你面对一张密密麻麻的航空客流数据表时,是否曾好奇这些数字背后隐藏着怎样的规律?每个月客流量的起伏是纯属偶然,还是暗含着某种周期性模式?今天,我们将用Python中的statsmodels库,像拆解钟表一样剖析这些数据,揭示其中的季节规律、长期趋势和随机波动。
在开始STL分解之前,我们需要确保工具就位。假设你已经安装了Python 3.7或更高版本,接下来通过pip安装必要的库:
bash复制pip install statsmodels pandas matplotlib numpy
经典的航空乘客数据集(AirPassengers.csv)可以从多个数据源获取,这里我们使用statsmodels自带的示例数据集:
python复制import pandas as pd
import matplotlib.pyplot as plt
from statsmodels.datasets import get_rdataset
# 获取航空乘客数据
data = get_rdataset("AirPassengers")
df = data.data
df['Month'] = pd.to_datetime(df['time'])
df.set_index('Month', inplace=True)
提示:如果你的网络环境无法直接获取该数据集,可以手动下载CSV文件并用pd.read_csv()加载。
让我们先看一眼原始数据的样貌:
python复制plt.figure(figsize=(12, 6))
plt.plot(df.index, df['value'], label='原始数据')
plt.title('1949-1960年国际航空乘客量')
plt.xlabel('日期')
plt.ylabel('乘客数(千)')
plt.grid(True)
plt.legend()
plt.show()
这段代码会输出一条明显呈上升趋势并带有周期性波动的曲线。仔细观察,你会发现每年夏季(6-8月)都会出现一个客流高峰,这正是我们要分解的季节性成分。
STL(Seasonal-Trend decomposition using LOESS)是一种鲁棒性强的时间序列分解方法,特别适合处理有复杂季节性的数据。statsmodels库中的STL类主要接受以下关键参数:
| 参数名 | 数据类型 | 说明 | 默认值 | 设置建议 |
|---|---|---|---|---|
| endog | array-like | 待分解的时间序列 | 无 | 必须提供 |
| period | int | 季节性周期 | None | 月度数据通常为12 |
| seasonal | int | 季节性平滑窗口 | 7 | 必须为奇数,建议≥7 |
| trend | int | 趋势平滑窗口 | None | 通常取period的1.5倍 |
| robust | bool | 是否使用鲁棒性分解 | False | 有异常值时设为True |
对于我们的航空乘客数据,典型的初始化代码如下:
python复制from statsmodels.tsa.seasonal import STL
result = STL(
endog=df['value'],
period=12, # 月度数据的年度周期
seasonal=13, # 比默认稍大的平滑窗口
trend=19, # period的1.5倍左右
robust=True # 增强对异常值的鲁棒性
).fit()
注意:当输入数据是Pandas Series且具有DatetimeIndex时,period参数可以自动推断。但显式指定能避免意外错误。
拟合完成后,我们可以直接查看分解后的三个分量:
python复制# 绘制分解结果
plt.figure(figsize=(12, 8))
result.plot()
plt.tight_layout()
plt.show()
这张图会显示四个子图:原始数据、趋势成分、季节成分和残差。仔细观察可以发现:
让我们把分解结果保存到DataFrame中,方便后续分析:
python复制df['trend'] = result.trend
df['seasonal'] = result.seasonal
df['residual'] = result.resid
健康的分解应该使残差接近随机噪声。我们可以通过以下检查:
python复制print(f"残差均值:{df['residual'].mean():.4f}")
print(f"残差标准差:{df['residual'].std():.4f}")
# 残差直方图
plt.figure(figsize=(10, 4))
plt.subplot(1, 2, 1)
df['residual'].hist(bins=20)
plt.title('残差分布')
# 残差Q-Q图
plt.subplot(1, 2, 2)
from scipy import stats
stats.probplot(df['residual'].dropna(), plot=plt)
plt.tight_layout()
理想情况下,残差均值应接近0,分布近似正态。如果出现明显偏离,可能需要调整seasonal或trend参数。
我们可以用数学方法量化趋势和季节性的显著程度:
python复制# 计算去趋势数据
df['detrended'] = df['value'] - df['trend']
# 计算去季节数据
df['deseasonalized'] = df['value'] - df['seasonal']
# 计算趋势强度
trend_strength = max(0, 1 - (df['residual'].var() / df['deseasonalized'].var()))
# 计算季节强度
seasonal_strength = max(0, 1 - (df['residual'].var() / df['detrended'].var()))
print(f"趋势强度:{trend_strength:.3f}")
print(f"季节强度:{seasonal_strength:.3f}")
这两个指标范围在0到1之间:
找出每年客流最高的月份:
python复制seasonal_component = df['seasonal'].values[:12] # 取第一年的季节成分
peak_month = seasonal_component.argmax() + 1 # 月份从1开始计数
print(f"季节性峰值月份:{peak_month}月")
结果显示7月为客流高峰,这与旅游旺季的实际情况相符。航空公司可以利用这一信息优化:
当季节性周期不是整数时(如每日数据的年度周期365.24),可以:
python复制result = STL(
endog=df['value'],
period=365.24,
seasonal=21, # 更大的平滑窗口
...
).fit()
对于同时具有周和年周期的数据(如每日电力负荷),可先分解主要周期,再对残差二次分解:
python复制# 第一次分解年度周期
result_yearly = STL(endog=df['load'], period=365).fit()
# 对残差分解周周期
result_weekly = STL(endog=result_yearly.resid, period=7).fit()
错误1:"ValueError: period must be a positive integer"
错误2:分解后残差呈现明显模式
python复制STL(..., seasonal=15, trend=21).fit() # 增大平滑窗口
错误3:趋势成分过于波动
在实际项目中,我发现设置seasonal=13, trend=19的组合对大多数月度数据效果良好。当数据存在明显异常点时,启用robust选项能显著改善分解质量。