ARTICLE · INTELLIGENCE

战地情报 · 详情页

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

Python SARIMA 实战:从定阶到滚动预测的完整流程

Python SARIMA 实战:从定阶到滚动预测的完整流程

简介:本资源面向计算机、电子信息工程、数学等专业的大学生及时间序列预测初学者,提供一套完整的SARIMA模型Python实现方案,可用于课程设计、期末大作业或毕业设计。压缩包共5个文件,包含3个csv数据文件、1个xlsx表格和1个py源码脚本,整体约61KB,数据与代码配套齐全,便于直接运行与验证。代码采用参数化编程,参数可灵活调整,且配有保姆级注释,几乎一行一注释,极大降低小白入门门槛。读者可从中掌握SARIMA建模的完整流程,包括数据读取、平稳性检验、差分处理、模型定阶、训练预测及结果可视化,同时理解时间序列预测的核心思路与调参方法。目前已有448人学习下载,适合希望快速上手SARIMA并完成实战项目的学习者参考。

1. 从一份带季节波动的销量表说起:SARIMA 到底解决什么问题

手里有一份按天或按月记录的销量、流量、能耗数据,画出来是一条明显带周期起伏的曲线——每年夏天高、冬天低,或者每周五冲顶、周末回落。这种数据用普通 ARIMA 去拟合,残差里会残留一大块规律性波动,预测值总是慢半拍,误差在周期拐点上集中爆发。SARIMA(季节性差分自回归移动平均)就是为这种「趋势 + 周期」叠加的场景准备的:它在 ARIMA 的自回归、差分、移动平均三件套之外,额外加了一组季节性参数,专门吃掉周期成分。这篇笔记讲的是用 Python 的 statsmodels 把 SARIMA 从数据到预测完整跑通,包含可复现的源码结构和一份能直接替换的数据模板。适合已经会 pandas 基础操作、想给业务做短期预测的工程师,也适合被「预测总在周期点翻车」折磨过的人。核心结论先放这:SARIMA 不难,难的是定阶和季节性差分这两步,玄学感主要来自这里。

2. 先把数据和时间轴对齐:SARIMA 的输入长什么样

2.1 时间序列的硬性格式要求

statsmodels 的 SARIMA 对输入挑剔得有点不讲道理。它要的是一个带 DatetimeIndex 的一维 Series,索引必须是规整的、无缺失的时间点,值必须是数值型。很多人翻车就翻在第一步:从 Excel 或数据库读出来的日期是字符串,或者中间缺了几天,模型不会报错,但拟合出来的结果完全没法解释。

我一般会先做三件事:把日期列转成 datetime、设为索引、用 asfreq 把频率固定下来。频率这个参数很关键,月度数据是 'MS'(月初)或 'M',日度是 'D',小时是 'H'。频率定错,后面的季节性周期 s 就全错了。

import pandas as pd import numpy as np # 读取原始数据,假设两列:date, value df = pd.read_csv("sales.csv") # 1. 日期转 datetime,并设为索引 df["date"] = pd.to_datetime(df["date"]) df = df.set_index("date").sort_index() # 2. 固定频率,月度数据用 MS(月初),缺失点补 NaN series = df["value"].asfreq("MS") # 3. 检查缺失和类型 print(series.dtype) # 必须是 float64 或 int64 print(series.isna().sum()) # 缺失数量,后面要处理 print(series.index.freq) # 确认频率已固定

这段代码的逻辑是:先把时间轴变成模型能识别的对象,再强制频率对齐。asfreq 的作用是把不规整的时间点补齐成等间隔,缺的地方填 NaN,这样模型才知道「每个时间步代表多久」。参数上,'MS' 表示每月第一天,如果你的数据是每月最后一天记录,用 'M'。日度数据用 'D',别用 'B'(工作日),除非你的业务确实只在工作日发生。

2.2 缺失值处理与平稳性初判

补齐频率后大概率会出现 NaN。SARIMA 本身不处理缺失,得先填。常见做法是线性插值或前向填充,但要注意:如果缺失是连续一大段,插值会引入虚假规律,这时候宁可截断数据。我一般对零星缺失用 interpolate,对连续超过 3 个点的缺失直接标记出来人工确认。

填完之后做平稳性初判。SARIMA 的 d 和 D 两个差分参数就是为平稳性服务的。用 ADF 检验看是否需要差分,用季节差分看周期是否被消掉。

