
简介这是一份面向Python初学者与高校学生的ARIMA-SSA-LSTM组合时间序列预测完整实现覆盖数据、源码与保姆级注释适合计算机、电子信息工程、数学等专业用于课程设计、期末大作业和毕业设计。压缩包共3个文件含两个CSV格式的时序数据集与一个Python脚本整体仅51KB轻量易用。代码采用参数化编程关键参数可方便更改几乎一行一注释便于入门者理解模型原理从数据加载、划分到ARIMA、SSA、LSTM三种模型构建及其组合预测流程均有清晰实现可直接运行参考并支持快速迁移到其他时间序列数据。实现将统计学模型与深度学习方法结合以实际城市时序数据为例既可作为教学演示也能作为毕业设计的基础框架。已有509人学习浏览适合希望系统掌握时序预测建模、快速搭建实验代码的读者下载使用。1. 先立住的选型逻辑为什么把 ARIMA、SSA、LSTM 串成一条线拿到一批时间序列数据直接套 LSTM 往往是第一反应但这个做法在课程设计或实际业务里很容易翻车。LSTM 擅长捕捉非线性模式但对趋势项和周期性成分的拟合效率并不高尤其在数据长度只有几百个点、噪声又明显的场景下纯 LSTM 训练出的曲线要么滞后严重要么把噪声也学进去了。ARIMA 恰好相反它对线性趋势和自相关结构有成熟的统计基础可一旦序列里混入强非线性波动残差就会带着明显的信息泄漏。把两者简单相加不如先做一步 SSA奇异谱分析分解先把原始序列拆成趋势、周期和噪声三类分量再分别交给 ARIMA 和 LSTM 处理最后合成预测结果。这条 ARIMA-SSA-LSTM 组合路径解决的核心问题是让线性模型和非线性模型各管一段而不是在一个模型里互相妥协。这套完整源码ARIMA-SSA-LSTM.zip附带焦作本地环境下的观测序列数据代码采用参数化编程几乎一行一注释全程基于 Anaconda PyCharm TensorFlow 运行。适合做课程设计、期末大作业或毕业设计的学生快速复现也适合刚接触组合预测的算法工程师拿来做 baseline。下面按「分解 → 线性建模 → 非线性建模 → 合成评估」的顺序展开每一步都会给出可以直接运行的参数配置和踩坑提示。2. SSA 分解与重构去噪边界、窗口长度和分组参数2.1 为什么组合预测要先做 SSA 而不是直接差分ARIMA 建模前通常要做差分消除趋势但差分是一次性操作会把趋势和周期信息揉在一起后续 LSTM 拿到的输入里仍然混着噪声。SSA 的核心思路是把一维序列嵌入成轨迹矩阵再做奇异值分解按奇异值大小把原始序列重构为若干加性分量系数大的分量对应趋势和主周期系数小的对应噪声和细节波动。这样做的关键是ARIMA 只处理光滑的趋势分量时定阶更稳LSTM 只处理非线性残差分量时不需要自己额外去噪收敛更快、过拟合更少这是组合模型优于单体模型的主要原因。实际项目中我一般先把序列标准化再做 SSA 分解。标准化不是必须的但如果原始序列量级在千以上LSTM 的梯度很容易震荡tanh 和 sigmoid 激活函数在输入过大时直接饱和。源码里用的是 MinMaxScaler把数据压缩到 [0,1] 区间后再进入分解环节注意 SSA 分解可以在标准化前也可以在标准化后做两者结果在还原预测值时会有细微差异建议在标准化后做这样噪声阈值更容易统一设置。2.2 窗口长度 L 的选择不只是一个超参数SSA 的窗口长度 L 决定轨迹矩阵的行数通常取序列长度 N 的 30% 到 50%且必须满足 2 到 N/2 之间。L 太小周期分量提取不完整L 太大矩阵规模变大分解出的分量数量变多分组时反而难选。对于焦作这个数据集如果序列长度在 500 个点左右L 取 120 左右效果比较稳既覆盖了主周期又不至于让噪声分量过于细碎。import numpy as np import pandas as pd from scipy.linalg import svd def ssa_decompose(series, L): # 嵌入将一维序列构造成轨迹矩阵 X每行是长度为 L 的滞后窗口 n len(series) K n - L 1 X np.zeros((L, K)) for i in range(K): X[:, i] series[i:i L] # 奇异值分解U 是左奇异向量s 是奇异值Vt 是右奇异向量的转置 U, s, Vt svd(X, full_matricesFalse) return U, s, Vt, K # 参数说明 # L窗口长度。L 越大趋势提取越平滑但计算越慢周期细节可能被吞掉。 # 返回值中的 s 是奇异值数组按从大到小排列后续分组时按 s 的贡献率选前 r 个分量。 # 常见做法是取累计贡献率超过 85% 的前 r 个分量作为主成分其余归为噪声。这里用到的是 scipy.linalg.svd 而非 numpy.linalg.svd因为前者对非方阵的处理更稳定。分解后每个分量可以独立重构回原长度重构方式是把对应奇异值和左右特征向量外积再对角平均。分组阶段源码里直接按奇异值贡献率排序取前 r 个但更讲究的做法是观察奇异值序列的拐点曲线从陡降转为平缓的位置就是信号和噪声的分界。2.3 分组重构把趋势和残差拆干净拆出来的分量不能直接丢给模型必须先做分组重构。把选中的 r 个分量求和得到趋势项趋势部分把剩余的分量求和得到残差项残差部分。判断标准是看重构后的趋势项是否还有明显周期性如果趋势项里还有规律波动说明 r 取小了如果残差项里还能看到长周期走势说明 L 取大了。def ssa_reconstruct(series, L, r): U, s, Vt, K ssa_decompose(series, L) # 取前 r 个主成分重构趋势项 trend np.zeros(len(series)) for i in range(r): # 重构矩阵奇异值 * 左向量外积右向量 Xi s[i] * np.outer(U[:, i], Vt[i, :]) # 对角平均把 L x K 的矩阵还原成长度为 N 的序列 for idx in range(len(series)): count 0 val 0 for row in range(L): col idx - row if 0 col K: val Xi[row, col] count 1 trend[idx] val / count residual series - trend return trend, residual # 参数说明 # r 是主成分个数。源码里默认按贡献率自动计算也可以手动指定。 # 判断 r 是否合适的快速方法把 trend 画出来看是否光滑把 residual 画出来看是否近似零均值随机波动。我第一次跑这个数据集时发现把 r 设得太小比如只取前 3 个奇异值趋势项里还是锯齿状设得太大比如前 20 个残差里几乎没什么信息留给 LSTM。最终是画奇异值折线图找到拐点在 7 到 9 之间于是 r 取 8趋势项足够光滑残差项的均值接近 0方差也没有明显时变。这一步是整个流水线的地基后面 ARIMA 和 LSTM 的效果都取决于趋势和残差是否拆得干净。3. ARIMA 对趋势分量的建模定阶、白噪声检验与滚动预测3.1 为什么趋势分量适合用 ARIMA 而不是直接用回归SSA 分解出的趋势项通常是缓慢变化的光滑曲线几乎没有突变和高频波动。对这种序列ARIMA 的优势在于它能显式建模自回归结构和滑动平均效应预测时不会像纯回归那样忽略时间顺序。另一个实际原因是趋势项的样本量往往不大几百个点ARIMA 在这种小样本场景下比 LSTM 更稳不会因为数据不足而过拟合。ARIMA(p,d,q) 三个参数里d 表示差分阶数。SSA 已经把趋势项变得相对光滑但未必平稳所以 d 一般取 1 或 2p 和 q 需要借助 ACF 和 PACF 图来定阶。源码里用的是自动定阶加人工校验的混合方式先跑一遍 AIC 网格搜索再看残差是否通过 Ljung-Box 白噪声检验如果通不过就手动扩一档 p 或 q。3.2 AIC 网格搜索与手动定阶结合import itertools from statsmodels.tsa.arima.model import ARIMA from statsmodels.stats.diagnostic import acorr_ljungbox def fit_arima(trend, max_p5, max_q5): best_aic np.inf best_order None best_model None # 网格搜索所有 (p, d, q) 组合d 限制在 0~2 for p in range(max_p 1): for q in range(max_q 1): for d in range(3): try: model ARIMA(trend, order(p, d, q)) fitted model.fit() if fitted.aic best_aic: best_aic fitted.aic best_order (p, d, q) best_model fitted except Exception: continue # Ljung-Box 检验p 值大于 0.05 说明残差是白噪声模型可接受 resid best_model.resid lb_test acorr_ljungbox(resid, lags10, return_dfTrue) return best_model, best_order, lb_test # 参数说明 # max_p 和 max_q 不要一开始就设成 10样本量不大时高阶参数会把残差拟合得很干净但预测失效。 # acorr_ljungbox 的 lags 一般取 min(10, n//5)n 是趋势项长度。 # 若 lags 太大检验对短期相关性不敏感太小则容易漏掉周期性残差。这段代码直接跑会有点慢因为 ARIMA 的参数估计需要迭代优化p、d、q 全组合接近 90 组每组都要做卡尔曼滤波。项目场景下我一般先固定 d1只搜索 p 和 q把候选组合压缩到 36 组找到最优后再尝试 d2 对比一次能省掉一半时间。3.3 滚动预测与一步误差反馈ARIMA 的预测不能一次性预测完整个测试集再结束更稳的做法是一步预测后把真实值滚动加入历史窗口用于下一步预测。这样能避免误差在长周期预测里指数累积同时更贴近实际业务里「每来一个新观测就更新一次模型」的节奏。def arima_rolling_forecast(trend_train, trend_test, order): history list(trend_train) forecasts [] for t in range(len(trend_test)): model ARIMA(history, orderorder) fitted model.fit() # 只预测下一步使用 forecast 而不是 predict自动处理差分还原 yhat fitted.forecast(steps1)[0] forecasts.append(yhat) # 把真实值加入历史确保下一步预测基于完整信息 history.append(trend_test[t]) return np.array(forecasts) # 说明 # 每次循环都重新 fit 一次代码更清晰但耗时。数据量大时可以用 fitted.append 增量更新。 # 核心逻辑是真实值一旦观测到就立刻进入历史窗口不允许使用未来信息。 # 这里用真实值滚动测试的是模型结构本身的质量如果换成预测值滚动测的是长周期稳定性但误差会累积。跑完这套滚动预测把 ARIMA 的预测结果记为pred_trend。注意这里预测的是 SSA 分解出来的趋势项还原到原始序列量纲时要做一次逆标准化否则后面和 LSTM 结果相加时量纲对不上。源码里这一步放在最后统一处理我建议在 ARIMA 和 LSTM 各自输出后分别做逆标准化再相加能更早暴露量纲错误。4. LSTM 非线性分量的训练细节与 TensorFlow 实现4.1 输入构造用多少个历史点预测下一个点SSA 分解出的残差分量是近似零均值的波动序列LSTM 的任务是学习这些波动里的短期依赖关系。输入构造是滑动窗口形式比如用过去 12 个时间点的残差值预测下一个点的残差值。这个窗口长度look_back是核心超参数太小模型学不到周期太大训练数据变少且引入无关噪声。def create_lstm_dataset(data, look_back12): X, y [], [] for i in range(len(data) - look_back): X.append(data[i:i look_back]) y.append(data[i look_back]) return np.array(X), np.array(y) # 参数说明 # look_back 建议设为序列主周期长度。焦作数据如果按日观测主周期可能是 7 或 30先看 ACF 图的第一个显著峰值。 # X 的形状是 (样本数, look_back, 1)LSTM 要求三维输入最后一维是特征数。 # 如果有多变量特征把特征数改成实际维度但本项目中只使用单变量残差序列。这里有个容易踩的坑LSTM 默认在序列维度上有时间步的概念look_back就是时间步数。如果直接把特征数放进去而忘记 reshapeTensorFlow 会在第一层就报三维输入错误。源码里在create_lstm_dataset之后用X X.reshape(X.shape[0], X.shape[1], 1)做了显式 reshape这个操作在目标函数里是必要的。4.2 构建 LSTM 模型层数、Dropout 与早停残差分量波动剧烈LSTM 不能做得太深两层已经足够。第一层 LSTM 返回完整序列第二层只返回最后一步输出然后接全连接层输出单值。Dropout 放在 LSTM 层之间而不是 LSTM 内部这样能在不破坏时序记忆的前提下抑制过拟合。from tensorflow.keras.models import Sequential from tensorflow.keras.layers import LSTM, Dense, Dropout from tensorflow.keras.callbacks import EarlyStopping def build_lstm(look_back12, units64): model Sequential() # 第一层 LSTMreturn_sequencesTrue 才能把每个时间步的输出传给下一层 model.add(LSTM(units, return_sequencesTrue, input_shape(look_back, 1))) model.add(Dropout(0.2)) # 第二层 LSTM只返回最终输出 model.add(LSTM(units, return_sequencesFalse)) model.add(Dropout(0.2)) # 全连接层输出一个预测值 model.add(Dense(1)) model.compile(optimizeradam, lossmse) return model # 参数说明 # units 是隐藏状态维度64 在大多数单变量序列上够用增大到 128 不会带来明显收益反而拖慢训练。 # Dropout 0.2 是常见起点如果训练集损失和验证集损失差距大可以提到 0.3 或 0.4。 # 损失函数用 mse 而不是 mae因为 mse 对大误差敏感能更快把显著偏差压下来。训练时加EarlyStopping监控验证集损失patience15表示连续 15 个 epoch 验证损失不下降就停止。源码还保留了ModelCheckpoint保存最优权重防止最后 epoch 过拟合导致权重变差。我一般会把训练 epoch 上限设到 200实际上大多在 40 到 80 个 epoch 就触发了早停。4.3 训练与预测验证集切分、shuffle 和还原残差序列构造出的样本是强时序相关的直接随机切分训练验证集会泄漏未来信息。这里必须按时间顺序切分前 70% 做训练后 30% 做验证。同时model.fit里的shuffleFalse也必须显式设置否则默认的 True 会把时序打乱验证集里混进训练窗口的前置信息评估结果虚高。import numpy as np def train_lstm(residual, look_back12, epochs200): X, y create_lstm_dataset(residual, look_back) X X.reshape(X.shape[0], X.shape[1], 1) # 按时间顺序切分不做随机 shuffle split int(len(X) * 0.7) X_train, X_val X[:split], X[split:] y_train, y_val y[:split], y[split:] model build_lstm(look_back) early_stop EarlyStopping(monitorval_loss, patience15, restore_best_weightsTrue) model.fit(X_train, y_train, validation_data(X_val, y_val), epochsepochs, batch_size32, shuffleFalse, callbacks[early_stop], verbose0) # 预测并还原为原始残差量纲 pred_residual model.predict(X_val).flatten() return pred_residual, y_val # 参数说明 # batch_size32 是适中配置。残差样本量小于 500 时改 16 会更稳大于 2000 时改 64 能加速。 # predict 返回的形状是 (样本数, 1)flatten 后和 y_val 对齐。 # 注意这里预测的是「下一个残差」还原时直接加到 trend 预测上即可。训练完 LSTM 后把pred_residual和上一步 ARIMA 的pred_trend对齐相加得到组合预测结果。这里最容易出错的点是两个预测序列的长度不一致ARIMA 滚动预测的长度等于测试集长度而 LSTM 的预测长度等于测试集长度减look_back需要把前look_back个点丢弃或补零对齐。源码里用的是丢弃前look_back个点的方式保证每个预测点都有完整的上下文信息。5. 模型评估与调参验证滑窗对比、残差复检和可复现配置5.1 评估指标RMSE、MAE、MAPE 一起看只看 RMSE 会忽略小量纲误差的影响只看 MAPE 又会在真实值接近零时失真。三个指标一起看才能判断组合模型是否真的比单体模型强。下面这组函数可以嵌进任何源码里后续换数据集也不用改。from sklearn.metrics import mean_squared_error, mean_absolute_error def evaluate_forecast(y_true, y_pred): mse mean_squared_error(y_true, y_pred) rmse np.sqrt(mse) mae mean_absolute_error(y_true, y_pred) # MAPE 计算时需要排除真实值为 0 的点避免除零 mask y_true ! 0 mape np.mean(np.abs((y_true[mask] - y_pred[mask]) / y_true[mask])) * 100 return {RMSE: rmse, MAE: mae, MAPE(%): mape} # 参数说明 # 如果 y_true 里有大量接近 0 的值MAPE 数值会异常大此时只看 RMSE 和 MAE。 # 对比基线时同一份数据上跑纯 LSTM 和纯 ARIMA差值就是 SSA 分解带来的增益。在焦作数据上按 70% 训练、30% 测试的切分跑一轮组合模型的 RMSE 通常会比纯 LSTM 低一截原因是 ARIMA 把趋势抓走后LSTM 只需要拟合残差波动不需要额外学习大尺度的上升或下降形态拟合压力小得多。如果发现 RMSE 没有明显改善优先检查 SSA 分组的 r 是否偏大残差里是否还残留趋势信息。5.2 残差复检预测误差的自相关能不能忽略预测做完残差检验是收尾前的最后一道关卡。对预测残差做 Ljung-Box 检验如果 p 值小于 0.05说明误差里还有可建模的结构模型没有把信息榨干。这时候不是简单地把 LSTM 加深就能解决更大概率是 SSA 分组没拆干净或者 ARIMA 定阶不充分。from statsmodels.stats.diagnostic import acorr_ljungbox def residual_check(y_true, y_pred): resid y_true - y_pred lb acorr_ljungbox(resid, lags10, return_dfTrue) return lb # 参数说明 # 返回的 DataFrame 中 lb_pvalue 列如果全部大于 0.05说明残差近似白噪声。 # 如果某一个 lag 的 p 值小于 0.01把对应 lag 的周期长度作为线索回到 LSTM 的 look_back 参数上调整。 # 这一步是模型质量的终检也是答辩或报告里最常见的加分项。我在复现这个流程时发现一个反复出现的问题LSTM 预测残差的方差比真实残差小一截导致合成后的预测曲线在波峰处偏保守。原因是 LSTM 的 MSE 损失函数天然倾向预测均值附近的点对尖峰不敏感。处理方法是在合成预测时对残差预测乘以一个 1.05 到 1.15 的放大系数批次验证后确定具体值。这个操作在论文里站不住脚但做课程设计或工程 baseline 时很实用可以作为一个可调的超参数写进配置文件。5.3 参数化配置与可复现性收尾源码里把 SSA 的窗口 L、分组数 r、ARIMA 的阶数 (p,d,q)、LSTM 的 look_back 和 units 全部集中在一个配置区改成字典形式之后换数据集只需要改几个数。我的习惯是把配置写成 JSON 或 Python 字典这样做多次实验时能快速回溯是哪组参数跑出的结果而不是修改完代码后找不到原始配置。config { ssa_window: 120, ssa_rank: 8, arima_order: [1, 1, 1], lstm_look_back: 12, lstm_units: 64, dropout: 0.2, batch_size: 32, epochs: 200, train_ratio: 0.7 } # 使用建议 # 每次实验前把 config 用 json.dump 存档文件名带时间戳。 # 调整顺序建议先调 ssa_rank再调 arima_order最后调 lstm_look_back。 # 前两个参数影响数据质量最后一个影响模型容量调参方向不要反。完整流程在 Anaconda PyCharm 环境下的依赖安装也值得单独说明TensorFlow 的 CPU 版本足够跑这个规模的序列数据不需要 GPUstatsmodels 和 scikit-learn 用 conda 安装最省事避免 pip 依赖冲突。源码自带的环境文件里如果没有固定版本号装最新稳定版即可API 层面的兼容性在这个代码量级下不会有问题。如果想进一步验证模型的稳定性可以改train_ratio做多组滑窗对比每组切分下记录三指标最终取均值和方差上报这就是一份完整的预测模型评估报告了。本文还有配套的精品资源点击获取