你好呀!看到你想入门时间序列分析,我特别开心。说实话,很多初学者(包括当年的我)都被 ARIMA 这几个字母吓到了,觉得那是金融分析师或气象专家的高深武器。
但其实,时间序列分析的本质很简单:看看过去,猜猜未来。
今天这篇教程,我不给你堆砌晦涩的数学公式,而是带你跑通一个真实的、从“脏数据”到“预测模型”的完整闭环。我们会用到一个非常经典的股票收盘价数据集,一步步拆解每一步在干什么。如果你跟着敲代码,半小时后你就能理解整个过程。
准备好了吗?我们开始吧。
第一步:为什么“原始数据”不能直接用?
想象一下,你站在喧闹的菜市场里听别人说话,周围全是噪音。时间序列数据往往就是这样——充满了随机波动(噪声)。如果我们直接把原始数据丢进模型,模型会被这些杂音带偏,学得全是乱码。
所以,信号预处理不是为了好看,而是为了“去噪”,让趋势更清晰。
1.1 数据加载与初步观察
我们先用 Python 的 yfinance 库拉取一些真实数据,或者使用 statsmodels 内置的示例数据。为了演示方便,这里我们用 statsmodels 自带的 airline 数据集(每月乘客数量),因为它既有趋势又有季节性,非常典型。
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
import seaborn as sns
from statsmodels.tsa.seasonal import seasonal_decompose
from statsmodels.tsa.stattools import adfuller
import warnings
warnings.filterwarnings('ignore')
# 设置中文显示和绘图风格
plt.style.use('seaborn-v0_8-whitegrid')
plt.rcParams['font.sans-serif'] = ['SimHei'] # 用来正常显示中文标签
plt.rcParams['axes.unicode_minus'] = False # 用来正常显示负号
# 加载经典的月度航空公司乘客数据
from statsmodels.datasets import get_dataset
data = get_dataset('airline', package='statsmodels.datasets')
df = pd.DataFrame(data.data)
df.index = pd.date_range(start='1949-01-01', periods=len(df), freq='MS')
df.columns = ['passengers']
# 简单的可视化
df.plot(figsize=(12, 6), title='原始数据:看似杂乱,实则蕴含规律')
plt.ylabel('乘客数量')
plt.xlabel('时间')
plt.show()
看上面的图,你会注意到两件事:
- 趋势(Trend):整体是向上走的。
- 季节性(Seasonality):每年夏季都会出现一个波峰。
这时候,直接拟合模型可能会失败,因为数据不是“平稳”的。什么是平稳?简单说,就是数据的均值和方差不随时间变化。我们的原始数据均值一直在变,所以它是非平稳的。
1.2 稳定性检验:ADF 检验
在建模前,我们必须用 ADF 检验(Augmented Dickey-Fuller Test) 来确认数据是否平稳。
def check_stationarity(series, name='Series'):
print(f'--- 检验 {name} 的平稳性 ---')
result = adfuller(series)
print(f'ADF 统计量: {result[0]:.4f}')
print(f'P 值: {result[1]:.4f}')
if result[1] < 0.05:
print(f'结论: P值 < 0.05,数据是平稳的 (拒绝原假设)')
else:
print(f'结论: P值 >= 0.05,数据是非平稳的 (不能拒绝原假设)')
print('-' * 30)
# 检验原始数据
check_stationarity(df['passengers'], '原始乘客数据')
输出结果通常会显示 P 值很大,这意味着数据是非平稳的。我们需要对它进行“处理”。
第二步:信号预处理——差分与去季节化
为了让数据变得平稳,我们最常用的两个工具是:差分(Differencing) 和 对数变换(Log Transformation)。
2.1 对数变换:压缩波动
时间序列中,如果波动幅度随着数值增大而变大(比如股票价格,涨得越多波动越大),我们可以取对数来稳定方差。
df['log_passengers'] = np.log(df['passengers'])
df['log_passengers'].plot(figsize=(12, 4), title='对数变换后:方差更稳定')
plt.show()
check_stationarity(df['log_passengers'], '对数变换后')
注意:对数变换后,P 值可能变小了,但数据依然有季节性波动,均值依然随时间变化。我们需要下一步。
2.2 差分(Differencing):去除趋势
一阶差分就是“今天减去昨天”。这能去除线性趋势。
df['diff_1'] = df['log_passengers'].diff(1)
# 丢弃第一行 NaN
df_diff = df.dropna()
df_diff['diff_1'].plot(figsize=(12, 4), title='一阶差分后:去除了趋势')
plt.show()
check_stationarity(df_diff['diff_1'], '一阶差分后')
现在 P 值应该小于 0.05 了!说明一阶差分已经去掉了趋势,数据变得平稳了。
2.3 季节性差分:去除周期
但是,ARIMA 模型中的 “I”(Integrated,差分阶数)通常只处理趋势。对于明显的季节性数据(比如航空乘客),我们还需要处理季节性。
我们可以使用 seasonal_decompose 来可视化分解结果,或者直接进行季节性差分(间隔12个月的差分)。
# 季节性差分:当前值 - 12个月前的值
df['seasonal_diff'] = df['log_passengers'].diff(12)
df_seasonal = df.dropna()
df_seasonal['seasonal_diff'].plot(figsize=(12, 4), title='季节性差分后:去除了周期')
plt.show()
check_stationarity(df_seasonal['seasonal_diff'], '季节性差分后')
这时候,数据既平稳,又去除了季节性趋势。接下来,我们要决定用什么样的 ARIMA 模型。
第三步:ARIMA 模型参数选择(P, D, Q)
ARIMA 全称是 AutoRegressive Integrated Moving Average,也就是自回归积分滑动平均模型。它有三个参数:
- P (AR, 自回归阶数):利用过去的多少时间点数据来预测现在?
- D (I, 差分阶数):为了让数据平稳,需要差分几次?(我们刚才做了1次一阶差分,所以 D=1)
- Q (MA, 滑动平均阶数):利用过去的预测误差来调整现在?
如何确定 P 和 Q?这需要看 ACF(自相关函数) 和 PACF(偏自相关函数) 图。
3.1 观察 ACF 和 PACF
from statsmodels.graphics.tsaplots import plot_acf, plot_pacf
fig, axes = plt.subplots(1, 2, figsize=(14, 5))
# 注意:这里我们使用一阶差分后的数据来画 ACF/PACF,因为模型是基于平稳序列构建的
# 我们使用 df_diff['diff_1'] 作为基础
plot_acf(df_diff['diff_1'], lags=40, ax=axes[0], title='ACF (自相关函数)')
plot_pacf(df_diff['diff_1'], lags=40, ax=axes[1], method='ywm', title='PACF (偏自相关函数)')
plt.tight_layout()
plt.show()
怎么看这两个图?
- ACF:看蓝色的阴影区域外面还有多少根柱子?如果第一根柱子在外,后面迅速衰减,可能 MA 阶数 Q=1。
- PACF:看第几个柱子之后开始进入阴影区域(不显著)?
小白技巧:对于航空数据这种强季节性数据,手动选参数很麻烦。在实际工作中,我们通常使用 AutoARIMA 库来自动寻找最优参数,或者通过尝试几个常见组合来对比。
为了让你理解原理,我们手动选取一个经典组合:ARIMA(1, 1, 1) x (1, 1, 1, 12)。
(1, 1, 1)处理非季节性部分。(1, 1, 1, 12)处理季节性部分(因为数据是每月的,周期为12)。
如果你不想用手算,可以用 pmdarima 库自动搜索:
# 如果没有安装,请运行: pip install pmdarima
from pmdarima import auto_arima
# 自动搜索最佳参数
stepwise_fit = auto_arima(
df['passengers'],
seasonal=True, # 开启季节性检测
m=12, # 周期为12个月
trace=True, # 打印搜索过程
suppress_warnings=True,
error_action='ignore',
stepwise=True
)
print(stepwise_fit.summary())
print(f"最佳参数: {stepwise_fit.order}, 季节性参数: {stepwise_fit.seasonal_order}")
通常自动选择的结果会是 (1, 1, 1) 和 (1, 1, 1, 12)。我们就用这个吧!
第四步:搭建与训练模型
现在我们用 statsmodels 来正式建立 SARIMAX 模型(ARIMA 的季节性扩展版)。
from statsmodels.tsa.statespace.sarimax import SARIMAX
# 定义模型
# order=(p,d,q): 非季节性参数
# seasonal_order=(P,D,Q,m): 季节性参数
model = SARIMAX(df['passengers'], order=(1, 1, 1), seasonal_order=(1, 1, 1, 12))
# 训练模型
results = model.fit()
# 打印模型摘要,看看各项指标
print(results.summary())
如何看懂输出结果?
- Log Likelihood: 越大越好。
- AIC / BIC: 越小越好。这是衡量模型拟合优度和复杂度的平衡指标。
- coef (系数): 看看哪些变量显著。如果 P>|z| 列的值小于 0.05,说明该系数显著不为0,是有效的。
第五步:模型诊断——你确定模型没毛病吗?
很多初学者建模就结束了,这是大错特错!你必须检查残差(Residuals)。
残差应该是白噪声:即残差之间没有相关性,均值为0,方差恒定。如果残差还有规律,说明模型没把信息学干净,还可以优化。
# 绘制残差分析图
results.plot_diagnostics(lags=12, figsize=(12, 8))
plt.tight_layout()
plt.show()
你需要关注的地方:
- 实际 vs 拟合 (Observed vs Fitted):蓝线(拟合值)应该紧紧跟随红线(实际值)。
- 残差直方图 (Residuals):应该是一个对称的钟形曲线(正态分布)。
- 散点图 (Residuals vs Fitted):点应该随机分布在0附近,不要形成漏斗状或曲线状。
- Correlogram (ACF of Residuals):所有柱子都应该在阴影区域内(不显著相关)。如果有柱子伸出阴影,说明还有自相关没被捕捉。
如果 ACF 图里还有柱子伸出来,你可以尝试调整 P 或 Q 的值,或者增加季节性阶数,重新训练。
第六步:预测未来
模型诊断通过后,我们就可以预测未来了。假设我们要预测接下来 12 个月(1960年)的数据。
# 预测未来12步
forecast = results.get_forecast(steps=12)
# 获取预测值的均值和置信区间
forecast_mean = forecast.predict_mean
forecast_ci = forecast.conf_int()
# 将结果合并回原数据以便绘图
forecast_df = pd.DataFrame({
'forecast': forecast_mean,
'lower': forecast_ci.iloc[:, 0],
'upper': forecast_ci.iloc[:, 1]
}, index=pd.date_range(start='1960-01-01', periods=12, freq='MS'))
# 绘图
plt.figure(figsize=(12, 6))
plt.plot(df.index, df['passengers'], label='历史数据', color='blue')
plt.plot(forecast_df.index, forecast_df['forecast'], label='预测值', color='red', linewidth=2)
plt.fill_between(forecast_df.index, forecast_df['lower'], forecast_df['upper'], color='red', alpha=0.2, label='95% 置信区间')
plt.title('ARIMA 模型:未来12个月乘客数量预测')
plt.xlabel('时间')
plt.ylabel('乘客数量')
plt.legend()
plt.grid(True)
plt.show()
看那个红色的阴影区域了吗?那就是不确定性。时间越远,阴影越宽,意味着我们越不确定。这是预测的正常现象,不要指望一条直线就能精准击中未来。
第七步:评估模型效果
光看图不够,我们需要量化指标。常用的有 RMSE (均方根误差) 和 MAPE (平均绝对百分比误差)。
from sklearn.metrics import mean_squared_error, mean_absolute_percentage_error
# 为了演示评估,我们可以使用训练数据进行“样本内”预测对比
# 获取模型拟合值(in-sample fit)
fitted_values = results.fittedvalues
# 计算 RMSE
rmse = np.sqrt(mean_squared_error(df['passengers'][1:], fitted_values)) # 注意对齐索引,去掉第一个NaN
print(f"训练集 RMSE: {rmse:.2f}")
# 计算 MAPE
# 注意:sklearn 的 MAPE 在真实值为0时会报错,这里数据没有0,放心用
mape = mean_absolute_percentage_error(df['passengers'][1:], fitted_values) * 100
print(f"训练集 MAPE: {mape:.2f}%")
MAPE 为 5% 意味着平均预测误差在 5% 左右,对于这种波动较大的经济数据来说,已经是一个相当不错的结果了。
总结与避坑指南
恭喜你!你已经走完了一个完整的时间序列分析流程。让我们快速回顾一下:
- 可视化:先看数据,找趋势和季节。
- 平稳性检验:ADF 检验是必须的。
- 预处理:差分(去趋势)和对数变换(稳方差)。
- 参数定阶:看 ACF/PACF 图,或者用 AutoARIMA 自动搜索。
- 建模与诊断:训练模型后,务必检查残差是否为白噪声。
- 预测与评估:用 RMSE/MAPE 说话。
给初学者的几个“血泪”建议:
- 不要盲目套用公式:ARIMA 不是万能的。如果数据有极强的非线性关系,或者受外部因素(如节假日、促销)影响巨大,简单的 ARIMA 效果可能不好。这时候可以考虑 Prophet(Facebook开源)或 LSTM(深度学习)。
- 数据泄露是魔鬼:在做预测时,永远只用“过去”的数据预测“未来”。不要利用未来的信息来训练过去的模型。
- 季节性差分不一定非要做:如果
seasonal_decompose显示季节性很强,做季节性差分(D_s > 0)效果通常比不做要好。但如果季节性很弱,强行加季节性参数反而会过拟合。 - 保持耐心:调参是一个迭代的过程。第一次模型如果不理想,检查残差,调整 P/D/Q,再试。
时间序列分析是一门艺术,也是一门科学。希望这篇指南能帮你打开这扇大门。如果你在实际操作中遇到报错,或者想尝试其他模型(比如 Exponential Smoothing 指数平滑),随时可以再来问我!
祝你代码跑得通,预测准又稳!
