ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

ARIMAX多变量时间序列预测实战:基于statsmodels的Python实现与坑点解析

ARIMAX多变量时间序列预测实战:基于statsmodels的Python实现与坑点解析 简介基于ARIMAX的多变量预测模型Python源码与配套数据集是一份面向毕业设计、课程设计与期末大作业的完整项目资料尤其适合计算机相关专业、需要多变量时间序列预测实战代码的学生。项目由导师指导并获评99分代码完整可运行小白也能按说明快速上手。压缩包共8个文件包括2个Python脚本datapre.py负责数据预处理arimax.py负责模型构建与预测、2个CSV数据集、2张预测效果展示图、1个README说明文档及1个环境配置文件压缩后仅148KB结构简洁既便于阅读源码也方便替换数据做二次开发。借助这些文件学习者可以完整理解ARIMAX从数据载入、预处理、建模到预测输出的流程同时结合效果图与说明文档便于在课程报告或答辩中展示项目亮点。已有55人学习下载适合作为多变量预测类毕业设计或期末大作业的参考模板。1. ARIMAX 的多变量预测从一次“单变量失灵”说起业务方递来一张表过去三年的日销售额、当日平均气温、是否促销、是否节假日。需求只有一句话——把明天、下周的销售额预测出来。直接用 ARIMA 拟合销售额残差里全是促销和节假日的影子预测值在促销日被系统性低估。换成随机森林短期波动跟得很好但预测步长一拉长时序自相关那部分又被学成了一堆滞后特征的线性叠加。ARIMAX 刚好卡在这两类方法中间保留 ARIMA 对自相关结构的建模能力同时允许外部变量以回归项的形式进入模型。所谓“多变量”指的不是把几千个特征塞进去而是外生变量可以同时存在多个且每个外生变量都有自己的回归系数。这篇文章按从业者会走的完整路径展开先用 statsmodels 把最小可用的 ARIMAX python 源码跑通再处理数据集的对齐与清洗接着解决 pdq 定阶和外生变量取舍最后落到预测阶段外生变量未来值从哪来这个最容易被忽略的坑上。偏向实操代码可以直接复制进 Jupyter 或脚本跑。2. 用 statsmodels 跑通 ARIMAXpython 源码的最小实现2.1 先明确一个容易混淆的点statsmodels 里没有 ARIMAX 类ARIMAX 的完整称谓是 ARIMA with eXogenous variables即带外生变量的 ARIMA。statsmodels 没有单独提供ARIMAX这个类它把外生变量支持做进了SARIMAX。SARIMAX的 S 指季节性当seasonal_order参数留空时它就是标准的 ARIMAX。很多人搜到SARIMAX以为必须处理季节项其实不传seasonal_order即可。模型的形式可以拆成三层来看差分后的内生部分负责捕捉被预测序列自身的惯性x1、x2…这些外生变量以回归项的方式叠加残差则要求是白噪声。写成数学式子就是Δ^d y_t φ1·Δ^d y_{t-1} … θ1·ε_{t-1} … β1·x1_t β2·x2_t ε_t注意外生项不参与差分它们在差分后的方程里直接以水平值进入。这带来一个实际含义外生变量不需要与被预测序列同阶差分但在后续解读系数时β 表示的是“差分后 y 对外生变量的边际响应”口径要统一。2.2 最小可运行源码模拟数据 拟合 预测先构造一份带两个外生变量的数据集保证逻辑可复现import numpy as np import pandas as pd from statsmodels.tsa.statespace.sarimax import SARIMAX from sklearn.metrics import mean_absolute_error # 生成 500 天演示数据日期从 2022-01-01 起 np.random.seed(42) n 500 date_range pd.date_range(2022-01-01, periodsn, freqD) # 外生变量气温正弦波动 噪声、促销标记0/1 temperature 20 10 * np.sin(np.arange(n) / 40) np.random.normal(0, 1, n) promotion np.where(np.random.rand(n) 0.9, 1, 0) # 内生序列受自身滞后项、气温和促销影响 y np.zeros(n) for t in range(2, n): y[t] 0.6 * y[t-1] - 0.2 * y[t-2] 0.5 * temperature[t] 3.0 * promotion[t] np.random.normal(0, 1) y y 100 # 加一个水平项便于展示 df pd.DataFrame({ sales: y[2:], temperature: temperature[2:], promotion: promotion[2:], }, indexdate_range[2:]) # 切分前 80% 训练后 20% 测试 train df.iloc[:int(len(df)*0.8)] test df.iloc[int(len(df)*0.8):] # 拟合order(2,0,0)两个外生变量同时进入 model SARIMAX( train[sales], exogtrain[[temperature, promotion]], order(2, 0, 0), enforce_stationarityFalse, enforce_invertibilityFalse, ) res model.fit(dispFalse) print(res.summary())拟合结果里重点看两个位置第一是x1/temperature、x2/promotion对应的系数及其 p 值第二是sigma2即残差方差。上面模拟数据里 promotion 的系数应该接近 3temperature 的系数接近 0.5与生成规则一致。如果某个外生变量的 p 值高于 0.05说明它对预测没有显著边际贡献后续考虑剔除。预测测试集时必须同时提供未来时段的外生变量值否则 statsmodels 会报ValueError# 预测后 20% 时段外生变量用测试集的真实值 forecast res.get_forecast( stepslen(test), exogtest[[temperature, promotion]], ) pred_mean forecast.predicted_mean print(MAE:, mean_absolute_error(test[sales], pred_mean))2.3 fit 参数里 4 个值得留意的开关dispFalse用来关掉迭代日志新版 statsmodels 里该参数已标注弃用但仍有兼容性我一般会同时配合warnings.filterwarnings(ignore)用。enforce_stationarity和enforce_invertibility默认是 True如果拟合时出现Non-stationary starting autoregressive parameters这类警告就要检查差分阶数 d 是否足够。cov_typeopg是另一个可选参数当样本量不足 200 时可以用它获得更稳健的系数标准差代价是计算慢一些。method默认走 L-BFGS一般不需要改。fit 参数默认值建议调整时机dispTrue自动脚本里改为 Falseenforce_stationarityTrue报非平稳起始参数时先检查 d再考虑置 Falseenforce_invertibilityTrue同上针对 MA 部分cov_typeapprox小样本可换 opg 验证系数显著性3. 数据集构建喂给 ARIMAX 前必须处理干净的 4 个问题3.1 数据集结构设计目标列与外生变量列分开ARIMAX 的数据集结构比普通回归严格内生序列必须是一维的外生变量必须是二维 DataFrame 或 ndarray两者行数要严格相等且索引要能对齐。演示数据集可以直接用 2.2 节的生成逻辑构造真实业务则通常从数据库取数组成宽表# 典型宽表示例date 列转索引其余列全部为数值型 df pd.read_csv(sales_with_exog.csv, parse_dates[date]) df df.set_index(date).asfreq(D) # 目标列必须无缺失 y df[sales] # 外生变量矩阵全部数值型不能有 object 列 X df[[temperature, promotion, holiday]] X X.astype(float)asfreq(D)这一步承担两个作用显式定义时间频率让缺失日期暴露成 NaN同时让 statsmodels 内部能够推断季节周期。如果不指定频率SARIMAX 会警告A date index has been provided, but it has no associated frequency information并默认按无季节处理。3.2 三种缺失值场景不同的处理方式ARIMAX 对缺失值没有自动填充机制所有缺失最终都会传导成似然估计失败。按经验目标列的缺失不要用均值填充而是用前向填充加插值组合外生变量的缺失要看业务含义——促销这种布尔量缺失等价于“没发生”填充 0 即可气温这种连续量使用线性插值更合理df[sales] df[sales].interpolate(methodlinear) df[promotion] df[promotion].fillna(0) df[temperature] df[temperature].interpolate(methodtime)外生变量的缺失逻辑以业务判断为主统计方法为辅。例如节假日本就应该预先生成完整的 0/1 序列不该留缺失。插值顺序建议先处理日期索引再补外生变量最后处理目标列避免差分时引入伪造的台阶。3.3 外生变量不能有“未来泄露”这是数据集切分时最容易被忽略的一点。常规机器学习要求训练集和测试集按行切分时序模型还要额外保证测试集的外生变量在真实预测场景里必须是能够提前拿到的。促销日期、节假日表属于这一类当日气温如果用于预测明天就属于未来泄露。外生变量类型预测明天的实际可用性能否入模型节假日标记已知可以促销计划业务排期已知可以当日天气实测明天未知不可天气数值预报预报可得可以但需注明来源数据集中带有“当日实测气象”这类列时这才是真正的变量之一。是否启用取决于是做回测还是上线预测回测可以用真实值上线必须切换成预报值两者误差会直接反映到预测偏差里。3.4 训练测试切分禁止随机打散train_size int(len(df) * 0.8) train, test df.iloc[:train_size], df.iloc[train_size:]代码本身很短但这里强调顺序切分背后的原因ARIMAX 的滞后项依赖时间结构随机采样会破坏时间连续性让模型学到的滞后关系在真实预测时失效。切分时还要检查是否存在“中间断档”比如训练集最后一周到测试集第一周之间隔了长假那测出来的误差会被低估。4. 参数定阶与外生变量取舍pdq 网格搜索加显著性检验4.1 先用 ACF/PACF 判断 d再对定阶后的残差看 p、qARIMAX 的定阶可以分两步走第一步先定差分数 d。对目标序列做 ADF 检验p 值大于 0.05 就做一阶差分再检验一次直到平稳。注意外生变量不需要参与这个检验它们不进滞后结构。from statsmodels.tsa.stattools import adfuller for d in range(3): adf adfuller(df[sales].diff(d).dropna()) print(fd{d}, p-value{adf[1]:.4f}) if adf[1] 0.05: break确定 d 之后对差分序列画 ACF 和 PACF 图判断 p 和 q 的候选范围PACF 在滞后 p 后截尾ACF 在滞后 q 后截尾。这只是粗筛最后以信息准则为准。4.2 网格搜索 AIC 的最小代码定阶网格搜索是常见的可靠做法。把 p/q 限制在 03、d 固定为上面检验出的值遍历组合拟合 ARIMAX记录 AIC/BICimport itertools import warnings warnings.filterwarnings(ignore) results [] for p, q in itertools.product(range(4), range(4)): try: model SARIMAX( train[sales], exogtrain[[temperature, promotion]], order(p, 0, q), enforce_stationarityFalse, enforce_invertibilityFalse, ) r model.fit(dispFalse) results.append((p, q, r.aic)) except Exception: continue results.sort(keylambda x: x[2]) print(results[:5])加 try/except 是必要的部分 p、q 组合会因数值奇异导致拟合失败抛异常会让整个搜索中断。AIC 最小的组合未必业务上最优规则的参考是 AIC 差值小于 2 时选更简单的组合参数更少。判断项方法说明AIC 最小网格搜索防止过拟合选择复杂度适中的模型外生变量 p 值model.summary()不显著时剔除减少噪声残差白噪声Ljung-Box 检验p 值大于 0.05 才说明信息提取充分4.3 外生变量的选择不能只靠相关性外生变量进入模型前先用相关性初筛有用但不要把它当充分条件。气温和冰淇淋销量有相关性加上促销后显著性可能消失这时保留两个会稀释系数估计。更稳妥的做法是把候选外生变量逐个单独加进模型看 AIC 是否下降、系数 p 值是否显著再两两组合加入看是否有共线性问题。from statsmodels.stats.outliers_influence import variance_inflation_factor exog_vars [temperature, promotion] X_exog train[exog_vars].values for i, col in enumerate(exog_vars): vif variance_inflation_factor(X_exog, i) print(f{col}: VIF{vif:.2f})VIF 大于 5 时两个外生变量之间存在较强共线性其中一个大概率要被剔除。实操中促销和节假日常常高度重叠这种情况我会保留更“可计划”的那个把另一个拆成交叉项而不是同时作为独立外生变量塞进模型。4.4 诊断阶段加一个对照实验ARIMA vs ARIMAX想要说服业务方或自己最好直接做对照。在同样的切分下拟合一个不带 exog 的纯 ARIMA然后对比两者测试集的 MAEmodel_arima SARIMAX(train[sales], order(p, 0, q)) res_arima model_arima.fit(dispFalse) fc_arima res_arima.get_forecast(stepslen(test)) mae_arima mean_absolute_error(test[sales], fc_arima.predicted_mean) print(fARIMA MAE: {mae_arima:.4f}) print(fARIMAX MAE: {mae_arima_arimax:.4f})如果 ARIMAX 的误差没有明显下降说明外生变量带来的方差解释能力小于它引入的参数不确定性。这在样本量小于 100 时尤其常见系数增多导致估计方差上升抵消掉外生变量的解释力。结果不理想时先检查外生变量是否选得有意义别急着调 p/q。5. 预测阶段最隐蔽的坑外生变量未来值从哪里来5.1 get_forecast 与 predict 的差异get_forecast要求传入完整的外生变量矩阵行数与steps一致predict则根据索引自动匹配。预测多步时一个常见错误是传了steps14但外生变量只给了 7 行statsmodels 会直接报维度不匹配。另一个更隐蔽的错误是传了start和end但外生变量起始时间对不上结果静默错位。5.2 三类稳定的外生变量未来值来源第一类日历型变量。节假日、工作日、月末这类变量可以直接从日历表生成未来任意长度的序列这是最可靠的外生变量来源。第二类业务计划型。促销排期、营销活动时段从排期表里取注意生成时要做成 0/1 或数值阶梯而不是往明细表里插值。第三类预测型。气温、油价这类外部变量需要用另一个模型先预测出来再喂给 ARIMAX。这里要明确外生变量的预测误差会传导进主模型多步预测时误差会累积放大所以步长越长越要谨慎。5.3 滚动预测是缓解误差累积的具体技巧history_y train[sales].values history_X train[[temperature, promotion]].values preds [] for i in range(len(test)): model SARIMAX(history_y, exoghistory_X, order(p, 0, q)) res model.fit(dispFalse) yhat res.get_forecast(steps1, exogtest[[temperature, promotion]].iloc[[i]]) preds.append(yhat.predicted_mean.iloc[0]) # 用真实观测滚动更新训练集避免误差累积 history_y np.append(history_y, test[sales].iloc[i]) history_X np.vstack([history_X, test[[temperature, promotion]].iloc[i].values])循环里每次只预测一步然后立刻把真实值拼回训练集重新拟合。这种做法的代价是耗时显著增加100 个测试点就要拟合 100 次但对比一次性多步预测滚动方式能直观看到模型在短期自适应上的表现。实际使用时如果时间不够可以先做一次性预测确认 MAE 异常后再抽样若干时间点做滚动复核帮助判断误差主要来自外生变量预测还是模型本身的衰减。本文还有配套的精品资源点击获取
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进