SARIMA调参实战:从差分检验到网格搜索的完整流程
2026/9/15 13:59:59 网站建设 项目流程

简介:一套基于SARIMA模型的时间序列预测实战代码包,面向具备一定统计学基础的时间序列分析学习者或需处理季节性数据的开发者,可解决从数据预处理到参数寻优的完整端到端建模问题。压缩包共7个文件,内含4个XML工程配置文件、1个IML模块描述、1个Python脚本与1个CSV数据集,整体仅7KB,结构精简便于快速定位核心逻辑。脚本覆盖数据读取、时间序列分解、ADF平稳性检验、SARIMA参数搜索(含手动/自动调参)、模型评估与预测输出等关键环节,并以残差图、AIC/BIC作为优劣判断依据;可直接运行复现女性出生人数预测案例,也可迁移至电商销量、气温变化等季节型数据场景。目前已有977人浏览学习,适合希望通过完整实战代码掌握SARIMA建模全流程的开发者参考,并可在理解后迁移至自己的业务数据。

1. 用 SARIMA 做时间序列预测,麻烦不在运行模型,而在“搜索调参”

拿到一份带趋势和季节波动的销量数据,第一反应是跑 SARIMA 模型,但大多数人在第一步就卡住了:到底选几阶差分?季节周期填 12 还是 4?(p,d,q) 和 (P,D,Q,s) 的组合怎么定?这个环节就是“搜索调参”,也是 SARIMA 实战里真正费时间、费算力的地方。本文按“先看数据、再定范围、然后搜索、最后验证”的顺序,把完整流程拆开,给出可直接复现的 Python 代码和参数设置思路。R 里auto.arima可以快速给出起点,但从头写网格搜索能让你真正理解每个参数在干什么。经验少的人可以照步骤走,干过几年的人也能从后面的收敛保护和滚动预测里找到可复用的技巧。整包数据与代码可以整理成“完整数据代码.rar”,最后一章会说明怎么组织这份交付物才不坑接手的人。

2. 不要直接搜索调参:先定差分阶数 d 和 D,收敛不了是白搜

很多教程一上来就 itertools 铺开几百组参数去跑ARIMA(p,d,q)(P,D,Q,s),结果一小时过去,一半模型报收敛警告,另一半的 AIC 低得离谱但预测全是直线。问题多半出在 d 和 D 是拍脑袋填的。搜索调参的第一步不是写搜索代码,而是用统计检验把 d 和 D 固定在合理范围里。

2.1 用 ADF 检验确定非季节差分阶数 d

ADF(Augmented Dickey-Fuller)检验的原假设是“序列存在单位根,即非平稳”。当 p 值小于 0.05 时拒绝原假设,认为序列已经平稳,差分到此为止。实际操作中我习惯写一个小循环,逐阶差分并打印 p 值,而不是靠肉眼看图猜平稳性。

import pandas as pd from statsmodels.tsa.stattools import adfuller def find_d(series, max_d=2): """逐阶差分并做 ADF 检验,返回首个 p<0.05 的差分阶数 d""" d = 0 s = series.copy() while d < max_d: stat, p, _, _, _, _ = adfuller(s.dropna(), autolag="AIC") print(f"d={d}, ADF p-value={p:.4f}") if p < 0.05: return d s = s.diff() d += 1 return d d = find_d(df["sales"], max_d=2)

这段代码的要点在于:adfuller返回的第二个值就是 p 值,autolag="AIC"表示自动选择滞后阶数,比手动指定maxlag更稳妥。p 值阈值为 0.05 是统计惯例,但真实业务数据里别卡得太死,0.06~0.08 时可以先取一阶差分再观察 PACF,过度差分反而会把趋势信息差分掉,导致模型只剩噪声。d取到 2 就够了,如果线性差分两次还不平稳,说明数据里有结构性突变,SARIMA 已经不合适,应该去看断点检测或干预分析,而不是继续加大差分阶数。

2.2 用季节分解判断 D 和周期 s

确定了 d 之后,接下来看季节项。SARIMA 的季节差分 D 表示对“季节周期差”做一阶差分,它跟 s 直接相关:月度数据 s=12,季度数据 s=4,周数据 s=52。s 一旦写错,后面的所有参数搜索都是在错误坐标系上打转。

判断 D 的方法是做季节分解,看季节分量的振幅是否随时间变化。

