1. 项目缘起与整体思路拆解
1. 1 为什么选汽车月销量这个场景
汽车月销量这个数据,做时间序列预测算是相当经典的一个练手场景。它有几个天然优势:数据颗粒度是月度的,频率适中,既不像日度数据那样噪声大到让人头疼,也不像年度数据那样样本量少得可怜;同时汽车销量本身带有明显的季节性规律,比如年底冲量、春节前后淡季、金九银十这些行业性节奏,非常适合拿来验证季节性模型的效果。
我最初接触这个项目,是因为手头有一批月度销量数据需要做短期滚动预测,试过一些常规回归方法后发现效果一般,核心问题在于自回归特征和季节性特征没有被显式建模。后来转向ARIMA家族,才算找到了比较对路的工具。这篇文章就把我整个实操过程拆开来讲,从数据准备、平稳性检验、定阶、建模、诊断到预测评估,每一步都给出可复现的操作和踩过的坑。
适合谁看?如果你已经会用pandas做基本的数据清洗,对matplotlib画图不陌生,但一提到ARIMA就只知道调ARIMA(order=(p,d,q))却说不清p、d、q怎么来的,那这篇内容应该能帮你把整条链路打通。如果你已经用过SARIMAX但总是被收敛警告和预测效果不稳定困扰,里面关于定阶策略和诊断的部分也值得参考。
1. 2 三种模型的定位差异
这个项目要对比三个东西:ARIMA、SARIMAX和Auto ARIMA。很多人会把它们混为一谈,其实定位差别挺大。
ARIMA是非季节性自回归积分滑动平均模型,核心参数是(p, d, q)。p是自回归项数,d是差分次数,q是移动平均项数。它假设序列的统计特性不随季节变化,适合没有明显周期性的数据。用在汽车月销量上,如果不做季节性处理,效果通常会打折扣。
SARIMAX是在ARIMA基础上加了季节性成分(S, P, D, Q, m)和外生变量(X)。m是季节周期长度,月度数据一般取12。外生变量可以塞入促销月份、政策月、节假日天数等影响销量的因素。它的表达能力强很多,但参数空间也大得多,定阶更麻烦。
Auto ARIMA不是新模型,而是一套自动搜索最优(p, d, q)(P, D, Q, m)组合的流程。它通过逐步差分确定d和D,再用AICc等准则在候选模型里挑最优。省去了手工定阶的麻烦,但计算量大,而且自动选出来的不一定业务上最合理。
我个人的经验是:先用Auto ARIMA快速摸一个基准,再用SARIMAX手工调,最后拿ARIMA做对照,看季节性成分到底贡献了多少。这样对比下来,结论才有说服力。
1. 3 整体流程设计
整个项目的流程我拆成六步,后面每个章节会展开讲:
- 数据获取与清洗:拿到月度销量序列,处理缺失值、异常值,统一时间索引。
- 探索性分析:画时序图、分解图,看趋势、季节、残差。
- 平稳性检验与差分:ADF检验判断平稳性,确定差分阶数。
- 模型定阶与训练:分别用ACF/PACF手工定阶、网格搜索、Auto ARIMA自动定阶。
- 模型诊断:残差白噪声检验、Ljung-Box检验、正态性检查。
- 预测与评估:滚动预测,用MAE、RMSE、MAPE对比三个模型。
这个顺序不是随便排的。平稳性检验必须在定阶之前,因为ARIMA的前提是序列平稳;模型诊断必须在预测之前,因为残差有自相关的模型预测不可信。很多人跳过诊断直接看预测图,结果被表面拟合效果骗了。
提示:时间序列建模最忌讳“先跑通再补检验”。平稳性和残差诊断这两步如果省了,后面所有结论都站不住脚。
2. 数据准备与探索性分析的关键细节
2. 1 数据清洗里最容易忽略的坑
拿到月度销量数据,第一件事不是急着建模,而是把时间索引对齐。我见过太多人栽在这上面:数据里月份是字符串“2023-01”,索引却是默认的整数0、1、2……结果模型学到的“时间”其实是行号,预测出来的东西完全没有时间意义。
正确做法是用pd.to_datetime转成时间戳,再设为索引,并且用asfreq('MS')把频率显式声明为月初。为什么要用MS而不是M?因为月度数据的默认频率在不同pandas版本里行为不一致,显式指定月初可以避免重采样时的歧义。
缺失值处理也有讲究。汽车销量数据偶尔会因为统计口径调整出现某月缺失。如果缺失在中间,用线性插值补上通常没问题;如果缺失在末尾,千万别插值,因为那等于凭空造数据,应该直接截断到最后一个真实观测。异常值方面,我一般用滚动中位数加3倍MAD(绝对中位差)来识别,比均值和标准差稳健得多,因为销量数据里偶尔的促销冲量会把标准差拉偏。
import pandas as pd import numpy as np # 假设原始数据有两列:month, sales df = pd.read_csv('car_sales.csv') df['month'] = pd.to_datetime(df['month']) df = df.set_index('month').sort_index() df = df.asfreq('MS') # 中间缺失线性插值,末尾缺失截断 if df['sales'].isna().any(): last_valid = df['sales'].last_valid_index() df = df.loc[:last_valid] df['sales'] = df['sales'].interpolate(method='linear') # MAD异常值识别 rolling_median = df['sales'].rolling(12, center=True, min_periods=6).median() mad = (df['sales'] - rolling_median).abs().rolling(12, center=True, min_periods=6).median() outlier_mask = (df['sales'] - rolling_median).abs() > 3 * 1.4826 * mad print(f"识别到异常点数量: {outlier_mask.sum()}")这段代码里1.4826是MAD到标准差的换算系数,在正态假设下让MAD和标准差可比。识别出来的异常点不要直接删,先看看是不是真实的促销月,如果是真实业务事件,保留反而有助于模型学习。
2. 2 时序分解告诉你数据里有什么
清洗完数据,下一步是看。我习惯先画三张图:原始时序图、STL分解图、滚动统计图。
原始时序图看整体趋势和明显的季节波动。汽车销量通常有长期增长或周期波动,加上每年重复的月度模式。STL分解把序列拆成趋势、季节、残差三部分,能直观看到季节性振幅是否稳定。如果季节振幅随趋势增大而增大,说明可能需要做对数变换或者用乘法模型。
滚动统计图看滚动均值和滚动标准差是否随时间变化。如果滚动均值有明显漂移,说明序列非平稳,需要差分;如果滚动标准差变化大,说明方差非平稳,可能要考虑Box-Cox变换。
from statsmodels.tsa.seasonal import STL import matplotlib.pyplot as plt # STL分解,period=12对应月度数据 stl = STL(df['sales'], period=12, robust=True) res = stl.fit() fig, axes = plt.subplots(4, 1, figsize=(12, 10)) df['sales'].plot(ax=axes[0], title='原始序列') res.trend.plot(ax=axes[1], title='趋势项') res.seasonal.plot(ax=axes[2], title='季节项') res.resid.plot(ax=axes[3], title='残差项') plt.tight_layout() plt.show()robust=True这个参数很关键,它让STL对异常值不敏感。如果不加,一个促销冲量点可能把整个趋势项带偏。
看完分解图,心里要有个判断:趋势项是不是线性?季节项振幅稳不稳?残差项有没有明显结构?这些判断直接决定后面差分阶数和是否取对数。
2. 3 平稳性检验的正确打开方式
平稳性是ARIMA的命根子。ADF检验(增广迪基-福勒检验)是标配,但很多人只看p值小于0.05就完事,这是不够的。
ADF检验的原假设是“存在单位根,序列非平稳”。p值小于0.05拒绝原假设,认为序列平稳。但ADF对滞后阶数敏感,默认的滞后选择可能不合适。我一般会同时看三种信息准则(AIC、BIC、t-stat)选出的滞后阶数,如果结论一致才放心。
更重要的是,ADF检验对“趋势平稳”和“差分平稳”区分能力有限。一个带确定性趋势的序列,ADF可能拒绝单位根,但它其实不是我们想要的平稳。所以我会配合KPSS检验一起看。KPSS的原假设是“序列平稳”,和ADF正好相反。两个检验结论一致时最可靠:ADF拒绝+KPSS不拒绝,说明序列平稳;ADF不拒绝+KPSS拒绝,说明需要差分。
from statsmodels.tsa.stattools import adfuller, kpss def check_stationarity(series, name=''): adf_result = adfuller(series, autolag='AIC') kpss_result = kpss(series, regression='c', nlags='auto') print(f"{name} ADF p值: {adf_result[1]:.4f}, KPSS p值: {kpss_result[1]:.4f}") return adf_result[1], kpss_result[1] # 原始序列 check_stationarity(df['sales'], '原始序列') # 一阶差分 check_stationarity(df['sales'].diff().dropna(), '一阶差分') # 一阶差分后再季节差分 check_stationarity(df['sales'].diff().diff(12).dropna(), '一阶+季节差分')实际操作中,汽车月销量通常需要一阶差分消除趋势,再做一次12阶季节差分消除季节性。但也不是绝对的,如果季节振幅稳定且趋势平缓,可能只需要季节差分。差分次数d和D不是越多越好,过度差分会让序列方差膨胀,模型变得不稳定。
注意:每次差分后都要重新做平稳性检验,不要凭感觉连差两次。差分一次后如果ADF和KPSS都指向平稳,就停手。
3. 三种模型的定阶策略与实操对比
3. 1 ARIMA手工定阶:ACF和PACF怎么读
ARIMA的定阶靠ACF(自相关函数)和PACF(偏自相关函数)图。理论上的规则是:ACF拖尾、PACF截尾,用AR模型;ACF截尾、PACF拖尾,用MA模型;两者都拖尾,用ARMA。
但实际数据里“拖尾”和“截尾”的边界很模糊,尤其是加了差分之后。我的经验是不要死磕理论规则,而是把ACF/PACF图当作候选范围的参考。比如PACF在滞后1、2、3阶都超出置信区间,4阶之后落回,那p可以从1到3都试试;ACF类似地给q一个范围。然后用AIC/BIC在候选网格里选。
from statsmodels.graphics.tsaplots import plot_acf, plot_pacf fig, axes = plt.subplots(2, 1, figsize=(12, 8)) plot_acf(df['sales'].diff().dropna(), lags=36, ax=axes[0]) plot_pacf(df['sales'].diff().dropna(), lags=36, ax=axes[1]) plt.show()看ACF/PACF时有个细节:置信区间是±1.96/√n,n是有效样本量。如果样本量小,置信区间会很宽,很多滞后都会落在区间内,这时候定阶要保守,宁可取小阶数。另外,季节差分后的序列在滞后12、24处会有显著相关,这是季节性的残留,需要在SARIMAX里用季节项处理,而不是靠加大非季节阶数。
手工定阶的网格搜索可以这样写:
import warnings from statsmodels.tsa.arima.model import ARIMA warnings.filterwarnings('ignore') best_aic = np.inf best_order = None for p in range(4): for q in range(4): try: model = ARIMA(df['sales'], order=(p, 1, q)) result = model.fit() if result.aic < best_aic: best_aic = result.aic best_order = (p, 1, q) except: continue print(f"最优ARIMA阶数: {best_order}, AIC: {best_aic:.2f}")这里d固定为1,因为前面平稳性检验已经确定了一阶差分。如果检验结果是d=0或d=2,这里要相应调整。网格范围我一般从0到3,再大就容易过拟合,而且计算量陡增。
3. 2 SARIMAX的季节项与外生变量
SARIMAX的完整阶数是(p, d, q)(P, D, Q, m)。m对月度数据取12。P、D、Q是季节部分的阶数,通常都很小,0或1居多。D是季节差分次数,前面检验如果做了一次季节差分,这里D=1。
季节项的定阶同样看季节滞后处的ACF/PACF。比如ACF在滞后12处显著,PACF在滞后12处也显著,那P和Q都可能非零。但季节阶数一高,参数数量增长很快,容易不收敛。我的策略是先把P和Q限制在0或1,跑一轮看AIC,再决定要不要扩到2。
外生变量是SARIMAX的加分项。汽车销量可以加入的外生变量包括:月份哑变量(捕捉固定月度效应)、春节所在月份标记、促销活动月标记、季度末标记等。加入外生变量前要想清楚:这个变量在预测期是否已知?如果预测未来12个月,春节月份是已知的,可以用;但促销活动如果还没定,就不能用,否则预测时没有输入。
from statsmodels.tsa.statespace.sarimax import SARIMAX # 构造外生变量:月份哑变量 month_dummies = pd.get_dummies(df.index.month, prefix='month', drop_first=True) month_dummies.index = df.index # 春节月份标记(假设2月为春节月,实际按年份调整) df['spring_festival'] = (df.index.month == 2).astype(int) exog = pd.concat([month_dummies, df[['spring_festival']]], axis=1) model = SARIMAX(df['sales'], exog=exog, order=(1, 1, 1), seasonal_order=(1, 1, 1, 12), enforce_stationarity=False, enforce_invertibility=False) result = model.fit(disp=False) print(result.summary())enforce_stationarity=False和enforce_invertibility=False这两个参数在调试阶段很有用,它们放宽了参数约束,让模型更容易收敛。但最终模型最好还是打开约束,确保参数在平稳可逆区域内。如果打开约束后不收敛,说明阶数可能过高,需要降阶。
3. 3 Auto ARIMA自动定阶的实操与局限
Auto ARIMA我用的是pmdarima库,它的auto_arima函数封装了逐步搜索流程。核心参数包括:seasonal=True开启季节搜索,m=12指定周期,stepwise=True用逐步算法加速,suppress_warnings=True屏蔽收敛警告。
import pmdarima as pm auto_model = pm.auto_arima( df['sales'], seasonal=True, m=12, stepwise=True, suppress_warnings=True, trace=True, error_action='ignore', max_p=3, max_q=3, max_P=2, max_Q=2, information_criterion='aicc' ) print(auto_model.summary())trace=True会打印每一步搜索的模型和AICc,方便你看到搜索路径。information_criterion='aicc'用AICc而不是AIC,因为AICc对小样本有修正,更不容易过拟合。
Auto ARIMA的局限要说清楚。第一,它只优化统计准则,不考虑业务合理性。比如它可能选出一个季节阶数很高的模型,AICc略优但参数解释性差。第二,它默认的搜索范围有限,如果最优模型在范围外,它找不到。第三,它对异常值敏感,一个促销冲量点可能让它选错差分阶数。所以我的做法是:Auto ARIMA的结果作为基准和参考,不直接当作最终模型。把它选出的阶数拿到SARIMAX里,加上外生变量再训练,往往效果更好。
三个模型的定阶策略对比可以整理成表:
| 模型 | 定阶方式 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|---|
| ARIMA | ACF/PACF手工+网格搜索 | 简单直观,计算快 | 无法处理季节性 | 无季节性的短序列 |
| SARIMAX | 手工定阶+外生变量 | 表达能力强,可解释 | 参数多,易不收敛 | 有季节性+外部因素 |
| Auto ARIMA | 自动逐步搜索 | 省时,基准可靠 | 不考虑业务,范围有限 | 快速摸基准 |
4. 模型诊断与预测评估的完整流程
4. 1 残差诊断:模型是否真的学到了东西
模型训练完,第一件事不是看预测图,而是看残差。残差应该像白噪声:均值接近零、方差恒定、无自相关、近似正态。如果残差还有结构,说明模型没学干净。
我一般做四张诊断图:残差时序图、残差直方图、Q-Q图、残差ACF图。残差时序图看有没有趋势或周期残留;直方图和Q-Q图看正态性;ACF图看自相关。
from statsmodels.stats.diagnostic import acorr_ljungbox from scipy import stats resid = result.resid fig, axes = plt.subplots(2, 2, figsize=(12, 8)) resid.plot(ax=axes[0, 0], title='残差时序') axes[0, 1].hist(resid, bins=20, edgecolor='black') axes[0, 1].set_title('残差直方图') stats.probplot(resid, dist='norm', plot=axes[1, 0]) axes[1, 0].set_title('Q-Q图') plot_acf(resid, lags=24, ax=axes[1, 1]) axes[1, 1].set_title('残差ACF') plt.tight_layout() plt.show() # Ljung-Box检验 lb_test = acorr_ljungbox(resid, lags=[12, 24], return_df=True) print(lb_test)Ljung-Box检验的原假设是“残差无自相关”。p值大于0.05才说明残差干净。如果滞后12或24的p值小于0.05,说明模型没捕捉到某些周期性,需要调整季节阶数。
残差正态性不是ARIMA的硬性要求,但影响预测区间的准确性。如果残差严重偏态,预测区间会不对称,这时候可以考虑对原始数据做Box-Cox变换后再建模。
提示:诊断阶段如果发现残差ACF在滞后12处显著,优先检查季节差分D是否设对,而不是盲目加大P和Q。
4. 2 滚动预测与评估指标
预测评估不能只看一步预测。我一般做滚动预测:用前n个月训练,预测下一个月,然后窗口后移,重复多次。这样得到的预测误差更接近真实使用场景。
def rolling_forecast(model_class, data, order, seasonal_order=None, exog=None, horizon=12, train_size=None): n = len(data) if train_size is None: train_size = n - horizon predictions = [] actuals = [] for i in range(train_size, n): train = data.iloc[:i] test = data.iloc[i:i+1] if seasonal_order: model = model_class(train, exog=exog.iloc[:i] if exog is not None else None, order=order, seasonal_order=seasonal_order, enforce_stationarity=False, enforce_invertibility=False) else: model = model_class(train, order=order) result = model.fit(disp=False) pred = result.forecast(steps=1, exog=exog.iloc[i:i+1] if exog is not None else None) predictions.append(pred.values[0]) actuals.append(test.values[0]) return np.array(predictions), np.array(actuals) # 评估指标 def evaluate(actual, pred): mae = np.mean(np.abs(actual - pred)) rmse = np.sqrt(np.mean((actual - pred) ** 2)) mape = np.mean(np.abs((actual - pred) / actual)) * 100 return mae, rmse, mape滚动预测的计算量比一次性预测大很多,但结果更可信。如果数据量很大,可以适当减少滚动次数,比如只滚动最后24个月。
三个模型的评估结果我一般整理成表:
| 模型 | MAE | RMSE | MAPE | 训练时间 | 收敛情况 |
|---|---|---|---|---|---|
| ARIMA | 较高 | 较高 | 较高 | 快 | 稳定 |
| SARIMAX | 最低 | 最低 | 最低 | 中等 | 偶尔警告 |
| Auto ARIMA | 中等 | 中等 | 中等 | 慢 | 稳定 |
具体数值因数据而异,但通常SARIMAX因为加入了季节项和外生变量,误差最小。ARIMA因为没有季节项,误差最大。Auto ARIMA介于两者之间,但它的优势是省去了手工定阶的时间。
4. 3 预测结果的可视化与业务解读
预测图要包含三部分:历史实际值、预测值、预测区间。预测区间用get_forecast的conf_int方法获取。
forecast = result.get_forecast(steps=12, exog=exog_future) forecast_mean = forecast.predicted_mean forecast_ci = forecast.conf_int(alpha=0.05) plt.figure(figsize=(12, 6)) plt.plot(df.index, df['sales'], label='历史实际') plt.plot(forecast_mean.index, forecast_mean, label='预测', color='red') plt.fill_between(forecast_ci.index, forecast_ci.iloc[:, 0], forecast_ci.iloc[:, 1], color='red', alpha=0.2, label='95%置信区间') plt.legend() plt.title('汽车月销量预测') plt.show()业务解读时要注意:预测区间比点预测更有价值。如果区间很宽,说明模型不确定性大,业务决策要保守。另外,预测值如果出现明显不符合业务常识的跳变,要回头检查外生变量在预测期的取值是否合理。
5. 常见问题与排查技巧实录
5. 1 收敛警告与参数估计失败
SARIMAX最常见的报错就是“Maximum Likelihood optimization failed to converge”或者“非正定矩阵”。原因通常有三个:阶数过高、数据尺度太大、初值不好。
阶数过高是最常见的。解决办法是降阶,先把季节阶数降到0或1,非季节阶数降到2以内,跑通后再逐步加。数据尺度太大也会导致数值问题,比如销量是几十万级别,可以除以10000把尺度降到个位数。初值问题可以通过start_params手动指定,或者用fit(method='nm')先用Nelder-Mead跑一轮,再用默认的BFGS。
# 先用nm方法找初值,再用bfgs精调 result = model.fit(method='nm', maxiter=200, disp=False) result = model.fit(start_params=result.params, method='bfgs', disp=False)这个方法我实测下来很稳,尤其是对季节阶数较高的模型。
5. 2 预测值偏离实际太远
预测值偏离实际,先分情况。如果历史拟合很好但预测很差,通常是过拟合,模型把噪声也学了。解决办法是降低阶数,或者增加训练数据量。如果历史拟合就差,说明模型设定有问题,检查差分阶数、季节周期、外生变量。
还有一种情况是预测期外生变量取值不合理。比如月份哑变量在预测期如果全取0,模型会失去月度效应。正确做法是预测期也要构造对应的哑变量矩阵,和训练期对齐。
5. 3 差分次数选择困难
差分次数d和D的选择,我的经验是:先看ADF和KPSS,两个都指向平稳就停。如果两个结论矛盾,优先相信KPSS,因为ADF在近单位根情况下检验力低。另外,d一般不超过2,D一般不超过1。过度差分会让序列过度平滑,预测变得迟钝。
如果一阶差分后ADF还是不拒绝,但KPSS也不拒绝,说明序列可能在单位根附近,这时候可以试试带漂移项的ADF检验,或者直接取d=1,因为实际数据很少有真正的单位根,多数是近单位根过程。
5. 4 常见问题速查表
| 问题现象 | 可能原因 | 排查方法 | 解决措施 |
|---|---|---|---|
| 收敛警告 | 阶数过高/尺度大 | 检查order和seasonal_order | 降阶/缩放数据 |
| 残差ACF显著 | 季节项不足 | 看滞后12/24的ACF | 增加P或Q |
| 预测区间过宽 | 模型不确定大 | 看残差方差 | 增加数据/简化模型 |
| 预测值跳变 | 外生变量异常 | 检查预测期exog | 修正exog取值 |
| AIC很小但预测差 | 过拟合 | 对比训练/测试误差 | 降阶/正则化 |
5. 5 几个独家避坑技巧
第一,训练SARIMAX时把enforce_stationarity和enforce_invertibility先设为False跑通,再设为True重跑。这样能快速定位是参数约束问题还是模型设定问题。
第二,Auto ARIMA的m参数一定要显式指定。月度数据m=12,季度数据m=4。如果不指定,它可能把周期识别错,选出的季节阶数完全不对。
第三,滚动预测时,外生变量矩阵要和训练期列对齐。用pd.concat拼接后,检查列名和顺序是否一致,否则预测会报错或结果错位。
第四,评估指标不要只看MAPE。销量数据如果有接近零的月份,MAPE会爆炸。这时候用MAE或RMSE更稳。我一般三个都算,互相印证。
第五,保存模型用result.save('model.pkl'),下次直接SARIMAXResults.load('model.pkl'),省去重新训练的时间。但要注意,加载后的模型预测时exog列必须和训练时完全一致。
6. 模型选择与后续扩展的个人体会
三个模型跑下来,我的结论是:如果数据有明显季节性,SARIMAX是首选,但前提是愿意花时间定阶和调外生变量。Auto ARIMA适合快速摸基准,或者作为SARIMAX定阶的起点。ARIMA适合作为对照,帮你判断季节性成分到底贡献了多少预测力。
后续扩展有几个方向。一是加入更多外生变量,比如宏观经济指标、油价、竞品销量,但要注意预测期这些变量是否可得。二是尝试Prophet或LightGBM等替代方法,和SARIMAX做对比。三是做集成,把三个模型的预测加权平均,通常比单模型稳。
我在实际使用中发现,SARIMAX的预测效果对差分阶数特别敏感。同样一组数据,d=1和d=2的预测结果可能差很多。所以每次换数据,平稳性检验这一步绝对不能省。另外,季节阶数不要贪高,P=1、Q=1在多数月度数据上已经够用,加到2往往只是AIC略优,但预测稳定性下降。
最后分享一个小技巧:如果SARIMAX训练太慢,可以先用Auto ARIMA定出阶数,再把这个阶数固定,只优化外生变量系数。这样计算量小很多,效果通常也不差。