ARTICLE · INTELLIGENCE

战地情报 · 详情页

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

基于Python的时间序列预测:从ARIMA到LSTM的毕设级闭环

基于Python的时间序列预测:从ARIMA到LSTM的毕设级闭环

简介:资源围绕 Python 深度学习的时间序列预测展开,完整覆盖标普500与太阳黑子两类实验场景,适合高校学生以毕设、课程设计或工程实训为背景进行入门与进阶实践。包内代码按数据集、标普500、太阳黑子三个模块组织,包含基于 LSTM、ARIMA、SVM、ARMA 等模型的 Python 脚本,以及对应 CSV 数据文件、说明文档与数据集目录,便于对照实验流程理解数据预处理、模型构建、预测与评估。资源共 19 个文件,以 Python 脚本为主,辅以 CSV 数据、Markdown 说明和 dataset 目录,压缩包约 177KB,轻量易用。目前已有 158 人学习下载,适合希望从实际案例入手掌握时间序列建模与深度学习应用的学习者,可直接参考工程结构、运行思路与项目文档进行二次开发。

1. 时间序列预测的 Python 落地版图:从 ARIMA 到 LSTM 的毕设级闭环

用 Python 做深度学习时间序列预测,最容易卡住的不是模型本身,而是数据、基准方法和评估链路没有一个能打通的完整样例。这套基于 Python 的深度学习时间序列预测项目,同时放了太阳黑子月均值和标普500 两份数据集,每个数据集目录下都有 loader、svm、arima/arma、lstm、predict、EvaluationIndex 全套脚本,属于典型的「一份资源跑完三类模型」的毕设级工程。

它能解决什么?首先是给你一个可以直接对照的模型选型参照系:统计模型、核方法、LSTM 在同样的数据上谁更稳、谁更吃数据量、谁需要先做平稳化。其次是给出一套可改参数复用的代码骨架——换数据集时只需要动 loader 和归一化部分。适合正在准备毕设、课程设计、大作业的学生,也适合想短时间横向比较三类模型效果、不想从零搭框架的从业者。

我拆这类资源的习惯是:先不看模型,先看数据文件和预处理脚本,因为时间序列项目翻车十有八九发生在差分还原、归一化泄漏、指标计算这些细活上。下面我按数据与选型、代码模块、复现流程、避坑记录、进阶技巧的顺序,把这份资源完整讲透。

2. 数据与模型选型:太阳黑子、标普500 为什么能当基准

2.1 四份 CSV:原始序列与差分序列的搭配逻辑

dataset 目录下四份文件,对应两类研究对象。

sunspot_ms.csv 是太阳黑子月均值,数值在 0 到 300 量级波动,带有非常明显的约 11 年周期。周期性强的序列适合检验模型能不能学到相位。sunspot_ms_diff.csv 是它的一阶差分结果,表示每月增量,差分后序列变得平稳,专供 ARMA 这类要求平稳输入的统计模型使用。

spv2.csv 是标普500 处理后的收盘价序列,趋势加波动混合,非平稳属性明显。sp2005-2015.csv 是从中截取的 2005 到 2015 年区间版本,样本量更小,适合快速迭代调试,也方便做同区间横向对比。标普这条线跟金融量化场景直接相关,自己扩展成收益率序列就是量化策略里常见的预处理,这份数据可以直接当练手入口。

把强周期序列和金融序列放在同一套代码下,本身就是对模型泛化能力的压力测试:太阳黑子考验周期相位捕捉,标普500 考验趋势与随机波动的分离。如果一套 loader 加三套模型在两类数据上都能给出合理指标,这个资源可信度就立住了。

拿到压缩包后我建议先跑一段探查脚本,确认每份 CSV 的列名格式,避免后面 loader 传参对不上:

import pandas as pd for path in ['dataset/sunspot_ms.csv', 'dataset/sunspot_ms_diff.csv', 'dataset/spv2.csv', 'dataset/sp2005-2015.csv']: df = pd.read_csv(path) print(path, df.shape, df.columns.tolist()) print(df.head(3).values.tolist())