from statsmodels.tsa.seasonal import seasonal_decompose # 月度数据,period=12 dec = seasonal_decompose(df["sales"], model="additive", period=12) dec.plot() # 观察 season 子图的振幅:若波动幅度逐年变大,则季节分量不稳定,考虑 D=1

model="additive"假设季节波动幅度不随水平变化;如果观察数据发现量级越大波动越明显,加性模型残差会带喇叭口,改成model="multiplicative"更合适。判断 D 的标准没有 ADF 那么机械:如果季节子图看起来像一个稳定的正弦波,D=0;如果周期内峰谷差距随时间显著扩张,D=1。注意 D 别超过 1,季节二阶差分会让模型失去可解释性,而且预测区间会急剧变宽。

一个容易被坑的细节:seasonal_decomposeperiod参数必须显式指定。如果你喂入的是按月采样的数据而忘记写period,函数默认按 1 处理,季节分解图里只剩趋势和残差,你根本看不到季节模式。完成这一步,p、q、P、Q 的候选范围缩小了一轮,搜索压力小了很多。

3. 搜索调参的评估框架:验证窗口和误差口径先定好

网格搜索跑出来一堆候选模型,选谁不选谁,取决于你用什么标准排序。很多项目直接把 AIC 最低的那个模型拿去上线,做出来效果却不如 BIC 稍高但更简单的模型。这里要说清楚两件事:AIC/BIC 是拟合优度指标,不是预测效果指标;你真正关心的是新数据上的误差。

3.1 AIC 低不等于预测准

AIC 和 BIC 都包含极大似然值和参数惩罚项,它们的本质是“在拟合度和复杂度之间取平衡”,衡量的是训练样本内的信息损失。时间序列预测要面对的是样本外数据,尤其是有季节性和漂移的序列,AIC 最低的模型往往在验证集上表现平平,原因无非两类:一是惩罚力度不够导致参数过多,把训练段的随机噪声也拟合进去了;二是搜索范围里根本没有正确的结构,AIC 矮子里面拔高个。

所以我的做法是把信息准则当作“筛选器”,把验证集误差当作“排序器”。先过滤掉 AIC 或者 BIC 明显偏大(比如比最小 BIC 大 6 以上)的模型,再用真实留出集上的 RMSE 或 MAE 排序。

3.2 按时间顺序切分训练集和验证集

时间序列切分不能用随机抽样,这一点新人特别容易翻车。随机切会破坏时间顺序,让模型在训练时“偷看”未来的信息,验证集误差会偏小,上线后立刻原形毕露。正确做法是拿最近一段数据当验证集,并保留最后几天做最终测试。

# 用最后 30 个观测作为滚动验证的留出集 val_len = 30 train = df["sales"].iloc[:-val_len] val = df["sales"].iloc[-val_len:] # 注意:索引要保持原始时间顺序,不要 reset_index 后打乱

这里的val_len需要根据业务周期设定。如果是月度数据,至少留出一个完整季节周期(12 个点),否则验证集里缺了某个月份,误差会被季节效应污染;如果是周数据,留出 8~12 周比较稳妥。训练集切完后,用train去做差分和模型拟合,val只用于最后计算预测误差,任何提前看过val的操作都可能让后续调参过拟合。

3.3 用 RMSE、MAE 还是 MAPE

误差指标的选择跟业务口径相关,直接决定了“哪个模型最好”的结论。

指标计算方式适用场景注意点
RMSEsqrt(mean((y-yhat)^2))大误差敏感,库存备货、容量规划受异常值影响大,量纲大时数字吓人
MAEmean(abs(y-yhat))日常销量、收入预测对离群点不敏感,但无法区分小偏差和系统性偏差
MAPEmean(abs((y-yhat)/y))汇报给业务方看百分比实际值为 0 时计算无效,低销量月份会主导结果

我一般以 RMSE 为主排序,MAE 辅助验证稳定性。因为库存类决策最怕“某天突然差很多”,RMSE 对这类风险敏感。如果你在 R 里用forecast::accuracy,同一套指标也有对应输出,方便和 Python 结果对照。

4. 用网格搜索跑完 (p,d,q)×(P,D,Q,s) 候选组合

前置条件准备好之后,可以开始写搜索逻辑了。搜索调参的核心不是把参数范围铺得越大越好,而是用合理范围、提前截断、结果记录三步走,把几百个候选组合压缩到分钟级出结果。

