汽车月销量预测这件事,说简单也简单,说难也真难。简单在于数据本身是公开的、连续的、有明确业务含义的;难在于它同时包含趋势性、季节性、节假日效应和外部冲击,随便套个线性回归基本等于扔硬币。我前后用三套方案跑过同一份国内汽车月销量数据——经典ARIMA、带外生变量的SARIMAX、以及Auto ARIMA自动定阶,中间踩了不少坑,也积累了一些文档里不会写的经验。这篇就把整个思路、参数选择、代码实现和排查过程完整摊开讲一遍。
不管你是刚接触时间序列的新手,还是已经用过ARIMA但总调不好参数的老手,下面这些内容应该都能直接拿去复现。数据我用的是公开的月度销量统计口径,代码基于Python的statsmodels和pmdarima,环境准备好之后基本可以照着跑。
1. 项目整体设计与方案选型思路
1.1 为什么选这三套模型而不是别的
拿到一份汽车月销量数据,第一反应可能是上LSTM或者Prophet。我一开始也试过,但后来还是回到ARIMA家族,原因很实际:数据量不大,大概一百多个月度观测点,深度学习模型在这种体量下过拟合风险极高,调参成本也大;Prophet虽然开箱即用,但对参数背后的逻辑遮蔽太多,出了问题不好排查。ARIMA系列的好处是每一阶参数都有明确的统计含义,残差诊断、显著性检验都能做,可控性强。
三套方案的分工是这样的:ARIMA作为基线,只利用销量自身的滞后信息;SARIMAX在ARIMA基础上加入季节性项和外生变量(比如月份哑变量、政策冲击标记),处理周期性波动;Auto ARIMA则用自动化搜索代替人工定阶,适合快速拿到一个可用的起点。三者不是替代关系,而是递进关系——先用Auto ARIMA探路,再用ARIMA验证,最后用SARIMAX精细化。
1.2 数据特征决定了建模策略
汽车月销量有几个绕不开的特征。第一是明显的年度周期性,每年春节前后是低谷,金九银十是高峰,这个周期长度是12个月,所以季节差分阶数D大概率取1。第二是趋势的非平稳性,长期看销量有增长也有回落,均值不恒定,需要一阶普通差分d=1。第三是外部冲击,比如购置税优惠政策调整、芯片供应波动,这些会在某几个月造成异常值,纯靠历史销量解释不了,必须引入外生变量。
我实测下来,如果忽略季节性直接上ARIMA,残差的自相关图在滞后12、24阶会明显超出置信区间,说明模型没吃干净周期信息。这也是为什么后面一定要上SARIMAX。
1.3 评估口径的确定
预测类项目最容易犯的错是只看一个指标。我同时看三个:MAE(平均绝对误差,反映平均偏差大小)、RMSE(均方根误差,对大误差更敏感)、MAPE(平均绝对百分比误差,跨量纲可比)。汽车销量基数大,MAE动辄几万辆,单看这个数字没感觉,MAPE控制在5%以内才算能用于业务参考。另外我会留出最后12个月做样本外测试,而不是随机划分——时间序列必须按时间顺序切,随机切会引入未来信息泄露。
注意:时间序列的train/test切分绝对不能shuffle,这是新手最常犯的致命错误。一旦打乱顺序,模型在训练时"看到"了未来的数据,评估结果会虚高得离谱。
2. 数据准备与平稳性处理的核心细节
2.1 数据清洗的几个隐蔽坑
原始月度销量数据看起来干净,实际上有几个隐蔽问题。第一是口径不一致,有些月份统计的是批发销量,有些是零售销量,混在一起会导致序列出现莫名其妙的跳变。第二是缺失月份,偶尔有一两个月没有记录,直接drop会让时间轴断裂,正确做法是按时间索引reindex后插值或前向填充。第三是异常值,比如某个月因为统计口径调整突然翻倍,这种点如果不处理,会把整个模型的方差带偏。
我的处理流程是:先统一为月度频率的时间索引,用pd.date_range生成完整月份序列,再reindex原数据,缺失值用线性插值补上。异常值用IQR方法识别,超过上下界的点做winsorize缩尾处理,而不是直接删除——删除会丢失时间连续性。
import pandas as pd import numpy as np # 构建完整月度索引 full_idx = pd.date_range(start='2015-01-01', end='2024-12-01', freq='MS') df = df.set_index('month').reindex(full_idx) df['sales'] = df['sales'].interpolate(method='linear') # IQR缩尾 Q1, Q3 = df['sales'].quantile([0.25, 0.75]) IQR = Q3 - Q1 lower, upper = Q1 - 1.5*IQR, Q3 + 1.5*IQR df['sales'] = df['sales'].clip(lower, upper)2.2 平稳性检验怎么做才靠谱
ARIMA的前提是序列平稳,但"平稳"不是看一眼折线图就能判断的。我一般三步走:时序图目测看趋势和周期,ADF检验判断单位根,ACF/PACF图辅助定阶。ADF检验的原假设是"存在单位根(非平稳)",p值小于0.05才能拒绝原假设,认为平稳。
实测原始销量序列的ADF p值通常在0.3以上,明显非平稳。做一阶差分后p值降到0.01以下,但ACF在滞后12阶仍有显著尖峰,说明还需要季节差分。做完d=1, D=1之后,序列基本围绕零均值波动,ADF和KPSS检验都通过。
这里有个经验:ADF和KPSS要一起看。ADF通过但KPSS不通过的情况很常见,说明序列可能是趋势平稳而非差分平稳,这时候强行差分反而会过度差分,导致MAF项不可逆。我一般以ADF为主,KPSS为辅,两者矛盾时优先相信ACF/PACF的形态。
2.3 季节周期的确认
汽车销量的季节周期是12,这个不用猜,业务逻辑上就成立。但确认方法还是要有:对去趋势后的序列做周期图(periodogram)分析,能量峰值出现在频率1/12附近,对应周期12个月。另外可以算每个月的平均销量,做成柱状图,春节月明显偏低,年末冲量月明显偏高,周期结构一目了然。
提示:如果数据跨度不足两年,季节项根本估不准。至少需要3个完整年度周期,也就是36个以上的观测点,SARIMA的季节参数才有统计意义。我这份数据有十年跨度,完全够用。
3. ARIMA基线模型的完整实现
3.1 定阶:从ACF/PACF到AIC网格搜索
ARIMA(p,d,q)的定阶,教科书说看ACF拖尾还是截尾,但实际操作中ACF/PACF图经常模棱两可,尤其是p和q都大于0的时候。我的做法是:先用ACF/PACF圈定一个大致的范围(比如p和q都在0到3之间),然后做网格搜索,用AIC或BIC选最优。
AIC偏向拟合优度,BIC对参数个数惩罚更重,样本量大时BIC更稳。我这份数据一百多个点,两个指标结论基本一致,最终选出的阶数是ARIMA(2,1,2)。这里d=1是前面平稳性检验定下来的,不参与搜索。
import itertools import warnings from statsmodels.tsa.arima.model import ARIMA warnings.filterwarnings('ignore') best_aic = np.inf best_order = None for p, q in itertools.product(range(4), range(4)): try: model = ARIMA(train, order=(p, 1, q)) result = model.fit() if result.aic < best_aic: best_aic = result.aic best_order = (p, 1, q) except Exception: continue print(f"最优阶数: {best_order}, AIC: {best_aic:.2f}")3.2 残差诊断不能省
模型拟合完,summary()里一堆数字,但真正决定模型能不能用的是残差。残差必须是白噪声:均值接近零、方差恒定、无自相关。我用Ljung-Box检验看残差是否还有自相关,p值大于0.05才合格。另外画残差的ACF图,所有滞后阶的柱子都应该落在置信区间内。
我第一次跑ARIMA(2,1,2)时,Ljung-Box在滞后12阶的p值是0.03,说明残差里还残留季节性自相关,模型没吃干净。这就是必须上SARIMAX的直接证据。
3.3 基线预测与误差评估
用ARIMA(2,1,2)对最后12个月做样本外预测,MAPE大概在8%到10%之间。这个精度对于有强季节性的汽车销量来说只能算及格,因为模型完全没利用季节信息,预测曲线是一条相对平滑的线,抓不住金九银十的尖峰。但它的价值在于提供了一个基准线——后面SARIMAX如果比它好不了多少,说明外生变量没起作用。
forecast = result.get_forecast(steps=12) pred = forecast.predicted_mean conf_int = forecast.conf_int() mape = np.mean(np.abs((test - pred) / test)) * 100 print(f"ARIMA基线 MAPE: {mape:.2f}%")4. SARIMAX引入季节项与外生变量
4.1 季节参数的确定
SARIMAX的阶数是(p,d,q)(P,D,Q,s),其中s=12。D前面已经确定是1,P和Q的搜索范围一般取0到2就够了,因为季节项阶数太高容易过拟合。我同样用网格搜索,但这次搜索空间是p,q,P,Q四个维度的组合,计算量大了不少,所以先用Auto ARIMA缩小范围再精搜。
最终选出的模型是SARIMAX(1,1,1)(1,1,1,12)。这个结构意味着:非季节部分有一个AR项和一个MA项,季节部分也有一个AR和一个MA项,配合一阶普通差分和一阶季节差分。参数数量适中,AIC比纯ARIMA低了一大截。
4.2 外生变量的构造与选择
外生变量是SARIMAX相对ARIMA的核心增量。我加了这么几类:
- 月份哑变量:11个0/1变量(避免共线性去掉一个基准月),捕捉每个月的固定效应。
- 政策冲击标记:在购置税优惠调整的月份标1,其余为0。这类事件对销量有即时拉动或透支效应。
- 春节移动标记:春节在1月还是2月对当月销量影响巨大,用一个变量标记春节所在月份。
外生变量不是加得越多越好。我试过把季度GDP增速也加进去,结果系数不显著,还引入了额外噪声。判断标准是看summary()里每个外生变量的p值,不显著的果断删掉。最终保留的是月份哑变量和春节标记,政策标记因为样本内只出现两次,统计功效不足,也去掉了。
from statsmodels.tsa.statespace.sarimax import SARIMAX exog_train = pd.get_dummies(train.index.month, prefix='m', drop_first=True).astype(float) exog_train['spring_festival'] = spring_flag.values model = SARIMAX(train, exog=exog_train, order=(1,1,1), seasonal_order=(1,1,1,12), enforce_stationarity=False, enforce_invertibility=False) result = model.fit(disp=False) print(result.summary())4.3 预测时外生变量的对齐问题
这是SARIMAX实操中最容易翻车的地方。预测未来12个月时,你必须提供对应的外生变量矩阵,而且列的顺序、名称必须和训练时完全一致。月份哑变量可以按预测月份直接生成,春节标记需要查未来年份的春节日期。如果外生变量对不齐,statsmodels会直接报错或者给出完全错误的预测。
我的做法是把外生变量的构造封装成一个函数,训练和预测都调用同一个函数,保证逻辑一致。预测期的外生变量用pd.get_dummies生成后,用reindex对齐训练期的列,缺失的列补0。
注意:
enforce_stationarity=False和enforce_invertibility=False这两个参数在statsmodels里建议打开(设为False),否则优化器可能因为参数边界问题报错。这不是让模型不平稳,而是放宽优化时的约束,实际拟合出的参数通常仍在平稳域内。
4.4 SARIMAX的预测效果
加上季节项和外生变量后,MAPE从基线的9%左右降到了5%以内,预测曲线能明显看出年度周期的波峰波谷。残差的Ljung-Box检验在滞后12和24阶的p值都大于0.05,说明季节性自相关被吃干净了。置信区间也比ARIMA窄,说明模型对自身预测更有信心。
不过SARIMAX也有代价:参数多了,拟合时间变长,而且对外生变量的质量很敏感。如果某个外生变量在预测期取值不确定(比如政策是否延续),预测结果就会有额外的不确定性,这部分不确定性标准置信区间是覆盖不到的。
5. Auto ARIMA自动化定阶实战
5.1 Auto ARIMA的工作原理
Auto ARIMA(pmdarima库)本质上是把前面手工做的网格搜索自动化了,但更聪明。它用Hyndman-Khandakar算法:先通过KPSS检验确定差分阶数d,然后在给定d下用逐步搜索(stepwise search)遍历(p,q)和(P,Q)的组合,每一步都基于AICc(小样本校正的AIC)判断是否接受新模型。相比暴力网格,逐步搜索能大幅减少拟合次数。
它的优势是快、省心,适合在项目初期快速拿到一个可用的模型。劣势是它默认只优化AICc,不关心残差诊断,也不自动加外生变量。所以我的用法是:用Auto ARIMA定阶,然后把阶数喂给SARIMAX,再手工加外生变量。
5.2 关键参数配置
auto_arima有几个参数必须显式设置,否则结果可能不符合预期:
seasonal=True:开启季节项搜索,否则它只搜非季节部分。m=12:季节周期,汽车销量必须设12。stepwise=True:用逐步搜索,比stepwise=False的全网格快很多。suppress_warnings=True:屏蔽收敛警告,不然输出刷屏。trace=True:打印搜索过程,方便看它试了哪些模型。information_criterion='aicc':小样本用AICc,样本量大可以用BIC。
import pmdarima as pm auto_model = pm.auto_arima( train, seasonal=True, m=12, stepwise=True, suppress_warnings=True, trace=True, information_criterion='aicc', max_p=3, max_q=3, max_P=2, max_Q=2, d=1, D=1 ) print(auto_model.order, auto_model.seasonal_order)5.3 Auto ARIMA与手工定阶的对比
我这份数据上,Auto ARIMA选出的阶数是(1,1,1)(1,1,1,12),和我手工网格搜索的结果完全一致。这验证了手工流程的可靠性,也说明Auto ARIMA在中等规模数据上确实能省事。但我也遇到过它选出的模型残差诊断不过关的情况——AICc最优不等于残差最白,这时候还是得回到手工流程做诊断。
另外Auto ARIMA的d和D是自动确定的,有时候它选的差分阶数和我基于业务判断的不一样。比如它可能选d=0,因为KPSS检验通过了,但从业务上看销量明显有趋势。这种时候我会用d=1, D=1强制覆盖,不让它自动选。
5.4 用Auto ARIMA做滚动预测
Auto ARIMA还有一个实用场景是滚动预测:每次预测一个月,把真实值加回训练集,重新拟合再预测下一个月。这样能动态更新模型参数,适应最新的数据模式。代价是计算量大,12个月的滚动预测要拟合12次。我实测下来,滚动预测的MAPE比一次性预测12个月低1到2个百分点,对于精度要求高的场景值得做。
history = list(train) predictions = [] for t in range(12): model = pm.auto_arima(history, seasonal=True, m=12, suppress_warnings=True, d=1, D=1) yhat = model.predict(n_periods=1)[0] predictions.append(yhat) history.append(test[t])6. 常见问题与排查技巧实录
6.1 模型不收敛怎么办
SARIMAX拟合时最常见的报错是"Maximum Likelihood optimization failed to converge"。原因通常有三个:参数初值不好、数据尺度太大、模型阶数过高。我的排查顺序是:先把销量数据除以10000缩小尺度,再检查阶数是不是设太高(比如p或q超过3),最后尝试换优化器(method='nm'用Nelder-Mead,比默认的BFGS更稳但更慢)。
如果还是不收敛,把enforce_stationarity和enforce_invertibility都设为False,通常能解决。这两个参数放宽的是优化过程中的参数约束,不影响最终模型的有效性。
6.2 预测结果是一条直线
这是新手最常遇到的困惑:预测出来所有月份的值几乎一样。根本原因是模型没有捕捉到季节性,要么是季节阶数P、Q都设成了0,要么是D没设成1。检查seasonal_order的第四个参数是不是12,第三个参数D是不是1。另外如果数据本身季节性很弱,模型确实会给出接近均值的预测,这时候要回到数据层面确认季节性是否真实存在。
6.3 外生变量导致的维度错误
SARIMAX预测时报"exog shape mismatch",几乎都是预测期外生变量的列数和训练期不一致。排查方法:打印exog_train.shape和exog_test.shape,对比列数。常见原因是get_dummies在预测期生成的月份类别少于训练期(比如预测期只有12个月,但训练期有120个月,月份类别都是12个,一般不会少),或者春节标记的列名不一致。用reindex(columns=exog_train.columns, fill_value=0)强制对齐最稳妥。
6.4 常见问题速查表
| 问题现象 | 可能原因 | 解决方法 |
|---|---|---|
| 模型不收敛 | 数据尺度过大、阶数过高 | 数据除以10000、降低p/q、设enforce参数为False |
| 预测为直线 | 季节项缺失 | 检查seasonal_order的s=12、D=1 |
| 残差有自相关 | 阶数不足 | 看ACF图,提高p或q,或加季节项 |
| 外生变量维度错误 | 预测期列不一致 | reindex对齐列,缺失补0 |
| MAPE异常高 | 训练集含异常值 | IQR缩尾或对数变换 |
| 置信区间过宽 | 模型不确定性大 | 增加数据量或简化模型 |
6.5 几个文档里不会写的经验
第一,对数变换要谨慎。销量数据方差随均值增大时,取对数能稳定方差,但预测后要记得用np.exp还原,而且还原后的预测是有偏的(Jensen不等式),严格来说需要加方差校正项。我一般只在方差明显异质时才用对数。
第二,AIC不是唯一标准。两个模型AIC差2以内,统计上没本质区别,这时候选参数少的那个,泛化更稳。
第三,预测期越长越不准。12个月的预测,前3个月通常还行,后3个月基本只能看趋势。如果业务只需要短期预测,别硬做12个月。
第四,保存模型用pickle。SARIMAX的Results对象可以直接pickle,下次加载不用重新拟合,省时间。但要注意statsmodels版本兼容性,跨版本加载可能报错。
import pickle with open('sarimax_model.pkl', 'wb') as f: pickle.dump(result, f) with open('sarimax_model.pkl', 'rb') as f: loaded = pickle.load(f)这套流程我反复跑过好几轮,从数据清洗到最终预测,最耗时的不是建模本身,而是外生变量的构造和残差诊断的反复迭代。ARIMA给基线,SARIMAX提精度,Auto ARIMA省定阶的力气,三者配合能把汽车月销量预测做到MAPE 5%以内的可用水平。真正上手之后你会发现,参数怎么调是次要的,把数据的季节结构和外部冲击搞清楚才是决定预测质量的关键。