先打印列名对齐一遍是血泪教训换来的习惯。这个项目里不同文件的列名不完全一致,有的是 close,有的是 value,直接套同一个 target_col 参数会报 KeyError。确认格式后再决定 loader.py 里怎么传参,能省掉后面一大半无意义的报错排查。

2.2 三类模型的适用边界:统计基线、核方法与深度网络

ARIMA/ARMA 是统计基线。ARMA 要求序列平稳,所以项目里配套了 sunspot_ms_diff 差分序列;ARIMA 则自带差分参数 d,可以直接作用在非平稳序列上。它的优势是参数少、可解释性强,对线性自相关结构拟合很稳,但处理强非线性、长程依赖会明显力不从心。

SVM(实际是 SVR,支持向量回归)在这里代表核方法路线。把滑窗生成的向量当特征、下一时刻当标签,用 RBF 核在高维空间做回归。SVR 在小样本下往往能跟 LSTM 打平,训练快、不需要 GPU,缺点是窗口拉长时特征维度变高,核矩阵计算成本涨得快,且对归一化尺度非常敏感。

LSTM 是深度学习路线的主角。遗忘门和输入门能记住长程依赖,理论上最适合带周期和趋势混合的序列。但它也是三者里最吃数据和调参的:lookback 窗口、隐层数、学习率、随机种子差一点,结果就差一大截,属于典型的需要靠固定种子和多次实验压住波动性的模型。

模型输入前提样本量需求长程依赖可解释性本资源中的定位
ARIMA/ARMA差分后平稳小弱高(p,d,q 直接可读)统计基线
SVR滑窗特征标准化中中中(靠核函数)核方法对照
LSTM窗口序列归一化大强低(黑匣子)深度学习主角

这张表建议打印出来贴在工位上,每跑完一组实验就把三个模型的 RMSE、MAE 往里填一行。时间序列预测项目的答辩或汇报,最能说明问题的就是这张对比表。

2.3 为什么要准备差分数据:ADF 检验与平稳化

统计模型对平稳性的要求是硬约束,不是可选项。平稳意味着序列的均值、方差不随时间漂移,自相关结构稳定,ARMA 系数估计才有统计意义。太阳黑子原始序列均值随周期波动,标普500 更是长期趋势明显,直接喂给 ARMA,系数会失真。

常见做法是用 ADF 检验先定量判断,statsmodels 里一行就能出结果:

from statsmodels.tsa.stattools import adfuller def check_stationarity(series, name='series'): res = adfuller(series, autolag='AIC') print(f'{name}: ADF={res[0]:.3f}, p-value={res[1]:.4f}') if res[1] < 0.05: print(' -> 拒绝单位根假设,序列平稳,可直接建模') else: print(' -> 存在单位根,建议做一阶差分后再检验') # sunspot_original / sunspot_diff 是两个 numpy 一维数组 check_stationarity(sunspot_original, 'sunspot') check_stationarity(sunspot_diff, 'sunspot_diff')

ADF 检验的零假设是序列存在单位根,也就是非平稳。p 值小于 0.05 才能拒绝零假设、认定平稳,所以看到 p 值大不要慌,那不是"平稳性显著",而是"没法证明平稳"。差分一次后 p 值通常会掉到 0.01 以下,这才是能拿去建 ARMA 的信号。项目直接备好了 diff 版本,但我还是建议自己跑一遍检查,这是理解 ARIMA 里 d=1 含义的关键一步。

3. 代码模块拆解:从 loader.py 到 EvaluationIndex.py 的完整链路

3.1 loader.py:统一数据入口、滑窗与可逆归一化

sp500 和 sunspot 两个目录里都有 loader.py,职责一致:读 CSV、切分训练测试、构造滑窗样本、做归一化。这个模块是整个工程的地基,因为三个模型的输入格式都要从这里统一出去。