4.1 用 itertools.product 生成候选参数

SARIMA 的参数空间是 (p,d,q) × (P,D,Q,s),典型情况是 4×4×3×3×2 = 288 组候选。直接用嵌套 for 循环当然可以,但 itertools 写起来更清晰,后续要加过滤条件也方便。

import itertools import warnings import pandas as pd from statsmodels.tsa.statespace.sarimax import SARIMAX from statsmodels.tools.eval_measures import rmse warnings.filterwarnings("ignore") p_range = range(0, 4) # 非季节AR阶数 q_range = range(0, 4) # 非季节MA阶数 P_range = range(0, 3) # 季节AR阶数 Q_range = range(0, 3) # 季节MA阶数 candidates = list(itertools.product( p_range, q_range, P_range, Q_range ))

product生成的是所有可能的 (p, q, P, Q) 四元组,d、D、s 已经在前面固定下来。这样网格的大小是 4×4×3×3=144 组,比 288 少了一半,因为 d 和 D 不再参与遍历。

4.2 完整的搜索调参代码

下面这段代码会逐一拟合每个候选模型,记录 AIC、BIC 和验证集 RMSE,最后按 AIC 升序排列返回结果表。

def sarima_grid_search(train, val, d, D, s, candidates): results = [] for p, q, P, Q in candidates: order = (p, d, q) seasonal_order = (P, D, Q, s) try: model = SARIMAX( train, order=order, seasonal_order=seasonal_order, enforce_stationarity=False, enforce_invertibility=False, trend="n", ) fitted = model.fit(disp=False, maxiter=100) # 预测验证集长度 forecast = fitted.get_forecast(steps=len(val)).predicted_mean loss = rmse(val.values, forecast.values) results.append({ "p": p, "d": d, "q": q, "P": P, "D": D, "Q": Q, "s": s, "AIC": fitted.aic, "BIC": fitted.bic, "RMSE": loss }) except Exception: # 收敛失败或参数边界越界:跳过即可 continue return pd.DataFrame(results).sort_values("AIC") results_df = sarima_grid_search(train, val, d=1, D=1, s=12, candidates=candidates) print(results_df.head(10))

这段代码里有几个参数需要说明:

  • enforce_stationarity=Falseenforce_invertibility=False这两个开关建议在搜索阶段关掉。SARIMA 在某些参数组合下会落在平稳域或可逆域的边界附近,强迫约束会直接抛异常或被截断,导致一组本来有潜力的候选被误杀。
  • maxiter=100是迭代上限。搜索阶段不需要完全收敛,先用粗拟合筛掉明显差的组合,等候选缩小到 Top 5 之后再加大迭代次数精修。
  • trend="n"表示不额外添加趋势项。因为 d=1 已经做了一阶差分,趋势信息已被处理,再加趋势项可能导致双重差分。
  • disp=False关闭迭代日志,否则搜索时会刷屏,输出文件会非常大。

4.3 搜索范围怎么设计才算“不瞎搜”

很多教程给的候选范围是 p、q 都取 0~5,看着全面,实际毫无必要。对月度业务数据,p 和 q 超过 3 之后模型开始拟合噪声,而且相邻阶数的 AIC 差距很小,算力却翻倍涨。合理的初始范围应结合 ACF/PACF 图收敛判断。

参数推荐候选范围设置理由
p (非季节AR)0~3ACF 呈拖尾、PACF 截尾时,p 落在低阶区间
q (非季节MA)0~3PACF 拖尾、ACF 截尾时,q 落在低阶区间
P (季节AR)0~2季节周期 s 较大时,高阶会大量消耗自由度
Q (季节MA)0~2同 P,超过 2 通常过拟合
d由 ADF 检验确定第 2 章已说明
D由季节分解确定第 2 章已说明
s由采样周期决定月=12,季=4,周=52,不要随手填

如果初始搜索的 Top 模型落在范围的边界上,比如最优结果 p=3 或 Q=2,不要急着扩大范围,先检查边界处模型的残差是否还是白噪声。如果残差表现良好,边界候选只是“矮子里的将军”,扩大范围通常还会引入收敛问题。反之,如果边界模型残差仍有明显相关性,再把对应范围加 1 重跑一轮。这个流程比一次性铺大网格更高效。

4.4 从结果里挑模型:先 BIC 后 RMSE

