简介:基于ARIMAX的多变量预测模型Python源码与配套数据集,面向需要处理含外部回归变量的时间序列预测任务的数据分析人员及Python开发者。资源围绕ARIMAX模型展开,兼顾数据预处理与建模实现,可帮助读者理解多变量输入下如何构建稳健的预测流程。包体共7个文件,包含2个Python脚本(分别对应数据预处理与ARIMAX建模)、2个CSV数据集(供实验调用)、2张说明图片及1份Markdown说明文档,压缩包大小约146KB,结构紧凑,适合快速上手。目前已有408人学习使用,可作为课程作业、项目验证或算法对比的参考实现。通过源码注释与配套数据,读者能够直接运行并观察数据预处理、模型拟合与结果展示的完整链路,同时Markdown文档提供必要的使用说明,降低了理解与复现门槛。
1. 为什么单拿ARIMAX做多变量预测而不是直接上深度学习
做过门店销量预测的工程师大概都经历过这种翻车:只把历史销量喂给 ARIMA 时,平时拟合得挺好的模型,一到节假日就彻底失效,误差拉出一条长长的上扬曲线。问题不在 ARIMA 本身,而是你根本没告诉它节假日要来了,外部信息被完全丢掉了。ARIMAX(带外生变量的 ARIMA)就是为了解决这件事而存在的:它在保留差分平稳化能力的同时,额外接受一组与目标序列同步的外生变量,把节假日标记、温度、促销开关这些信息作为回归项接入模型。这个 zip 方案把 ARIMAX 在多变量场景下的完整链路都装了进去——Python 源码负责数据对齐、模型训练、预测还原,配套数据集让你不需要到处找数据就能对照着复现。它的受众很明确:正在做销售、客流、能源负荷预测的工程师,以及想让预测结果不靠运气、能说清外生变量真贡献的团队。
2. ARIMAX原理与选型:外生变量到底如何进入ARIMA
2.1 从ARIMA到ARIMAX:一个回归项把外部因素接进来
ARIMA(p,d,q) 做的事是:对目标序列做 d 阶差分拿到平稳序列,再用 p 阶自回归和 q 阶移动平均描述这个平稳序列的时域结构。换句话说,ARIMA 只关心目标序列自己过去的值和过去的误差。这在很多场景下够用,但一旦外部条件发生结构性变化,比如大促、寒潮、节假日调休,单靠历史值的惯性根本扛不住。
ARIMAX 的数学路径可以这样理解:y_t = X_t β + η_t,其中 η_t 服从 ARIMA(p,d,q) 过程。外部变量 X_t 以普通线性回归的方式进入模型,AR/MA 部分负责吸收回归后剩余误差中的时序相关性。
这里有个关键点值得停下来想清楚:当 d=1 时,模型实际拟合的是 Δy_t 对 X_t 的回归,再加上对 Δy 的 AR/MA 项。也就是说,外生变量通常保持原始水平值进入差分后的方程,不需要像目标变量那样做差分。举个例子,预测日销售额时,目标序列 y 做了 1 阶差分,而 x1(节假日 0/1)、x2(最高气温)、x3(促销开关)这些外生变量就直接按原始值放进回归式。β 的含义是"X_t 每变化一个单位,Δy_t 平均变化多少",这跟普通线性回归里有微妙差别,解读系数时不能望文生义。
很多初学者栽在这个地方:他们以为 ARIMAX 就是"把 ARIMA 里的 y 换成多个变量",结果跑去用 VAR。实际上 ARIMAX 是单方程模型,外生变量是"被解释"侧的外部输入,内生变量依然只有一个。搞清楚这个边界,后面调参、看残差才不至于跑偏。
2.2 为什么不用VAR、SARIMAX和Prophet:选型对比表
ARIMAX 在多变量预测领域并不是唯一选择,甚至不算最"高级"的选择。做选型时我一般会把 VAR、SARIMAX、Prophet 拉出来一起比,因为它们的易混程度很高,用错了就是整个项目返工。
| 模型 | 适用场景 | 主要限制 | 外生变量处理方式 |
|---|---|---|---|
| ARIMAX | 单目标序列 + 少量已知外生变量 | 不能捕捉复杂非线性关系 | 以线性回归项直接接入 |
| VAR | 多变量互相影响且都要预测 | 参数数量随变量数暴增,样本需求量极大 | 所有变量都是内生变量 |
| SARIMAX | 目标有明确日/周/月周期 | 季节阶数设置敏感,调参难度大增 | 与 ARIMAX 相同 |
| Prophet | 强节假日效应、趋势突变点明显 | 外生变量支持较弱,滞后的灵活度低 | 只支持线性自定义回归器 |
VAR 的问题在于它对"多变量"的理解和 ARIMAX 完全不同。VAR 假设每个变量都被其他变量影响,建模时要把所有变量的滞后项全部铺开,变量一多参数立刻爆炸。如果你只想预测一个目标序列,其他变量只是辅助信息,那 VAR 就是在浪费自由度。
SARIMAX 是 ARIMAX 的季节扩展,当数据有明显的周内模式或年度模式时,加一组(P,D,Q,s) 的效果立竿见影。代价是参数搜索空间变大,样本不足时很容易过拟合。Prophet 在处理节假日和趋势转折时确实省心,但如果你需要在预测期对某个外生变量做假设推演,Prophet 的灵活度远不如 ARIMAX——它的自定义回归器不支持滞后结构。所以我的判断标准很简单:只有一个目标序列、外生变量在预测期已知或可预报、周期性不显著或可以用哑变量消化,就直接用 ARIMAX,少给自己加戏。
2.3 外生变量能用的三个硬性前提
在动手写代码之前,先要确认你的外生变量"配得上"这个模型。ARIMAX 对 X 的要求比普通回归更苛刻,因为它要把 X 延伸到预测期去,这决定了整个方案的可行性。
第一,外生变量在预测期必须是已知的或可高精度预报的。节假日日历是提前确定的,天气预报能看未来几天,促销排期是业务自己定的。但如果你计划用"政策补贴金额""竞品活动力度"这种变量,就得先想清楚预测期这些值从哪来,否则模型训练完却没东西可推算,等于白做。这一点决定了外生变量能不能最终用于滚动预测。
第二,外生变量与目标序列的因果方向要一致且稳定。ARIMAX 估计的 β 是样本期的平均效应,如果预测期里这个关系发生反转或消失,模型的预测误差会比不用它更难看。一个典型场景是"促销折扣":样本期里折扣拉动销量,但如果预测期里折扣频繁出现、消费者已形成价格锚定,效应就会衰减。遇到这种情况,宁可砍掉这个变量也要保住模型的干净度。
第三,外生变量的滞后效应需要你手动构造。ARIMA 结构只负责处理目标序列自身的滞后,X 的滞后不会自动进入模型。温度对饮品销量的影响可能滞后一天,补贴对订单量的拉动可能要一周才体现,这些滞后项需要你在特征工程阶段显式制造出来,模型本身没有这个能力,这也是 ARIMAX 和深度学习模型在"特征自动提取"上的核心差异。
3. 源码结构与最小复现:从python环境到ARIMAX训练预测全流程
3.1 解压zip之后先看什么:目录结构与依赖清单
拿到这个 zip,解压后的目录组织一般是常见的工程样式:数据文件和代码文件分开,代码再按职责拆成数据加载、训练、预测几个脚本,外加一个 requirements.txt 和一个 README。你应该先读 README 和 requirements.txt,而不是急着跑训练脚本——源码能不能跑起来,九成取决于依赖版本是否匹配。
在 Python 环境准备上,我建议直接用虚拟环境,不要往全局站点里装。这里有个来自 python 安装教程的血泪经验:装完 Python 后命令行输入 python 没反应或者 pip 找不到,基本都是 PATH 没配好,连累后续所有依赖安装。下面是环境准备的标准操作:
python -m venv .venv source .venv/bin/activate # Windows 下用 .venv\Scripts\activate pip install --upgrade pip pip install -r requirements.txtrequirements.txt 里至少会包含 statsmodels、pandas、numpy、matplotlib 这几个核心库。需要特别盯住的是 statsmodels 的版本,这个库的 API 变化比较折腾,我一般会把它锁在 >=0.13,<0.15 这个区间,既能使用较新的 ARIMA 类,又避开最新版可能引入的破坏性变更。
建议在虚拟环境里跑通后再动源码,因为这种解压包最怕的就是"能跑但不知道跑在哪套环境上"。先把 run 脚本跑出来一个结果,再改参数,你的可控感会强很多。
3.2 数据加载与频率规范化:asfreq这步不能省
多数这种项目的数据集都长得很规矩:第一列是日期,后续列是目标序列和外生变量。但"看起来规矩"和"statsmodels 认账"之间还差一步,就是频率标注。ARIMA 类模型要求 endog 和 exog 要么没有索引,要么带明确频率的索引;如果你只是 set_index("date") 而不声明频率,训练阶段经常报"Unable to infer frequency",或者预测时索引对不上。代码里显式声明频率是最省心的解药:
import pandas as pd # 解析日期并设为索引,asfreq("D") 强制声明为日频 df = pd.read_csv("data/train.csv", parse_dates=["date"]) df = df.set_index("date").asfreq("D") # 目标序列和外生变量统一转数值,脏值先转 NaN df["target"] = pd.to_numeric(df["target"], errors="coerce") df["x1"] = pd.to_numeric(df["x1"], errors="coerce") # 节假日标记 0/1 df["x2"] = pd.to_numeric(df["x2"], errors="coerce") # 最高气温 df["x3"] = pd.to_numeric(df["x3"], errors="coerce") # 促销开关 0/1 # 看一眼缺失值分布,后面按列处理 print(df.isna().sum()) print(df.dtypes)这段代码的逻辑是先把时间列解析成标准格式,再用 asfreq("D") 把索引强制标记为日频。asfreq 的实际效果是:如果原始数据中间缺了某些日期,它会把缺失日期补进来,target 和 x 对应位置填 NaN。这样做的代价是引入了额外缺失值,收益是模型能稳定推断频率。
参数说明里重点看两处:parse_dates 负责把字符串日期转成 Timestamp,避免之后手动 to_datetime 出意外;asfreq("D") 的频率码 "D" 是日频,如果你的数据是小时级就用 "H",是工作日数据可以试 "B",但 "B" 会忽略周末,必须确认你的业务语义真的没有周末。缺失值在下文会专门讲策略,这里只需确认"有缺、知道缺在哪"。
3.3 训练ARIMAX的最小命令:order参数怎么定
数据加载干净后,训练一个 ARIMAX 模型在 statsmodels 里其实就两三行。需要注意,statsmodels 里没有一个叫 ARIMAX 的类,ARIMAX 是通过 ARIMA 类传入 exog 参数实现的,源码里大概率也是这么组织的。这里给出最简可跑版本:
import statsmodels.api as sm # order 先按 (2,1,2) 起步:d=1 表示 1 阶差分,p/q 等 ACF/PACF 初步判断 model = sm.tsa.ARIMA( df["target"], exog=df[["x1", "x2", "x3"]], order=(2, 1, 2), ) result = model.fit(method="innovations_mle", maxiter=200) print(result.summary())fit 方法里 method="innovations_mle" 是我比较推荐的参数,它用状态空间表示做极大似然估计,收敛速度比旧的 css-mle 快,对中等长度序列很友好。maxiter=200 是防止复杂模型在默认 50 次迭代内没收敛就静默退出,训练时留意 summary 里有没有"Maximum number of iterations reached"字样,有就把 maxiter 再调大。
order 的 (2,1,2) 不是拍脑袋来的:先用 adf 检验确定 d 需要几阶差分才能让序列平稳,再对差分后的序列画 ACF/PACF 图粗看截尾和拖尾模式。一般项目到这个深度就够了,更细的 p、q 挑选放到 grid search 阶段,用 AIC 兜底。执行这段代码时如果报错说 exog 不是二维的,回到 3.2 节检查 df[["x1", "x2", "x3"]] 这种 DataFrame 写法,别手滑写成 df["x1"] 这种 Series。
3.4 预测与差分还原:用get_forecast避免自己算错
训练完成后,下一步是做预测。这里我强烈建议用 get_forecast 而不是 forecast,因为前者直接给你带置信区间的对象,后者只返回点预测数组,一旦后期要画区间图就得返工。多步预测时 exog 的行数必须等于 steps,这是源码里最容易写错的地方。
import pandas as pd # 假设预测未来 3 天,外生变量提前给定:x1 假日、x2 气温、x3 促销 future_exog = pd.DataFrame({ "x1": [0, 1, 0], "x2": [24.5, 27.2, 26.8], "x3": [0, 1, 0], }) future_exog.index = pd.date_range(start="2024-06-01", periods=3, freq="D") # get_forecast 返回完整预测对象,predicted_mean 是点预测序列 forecast_result = result.get_forecast(steps=3, exog=future_exog) pred = forecast_result.predicted_mean conf = forecast_result.conf_int(alpha=0.05) print(pred) print(conf)这段代码的逻辑是:先手动构造预测期的外生变量 DataFrame,再把它的索引设成未来日期。注意 get_forecast 里 exog 的行数必须严格等于 steps,多一行少一行都会直接抛错,这是 ARIMAX 预测环节最典型的翻车点,后面避坑章还会展开。
因为 d=1,上面拿到的 pred 是差分序列的预测值,需要手动还原成原始量纲。还原的公式是 y_t = y_{t-1} + Δy_t,多步预测时要按顺序累加。把上一节最后一个训练期真实值作为基准,用 cumsum 就能完成还原。如果你在源码里看到"手动还原"的循环代码,检查它的每一步基准是否用上一步的预测值而不是真实值——用错了就是灾难性的误差累积。
4. 把数据集处理好:外生变量构造、对齐与切分的完整套路
4.1 数据集字段怎么设计:target加三个外生变量的样例
ARIMAX 项目真正值钱的部分不是模型,而是数据集。同一个模型,外生变量设计得好坏,能让最终预测误差差出一倍。这个方案里配套的数据集,通常是典型的销售/客流预测格式:一列日期、一列目标值、外加若干外生变量列。
| 字段名 | 类型 | 示例 | 说明 |
|---|---|---|---|
| date | datetime | 2024-05-31 | 时间索引,必须无重复且按升序 |
| target | float | 1280.5 | 目标序列,比如日销售额 |
| x1 | int | 0/1 | 节假日标记,1 表示当天是节假日 |
| x2 | float | 28.3 | 连续型外生变量,比如最高温度 |
| x3 | int | 0/1 | 离散型外生变量,比如是否促销 |
字段设计的原则是"宁少勿多"。外生变量不是越多越好,每多一个变量就多一份共线性风险和一份预测期取值的不确定性。如果某列在预测期没办法拿到准确值,就不要把它放进特征列表,否则模型训练完没法推理。
另外要注意离散型外生变量和连续型外生变量在预处理上的差异:节假日和促销这种 0/1 标记,填充缺失时用前向填充是合理的,因为"不知道是不是节假日"通常意味着按上一个状态延续;温度这种连续变量,用前向填空会留下锯齿,更适合做线性插值。数据集的原始文件里一般会留出缺失,让你自己处理,这也正好是练手的好机会。
4.2 时间索引对齐的三个坑:重复、乱序、交易日混自然日
ARIMAX 对 exog 和 endog 的对齐要求比大多数人想象的严格:它要求两者索引完全一致,差一行都不行。我在实际项目里遇到过三次不同的对齐问题,症状各异,根源相似。
第一个坑是索引重复。原始 csv 里偶尔会有两行相同日期的数据,通常是数据上报时重复追加造成的。如果不去重,asfreq 后 statsmodels 会拒绝执行,或者更糟,它不报错但结果失真。处理办法是先看重复再决定保留哪条:
# 保留每天最后一条记录,然后排序 df = df[~df.index.duplicated(keep="last")] df = df.sort_index() # 显式声明日频,把缺失日期补成 NaN df = df.asfreq("D") # 连续变量用线性插值,离散哑变量用前向填充 df["target"] = df["target"].interpolate(method="linear", limit=3) df["x1"] = df["x1"].fillna(method="ffill")这段代码先说去重:duplicated(keep="last") 遇到同日期多行时保留最后一条,这个选择是假设"后写入的记录更新"。排序是为了让后续 asfreq 在有序索引上工作,乱序索引在 asfreq 时会警告甚至报错。
第二个坑是交易日和自然日混在一起。很多销售数据是交易日记录,周日没营业就没有记录;如果你用 asfreq("D") 声明日频,周六周日会被补成 NaN。此时 target 的缺失不能无脑 fillna——销售额为 0 和没营业是两码事。要么把频率改成 "B"(工作日频率),要么在业务上确认"补零"是否合理。我这个方案里默认 asfreq("D") 加线性插值,是因为示例数据集本身就是自然日连续记录;你用自己的数据时务必先确认业务口径。
第三个坑是外生变量和 target 的 DataFrame 索引没对齐。常见做法是把所有列放在同一个 DataFrame 里再 asfreq,这样天然对齐。如果你是分别加载 target 和 exog 再合并,记得用 df.merge(..., on="date", how="outer") 后检查是否有 NaN,别在 fit 时等报错。
4.3 构造滞后外生变量:温度影响销售一般慢一天
外生变量直接影响当期的情况存在,但更多的时候是滞后影响。比如最高气温升到 35 度,冰淇淋当天的销量确实会上涨,但上涨最明显的时间往往是热浪持续后的第二天——消费者先意识到天热,再决定囤货。这个滞后效应 ARIMA 结构管不了,得自己在特征工程里造:
# 温度滞后 1 天 df["x2_lag1"] = df["x2"].shift(1) # 星期几作为外生变量,捕捉周周期性 df["dow"] = df.index.dayofweek # 新造的滞后列第一行是 NaN,用前向填充或直接丢弃前 N 行 df = df.iloc[1:]shift(1) 的逻辑是"把昨天的值放到今天这一行",这样模型拟合时看到的就是昨天的温度对今天销量的影响。注意 shift 后第一行成了 NaN,如果数据量不大我建议直接丢弃前一行,因为补出来的滞后值其实没有真实信息。
星期几这个变量值得说一说。ARIMAX 处理周期性有两种思路:一种是上 SARIMAX 的季节项,另一种就是把星期哑变量直接当外生变量。后者更直观并且系数可解释,比如周一系数 -0.15 就表示周一的销量平均比基准低 15%。做这类离散变量时,不要用 0-6 的整数当连续值喂进去,模型会误以为周日到周一的影响是线性的,应该转成哑变量或至少做 one-hot。statsmodels 对 DataFrame 里的 get_dummies 列支持得很好,直接拼进 exog 就行。
4.4 训练集/测试集切分:差分还原用cumsum
切分逻辑在时序预测里与普通机器学习完全不同:不能随机打乱,必须按时间顺序切,而且测试集必须紧跟在训练集之后,中间不能有空档,否则外生变量的滞后特征会在切分边界断裂。
n_test = 14 # 最后 14 天作为测试集 train = df.iloc[:-n_test] test = df.iloc[-n_test:] exog_cols = ["x1", "x2", "x3", "x2_lag1", "dow"] model = sm.tsa.ARIMA(train["target"], exog=train[exog_cols], order=(2, 1, 2)).fit() # 预测时传入测试期的真实外生变量值 pred_diff = model.forecast(steps=n_test, exog=test[exog_cols]) # 差分还原:最后一真实值 + 累计差分 last_train = train["target"].iloc[-1] pred = last_train + pred_diff.cumsum()这段代码展示了两种常见写法的区别。model.forecast 返回的是差分序列的预测,pred_diff.cumsum() 是做还原,逻辑是"每一步都在上一步预测值基础上累加差分预测",所以第一步的基准是 last_train,第二步的基准变成了第一步的预测值,以此类推。
这个还原过程非常容易踩坑。有些人为了方便,在还原时用测试集的真实值做基准,结果评估指标看起来好得惊人,但上线后立刻崩掉——因为在线滚动预测时你根本没有未来真实值。判断一段还原代码是否正确的方法很简单:把 pred 和 test["target"] 画在一张图里,看两条线是否在同一个量级上;如果 pred 明显偏离,先检查基准是不是用了真实值。
5. 避坑与排查:ARIMAX实战中常见的5个翻车点
5.1 报错"exog must be array-like":多数是object列
现象:fit 或者 forecast 阶段直接抛 ValueError,提示 exog must be array-like or pandas DataFrame,英文原文里还往往带一句"e.g. a pandas DataFrame"。
原因:最常见的是 exog 里混入了 object 类型的列。pandas 读 csv 时,某列如果既有数字又有空字符串,会被整体推断为 object 类型,而 statsmodels 底层把它当成 Python list 处理,形状检查就过不去。还有一种情况是你传了 Series 而不是 DataFrame,模型要求二维结构。
解决:在传入模型之前打印 df[exog_cols].dtypes,把所有 object 列全部转换。比较稳妥的写法是逐列做 pd.to_numeric(errors="coerce"),把非法字符变成 NaN 后再填充。之后再检查 df[exog_cols].shape[1] 和 df["target"].shape[0] 是否匹配。我自己的做法是把这两步封装成一个 prepare_exog() 函数,每次训练前调用,宁可多花两行代码也不赌数据类型正确。
5.2 预测值几步就飞:差分还原逻辑写错了
现象:预测结果前 1-2 步还像样子,到第 3 步之后开始一路偏离,误差持续扩大,甚至出现明显违背业务常识的数值。
原因:差分还原时基准用错了。很多人还原时习惯用"真实值更新基准",即每一步都用测试集的真实 y 来加差分预测,这在离线评估中显得误差很小,但一旦到了真实预测场景,你不可能有未来真实值,模型就"无从压住误差"。另一类原因是 d 设得太高,比如 d=2 时还原要连续累加两次差分,步骤越多误差累积越凶。
解决:确认 d 的取值用 adf 检验,而不是靠直觉。差分还原时严格遵循"上一步预测值 + 这一步差分预测"的链条,基准只取最后一个训练集真实值。验证方法是把还原后的 pred 和测试集真实值画在同一张图上,看残差是否平稳地围绕零波动;如果残差呈现出越来越大的趋势,优先检查还原链路而不是模型参数。
5.3 外生变量系数很大但预测更差:量纲和共线性
现象:summary 里 x2 的系数大得离谱,比如 5000 多,标准误也不小;把这个变量加进去后 MAE 反而恶化了。
原因:外生变量之间的量纲差异巨大,比如 x1 是 0/1 哑变量,x2 是几千量级的累积值,同一套回归框架下系数会被量纲拉伸。更隐蔽的是共线性,两个外生变量彼此强相关,模型本身无法分离出各自的效应,系数符号可能不符合直觉。
解决:先对连续型外生变量做标准化,让它和哑变量处在同一量纲水平,再看系数是否落到合理范围。如果添加后模型性能反而变差,用方差膨胀因子检查共线性,VIF 高于 10 的变量优先剔除。这属于"多变量预测"的典型坑:变量不是越多越好,局部很漂亮的新变量可能全局拖后腿。
5.4 环境一换脚本就报错:statsmodels版本API差异
现象:同一个脚本在自己的机器上跑得好好的,放到同事机器或服务器上就报 TypeError 或者"unexpected keyword argument 'exog'"。
原因:statsmodels 在 0.12 版本才把 ARIMA 类重构并正式支持 exog 参数,0.12 之前大家用的是 SARIMAX 类来模拟 ARIMAX。如果同事环境的 statsmodels 还停留在 0.11,ARIMA 类根本没有 exog 参数,自然报错。反过来,如果你拿旧版写法 SARIMAX(order=(p,d,q), seasonal_order=(0,0,0,0)) 去新版跑,虽然兼容,但参数名和语义容易造成混乱。
解决:在虚拟环境中直接锁定版本,逼所有人在同一版本上开发。稳妥的做法是在 requirements.txt 里写 statsmodels>=0.13,<0.15。如果你依赖的源码包比较老旧,改用 sm.tsa.SARIMAX(endog, exog, order=...) 的写法兼容性最好,因为它从 0.9 到 0.14 都支持 exog。这个坑属于 python 环境配置里的常见病,根源是开源库 API 演进太快,锁定版本是唯一的后悔药。
5.5 zip解压乱码或伪加密:先确认编码再读csv
现象:zip 解压出来,csv 文件名是乱码,打开内容也是乱码;更奇怪的是,有些压缩包在双击时提示输入密码,但 README 里根本没提密码。
原因:这类压缩包多数是在 Windows 下用默认编码(GBK)压缩的,文件名的字节在 macOS/Linux 下被误读成 UTF-8,所以乱码。至于"提示要密码",大概率遇到的是 zip 伪加密:压缩包的加密标记位被置位,但文件本身并没有真正加密,很多解压工具检测到标记位就要求输密码。
解决:解压时用支持编码识别的工具,比如 7-Zip 或 Bandizip,它们对 GBK/UTF-8 文件名处理得更成熟。如果是伪加密,可以在命令行里用 7z 直接解,或者修改 zip 全局加密标志位后重新打包。读取乱码 csv 时,pandas 里多试几种编码,最常见的是 encoding="gbk" 和 encoding="utf-8-sig",先看列名是否正常再往下走。
伪加密这个东西其实不可怕,关键是别被"带密码的压缩包"吓住。判断方法很简单:用文本编辑器打开 zip 看是否能直接看到 csv 的开头内容,能看懂就说明没有真正加密。真遇到加密的压缩包,走正规渠道联系作者拿密码,不要花精力去破。
6. 进阶验证:滚动窗口与残差检验帮你判断外生变量是否真有效
6.1 滚动窗口:有X无X各跑一遍,看MAE差多少
单次训练集上的结果说服力有限,ARIMAX 项目上线前我最常做的验证是滚动窗口评估:把数据集切成多个连续窗口,每个窗口独立训练、预测下一段,最后对比有外生变量和无外生变量的 MAE。这个对比决定了外生变量到底是"真信号"还是"纸面繁荣"。
from sklearn.metrics import mean_absolute_error def rolling_eval(df, exog_cols, order=(2,1,2), h=7, windows=4): err_x, err_no = 0, 0 for i in range(windows): cut = len(df) - (windows - i) * h train, test = df.iloc[:cut], df.iloc[cut:cut + h] m1 = sm.tsa.ARIMA(train["target"], exog=train[exog_cols], order=order).fit() p1 = m1.get_forecast(steps=h, exog=test[exog_cols]).predicted_mean m0 = sm.tsa.ARIMA(train["target"], order=order).fit() p0 = m0.get_forecast(steps=h).predicted_mean err_x += mean_absolute_error(test["target"], p1) err_no += mean_absolute_error(test["target"], p0) return err_x / windows, err_no / windows mae_x, mae_no = rolling_eval(df, exog_cols) print(f"with exog: {mae_x:.2f}, without: {mae_no:.2f}")这段代码的核心是每次滚动只取前 cut 行训练、预测其后的 h 行,测试集不重叠也不越过末尾边界。外生变量版传入 test 的真实外生值,无外生变量版则完全靠 ARIMA 自身惯性。如果 mae_x 比 mae_no 低 10% 以上,说明外生变量确实提供了增量信息;如果差距很小甚至更差,就回头检查滞后项构造或共线性问题。
6.2 残差白噪声与AIC选阶:用检验数字说服别人
模型是否把时序信息榨干了,一个严肃的检验是 Ljung-Box 残差白噪声检验。残差的 p 值大于 0.05 说明没有明显的自相关残留,模型结构基本合理;p 值很小说明还有信息藏在残差里,应该增加 p 或 q 阶数。
from statsmodels.stats.diagnostic import acorr_ljungbox resid = result.resid print(acorr_ljungbox(resid, lags=[10], return_df=True))这个检验只对训练集残差有意义,所以我会在 order 调整阶段高频使用,而不是在最终模型上跑一次就完事。选阶方面,我在小范围内网格搜索 p,q,用 AIC 取最优,但不会把 p,q 上限拉得太高,因为 ARIMAX 的参数越多,对样本量的要求就越高,过拟合的风险增长比收益快得多。
我自己做这类项目养成了一个固定习惯:不管模型在测试集上成绩多好,上线前一定先把有 X 和无 X 两版在滚动窗口上的 MAE 都保存下来,因为只有拿到这份对照,才有底气象业务方解释清楚外生变量到底值不值得维护。这也算是这条路上最值得多做一步的一个验证动作,希望帮到你。
本文还有配套的精品资源,点击获取