import numpy as np import pandas as pd def load_series(path, date_col=None, target_col='close'): df = pd.read_csv(path) if date_col is not None: df[date_col] = pd.to_datetime(df[date_col]) df = df.sort_values(date_col).reset_index(drop=True) return df[target_col].to_numpy(dtype=np.float32) def split_series(data, train_ratio=0.8): cut = int(len(data) * train_ratio) return data[:cut], data[cut:] def make_windows(data, lookback=12, horizon=1): X, y = [], [] for i in range(len(data) - lookback - horizon + 1): X.append(data[i:i + lookback]) y.append(data[i + lookback:i + lookback + horizon]) return np.array(X), np.array(y) def minmax_fit_scale(train): vmin, vmax = train.min(), train.max() def scale(x): return (x - vmin) / (vmax - vmin + 1e-8) def inv_scale(x): return x * (vmax - vmin) + vmin return scale, inv_scale

make_windows 用滑窗把一维序列切成监督学习的输入输出对,X 是连续的 lookback 个历史值,y 是未来 horizon 个值。minmax_fit_scale 返回 scale 和 inv_scale 两个闭包函数,scale 把原始值映射到 [0,1],inv_scale 把预测结果还原回原量纲,后面 predict.py 一定用得到。三个函数里参数最需要注意的是归一化:拟合 min 和 max 只能用 train 部分,test 只能调用 scale 做变换,顺序不能反。

3.2 传统基线:arima.py/arma.py 的定阶与 svm.py 的窗口回归

ARMA/ARIMA 脚本的核心是定阶。p 是自回归阶数,q 是移动平均阶数,d 是差分次数。定阶有两条路:画 ACF/PACF 图肉眼判断,或者用 AIC 网格搜索自动选。工程上我一般直接网格搜索,范围压到合理区间:

import itertools from statsmodels.tsa.arima.model import ARIMA def best_arima_order(train, d=1, max_p=5, max_q=5): best_aic, best_order = float('inf'), None for p, q in itertools.product(range(max_p + 1), range(max_q + 1)): try: model = ARIMA(train, order=(p, d, q)).fit() if model.aic < best_aic: best_aic, best_order = model.aic, (p, d, q) except Exception: continue return best_order, best_aic

AIC 是拟合优度与参数数量的权衡,越小越好。p、q 上限取 5 是因为太阳黑子这类月度序列通常低阶就能拟合,盲目取 15 只会让搜索时间爆炸还容易过拟合。如果用的是 sunspot_ms_diff.csv,d 直接传 0,因为那份数据已经差分完毕,再差一次反而丢信息。

SVM 脚本走的是滑窗回归路线,特征就是窗口里的历史值。SVR 对特征尺度敏感,所以必须做标准化,而且注意这里按特征维度标准化,不是按样本:

from sklearn.svm import SVR from sklearn.preprocessing import StandardScaler def svm_forecast(X_train, y_train, X_test, kernel='rbf', C=1.0, epsilon=0.01): scaler = StandardScaler() X_train_s = scaler.fit_transform(X_train) X_test_s = scaler.transform(X_test) model = SVR(kernel=kernel, C=C, epsilon=epsilon) model.fit(X_train_s, y_train.ravel()) return model.predict(X_test_s)

C 是正则化强度,越大越倾向硬拟合、容易过拟合;epsilon 是 ε-不敏感损失的宽度,越大允许的误差越大、回归面越平滑。太阳黑子序列噪声不小,epsilon 给 0.01 和 0.1 出来的预测曲线平滑度差异很大,建议两个都试,再结合指标选。

3.3 lstm.py 与 model.py:可复现的深度网络骨架

LSTM 脚本在 PyTorch 下的写法可以拆成模型定义和训练循环两部分。model.py 里放模型类,lstm.py 里放训练与预测流程:

import torch import torch.nn as nn class LSTMPredictor(nn.Module): def __init__(self, input_size=1, hidden_size=64, num_layers=2, dropout=0.2): super().__init__() self.lstm = nn.LSTM(input_size, hidden_size, num_layers, batch_first=True, dropout=dropout) self.fc = nn.Sequential( nn.Linear(hidden_size, 16), nn.ReLU(), nn.Linear(16, 1) ) def forward(self, x): # x: (batch, lookback, input_size) out, _ = self.lstm(x) last = out[:, -1, :] return self.fc(last)