from statsmodels.tsa.stattools import adfuller # 线性插值填补零星缺失 series = series.interpolate(method="linear") # 原始序列 ADF 检验 adf_result = adfuller(series.dropna()) print("ADF统计量:", adf_result[0]) print("p值:", adf_result[1]) # 一阶季节差分(假设周期 s=12) series_seasonal_diff = series.diff(12).dropna() adf_result2 = adfuller(series_seasonal_diff) print("季节差分后p值:", adf_result2[1])

ADF 的原假设是「存在单位根,不平稳」,p 值小于 0.05 才能拒绝原假设、认为平稳。如果原始序列 p 值很大,先做一阶普通差分 d=1;如果差分后仍有明显周期,再做季节差分 D=1。这里 s=12 是月度数据的常见周期,日度数据如果按周波动就是 s=7。注意:差分次数不是越多越好,每差一次就丢一批数据,d 和 D 通常不超过 1。

3. 定阶才是 SARIMA 的真正门槛:p、d、q 与 P、D、Q 怎么选

3.1 两组参数的分工与取值边界

SARIMA 的参数写成 (p, d, q)(P, D, Q)s,前一组管非季节部分,后一组管季节部分,s 是周期长度。p 是自回归阶数,看的是「当前值和前 p 个值相关」;q 是移动平均阶数,看的是「当前值和前 q 个预测误差相关」;d 是普通差分次数。带大写的 P、D、Q 是同样的逻辑,但作用在「相隔 s 个时间步」的滞后上。

取值边界上,p 和 q 一般不超过 3,P 和 Q 不超过 2,d 和 D 取 0 或 1。这不是硬规定,是经验:阶数越高,参数越多,过拟合风险越大,而且计算量涨得很快。我见过有人把 p 设到 10,结果模型把噪声也学进去了,样本外预测一塌糊涂。

定阶有两条路:看 ACF/PACF 图,或者网格搜索 AIC/BIC。前者快但主观,后者慢但客观。实际项目里我一般先用图定个大致范围,再用网格搜索在范围内挑最优。

3.2 用 ACF 和 PACF 图缩小范围

ACF 是自相关函数,PACF 是偏自相关函数。经验规则:AR 模型的 ACF 拖尾、PACF 截尾;MA 模型的 ACF 截尾、PACF 拖尾。截尾指的是某个滞后之后突然掉到置信区间内,拖尾是缓慢衰减。

import matplotlib.pyplot as plt from statsmodels.graphics.tsaplots import plot_acf, plot_pacf fig, axes = plt.subplots(2, 2, figsize=(12, 8)) # 非季节部分:看普通差分后的序列 plot_acf(series.diff(1).dropna(), ax=axes[0, 0], lags=20) plot_pacf(series.diff(1).dropna(), ax=axes[0, 1], lags=20) # 季节部分:看季节差分后的序列,滞后按 s 的倍数看 plot_acf(series.diff(12).dropna(), ax=axes[1, 0], lags=36) plot_pacf(series.diff(12).dropna(), ax=axes[1, 1], lags=36) plt.tight_layout() plt.show()

看图时重点找「第几个滞后开始落进蓝色置信带」。比如 PACF 在第 2 个滞后后截尾,p 就试 2;ACF 在第 1 个滞后后截尾,q 就试 1。季节部分看滞后 12、24、36 的位置,如果 PACF 在 12 处有个明显尖峰然后截尾,P 试 1。图只是给范围,别指望它给出唯一答案。

3.3 网格搜索 AIC 选最优组合

把图给出的范围做成候选集,用循环跑一遍,比 AIC 或 BIC。AIC 越小越好,它同时惩罚拟合误差和参数数量。这一步计算量大,建议先把范围压小。

import warnings from statsmodels.tsa.statespace.sarimax import SARIMAX warnings.filterwarnings("ignore") # 抑制收敛警告,正式项目要记录 best_aic = np.inf best_order = None best_seasonal = None # 候选范围,按 ACF/PACF 图缩小后设定 p_range = range(0, 3) d_range = [1] q_range = range(0, 3) P_range = range(0, 2) D_range = [1] Q_range = range(0, 2) s = 12 for p in p_range: for q in q_range: for P in P_range: for Q in Q_range: try: model = SARIMAX( series, order=(p, 1, q), seasonal_order=(P, 1, Q, s), enforce_stationarity=False, enforce_invertibility=False, ) result = model.fit(disp=False) if result.aic < best_aic: best_aic = result.aic best_order = (p, 1, q) best_seasonal = (P, 1, Q, s) except Exception: continue print("最优AIC:", best_aic) print("非季节阶数:", best_order) print("季节阶数:", best_seasonal)

