
简介这套基于SARIMA模型的时间序列预测实战资料是面向数据分析初学者与时间序列学习者的完整代码包解决季节性数据建模与预测落地的实操难题。压缩包共7个文件以Python脚本、csv数据集和PyCharm项目配置文件为主整体仅7KB轻量易用便于直接对照运行。脚本完整覆盖数据加载与清洗、STL季节趋势分解、ADF平稳性检验、SARIMA参数搜索调参、模型训练与评估、未来值预测等环节csv数据为每日女性出生人数带有明显的季节周期适合体会SARIMA中季节性AR/MA分量如何捕捉周期模式xml/iml属于IDE配置不影响核心代码逻辑可跳过。已有979人学习浏览读者可借助完整代码理解(p,d,q)(P,D,Q)参数选择与AIC/BIC评估思路快速上手实际业务中的季节性预测任务。1. 为什么 SARIMA 仍是时间序列预测绕不开的模型拿到一份带明显季节波动的历史数据——日销售额、月用电量、周客流——很多人的第一反应是上 XGBoost 或者 LSTM结果发现要么特征工程做到手软要么小样本下模型根本训不动。其实这类带周期、带趋势的单变量序列SARIMA 模型常常用几行代码就能拿到足够稳的预测结果。所谓基于 SARIMA 模型的时间序列预测实战核心就是两件事把序列拆成趋势、季节、随机三部分再用季节性差分自回归滑动平均结构去拟合残差最后通过网格搜索把七个参数组合调出来。它的价值在于可解释性强、训练快、小样本友好特别适合业务上需要快速给出预测值并说清楚依据的场景。这篇文章不绕弯子直接从参数含义、数据预处理、搜索调参、踩坑记录到回测验证讲完一整套能直接落到自己数据上的流程。2. SARIMA 模型拆解看懂七个参数比跑一百个组合更重要2.1 从 ARIMA 到 SARIMA季节性到底被藏在了哪里ARIMA 模型的结构是ARIMA(p,d,q)其中p是自回归阶数d是非季节性差分次数q是移动平均阶数。它处理的是平稳序列中的短期相关性但对周期性成分几乎无能为力。假设你手头是某零售企业过去两年的日销售额数据每周周末都会有一个峰值ARIMA 模型会把这种周周期性当作残差里的自相关去拟合结果就是预测曲线平滑得离谱周末波峰完全丢失。SARIMA 在 ARIMA 的基础上增加了一组季节性参数完整记法是SARIMA(p,d,q)(P,D,Q)m。括号里第一组处理非季节成分第二组处理季节成分m是周期长度。这里的P是季节性自回归阶数D是季节性差分次数Q是季节性移动平均阶数。以日数据为例如果周期是周那m7如果是月度数据且周期为年那m12。m的确定不是靠算法搜出来的而是取决于业务周期——这一点后面会单独讲。用滞后算子 B 写出来更直观。非季节部分是 ARIMA 对当期值和滞后值的线性组合而季节部分把滞后m期、2m期……的值也纳入回归。举个例子(0,1,1)(0,1,1)7表示非季节一阶差分一次、MA 一阶季节差分一次、季节 MA 一阶周期为 7。这个组合在日数据周季节场景里出现频率极高原因是它结构简单只估计两个 MA 系数却能把相邻日和相邻周的相关性同时捕获。2.2 每个参数到底影响模型的什么行为参数调起来像玄学因为七个参数相互纠缠。我一般会把参数按影响范围分成三组来看待。第一组是差分次数d和D。d负责消除趋势让序列均值稳定D负责消除季节性导致的均值周期波动。两者都取 0 或 1 就够了取 2 以上基本是在制造麻烦过度差分会让原本可预测的信号被差分运算冲掉预测结果反而更差。判断方法很直接对原始序列做 ADF 检验p 值大于 0.05 就做一阶差分再检验直到平稳D的确定则看季节性分解图里 seasonal 分量的波动幅度是否随时间变化如果每年同期的差值基本稳定D0即可否则取 1。第二组是自回归阶数p和P。p描述的是“今天和昨天、前天……的相关性”P描述的是“这周和上周、上上周……的相关性”。它们的确定可以看差分后序列的 PACF 图PACF 在滞后 1 阶处截尾p1在滞后 7 处还有显著尖峰P1。当然实际数据不会这么规整所以网格搜索才是主力。第三组是移动平均阶数q和Q。q吸收的是随机冲击的残留影响Q吸收的是周期内的随机冲击残留。看 ACF 图的截尾位置来判断规则和 PACF 类似。这三个参数的取值范围建议限制在 0~2 或 0~3超过 3 的模型不仅训练时间暴涨还极易过拟合。2.3 为什么非要用 statsmodels 的 SARIMAX 而不是手写实现 SARIMA 的常见做法是使用 statsmodels 库里的SARIMAX类。它虽然是带外生变量的扩展版但不传exog参数时就等价于 SARIMA而且底层用的是状态空间表示和卡尔曼滤波估计能处理缺失值、输出置信区间、支持预测区间的解析计算。我用过的其他实现里有些只提供点预测不给区间这在业务上很伤——业务方要的是“下周销量大概在 800 到 1200 之间”而不是孤独的一个数字。另外SARIMAX还内置了诊断工具比如plot_diagnostics、残差正态性检验、Ljung-Box 检验这些是判断模型是否可信的关键依据。后面所有代码都基于以下结构import pandas as pd import numpy as np import warnings from statsmodels.tsa.statespace.sarimax import SARIMAX from statsmodels.tsa.stattools import adfuller from statsmodels.tsa.seasonal import seasonal_decompose from statsmodels.stats.diagnostic import acorr_ljungbox from itertools import product warnings.filterwarnings(ignore)说得直白一点SARIMAX把差分、回归、误差估计全封装好了你要做的是定参数、喂数据、读诊断结果。这也是这个标题能成立的基础——模型本身的实现是成熟的真正考验人的是数据预处理和参数搜索策略。3. 数据准备与季节性定阶先让序列变成可建模的状态3.1 数据加载、频率对齐与缺失值处理很多人在第一步就翻车。拿到的 Excel 里日期列是字符串索引没有显式频率直接扔给SARIMAX也能跑但预测结果可能完全错位。原因是模型内部需要知道周期m对应的时间跨度没有频率信息的索引会让季节分量的对齐出错。我一般会先把日期列解析成datetime类型再设为索引然后显式声明频率。日数据用asfreq(D)月度数据用asfreq(MS)小时数据用asfreq(H)。声明频率后缺失值会变成NaNSARIMAX的状态空间表示能处理少量缺失但大量连续缺失会严重影响季节项估计建议先填充。df pd.read_csv(sales_data.csv, parse_dates[date]) df df.set_index(date) series df[sales].asfreq(D) # 连续缺失少用线性插值多则用前后周期均值补 series series.interpolate(methodlinear, limit3) series series.fillna(series.groupby(series.index.dayofweek).transform(mean))逻辑说明asfreq(D)把索引规整为连续日历日缺失日期显式标记出来。interpolate处理零散缺口按星期分组的均值填充处理周期内的系统性缺失——比如每周固定某天没数据。参数说明limit3表示连续缺失超过 3 天不插值避免在长缺口中间硬造数据groupby(series.index.dayofweek)把周一到周日分组每组的均值作为填充值。这个填充逻辑对日数据周季节场景很有效但如果你的数据是月度且周期为年需要换成groupby(series.index.month)。3.2 ADF 检验定 d趋势到底要不要差分确定差分次数d的标准做法是 ADF 检验。原假设是序列存在单位根即非平稳p 值小于 0.05 就拒绝原假设认为序列平稳。对原始序列做一次检验不平稳就diff()一次再检验直到平稳。def check_stationarity(ts, max_diff2): for i in range(max_diff 1): if i 0: test_ts ts.copy() else: test_ts ts.diff(i).dropna() adf_stat, p_value adfuller(test_ts, autolagAIC)[:2] print(fdiff{i}, p_value{p_value:.4f}, ADF stat{adf_stat:.4f}) if p_value 0.05: return i return max_diff best_d check_stationarity(series) print(推荐 d , best_d)逻辑说明对同一序列从 0 阶差分开始逐次检验第一个 p 值小于 0.05 的差分阶数就是推荐值。autolagAIC让adfuller自动选择用于检验的滞后阶数不用手动指定。注意series.diff(i)在 i 大于 0 时会丢弃前 i 个值所以丢dropna()避免 NaN 传入检验函数。这里面有个细节ADF 检验对趋势敏感如果序列有明显上升趋势且季节性波动幅度随时间变大可以考虑先对原序列做对数变换再检验。业务场景里销售额、客流这类数据经常呈现乘法季节性对数变换能把乘法关系变成加法关系更符合 SARIMA 的线性假设。判断方法是看seasonal_decompose的残差图——残差波动幅度随时间扩大就做np.log1p(series)。3.3 季节性分解定 D 和 m别让周期长度靠猜m的值必须来自业务知识。日数据看周季节就是m7看月度季节效应则是m30或m31——不推荐用 30 和 31因为季节差分要求固定的周期长度建议按业务口径统一为周7或年365 或 366但年周期对日数据来说太长需要至少 3 年数据才能估。月数据看年季节就是m12周数据看年季节则是m52。没有业务依据时可以用自相关图辅助确认plot_acf(series)里在哪个滞后位置出现峰值簇那个滞后数就是候选周期。D的确定也依赖分解图。用seasonal_decompose把序列拆成趋势、季节、残差三个部分decomp seasonal_decompose(series, modeladditive, period7) decomp.plot() seasonal_signal decomp.seasonal # 逐周期看季节分量幅度 amplitude seasonal_signal.groupby(seasonal_signal.index.weekday).agg([mean, std]) print(amplitude)逻辑说明modeladditive选加法分解乘法序列要先对数变换period7告诉分解器周期长度。输出每个星期几的季节分量均值和标准差如果标准差远大于均值说明季节形态随周不同而变化D考虑取 1如果标准差明显小于均值季节形态稳定D0。参数说明分解的period必须和 SARIMA 的m保持一致否则你看到的季节分量是错的。有个典型错误是日数据直接period30月底效应、月初效应这种不规则周期会被强行拆进趋势项里误导D的判断所以优先从业务周期出发不建议数据驱动硬选。4. 搜索调参两阶段网格搜索把七个参数组合跑明白4.1 为什么不能盲搜全组合7 个参数的完整网格搜索如果 p、q、P、Q 各取 0~3d、D 取 0~1m 固定为 7组合数是 4×4×4×4×2×2 1024 组。每组合一次全量拟合日数据三年样本大概耗时几秒全跑完要一小时以上。而且这里面有很大一部分组合在数值上不可估计——比如p3, d1, q3同时出现时参数冗余导致估计矩阵奇异SARIMAX直接抛异常。真正有效的组合可能只占两三成盲搜既浪费时间又容易把异常路径带进结果。更麻烦的问题是过拟合。AIC 随参数增多而下降搜索范围越大越容易选中一个对历史数据拟合极好但对未来预测很差的组合。SARIMA 的 AIC 比较必须在同一份训练数据上进行一旦你在全量数据上做搜索选出来的参数其实是“事后诸葛亮”——它看过全部历史包括最后一段你要预测的时期。整个搜索过程跑下来输得最多的不是时间而是这种信息泄漏导致的虚假最优。两阶段搜索是常见做法。第一阶段粗搜确定 d、D、m第二阶段对 p、q、P、Q 做精搜。这样做还有个附带好处每组候选参数可以留一部分样本做验证不需要额外交叉验证训练速度也快。4.2 第一阶段定 d、D、m 的快速扫描第一阶段不做完整搜索而是固定一组基础参数比如(1, d, 1)(1, D, 1)m然后在 d、D、m 的候选值上做少量拟合比较 AIC。因为 d 和 D 的取值本来就只有 0/1m 又是业务确定的这个阶段的组合量非常小几秒就能跑完。best_aic float(inf) best_cfg None for d in [0, 1]: for D in [0, 1]: for m in [7]: # 日数据周周期业务确认后固定 try: mod SARIMAX( series, order(1, d, 1), seasonal_order(1, D, 1, m), enforce_stationarityFalse, enforce_invertibilityFalse, ) res mod.fit(dispFalse) if res.aic best_aic: best_aic res.aic best_cfg (d, D, m) except Exception as e: print(fd{d}, D{D}, m{m} 拟合失败: {e}) print(第一阶段最优:, best_cfg)逻辑说明这个阶段只做 4 次拟合d 两种、D 两种、m 由业务定死为 7目标是快速锁定差分层级。enforce_stationarityFalse和enforce_invertibilityFalse必须关掉因为搜索过程中很多候选参数对应的是非平稳或不可逆的模型结构开了这两个开关直接抛异常搜索就中断了。代价是极端情况下模型可能不稳定所以后面要检查arroots和maroots的模长。注意这里有个陷阱如果粗搜索阶段就不稳定拟合AIC 比较会失真。所以在第一阶段结束后我会把最优配置重新拟合一遍查看res.arroots和res.maroots所有根的模必须大于 1否则该组参数不可用退回次优配置。4.3 第二阶段对 p、q、P、Q 做网格精搜确定 d、D、m 后剩下的四个参数取值范围收窄。p 和 q 设定 0~3P 和 Q 设定 0~2因为季节部分参数过多极易拟合出锯齿状预测。用itertools.product生成全部组合逐个拟合比较 AIC同时记录 BIC 作为备选。best_aic float(inf) best_params None best_seasonal None results [] for p in range(0, 4): for q in range(0, 4): for P in range(0, 3): for Q in range(0, 3): try: mod SARIMAX( series, order(p, best_cfg[0], q), seasonal_order(P, best_cfg[1], Q, best_cfg[2]), enforce_stationarityFalse, enforce_invertibilityFalse, ) res mod.fit(dispFalse) # 稳定性检查所有特征根必须在单位圆外 if np.all(np.abs(res.arroots) 1) and np.all(np.abs(res.maroots) 1): if res.aic best_aic: best_aic res.aic best_params (p, best_cfg[0], q) best_seasonal (P, best_cfg[1], Q, best_cfg[2]) results.append((res.aic, (p, q, P, Q))) except Exception: continue print(最优参数:, best_params, best_seasonal, AIC:, best_aic)逻辑说明双重循环遍历候选参数np.abs(res.arroots)检查 AR 部分特征根是否全在单位圆外这是平稳性的充要条件maroots同理检查可逆性。两者都满足才参与 AIC 比较。这里的dispFalse关闭拟合过程日志不然控制台会被刷屏。参数说明p、q 范围 0~3 是保守且稳妥的实际业务序列超过 3 阶自相关的很少阶数过高只会把噪声学进去。P、Q 范围 0~2 是因为季节部分一阶就已经代表“与上周期同期的关系”二阶以上通常是对上一周期的二次修正实战中极少用到。enforce_stationarity关掉是为了让搜索继续但稳定性检查必须跟上两件事互相平衡才是完整策略。4.4 网格搜索输出解读AIC 差多少才算真的有差别搜索结果是一个按 AIC 排序的列表但 AIC 差多少有意义我的经验是两个模型的 AIC 差小于 2基本算同一水平选参数更简单的那个差在 4~7 之间有实质差异差大于 10优者明显胜出。不要看到 AIC 降了 1.5 就欢呼那大概率是过拟合带来的虚假优势。为了看得更清楚可以把搜索结果做成一个小表格按 AIC 升序排列挑前五个配置一起进入后续诊断——跑残差检验、看预测稳定性最后再定唯一参数。这样比单独盯着最小 AIC 更稳因为 AIC 是历史拟合的度量离预测好还差一个残差诊断的距离。sorted_results sorted(results, keylambda x: x[0]) for aic, cfg in sorted_results[:5]: print(fAIC{aic:.2f}, p,q,P,Q{cfg})这段代码把前五名打印出来为后续多配置对比做准备。到这里搜索调参实际已经跑完一大半了剩下的是用诊断和回测去确认这个参数组合真的可信。5. 调参避坑指南五次翻车换来的血泪经验5.1 现象预测曲线异常平滑季节波峰完全丢失原因D0且PQ0被 AIC 选中。AIC 在样本量小时偏向简单模型季节项作为额外参数容易被惩罚掉但业务上你知道这个序列一定有周季节。解决把季节参数 P、Q 的最小值设为 1不要让搜索器有权完全去掉季节结构。具体操作是把range(0, 3)改成range(1, 3)或者在搜索结果中强制过滤掉P0 and Q0的组合。5.2 现象同样的数据换个时间窗口最优参数完全变了原因数据非平稳季节性的强度和相位随时间漂移。比如某电商平台的大促期间销量模式和平日完全不同整个序列的季节形态并不稳定。解决把训练集按业务周期切段分别搜索参数看是否收敛到同一组如果发散说明数据本身不适合全局单一 SARIMA考虑对最后一段数据重新定阶或使用分段模型。还有一种情况是样本量太小SARIMA 对参数变化过于敏感建议至少保留 3~4 个完整周期周季节就是 3~4 周年季节就是 3~4 年再建模。5.3 现象拟合图很好看但预测区间宽到没法用原因残差存在条件异方差或者重尾分布SARIMAX默认假设残差服从正态分布区间估计基于这个假设偏了之后预测区间失真。解决看plot_diagnostics的残差 QQ 图如果两端偏离直线考虑对数据做 Box-Cox 变换后再建模。另外检查残差平方的自相关图如果显著相关说明波动率聚集直接改 GARCH 族模型更合适SARIMA 在这里已经到边界了。5.4 现象Ljung-Box 检验显示残差仍有强自相关模型却被选为最优原因AIC 只衡量拟合优度和参数量的平衡不保证残差是白噪声。搜索的最优参数完全可能落在残差自相关显著的组合上。解决网格搜索的评分函数不要只用 AIC加上一步acorr_ljungbox(res.resid, lags[10])p 值小于 0.05 的组合直接剔除。实际操作中我会在搜索循环里加一个条件只有 Ljung-Box p 值大于 0.05 的组合才参与 AIC 比较。5.5 现象SARIMAX 拟合时抛出收敛警告但代码没报错原因数值优化达到最大迭代次数但没有收敛常见于数据量太小或参数组合复杂。解决增加maxiter和method参数或者换methodnm做无梯度优化。我一般固定maxiter200如果还警告就把该组合标记为失败不要硬收结果。收敛警告的模型参数估计没有意义直接拿着预测上线属于自欺欺人。以上五条是按出现频率排序的前三条几乎每个项目都能遇到。避坑的总原则是搜索调参是手段不是目的AIC 最低不等于预测最好残差白噪声才是模型可信的前提。6. 时序回测与预测区间上线前验证的一个具体技巧参数定了、残差也过了最后一关是回测。这里说的是真正能把预测能力量化出来的方法——滚动时间序列交叉验证而不是随机 K 折。时间序列数据不能打乱否则未来信息会泄漏进训练集回测结果虚高得离谱。我常用的做法是扩展窗口滚动预测从第 N 个历史点开始每次训练集加长一个周期预测下一个周期计算误差然后滚动推进。这个方法能模拟真实上线时的预测环境——你永远是在当前时点预测未来而不是拿整段历史拟合完再预测最后一段。from sklearn.metrics import mean_absolute_error def rolling_backtest(series, order, seasonal_order, horizon7, min_train60): n len(series) errors [] start min_train while start horizon n: train series.iloc[:start] test series.iloc[start:start horizon] try: mod SARIMAX( train, orderorder, seasonal_orderseasonal_order, enforce_stationarityFalse, enforce_invertibilityFalse, ) res mod.fit(dispFalse, maxiter200) pred res.get_forecast(stepshorizon).predicted_mean errors.append(mean_absolute_error(test, pred)) except Exception: pass start horizon return np.mean(errors), np.std(errors) mae_mean, mae_std rolling_backtest( series, best_params, best_seasonal, horizon7, min_train60 ) print(f滚动回测 MAE 均值{mae_mean:.2f}, 标准差{mae_std:.2f})逻辑说明min_train是训练集最小长度保证第一次拟合有足够样本每轮训练集增长horizon步测试集固定为未来 7 步。这模拟的是每周滚动重新训练一次的上线节奏。参数说明maxiter200防止部分窗口不收敛导致的报错forecast是点预测如果想要置信区间改用get_forecast(stepshorizon).conf_int()。回测结果出来后对比同一组参数在不同窗口的表现。如果 MAE 均值低但标准差高说明模型稳定性差某些时段预测崩盘这时考虑调整P或Q让季节结构更平滑如果 MAE 均值和标准差都高大概率是D多差了一阶试试把D降为 0 重新搜索。最后讲一个细节习惯也是我踩过最深的坑永远保留最后三个完整周期不参与调参单独做最终验证。原因是网格搜索已经看了全量数据的统计特征最后这段哪怕不参与拟合它的统计信息也已经通过数据整体泄漏进去了。严格的做法是调参过程只在训练段上进行最后用测试段跑一次确认结果和回测一致才能放心部署。项目上线后我会每周记录一次预测值和实际值的差值画成控制图——点落在两倍标准差内说明模型还在正常工作一旦连续多周偏差同号就该重新做参数搜索了这就是模型漂移的信号。SARIMA 这个方向值不值得投入我的判断是只要你的数据是单变量、有稳定周期性、样本量在几百到几千这个区间它依然是最值得先试的模型成本低、可解释、业务方愿意接受。它解决不了的是多变量协同、非线性突变、长周期依赖这些复杂场景那些交给更强的模型不亏。但先把 SARIMA 的调参、诊断、回测这套流程跑通对你上任何时序模型都有帮助。希望帮到你。本文还有配套的精品资源点击获取