x 的维度必须是 (batch, lookback, input_size),所以 loader 构造出的滑窗样本要 unsqueeze 出最后一维才能喂进来。取 out[:, -1, :] 是拿最后一个时间步的隐状态做全连接映射,因为要预测的是窗口之后的未来值,中间时刻的隐状态都用不上。dropout 在 nn.LSTM 里只作用于层与层之间,单层 LSTM 传 dropout 会被直接忽略,这个细节容易当玄学踩到。

训练循环必须固定随机种子,否则同一套参数两次跑完指标能差好几个点:

def set_seed(seed=42): np.random.seed(seed) torch.manual_seed(seed) torch.cuda.manual_seed_all(seed) def train_model(model, X_train, y_train, epochs=100, lr=1e-3, batch_size=64): opt = torch.optim.Adam(model.parameters(), lr=lr) loss_fn = nn.MSELoss() loader = torch.utils.data.DataLoader( torch.utils.data.TensorDataset( torch.tensor(X_train, dtype=torch.float32).unsqueeze(-1), torch.tensor(y_train, dtype=torch.float32).view(-1, 1), ), batch_size=batch_size, shuffle=True) for epoch in range(1, epochs + 1): model.train() total = 0.0 for xb, yb in loader: opt.zero_grad() loss = loss_fn(model(xb), yb) loss.backward() opt.step() total += loss.item() if epoch % 20 == 0: print(f'epoch {epoch:3d}, loss {total / len(loader):.5f}')

这里 shuffle=True 是安全的,因为每个样本是独立滑窗,窗口内部顺序保持原样,不存在把未来信息泄进过去的问题。batch_size 64、lr 1e-3 是这套代码的稳妥起点;如果 loss 震荡不降,把 lr 降到 3e-4 再试。

3.4 predict.py 与 EvaluationIndex.py:差分还原与指标闭环

predict.py 是连接训练与评估的关键,尤其要处理统计模型的差分还原。ARMA 预测的是差分值,直接画图会得到一条围绕 0 波动的线,必须做累积求和还原:

def diff_restore(diff_forecast, last_original): restored = [last_original] for v in diff_forecast: restored.append(restored[-1] + v) return np.array(restored[1:])

还原要从训练集最后一个原始值开始累加,不是从 0 开始。如果跳过这一步,LSTM 和 ARIMA 的预测曲线不在同一量纲上,对比表就是废的。EvaluationIndex.py 提供统一的评价入口,四个模型共用一套指标:

import numpy as np def evaluate(y_true, y_pred): y_true = np.asarray(y_true, dtype=np.float32) y_pred = np.asarray(y_pred, dtype=np.float32) rmse = np.sqrt(np.mean((y_true - y_pred) ** 2)) mae = np.mean(np.abs(y_true - y_pred)) mape = np.mean(np.abs((y_true - y_pred) / (np.abs(y_true) + 1e-8))) * 100 ss_res = np.sum((y_true - y_pred) ** 2) ss_tot = np.sum((y_true - y_true.mean()) ** 2) r2 = 1 - ss_res / (ss_tot + 1e-8) return {'RMSE': rmse, 'MAE': mae, 'MAPE': mape, 'R2': r2}

RMSE 对大误差敏感,适合看有没有离谱的预测点;MAE 更稳健,是常用主指标;MAPE 在太阳黑子趋近 0 的月份会飙到几千,因为分母接近 0,这时候我只看 RMSE 和 MAE;R2 是决定系数,越接近 1 说明模型解释了越多的方差,但非平稳序列上 R2 天然虚高,要配合预测曲线图一起看,不能单独用。

4. 完整复现流程:环境搭建、训练参数与指标解读

4.1 环境准备与依赖清单

先确认 Python 版本。这套代码按 Python 3.8 设计没有问题,3.10 也兼容,但低于 3.7 时 torch 的预编译包不好找。如果电脑上还没装 Python,先按 3.8 装好再继续。我一般新建一个干净的虚拟环境,避免把系统 Python 搞乱,这也是动手深度学习类教程里反复强调的环境先行思路。