这段是定阶的核心。enforce_stationarity 和 enforce_invertibility 设成 False 是为了让搜索过程不因为参数越界而中断,但最终选出的模型要回头检查这两个性质。d 和 D 固定为 1 是因为前面已经验证过差分后平稳,如果数据本身平稳,这两个可以设 0。循环里用 try 包住是因为某些参数组合会导致不收敛,直接跳过。跑完拿到最优组合,别急着用,下一步要做残差诊断。

4. 拟合、诊断、预测:把模型跑成能交付的结果

4.1 拟合与残差白噪声检验

拿到最优阶数后正式拟合,然后检查残差。残差应该是白噪声——没有自相关、均值接近零、方差稳定。如果残差里还有规律,说明模型没吃干净信息,得回去调阶数。

from statsmodels.stats.diagnostic import acorr_ljungbox # 用最优阶数正式拟合 final_model = SARIMAX( series, order=best_order, seasonal_order=best_seasonal, enforce_stationarity=False, enforce_invertibility=False, ) final_result = final_model.fit(disp=False) # 残差 Ljung-Box 检验,滞后取 10 和 20 lb_test = acorr_ljungbox(final_result.resid, lags=[10, 20], return_df=True) print(lb_test) # 残差描述统计 print(final_result.resid.describe())

Ljung-Box 检验的原假设是「残差是白噪声」,p 值大于 0.05 才说明残差没有剩余自相关,模型合格。如果 p 值很小,说明还有信息没提取,常见原因是阶数不够或者季节性周期设错。残差均值应该接近 0,标准差要稳定,如果残差图里出现明显的喇叭口(方差随时间变大),考虑对数据取对数再建模。

4.2 预测与置信区间输出

预测分样本内和样本外。样本内看拟合效果,样本外才是真正要交付的。get_forecast 可以同时给出预测值和置信区间。

# 样本外预测未来 12 期 forecast_steps = 12 forecast = final_result.get_forecast(steps=forecast_steps) # 预测均值 pred_mean = forecast.predicted_mean # 95% 置信区间 pred_ci = forecast.conf_int(alpha=0.05) # 拼成结果表 result_df = pd.DataFrame({ "forecast": pred_mean, "lower": pred_ci.iloc[:, 0], "upper": pred_ci.iloc[:, 1], }) print(result_df) # 保存 result_df.to_csv("sarima_forecast.csv", encoding="utf-8-sig")

get_forecast 返回的对象里,predicted_mean 是点预测,conf_int 是区间。alpha=0.05 对应 95% 置信区间,想宽一点用 0.1,窄一点用 0.01。预测步长别设太大,SARIMA 的误差会随步长累积,月度数据预测 12 期还算靠谱,超过 24 期基本只能看趋势方向。保存时用 utf-8-sig 是为了 Excel 打开不乱码,这是血泪经验。

4.3 滚动预测评估真实误差

一次性预测 12 期看着漂亮,但真实业务里你每个月都会拿到新数据。滚动预测才是贴近实战的评估方式:每次只用历史数据预测下一期,然后纳入真实值再预测下一期。

from sklearn.metrics import mean_absolute_error, mean_squared_error # 留出最后 12 期做测试 train = series[:-12] test = series[-12:] history = list(train) predictions = [] for t in range(len(test)): model = SARIMAX( history, order=best_order, seasonal_order=best_seasonal, enforce_stationarity=False, enforce_invertibility=False, ) result = model.fit(disp=False) yhat = result.forecast(steps=1).iloc[0] predictions.append(yhat) history.append(test[t]) # 纳入真实值 mae = mean_absolute_error(test, predictions) rmse = np.sqrt(mean_squared_error(test, predictions)) print("MAE:", mae) print("RMSE:", rmse)

滚动预测的逻辑是「走一步、看一步、再预测下一步」,每一步都重新拟合。这样评估出来的误差才是模型上线后的真实水平。MAE 和 RMSE 结合看:MAE 反映平均偏差,RMSE 对大误差更敏感。如果 RMSE 远大于 MAE,说明存在个别预测严重偏离的点,要回去看那几个时间点是不是有异常事件。这段代码慢,因为每步都重新拟合,数据量大时可以把重新拟合改成用已有模型追加观测值。