结果表按 AIC 排序后,不要直接取第一行。AIC 和 BIC 的差异小于 2 时,模型在统计上几乎没有差别;直接取 AIC 最低往往捡到一个复杂模型,差分次数还可能与验证集误差排序矛盾。我的选取策略分两步:

# 第一步:用 BIC 过滤,相差超过 6 的直接淘汰 bic_threshold = results_df["BIC"].min() + 6 filtered = results_df[results_df["BIC"] <= bic_threshold] # 第二步:在过滤后的集合里按 RMSE 升序取最优 best = filtered.sort_values("RMSE").iloc[0] print(best)

为什么用 BIC 而不是 AIC 做第一步过滤?BIC 对参数数量的惩罚比 AIC 更重,在搜索阶段能更快剔除高阶冗余模型,剩下的小集合再用验证集误差去定最终胜负。这样做还有一个好处:RMSE 本身受验证集长度影响,直接全空间按 RMSE 选,可能挑到恰好踩中验证段噪声的模型,先 BIC 卡一道等于加了一层正则。最终选出的模型,再拿去跑诊断抑制,下一篇 5.1 会讲怎么判断它是不是真的过关。

5. 残差检验、滚动预测与 rar 包交付

选定最优参数只是开始,真正让模型具备可交付性的是残差白噪声验证、滚动预测表现,以及代码与数据的组织方式。

5.1 用 Ljung-Box 检验残差是否还有相关性

模型拟合完毕后,第一件事是看残差是否还有可利用的信息。Ljung-Box 检验的原假设是“残差序列相互独立”,p 值大于 0.05 说明没有显著自相关,模型提取信息比较充分。

from statsmodels.stats.diagnostic import acorr_ljungbox # best_fitted 是最优参数的拟合结果 best_fitted = SARIMAX( train, order=(best["p"], best["d"], best["q"]), seasonal_order=(best["P"], best["D"], best["Q"], best["s"]), enforce_stationarity=False, enforce_invertibility=False, ).fit(disp=False) lb = acorr_ljungbox(best_fitted.resid, lags=[12], return_df=True) print(lb)

如果 p 值小于 0.05,说明残差在 12 阶滞后内仍有显著自相关,这时回第 4 章调整 Q 或 q 的范围再搜一轮。注意 Ljung-Box 的lags建议设为季节周期的整数倍,月度数据取 12,周数据取 52,太小时检验不到周期残留。

5.2 滚动预测:比一次性预测更接近真实使用方式

一次性预测所有验证集点,误差会不断累积,对 SARIMA 这种自回归模型来说并不公平。实际业务里更常见的是滚动预测:每预测完一步,把真实观测值加回历史窗口,再预测下一步。

from statsmodels.tsa.statespace.sarimax import SARIMAX history = list(train.values) forecast = [] for t in range(len(val)): roll_model = SARIMAX( history, order=(best["p"], best["d"], best["q"]), seasonal_order=(best["P"], best["D"], best["Q"], s), enforce_stationarity=False, enforce_invertibility=False, ) roll_fit = roll_model.fit(disp=False) yhat = roll_fit.forecast(steps=1).iloc[0] forecast.append(yhat) history.append(val.iloc[t])

这个循环里每个时间步都重新拟合一次模型,计算成本更高,但带来的评估结果更贴近线上真实流程。对比一次性预测和滚动预测的 RMSE,如果差距过大,说明模型对误差累积敏感,上线后需要考虑结合真实值定期重估。

5.3 rar 包里的文件结构怎么组织

作为交付给对方或接手人的压缩包,要保证解压即能复现,所以建议按可重跑结构组织:

  • data/raw.csv:原始时间序列,字段名和时间索引保持统一
  • src/01_data_explore.py:数据探索与差分检验
  • src/02_sarima_grid_search.py:参数搜索与结果输出
  • src/03_forecast_and_eval.py:滚动预测与误差评测
  • output/model_summary.csv:最优参数、AIC、BIC、RMSE 汇总
  • README.md:写明 Python 版本、statsmodels 版本和运行顺序

压缩包命名里带.rar只是交付格式的选择,真正重要的是README.md里把 d、D、s 的选定结果和搜索代码的入口写清楚,否则对方拿到压缩包第一件事就是找你问参数哪里来的。搜索过程本身也要在代码里固定随机种子,保证别人重跑时得到同一份结果表。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询