包名用途建议版本
numpy数组与滑窗计算1.24.x
pandasCSV 读取2.0.x
matplotlib预测曲线绘图3.7.x
scikit-learnSVR 与标准化1.3.x
statsmodelsARIMA/ARMA 与 ADF0.14.x
torchLSTM 训练2.x CPU 版即可
pip install numpy pandas matplotlib scikit-learn statsmodels pip install torch

只跑 LSTM 且数据量就是太阳黑子三千多个点和标普 2005-2015 区间,CPU 完全够,几分钟到十几分钟一个训练周期。不要一上来就配 CUDA,先把流程跑通再谈加速,这能省掉大量环境折腾的时间。

4.2 按数据集跑通训练与预测

复现顺序建议从统计模型开始,再上 SVR,最后上 LSTM。这样能逐步建立基线,避免一上来就被深度模型的结果带偏。

# 太阳黑子:先统计模型后深度模型 python arima.py --csv ../dataset/sunspot_ms.csv --steps 60 python svm.py --csv ../dataset/sunspot_ms.csv --lookback 12 python lstm.py --csv ../dataset/sunspot_ms.csv --lookback 12 --epochs 100 --hidden 64 # 标普500:同样三连 python arima.py --csv ../dataset/sp2005-2015.csv --steps 60 python svm.py --csv ../dataset/sp2005-2015.csv --lookback 20 python lstm.py --csv ../dataset/sp2005-2015.csv --lookback 20 --epochs 150 --hidden 128

每个脚本跑完会调用 EvaluationIndex.py 打印一组指标,正常输出类似{'RMSE': 12.34, 'MAE': 9.12, 'MAPE': 8.76, 'R2': 0.93}。把这些数字按模型、按数据集填进表格,就是答辩或汇报时最有说服力的素材。如果 arima.py 报 statsmodels 的收敛警告,加 trend='c' 或者把 maxiter 调大一点通常会缓解。

4.3 关键参数怎么调:lookback、order、hidden 与 epochs

参数不能靠猜,要跟数据周期对齐。太阳黑子是月度数据,一个太阳活动周期约 132 个月,LSTM 用 lookback=12 只能看到一年,学到的周期感有限;但 lookback 拉到 132,训练样本会少掉 132 条,SVR 的核矩阵也会变大。常见做法是取 24 或 36 折中,条件允许就做 12/24/36/60 四组对比。标普500 是日线数据,lookback 20 对应约一个月交易日,比较合理。

参数取值范围说明
lookback太阳黑子 24~60,标普 10~20对齐数据周期,过短学不到周期,过长样本骤减
d(ARIMA)0 或 1diff 版传 0,原始序列传 1
p/q 搜索上限3~5低阶足够,过高过拟合且 AIC 惩罚
hidden_size32/64/128样本量小就从 32 起步,标普用 128 更稳
lr1e-3 起步loss 震荡就降到 3e-4
epochs100~200配早停,防止过拟合测试集

一个血泪经验:epochs 不是越大越好。我见过有人把 LSTM 跑到 1000 轮,训练 loss 压到 1e-4,测试集指标反而比 100 轮时差,典型的过拟合。更稳的做法是保留验证集,每 20 轮看一次验证 loss,连续三次不降就早停。

5. 避坑记录:五条高频翻车现场的定位与修复

5.1 归一化参数用到全量数据:隐蔽的数据泄漏

现象:训练 loss 很低,测试集预测曲线跟真实曲线几乎重合,曲线过度平滑,但换一段新数据立刻失效。
原因:min 和 max 是用整条序列含测试集一起算的,测试集的尺度信息提前进入训练,属于最隐蔽的数据泄漏。
解决:归一化参数只从训练集计算,测试集只做变换。loader 里 minmax_fit_scale 只喂 train,然后用返回的 scale 处理 test,顺序不能反。检查方法是在切分前后各打印一次 min/max,看测试集的 min/max 是否影响过参数。

5.2 差分预测只输出增量,不还原量纲

