简介:基于ARIMAX的多变量预测模型源码与配套数据,面向具备一定Python与统计基础的数据分析学习者,用于在销量预测、经济指标分析等场景中掌握带外生变量的时间序列建模方法。ARIMAX模型在经典时间序列模型基础上引入外生变量,能够同时利用目标序列自身历史和相关影响因素的信息。包内共7个文件,包含2个Python脚本、2个CSV数据集、2张可视化图片和1份Markdown说明文档:一个脚本负责数据预处理与特征构造,另一个完成模型训练、预测与结果可视化;CSV文件提供可直接复现的实验数据,图片展示实际值与预测值的对比,说明文档则整理了从数据清洗、平稳性检验、参数确定到预测评估的完整流程。整套资源仅146KB,代码注释详细,便于快速理解与二次修改。已有408人学习下载,非常适合课程设计、论文实验以及实际业务中多变量预测任务的参考,读者可直接运行并迁移到自己的数据上。
1. 基于ARIMAX的多变量预测模型 python 源码+数据集 zip:先看懂它解决的问题再动手
拿到一个基于ARIMAX的多变量预测模型 python 源码+数据集 zip,很多人的第一反应是解压、装依赖、跑通脚本,然后看一眼预测曲线就收工。但做预测最怕的就是这种“跑通了但没想明白”的状态。ARIMAX 是带外生变量的自回归积分滑动平均模型,本质是在经典 ARIMA 的基础上加入一条外部输入通道,让销量、流量、电力负荷这类时间序列,能同时利用促销活动、天气、价格等外部因子做多变量预测。这个 zip 的价值在于它给了你一个可以直接改的骨架,但外生变量未来值从哪来、数据怎么对齐、p/d/q 怎么定,才是决定项目成败的地方。这篇笔记按源码落地路径拆解,覆盖模型原理、数据格式、最小可运行脚本、参数整定和踩坑记录,新手能跟着复现,熟手能直接对照边界条件。
2. ARIMAX 模型结构:外生变量怎么进模型,统计口径要对齐
2.1 单变量 ARIMA 的局限:为什么需要外部变量通道
ARIMA(p,d,q) 只吃目标变量自身的时间序列历史。比如你预测一款商品的日销量,ARIMA 能捕捉到的只有销量序列内部的趋势、季节性和自相关性。但实际业务里,销量的波动经常是被外部事件驱动的:大促当天销量翻倍、连续雨天外卖单量上升、调价后转化率变化。这些信息不在目标序列的历史里,纯 ARIMA 只能把它们当作不可解释的残差。
ARIMAX 解决的就是这个问题:在 ARIMA 的框架里加一条外生变量通道,把促销标记、天气、价格这些外部因子作为解释变量一起建模。这里的“多变量预测”不是说把多个目标变量放进一个模型同时预测,而是指“一个目标变量 + 多个外生变量”的联合建模方式,这个区别很重要,很多初学者在第一次看到“多变量”这个词时会理解偏。
| 对比项 | ARIMA(p,d,q) | ARIMAX(p,d,q)+exog |
|---|---|---|
| 输入数据 | 仅单变量历史序列 | 单变量目标 + 外生变量矩阵 |
| 可解释性 | 只能解释自身历史模式 | 可以量化外部因素影响 |
| 预测场景 | 无外部干预的纯序列外推 | 有促销、天气、价格等外部干预 |
| 未来输入要求 | 只需要目标历史 | 还需要外生变量的未来值 |
做一个业务预测项目时,我会先问一个问题:未来这段时间,有什么已知但尚未发生的外部事件会影响目标?如果有,就用 ARIMAX;如果没有,ARIMA 就够了。硬给 ARIMA 加外生变量,只会增加数据准备成本和共线性风险,不会带来精度提升。
2.2 exog 进模型的数学位置:不是把 X 简单丢进回归
statsmodels 是 Python 里最常用的时间序列建模库,源码包里的建模部分几乎都是基于它写的。在 statsmodels 中,带外生变量的 ARIMA 模型写成ARIMA(endog, exog=exog, order=(p,d,q)),它实际拟合的是这样一结构:
y_t = X_t β + η_t
其中 η_t 是 ARIMA(p,d,q) 过程。也就是说,模型先把外生变量的线性回归项拿掉,再对剩下的残差部分用 ARIMA 做时序建模。严格来说这是“带 ARIMA 误差的回归模型”,和教科书里那种把外生变量直接塞进 ARIMA 多项式的写法略有差别,但在工程实践中大家都叫它 ARIMAX,源码包一般也采用 statsmodels 这套实现。
这种实现方式有个直接好处:外生变量的系数 β 可以直接解读。β 的含义是“外生变量每变化一个单位,目标变量平均变化多少”,显著性看 summary 输出里的 P>|z| 列。这个解释口径对业务方很有价值,比如促销标记系数是 200,意味着有促销的日子销量平均高出 200 件。
需要注意一个细节:当你设置 d=1 做一阶差分时,statsmodels 会对目标变量和外生变量同时做差分处理,然后在这组差分序列上估计 β。这时候 β 的解释就不再是“X 升高一单位 Y 升高多少”,而是“X 的变化量升高一单位,Y 的变化量升高多少”。我在给业务方汇报系数时吃过这个亏,后来养成习惯:先看模型是否做了差分,再决定怎么解释系数。
2.3 zip 源码包里的常见结构:数据、模型、说明三层
以我接触过的大多数时间序列源码包来看,结构基本是三段式:数据层、模型层、说明层。数据层是一个或多个 CSV 文件,至少包含时间列、目标列、外生变量列;模型层是 Python 脚本,负责读数据、切训练测试集、拟合、预测、画图;说明层是数据字典或 README,描述每个字段的含义、单位、时间跨度、缺失值情况。
这个 zip 的“数据集释”指的就是说明层,实际项目里这层往往比代码本身更重要。我见过很多项目换个人接手后,代码能跑,但没人说得清某列是毫米还是英寸、是累计值还是瞬时值。所以拿到源码包第一件事不是跑代码,而是把数据字典读一遍,确认目标列和外生变量的业务含义。
依赖方面,核心库一般就四个:pandas 做数据操作,statsmodels 做 ARIMAX 拟合,numpy 做数值计算,matplotlib 画图。如果你的环境是纯基础 Python,没有 pandas 和 statsmodels,需要先补齐这些依赖再跑源码。下一章我会把完整的环境准备和数据预处理步骤拆开讲。
3. 照源码包跑通最小 ARIMAX 示例:环境、数据格式与预测脚本
3.1 Python 环境准备与依赖安装
源码包的运行环境要求不苛刻,Python 3.8 到 3.10 之间基本都能跑。我建议你不要直接把依赖装进系统 Python,而是建一个独立的虚拟环境,避免把其他项目搞坏。在命令行里执行:
python -m venv venv在 Windows 上激活环境是venv\Scripts\activate,在 Linux 或 macOS 上是source venv/bin/activate。激活后命令行前面会出现(venv)前缀。如果本机还没有安装 Python,先去 python 官方网站下载对应操作系统的安装包,安装时勾选“Add Python to PATH”,不然后面python命令会提示找不到。
接下来安装依赖:
pip install pandas numpy statsmodels matplotlib装完后可以验证一下核心包版本:
python -c "import statsmodels, pandas, numpy; print(statsmodels.__version__, pandas.__version__, numpy.__version__)"statsmodels 从 0.13 开始把ARIMA类放在了statsmodels.tsa.arima.model模块里,早期在statsmodels.tsa.arima_model,两个接口在参数名上有差异。源码包如果是新写的,一般用的是新接口,也就是我下面演示的这种。如果你在 vscode 里跑代码之前,先确认右下角选择的解释器是你刚建好的 venv 环境,不然会一直提示找不到 statsmodels。
3.2 数据集格式与预处理
ARIMAX 要求数据是严格的按时间排序的表格,不能有随机顺序。典型的数据集长这样:
| date | sales | promo | temp |
|---|---|---|---|
| 2024-01-01 | 320 | 0 | 2.1 |
| 2024-01-02 | 410 | 1 | -1.3 |
| 2024-01-03 | 280 | 0 | -2.5 |
date 是时间索引,sales 是目标变量,promo 是促销标记(0/1),temp 是天气外生变量。实际项目里外生变量可能更多,但格式规律是统一的:每一列必须是一个数值序列,长度和时间索引严格对齐,不允许有空行错位。
预处理步骤我一般用这段代码:
import pandas as pd # 读取数据集,encoding 用 utf-8-sig 兼容 Windows 下 Excel 导出的 BOM 头 df = pd.read_csv("arimax_dataset.csv", encoding="utf-8-sig") # 时间列转成 pandas 的 datetime 类型,并设为索引 df["date"] = pd.to_datetime(df["date"]) df = df.sort_values("date").set_index("date") # 先排序再填充缺失值,防止 ffill 用错顺序 df = df.fillna(method="ffill") # 确认没有类型异常列 print(df.dtypes)这段代码做了三件事:第一,把 date 列从字符串转成 datetime,并设为行索引,这是 statsmodels 识别时间序列结构的前提;第二,按时间升序排序,这是时间序列分析区别于普通机器学习的最基本要求,一旦乱序,所有滞后期计算都是错的;第三,用前向填充处理缺失值,时序数据通常不适合用均值填充,因为均值会抹掉相邻时点的连续趋势。
需要注意fillna(method="ffill")在 pandas 2.x 里会提示改用df.ffill(),功能一样,遇到 warning 直接换写即可。如果目标变量本身有大量缺失,前向填充会让模型误以为缺失期间数值不变,这种情形建议直接删掉那段区间。
3.3 最小可运行脚本:从数据加载到预测出图
这是整个源码包最核心的一段,我拆成完整脚本。假设你已经把数据准备好,列名是sales、promo、temp:
import pandas as pd from statsmodels.tsa.arima.model import ARIMA from sklearn.metrics import mean_absolute_error import matplotlib.pyplot as plt # 1. 读取数据并排序 df = pd.read_csv("arimax_dataset.csv", parse_dates=["date"]) df = df.sort_values("date").set_index("date") # 2. 指定目标列和外生变量列 y = df["sales"] X = df[["promo", "temp"]] # 3. 按时间顺序切分训练集和测试集,不做随机打散 train_size = int(len(df) * 0.8) y_train, y_test = y.iloc[:train_size], y.iloc[train_size:] X_train, X_test = X.iloc[:train_size], X.iloc[train_size:] # 4. 拟合 ARIMAX 模型,order 里的三个数分别是 p, d, q model = ARIMA(y_train, exog=X_train, order=(2, 1, 2)) result = model.fit() print(result.summary()) # 5. 多步预测,预测 20 步,必须有对应的未来外生变量 steps = len(y_test) pred = result.forecast(steps=steps, exog=X_test) # 6. 评估预测误差 mae = mean_absolute_error(y_test, pred) print(f"Test MAE: {mae:.3f}") # 7. 画图对比 plt.figure(figsize=(10, 4)) plt.plot(y_test.index, y_test.values, label="actual") plt.plot(y_test.index, pred.values, label="forecast") plt.legend() plt.title("ARIMAX Forecast Result") plt.show()代码结构按六层拆开。第 3 步切分时用:train_size这种纯位置切片,而不是随机抽样,是因为时间序列一旦 shuffle,训练集和测试集之间的时序依赖就被破坏了,模型等于偷看了未来的部分信息。第 4 步order=(2,1,2)是超参数元组,第一个数 2 是自回归阶数 p,第二个数 1 是差分阶数 d,第三个数 2 是移动平均阶数 q,这三个值的含义在下一章展开,现在先用经验值跑通。第 5 步是 ARIMAX 和 ARIMA 使用差异最大的地方:forecast()必须指定exog,且X_test的行数必须等于steps,行索引不要求连续,但列名必须和训练时完全一致。
result.forecast()返回的是一个 Series,索引是预测时点,值和y_test对齐后直接喂给mean_absolute_error算误差。如果这里报错说长度不匹配,大概率是steps和len(y_test)不一致,或者X_test里有 NaN。
3.4 第一次跑通之后:先看这四行输出
跑通后不要急着调参,先看print(result.summary())的输出。重点看四个地方:第一,P>|z|列,外生变量 promo 和 temp 对应的 p 值,如果大于 0.05,说明这个外生变量在统计上不显著,可以考虑去掉;第二,AIC 和 BIC 两个信息准则值,后面网格搜索会用它比较模型;第三,Ljung-Box 检验的 p 值,如果小于 0.05 说明残差还有自相关,模型没把信息抽干净;第四,看coef列里外生变量的符号是否符合业务直觉,促销系数是负数就得警惕数据出问题了。
如果拟合过程中报了"Non-stationary starting autoregressive parameters"之类的警告,不用慌,先尝试把 p 和 q 调小,或者检查你需要不需要差分。有时候强行用高阶 AR 项拟合一个近乎随机的序列,会触发数值不稳定警告。
4. ARIMAX 参数整定:用 AIC 网格搜索确定 p、d、q 与外生变量组合
4.1 平稳性检验决定 d 值:先用 ADF 判断要不要差分
ARIMA 框架对数据有个基本要求:建模的序列需要是平稳的,也就是均值和方差不随时间变化。实际业务数据大多不平稳,销量有逐年上涨趋势,温度有年周期,所以需要差分。d 就是差分阶数,d=1 表示对原序列做一次一阶差分,d=2 表示再差分一次。
判断差分阶数不能靠肉眼观察,一般用 Augmented Dickey-Fuller 检验,也就是 ADF 检验。代码如下:
from statsmodels.tsa.stattools import adfuller result = adfuller(df["sales"].dropna()) print("ADF p-value:", result[1])ADF 检验的原假设是序列不平稳。p 值小于 0.05 时拒绝原假设,认为序列平稳;p 值大于 0.05 时不平稳,需要差分。如果第一次检验不平稳,就对df["sales"].diff().dropna()再做一次,直到 p 值达标。对目标变量差分后,外生变量对应的回归行为会变化,这一点在 2.2 里提过,解释系数时留个心眼。
4.2 ACF 和 PACF 给 p、q 一个初始范围
确定 d 之后,p(自回归阶数)和 q(移动平均阶数)的初步范围可以靠 ACF 和 PACF 图判断。ACF 是自相关函数图,PACF 是偏自相关函数图,两者分别对应 MA 项的阶数和 AR 项的阶数。
from statsmodels.graphics.tsaplots import plot_acf, plot_pacf import matplotlib.pyplot as plt # 对差分后序列画 ACF 和 PACF diff_series = df["sales"].diff().dropna() fig, axes = plt.subplots(1, 2, figsize=(12, 4)) plot_acf(diff_series, ax=axes[0], lags=20) plot_pacf(diff_series, ax=axes[1], lags=20) plt.show()看图规律有个粗略口诀:PACF 在滞后 k 阶后突然截尾,选 p=k;ACF 在滞后 k 阶后突然截尾,选 q=k;两边都是缓慢衰减则 p、q 都取一个适中小值。但实际序列很少给出教科书式的完美图形,所以网格搜索才是真正定参的手段,ACF/PACF 只是用来缩小搜索范围,避免网格搜索遍历一个明显不合理的大空间。
4.3 AIC 网格搜索:把候选空间交给循环
网格搜索的意思很简单:准备一组候选取值,遍历所有组合,分别拟合模型,记录每个组合的 AIC 或 BIC,选最小者。AIC 是赤池信息准则,BIC 是贝叶斯信息准则,两者都在衡量“拟合优度和参数个数”的平衡,数值越小越好。BIC 对参数惩罚更重,倾向于选择更简单的模型。
import warnings import itertools from statsmodels.tsa.arima.model import ARIMA warnings.filterwarnings("ignore") y = df["sales"] X = df[["promo", "temp"]] best_aic = float("inf") best_order = None best_model = None # 候选范围:p 和 q 从 0 到 3,d 从 0 到 1 for p, d, q in itertools.product(range(4), range(2), range(4)): try: model = ARIMA(y, exog=X, order=(p, d, q)) fit = model.fit() if fit.aic < best_aic: best_aic = fit.aic best_order = (p, d, q) best_model = fit except Exception: # 某些参数组合可能导致优化失败,直接跳过 continue print("Best order:", best_order, "AIC:", best_aic) print(best_model.summary())这段代码有几个细节值得说。第一,候选范围里 d 只试 0 和 1,因为业务数据一般一阶差分就够,过差分会导致信息丢失,这在实际项目里很常见。第二,异常处理不能省,某些高 p 高 q 的组合会让数值优化器发散,直接报错,比如 p=3,q=3 配合小样本经常失败。第三,搜索结果要结合显著性再判断,不要机械选 AIC 最小的模型,如果最小 AIC 模型的某个外生变量系数不显著,换一个 AIC 略高但系数全部显著的模型更稳妥。
网格搜索跑完后,把best_order带回第 3 章的最小脚本,替换order=(2,1,2),就是一套完成参数整定的 ARIMAX 训练流程。
4.4 外生变量的选择与滞后处理
外生变量不是越多越好。模型的\beta系数是联合估计的,两个高度相关的外生变量会互相争夺解释力,导致系数不稳定。一个简单的排查方法是用 VIF(方差膨胀因子)检查多重共线性,VIF 大于 10 的变量建议去掉。一个小项目里我一般只保留 2 到 5 个业务上明确有关联的外生变量,不搞“先全塞进去,再靠模型筛选”。
外生变量本身也可以做滞后。比如促销活动的影响可能在活动结束后还延续一两天,此时可以构造滞后列:
df["promo_lag1"] = df["promo"].shift(1)添加滞后变量前必须确认序列已经按时间排序,否则 shift 拿到的不是“前一天”。另外,一旦用了滞后外生变量,预测时也要提供对应的未来滞后值,比如预测明天的销量,需要“今天”的促销值,这对业务数据可得性提出了要求,方案设计时要提前想清楚。
5. ARIMAX 预测避坑指南:五个高频翻车的现象、原因与修复
5.1 现象:预测阶段报 ValueError,提示 exog 长度不一致
第一次跑result.forecast(steps=10)时,如果没传exog参数,statsmodels 会直接报错,因为模型结构里有外生变量,预测必须要未来的外生变量值。更隐蔽的错误是传了X_test,但X_test的行数和steps不相等,或者X_test里的列名和训练时不一致。
原因:ARIMAX 的预测公式里必须代入外生变量的未来值,缺一步都算不出来。列名不一致是因为 statsmodels 在训练时记住了 exog 的列顺序,预测时按位置匹配。
解决:预测前先assert len(X_test) == steps,并检查列名列表是否一致。最稳妥的写法是result.forecast(steps=len(y_test), exog=X_test),让 steps 和 exog 的行数从同一个变量派生,从源头消除不一致。
5.2 现象:训练集拟合得很漂亮,预测结果却是一条直线
我见过很多新手拿着 R²=0.95 的训练结果向业务方汇报,结果测试集预测是一条水平线,和均值差不多。这种情况多半是 d 取大了。差分阶数 d=2 会把序列里的大部分信息抹掉,只剩一个白噪声残差,模型找不到可预测的模式,预测值自然向均值回归。另一个常见原因是外生变量的预测值填错了,比如未来促销活动没被正确编码,模型以为所有未来时点外生变量都是零。
原因:过差分导致信号丢失,或者未来外生变量取值失真。
解决:把 d 降回去,重跑 ADF 检验确认平稳性,不要盲目多差。验证外生变量未来值时,打印前几行人工检查数值是否符合业务预期。这条经验我反复强调:预测曲线一旦看起来“太干净”,先怀疑输入数据,而不是模型。
5.3 现象:类别型外生变量直接传入报错或跑出一个不可解释的系数
业务里经常有“天气类型”“是否节假日”这种文本列。直接传给ARIMA的 exog,statsmodels 会尝试把字符串当作数值,要么报类型错误,要么硬编码成缺失值。
原因:statsmodels 的回归部分只接受数值矩阵,不做自动独热编码。
解决:先把类别处理成数值。用 pandas 的get_dummies生成哑变量,注意去掉第一列避免和常数项共线:
df = pd.get_dummies(df, columns=["holiday"], drop_first=True)生成的holiday_1列含义是“是否为节假日”,0/1 两个取值,可以直接传给 exog。如果有多个类别列,每个类别列都建议只生成 N-1 个哑变量,否则模型会因完全共线性而无法正常估计。
5.4 现象:用 train_test_split 随机切分,模型预测结果奇准无比
这是最常见也最危险的一个坑。train_test_split默认是随机切分,对于时间序列就是灾难:训练集里混杂着测试集之后的样本,模型相当于提前看到了未来数据,测试误差虚低。
原因:时间序列的样本顺序就是信息顺序,随机切分会破坏时序依赖关系,造成数据泄露。
解决:坚持用位置切片或专门的TimeSeriesSplit交叉验证。正确写法是:
train_end = int(len(df) * 0.8) y_train, y_test = y.iloc[:train_end], y.iloc[train_end:]另外,外生变量的滞后列也要在切分之前构造好,不能在切分后再用整段数据 shift,否则滞后列会带入未来信息。
5.5 现象:换一台机器结果不一样,或者解压后脚本报编码错误
statsmodels 的数值优化在多线程环境下可能有细微差异,但差异通常很小。结果差异大到无法接受时,先确认两台机器上 statsmodels 和 numpy 的版本是否一致,版本差异会导致优化路径变化。另外一个常见状况是 Windows 解压 zip 后,代码里的中文注释或 CSV 里的中文列名出现UnicodeDecodeError。
原因:源码包里的 CSV 可能是 UTF-8 编码,Windows 默认用 GBK 打开;中文路径也会让某些老版本 pandas 读取失败。
解决:读取 CSV 时显式指定编码encoding="utf-8-sig",解压后把整个项目目录放到纯英文路径下,所有列名和文件名避开中文。代码文件本身如果有中文注释,运行时一般没问题,但编辑器要设置为 UTF-8 保存。
6. 进阶验证方法:滚动预测与朴素基线对比
6.1 滚动预测:每步都重新拟合一次模型
单次划分训练测试集只能验证一个固定时间点的预测能力,业务环境里更常见的是滚动预测:每预测完一天,就把真实观测值并入历史,再预测下一天。这种策略更接近线上实盘逻辑,也比较残酷,因为它不允许模型偷看未来。
history_y = list(y_train) history_X = X_train.copy() preds = [] for i in range(len(y_test)): model = ARIMA(history_y, exog=history_X, order=best_order).fit() next_X = X_test.iloc[[i]] pred = model.forecast(steps=1, exog=next_X).iloc[0] preds.append(pred) # 把真实值追加到历史,完成滚动 history_y.append(y_test.iloc[i]) history_X = pd.concat([history_X, X_test.iloc[[i]]]) print("Rolling MAE:", mean_absolute_error(y_test, preds))这段代码里最关键的循环逻辑是:先用当前历史拟合模型,只预测一步,然后立刻把这一步的真实值放回历史,再进入下一轮。历史只增不减,模拟的是系统每天更新一次模型的真实状态。滚动预测的误差通常比一次性多步预测低,因为每次只预测一步,误差不会累积,这是它更适合验证模型能力的原因,但代价是计算量大,每次都要重新跑一遍拟合。
6.2 朴素基线:ARIMAX 不打赢它就没必要上线
ARIMAX 模型不是永远优于简单方法。我养成了一个雷打不动的习惯:任何预测模型上线前,先和一个朴素基线对比。时序预测里最经典的朴素基线是“用昨天的值预测今天”,也就是持久性预测:
naive_preds = list(y_train) # 用训练集最后一个值起步 for i in range(len(y_test)): # 预测值等于上一个真实值 naive_preds.append(y_test.iloc[i - 1] if i > 0 else y_train.iloc[-1]) # 对齐到测试集长度 naive_preds = pd.Series(naive_preds[-len(y_test):], index=y_test.index) naive_mae = mean_absolute_error(y_test, naive_preds)如果 ARIMAX 的滚动 MAE 没有明显低于朴素基线,说明外生变量没有带来增量信息,或者当前的数据生成过程本身就是随机游走,不值得上线。反过来说,如果 ARIMAX 赢了,还要再看外生变量的系数是否显著、方向是否合理,这样业务方才愿意用模型的解释结论。
我一般会做三组对比:ARIMAX 固定分点多步预测、ARIMAX 滚动预测、朴素基线。三组 MAE 摆在一起,模型值不值得投入人力做数据清洗和外生变量维护,一目了然。
做 ARIMAX 项目这两年,我最大的教训是:模型结构只是整个预测系统的一小部分,外生变量的数据质量和未来值的可得性,才真正决定预测能不能落地。任何预测都不是把数据喂给机器就能自动变准确的,把这套流程走通了,你会少走很多弯路。希望帮到你。
本文还有配套的精品资源,点击获取