5. 避坑与排查:SARIMA 落地时最容易翻车的 5 个点

5.1 现象:模型不收敛,报一堆警告

原因通常是阶数太高、数据里有极端异常值,或者差分后序列方差过大。解决:先把 p、q、P、Q 降到 1 以内试,确认能收敛再逐步加;对异常值做 winsorize 或剔除;对数据取对数压缩方差。收敛警告不要用 warnings.filterwarnings 一盖了之,正式项目里要记录哪些参数组合失败了。

5.2 现象:预测值是一条直线

原因多半是 d 或 D 设多了,差分把趋势和周期都消掉了,模型只剩均值可预测。解决:回头做 ADF 检验,确认到底需要几次差分。很多平稳序列根本不需要差分,d=0、D=0 反而预测更准。另一个可能是 p、q 全设成 0,模型退化成纯均值模型。

5.3 现象:样本内拟合完美,样本外一塌糊涂

典型的过拟合。原因:阶数过高、参数过多,模型把噪声当规律学了。解决:用 AIC/BIC 而不是只看拟合优度;做滚动预测评估;把数据分成训练、验证、测试三段,验证集上调参,测试集上只评估一次。我一般会强制要求样本外误差不能比样本内差 3 倍以上。

5.4 现象:季节性完全没被捕捉

原因:s 设错了。月度数据 s=12,季度数据 s=4,周数据 s=7,日数据如果按年周期是 s=365。很多人把月度数据的 s 设成 30,模型完全找不到周期。解决:先画图确认周期长度,用 pandas 的 seasonal_decompose 分解出季节项,看它的周期是多少,再设 s。

5.5 现象:预测置信区间宽得没法用

原因:数据本身噪声大,或者预测步长太长。解决:缩短预测步长;对数据做平滑或聚合(日度聚成周度);检查是否有结构性断点(比如政策变化、促销活动),断点前后的数据要分开建模。置信区间宽不一定是模型问题,有时候是数据本身就不支持精确预测,这时候要如实告诉业务方。

6. 把 SARIMA 用稳的几个进阶习惯

第一个习惯是给模型加外生变量。SARIMA 可以扩展成 SARIMAX,把促销、节假日、天气这些已知影响因素作为 exog 传进去,预测精度往往能提升一截。用法是在 fit 和 forecast 时都传入同名的外生变量矩阵,注意预测期的外生变量要提前准备好。

# exog 是形状为 (n_samples, n_features) 的数组 model = SARIMAX( series, exog=exog_train, order=best_order, seasonal_order=best_seasonal, ) result = model.fit(disp=False) forecast = result.get_forecast(steps=12, exog=exog_future)

第二个习惯是定期重估参数。业务数据会漂移,半年前的最优阶数现在可能不是最优了。我一般设一个季度重跑一次网格搜索,把新数据纳入,看最优阶数有没有变。变了就更新,没变就继续用。

第三个习惯是留后悔药。每次定阶、每次调参、每次预测,都把参数、AIC、样本外误差记进一张表。下面是我常用的记录格式:

日期orderseasonal_orderAIC样本外MAE备注
2024-01(1,1,1)(1,1,1,12)452.318.7基线
2024-04(2,1,1)(1,1,0,12)448.116.2去掉Q后更稳
2024-07(1,1,2)(1,1,1,12)450.517.9数据漂移,回退

这张表的价值在于:当业务方问「为什么这次预测不准」时,你能拿出历史对比,说清楚是数据变了还是模型退化了。没有这张表,调参就是黑匣子,出了问题只能重来。

最后一个习惯是别迷信单一模型。SARIMA 擅长线性周期,遇到复杂非线性模式会力不从心。我通常会把 SARIMA 和一个简单的基线(比如季节朴素法:直接用去年同期的值)对比,如果 SARIMA 打不过基线,说明数据里没有 SARIMA 能利用的额外信息,这时候硬上模型就是自欺欺人。基线跑起来只要几行代码,但能帮你省下大量无效调参时间。

这套流程我在几个销量预测项目里反复用过,最深的教训是:定阶花的时间应该占整个项目的六成,剩下四成才是拟合和预测。很多人反过来,急着跑模型,结果在定阶上偷的懒,全在样本外误差上还回来了。希望帮到你。

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

RELATED READING

延伸阅读

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