现象:ARIMA 在 diff 序列上 RMSE 只有个位数,看着很漂亮,画出来是条围绕 0 抖动的线,跟真实值对不上。
原因:预测目标是差分值,评价指标也对在了差分序列上,没有还原回原始值。
解决:predict.py 里用 diff_restore 从训练集最后一个原始值开始做累积求和,再进 EvaluationIndex。判断标准很简单:还原后的 RMSE 量纲应该和原始序列一致,而不是个位数的增量量纲。

5.3 LSTM 输入少了一维,窗口被当成整段特征

现象:LSTM 跑通但 loss 死活不降,或者预测结果几乎等于均值线。
原因:把 shape 为 (样本数, lookback) 的二维数组直接喂给了 LSTM,模型把它当成二维输入,缺了特征维度,等于把整个窗口当成一个时刻的特征向量。
解决:训练前用 unsqueeze(-1) 把输入补成 (batch, lookback, 1)。定位方式很简单,打印 X_train.shape,第三维必须是 1,如果是 2D 就补轴。

5.4 序列样本整体 shuffle,未来信息混进训练集

现象:训练 loss 异常低,测试集指标却很差,且换一个 shuffle 种子结果差距很大。
原因:如果在构造滑窗之前把原始序列整体打乱,窗口内容跨越了时间顺序,未来值被当成了历史值。
解决:shuffle 只作用于滑窗样本集合,窗口内部的顺序必须保留原始时间顺序。也就是先 make_windows 得到 X/y 之后再 shuffle,不能在 load_series 之后直接打乱数据。

5.5 随机种子没固定,模型对比失去意义

现象:同一套超参数,LSTM 跑两次 RMSE 差 20% 以上,结论根本没法写进报告。
原因:PyTorch 的权重初始化和 DataLoader 的数据打乱都带随机性,没固定种子时每次实验都是新开局。
解决:训练前固定 np.random.seed、torch.manual_seed,DataLoader 也指定 generator。报告里写清 seed 值,别人按同样参数复现才能对得上数字。

这五条基本覆盖了这套资源里最容易翻车的环节。每一条都是我先跑出错误结果、再回去读代码定位出来的,修复之后这套代码才真正称得上可复现。

6. 进阶技巧:多步预测、差分还原与集成判定

资源里的脚本默认做单步预测,也就是用 lookback 长度预测未来一个点。实际项目里往往要预测未来 30、60 个点,推荐递归法:把预测出的值拼到窗口末尾,丢掉最旧的值,继续滚动预测。这套思路在 arima.py 的 --steps 参数里已经体现了,LSTM 侧可以照搬:

def recursive_predict(model, last_window, steps, inv_scale): seq = last_window.copy() preds = [] model.eval() with torch.no_grad(): for _ in range(steps): x = torch.tensor(seq, dtype=torch.float32).view(1, -1, 1) y_hat = model(x).item() preds.append(y_hat) seq = np.append(seq[1:], y_hat) return inv_scale(np.array(preds))

递归法的问题在于误差会随步数累积,预测 60 步时后半段基本就是一条平滑线。补救做法有两种:一是直接多输出法,把 LSTM 最后一层改成输出未来 N 个值,损失改为多步 MSE;二是做残差集成,用 ARIMA 抓线性主趋势,再用 LSTM 学习 ARIMA 残差里的非线性部分,最后把两条预测相加。后者在这套资源上效果通常更稳,因为太阳黑子序列的线性自相关部分占比很高。

验证阶段除了看整体指标,我还会按区间分段算 rolling RMSE。太阳黑子的活跃期和极小期表现差异很大,如果模型只在极小期好、活跃期崩,那对比表上的漂亮数字就是被平均掉的假象。分段指标加上预测曲线叠加图,才能判断模型是真的学到了周期,还是只学会了跟均值。

从那以后我每次给时间序列模型下结论前,都强制把「差分还原、随机种子固定、分段指标」三步走一遍再写结果,这套资源我也是这么验证完才敢说能复现。希望帮到你。

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

RELATED READING

延伸阅读

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