简介本资源面向计算机、电子信息工程、数学等专业的大学生及算法入门者提供SARIMA-LSTM组合模型的完整时间序列预测实现方案可用于课程设计、期末大作业与毕业设计。资源包共3个文件包含2个CSV数据集与1个Python源码文件压缩包约51KB其中CSV用于模型训练与验证py文件承载季节性差分自回归移动平均与长短期记忆网络的融合建模流程。代码采用参数化编程参数可灵活调整编程思路清晰并配有保姆级注释几乎一行一注释便于零基础读者理解每一步建模逻辑。作者为某大厂资深算法工程师具备八年Matlab与Python算法仿真经验擅长智能优化算法、神经网络预测与信号处理等方向。目前已有742人学习适合希望快速掌握组合预测模型、完成实战项目的读者参考。1. 从单模型翻车说起SARIMA-LSTM 到底在时间序列预测里补了什么位电商大促的日销预测我最早只用 SARIMA。平稳性检验过了ACF/PACF 拖尾截尾也看得过去结果一到大促当天预测曲线像被人按住了头——它只认季节周期认不出「今年这个节点比去年猛一截」的非线性突变。后来换成纯 LSTM训练集拟合得漂亮验证集一遇到春节这种长周期位移又开始飘。SARIMA-LSTM 组合模型就是在这个夹缝里被逼出来的SARIMA 负责吃掉线性部分和季节性差分结构LSTM 去啃残差里的非线性关系两者串行叠加。这篇面向的是已经会用 Python 做时间序列、但被单一模型精度卡住的从业者从数据预处理、SARIMA 定阶、残差提取到 LSTM 训练和反向还原每一步都给可复现的代码和参数边界源码和数据按常见工程结构组织跟着改路径就能跑。2. SARIMA 与 LSTM 串行组合为什么不是简单加权平均2.1 组合模型的数学拆解与选型理由时间序列预测里有个反复被验证的结论没有任何单一模型能同时刻画线性和非线性。SARIMA 的本质是线性滤波器它把序列拆成趋势、季节、残差三块参数 (p,d,q)(P,D,Q,s) 描述的是自回归和移动平均的线性依赖。LSTM 靠门控机制记忆长程依赖擅长的是非线性映射但它对纯线性趋势的建模效率反而不如差分后的 ARIMA。串行组合的逻辑是先用 SARIMA 拟合原始序列拿到拟合值 ŷ_linear 和残差 e y - ŷ_linear。如果残差里还有结构白噪声检验不通过说明线性模型没吃干净这部分交给 LSTM 去学。最终预测 ŷ ŷ_linear ŷ_residual。这比并联加权平均更稳因为残差本身就是「线性模型解释不了的部分」LSTM 的目标明确不会和 SARIMA 抢同一段信息。常见做法是残差 LSTM 的输入用滑动窗口构造监督样本窗口长度取一个季节周期比如 s7 就取 7 或 14。我一般会先画残差的 ACF 图如果残差已经接近白噪声就别硬上 LSTM加了反而过拟合。2.2 环境准备与数据加载的最小可跑代码先把依赖钉死避免版本玄学。statsmodels 负责 SARIMAtorch 负责 LSTMsklearn 做归一化和指标。pip install pandas numpy statsmodels torch scikit-learn matplotlib数据加载和缺失处理是第一步时间序列最怕时间索引不对齐。import pandas as pd import numpy as np # 读取数据假设两列date, value df pd.read_csv(data/timeseries.csv, parse_dates[date]) df df.sort_values(date).set_index(date) # 按天重采样缺失用线性插值避免 SARIMA 报错 df df.asfreq(D) df[value] df[value].interpolate(methodlinear).ffill().bfill() series df[value].astype(float) print(series.shape, series.index.min(), series.index.max())逻辑说明asfreq(D)强制日频缺失日期会变成 NaN插值后再前后填充兜底。参数上如果数据本身是小时级把D换成H季节周期 s 也要跟着改。这一步不做后面 SARIMA 的差分和定阶会直接报「数组含 NaN」。2.3 SARIMA 定阶用 AIC 网格搜索代替肉眼判图ACF/PACF 判图对新手不友好我一般直接用 AIC 网格搜索范围给窄一点省时间。import warnings from statsmodels.tsa.statespace.sarimax import SARIMAX warnings.filterwarnings(ignore) p d q range(0, 3) P D Q range(0, 2) s 7 # 季节周期按业务改 best_aic np.inf best_order None best_seasonal None for _p in p: for _d in d: for _q in q: for _P in P: for _D in D: for _Q in Q: try: model SARIMAX( series, order(_p, _d, _q), seasonal_order(_P, _D, _Q, s), enforce_stationarityFalse, enforce_invertibilityFalse, ) res model.fit(dispFalse) if res.aic best_aic: best_aic res.aic best_order (_p, _d, _q) best_seasonal (_P, _D, _Q, s) except Exception: continue print(Best AIC:, best_aic) print(Order:, best_order, Seasonal:, best_seasonal)逻辑说明三重循环遍历低阶参数enforce_stationarityFalse是为了避免部分参数组合直接抛异常中断搜索。参数建议p、q 不超过 3P、Q 不超过 2d 和 D 一般取 0 或 1除非序列明显不平稳。AIC 最小不代表残差一定白噪声定完阶还要做残差检验。2.4 提取残差并构造 LSTM 监督样本SARIMA 拟合完残差就是 LSTM 的标签来源。# 用最优参数重新拟合 final_model SARIMAX( series, orderbest_order, seasonal_orderbest_seasonal, enforce_stationarityFalse, enforce_invertibilityFalse, ) final_res final_model.fit(dispFalse) fitted final_res.fittedvalues resid series - fitted # 残差 resid resid.dropna() print(残差均值:, resid.mean(), 标准差:, resid.std())残差拿到后用滑动窗口构造 LSTM 的输入输出。窗口长度我一般取一个季节周期s7 就取 7。def make_supervised(data, window): X, y [], [] for i in range(len(data) - window): X.append(data[i : i window]) y.append(data[i window]) return np.array(X), np.array(y) window 7 resid_values resid.values.reshape(-1, 1) from sklearn.preprocessing import MinMaxScaler scaler MinMaxScaler(feature_range(-1, 1)) resid_scaled scaler.fit_transform(resid_values).flatten() X, y make_supervised(resid_scaled, window) X X.reshape((X.shape[0], X.shape[1], 1)) print(X.shape, y.shape)逻辑说明MinMaxScaler缩到 (-1,1) 是因为 LSTM 默认 tanh 激活这个区间收敛更稳。窗口长度是核心参数太短学不到周期太长样本数骤减。残差本身均值接近 0缩放不会破坏结构。3. LSTM 残差建模网络结构、训练循环与反向还原3.1 LSTM 网络结构设计与关键参数残差序列通常不长网络别堆太深。我一般用一层 LSTM 加一层全连接隐藏单元 32 或 64 就够。import torch import torch.nn as nn class ResidLSTM(nn.Module): def __init__(self, input_size1, hidden_size64, num_layers1): super().__init__() self.lstm nn.LSTM(input_size, hidden_size, num_layers, batch_firstTrue) self.fc nn.Linear(hidden_size, 1) def forward(self, x): out, _ self.lstm(x) out out[:, -1, :] # 取最后一个时间步 return self.fc(out) device torch.device(cuda if torch.cuda.is_available() else cpu) model ResidLSTM(hidden_size64).to(device) criterion nn.MSELoss() optimizer torch.optim.Adam(model.parameters(), lr1e-3)逻辑说明batch_firstTrue让输入维度是 (batch, seq, feature)符合习惯。out[:, -1, :]只取最后时间步因为预测目标是窗口后的下一个值。hidden_size 从 32 起调超过 128 在残差这种小数据上基本是过拟合。3.2 训练循环与早停策略残差数据量小必须早停否则训练集 loss 降到底而验证集起飞。from torch.utils.data import TensorDataset, DataLoader X_tensor torch.tensor(X, dtypetorch.float32) y_tensor torch.tensor(y, dtypetorch.float32).unsqueeze(1) dataset TensorDataset(X_tensor, y_tensor) loader DataLoader(dataset, batch_size16, shuffleTrue) epochs 200 patience 20 best_loss np.inf wait 0 for epoch in range(epochs): model.train() epoch_loss 0 for xb, yb in loader: xb, yb xb.to(device), yb.to(device) optimizer.zero_grad() pred model(xb) loss criterion(pred, yb) loss.backward() optimizer.step() epoch_loss loss.item() avg_loss epoch_loss / len(loader) if avg_loss best_loss: best_loss avg_loss wait 0 torch.save(model.state_dict(), best_lstm.pth) else: wait 1 if wait patience: print(fEarly stop at epoch {epoch}) break if epoch % 20 0: print(fEpoch {epoch}, Loss {avg_loss:.6f})逻辑说明batch_size 16 适合几百到几千条残差样本。patience20 是经验值loss 波动大可以放宽到 30。保存 best 权重是关键最后加载的是验证最优而不是最后一轮。3.3 预测结果反向还原与组合LSTM 输出的是缩放后的残差预测要反缩放再加回 SARIMA 的线性预测。model.load_state_dict(torch.load(best_lstm.pth)) model.eval() # 取最后 window 个残差做预测输入 last_window resid_scaled[-window:].reshape(1, window, 1) last_window torch.tensor(last_window, dtypetorch.float32).to(device) with torch.no_grad(): resid_pred_scaled model(last_window).cpu().numpy() resid_pred scaler.inverse_transform(resid_pred_scaled) # 反缩放 print(下一期残差预测:, resid_pred.flatten()[0]) # SARIMA 线性预测 sarima_forecast final_res.forecast(steps1).values[0] final_pred sarima_forecast resid_pred.flatten()[0] print(SARIMA 线性预测:, sarima_forecast) print(组合模型最终预测:, final_pred)逻辑说明scaler.inverse_transform必须用同一个 scaler不能重新 fit。final_res.forecast(steps1)给出下一期线性预测加上残差预测就是组合结果。多步预测时SARIMA 用forecast(stepsn)LSTM 需要滚动构造窗口每预测一步把结果填回窗口。3.4 评估指标与对比基线别只看 MSE要和单 SARIMA、单 LSTM 对比才有说服力。from sklearn.metrics import mean_absolute_error, mean_squared_error def evaluate(y_true, y_pred): mae mean_absolute_error(y_true, y_pred) rmse np.sqrt(mean_squared_error(y_true, y_pred)) mape np.mean(np.abs((y_true - y_pred) / y_true)) * 100 return mae, rmse, mape # 假设 test_true 是测试集真实值test_pred 是组合预测 # mae, rmse, mape evaluate(test_true, test_pred) # print(fMAE{mae:.4f}, RMSE{rmse:.4f}, MAPE{mape:.2f}%)参数说明MAPE 在真实值接近 0 时会爆炸如果序列有零值改用 SMAPE 或直接看 RMSE。对比基线时单 SARIMA 的预测就是final_res.forecast单 LSTM 是把原始序列直接喂 LSTM三者用同一测试集切分。4. 避坑与排查SARIMA-LSTM 组合里最容易翻车的 5 个点4.1 现象SARIMA 拟合报「数组含 NaN 或 inf」→ 原因重采样后缺失没填 → 解决asfreq 后立即 interpolate ffill bfill这个坑几乎每个新手都踩。asfreq会把缺失日期变成 NaN直接丢给 SARIMAX 就报错。解决顺序不能乱先interpolate(methodlinear)补中间再ffill().bfill()兜首尾。如果数据本身有 inf先replace([np.inf, -np.inf], np.nan)再走同一套。4.2 现象AIC 网格搜索跑了几小时没结果 → 原因参数范围给太大 → 解决p/q 限 0-2P/Q 限 0-1d/D 限 0-1我见过有人把 p、q 开到 7六个循环嵌套直接爆炸。SARIMA 在业务序列上低阶就够p、q 超过 3 基本是过拟合。先把范围收窄跑一轮看最优落在边界再扩。另外enforce_stationarityFalse能避免大量参数组合抛异常但定完阶要用plot_diagnostics确认残差。4.3 现象LSTM 训练 loss 降到很低但预测一塌糊涂 → 原因残差已接近白噪声LSTM 在学噪声 → 解决先做 Ljung-Box 检验p0.05 就别上 LSTM这是最隐蔽的坑。SARIMA 定阶好的话残差可能已经是白噪声这时候 LSTM 学到的全是随机波动验证集必然翻车。跑 Ljung-Boxfrom statsmodels.stats.diagnostic import acorr_ljungbox lb acorr_ljungbox(resid, lags[10], return_dfTrue) print(lb)如果 p 值大于 0.05说明残差无自相关组合模型退化成单 SARIMA别硬加 LSTM。4.4 现象组合预测比单 SARIMA 还差 → 原因残差缩放用了全局 scaler预测时反缩放错位 → 解决scaler 只在训练集 fit预测用同一 scaler 反变换MinMaxScaler如果在全量残差上 fit测试集信息泄漏反缩放时数值对不上。正确做法是切训练/测试后scaler 只在训练残差上 fit测试残差用transform。反缩放时inverse_transform必须用同一个 scaler 对象不能重新建。4.5 现象多步预测越往后越离谱 → 原因LSTM 滚动预测误差累积 → 解决多步改用 SARIMA 主导LSTM 只修一步或限制预测步长组合模型的多步预测LSTM 每步的残差预测都会带入下一步窗口误差指数累积。我的做法是一步预测用组合多步预测只用 SARIMA或者 LSTM 最多修 2-3 步。如果业务必须长步长考虑在 LSTM 里用 seq2seq 结构直接输出多步而不是滚动。5. 把组合模型跑稳的进阶技巧残差白噪声检验与滚动重训5.1 用 Ljung-Box 决定要不要上 LSTM组合模型不是万能药上之前先做残差白噪声检验。如果 SARIMA 残差已经是白噪声LSTM 就是画蛇添足。我一般把 Ljung-Box 的 p 值当开关p 0.05 才继续 LSTM 分支否则直接输出 SARIMA 结果。这个判断能省掉大量无效训练时间。from statsmodels.stats.diagnostic import acorr_ljungbox lb_test acorr_ljungbox(resid, lags[7, 14, 21], return_dfTrue) print(lb_test) # p 值小于 0.05 说明残差还有自相关值得上 LSTM if (lb_test[lb_pvalue] 0.05).any(): print(残差非白噪声启用 LSTM 残差建模) else: print(残差接近白噪声直接用 SARIMA 预测)参数说明lags 取一个季节周期及其倍数s7 就取 7、14、21。只要有一个 lag 的 p 值小于 0.05就说明残差还有可利用结构。5.2 滚动重训让模型跟上分布漂移时间序列的分布会漂移一次性训练好的模型几个月后精度下降。我习惯用滚动窗口重训每次预测前用最近 N 个点重新定阶和训练 LSTM。N 取 3-5 个季节周期。def rolling_forecast(series, train_window, forecast_steps): predictions [] for i in range(len(series) - train_window - forecast_steps 1): train series.iloc[i : i train_window] # 这里复用前面的 SARIMA 定阶 LSTM 训练流程 # 为简洁假设封装成 fit_predict 函数 pred fit_predict(train, stepsforecast_steps) predictions.append(pred) return predictions逻辑说明滚动重训计算量大但能显著提升长期精度。train_window 太小模型不稳太大跟不上漂移。我一般先用固定窗口跑一遍看精度衰减曲线再决定重训频率。5.3 参数速查表参数常用范围作用调整方向SARIMA p/q0-3自回归/移动平均阶数AIC 最小SARIMA P/Q0-2季节自回归/移动平均残差 ACF 判断季节周期 s7/12/24周期长度按业务频率LSTM window1-2 个 s输入窗口残差 ACF 截尾LSTM hidden32-128隐藏单元从 32 起调LSTM lr1e-3 ~ 1e-4学习率loss 震荡则降patience20-30早停轮数波动大则增这张表是我踩坑后固化的默认值新手直接抄跑通再微调。5.4 一个我反复用的习惯每次跑完组合模型我一定把 SARIMA 预测、LSTM 残差预测、最终组合预测三条曲线画在同一张图上。如果 LSTM 残差预测的幅度和 SARIMA 预测幅度量级差不多说明残差没被缩放好或者 LSTM 在拟合趋势而不是残差。正常情况残差预测应该只占最终预测的 5%-20%。这个习惯帮我抓过好几次反缩放错位的 bug。希望帮到你。本文还有配套的精品资源点击获取
阅读完成 · 觉得有帮助?