流感时间序列预测:ARIMA、LSTM与Transformer对比实战

发布时间:2026/10/25 12:38:39
流感时间序列预测:ARIMA、LSTM与Transformer对比实战

简介:一份基于ARIMA、LSTM、Transformer等模型的流感时间序列预测Python源码项目,面向正在做课程设计或期末大作业的计算机相关专业学生,也适合需要项目实战练习的学习者。项目经导师指导并获认可,为98分高分作业,涵盖数据预处理、平稳性检验(ADF、ACF/PACF)、ARIMA/SARIMA定阶、残差分析、多模型对比与预测输出的完整流程,可直接参考用于结课报告或答辩演示。压缩包共25个文件、4.93MB,其中7个csv和4个xls提供数据与中间表格,7个py脚本覆盖差分、ADF检验、ACF/PACF分析、参数估计、残差、对比和预测等环节,2个ipynb便于交互式复现,另有5个zip为压缩归档。目前已有387人学习下载,代码模块清晰,便于按需修改;既能理解多个时序模型的实际衔接,也能替换数据跑通预测,是课程设计与时间序列建模入门的实用素材。

1. 流感时间序列预测为什么要同时用 ARIMA、LSTM、Transformer 三种模型

流感周报发布通常滞后一周以上,医院药房备货、急诊排班、疾控研判却需要提前两周预判高峰。把流感样病例百分比(ILI%)这类周度序列同时交给 ARIMA、LSTM、Transformer 三种模型去预测,就是这个 Python 源码项目的核心内容。它解决的不是「哪个模型更高级」的学术问题,而是「在真实小样本周数据上,怎么拿到可靠的多周预测值」的落地问题。这类高分项目源码通常包含数据预处理、三个独立模型脚本和结果对比,适合疾控数据从业者、公共卫生方向学生,以及想用时间序列预测但手里只有每周一条数据的人。一个反直觉的结论先放在这:在真实流感周报上,ARIMA 往往不输给深度学习模型,三种模型组合起来才是真正的拿分点。

2. 流感数据预处理:把周报切成模型能吃的监督样本

拿到流感时间序列预测源码后,第一件事不是跑模型,而是先把数据搞清楚。流感周报和股票分钟线、气象小时数据完全不同:它一年只有 52 条,十年才 520 条,而且有强烈的年度周期和冬季双峰。不做预处理就把序列丢给神经网络,训练出来的东西基本都是上一周数值的平移,没有任何预测价值。

2.1 ILI 周报的字段与时序特性

常见的流感监测周报会记录每周的流感样病例就诊数、ILI 百分比和阳性检出率。核心字段一般就三个:

字段含义典型范围
week_date统计周的截止日期每年 52/53 周
ili_percent流感样病例占门急诊比例1%~8%
positive_rate流感病毒检测阳性率0%~40%

拿到 CSV 后,先做两件事:补缺失值、查平稳性。公卫周报偶尔会跳一周,直接删行会破坏时间间隔,所以我一般用线性插值,两个相邻周之间差几天关系不大。平稳性用 ADF 检验,p 值大于 0.05 就说明序列非平稳,需要差分。

import pandas as pd from statsmodels.tsa.stattools import adfuller df = pd.read_csv("flu_weekly.csv", parse_dates=["week_date"]) df = df.set_index("week_date").sort_index() df["ili_percent"] = df["ili_percent"].interpolate(method="linear") def adf_report(series, name): res = adfuller(series.dropna(), autolag="AIC") print(f"{name}: ADF={res[0]:.3f}, p={res[1]:.5f}") return res[1] < 0.05 adf_report(df["ili_percent"], "原始序列") df["diff_1"] = df["ili_percent"].diff(1) adf_report(df["diff_1"], "一阶差分")

diff(1)是当前周减上周的差值,是最便宜的去趋势手段。流感序列做完一阶差分后 p 值通常会低于 0.05,但注意别高兴太早:一阶差分会把周期信号的一部分也吃掉,后面第 5 章会专门讲这个坑。这里autolag="AIC"表示 ADF 检验自动按 AIC 选滞后阶数,比你手写定滞后阶数更稳。

2.2 滑窗构造监督样本:时序切分与最小数据量

神经网络不能直接吃一维时间序列,需要把序列切成「用历史 52 周预测未来 1 周」的监督样本。窗口长度选 52 不是拍脑袋,流感有完整年度周期,窗口必须覆盖一个完整周期模型才看得到「去年这个时候在涨还是跌」。

import numpy as np from sklearn.preprocessing import MinMaxScaler def make_windows(values, seq_len=52, horizon=1): X, y = [], [] for i in range(len(values) - seq_len - horizon + 1): X.append(values[i:i + seq_len]) y.append(values[i + seq_len:i + seq_len + horizon]) return np.array(X, dtype=np.float32), np.array(y, dtype=np.float32) values = df["ili_percent"].to_numpy() scaler = MinMaxScaler() scaled = scaler.fit_transform(values.reshape(-1, 1)).flatten() X_all, y_all = make_windows(scaled, seq_len=52, horizon=1) split = int(len(X_all) * 0.8) X_train, X_test = X_all[:split], X_all[split:] y_train, y_test = y_all[:split], y_all[split:]

这段代码里有两个容易被忽略的参数。seq_len=52是回看窗口,设小了模型看不到年周期,设大了样本数骤减;horizon=1是最简设置,预测未来 1 周,后面要预测未来 14 周门诊压力时把它改成 14 就行。切分比例 0.8 按时间顺序切,绝对不能像图像分类那样随机打乱,否则测试集里混着训练集期间未来的数据,指标会好看得离谱,这在第 5.1 节展开。

提示:流感序列建议至少留 8 年完整周数据,少于 5 年神经网络的样本量连一个完整周期都覆盖不了,建议直接用 ARIMA。

3. 跑通 ARIMA:用 statsmodels 定阶、拟合与滚动预测

ARIMA 在流感时间序列预测里不是陪跑角色。它参数少、可解释、有置信区间,是检验一切深度学习模型的 baseline。问题是定阶这步做不好,后面全白搭。不少新手拿auto_arima一键跑完就交差,结果预测曲线是一条水平直线——大概率是差分阶数定多了。

3.1 定阶:ACF/PACF 粗定,AIC/BIC 精修

ARIMA(p,d,q) 里,p 是自回归阶数,d 是差分次数,q 是移动平均阶数。流感周报有强年度季节性和冬季双峰(12 月一个小峰、2 月一个主峰),直接上普通 ARIMA 残差里会残留明显周期,这时应该使用带季节项的 SARIMA,记作 SARIMA(p,d,q)(P,D,Q,m),m 是周期长度,周数据就取 52。

from statsmodels.graphics.tsaplots import plot_acf, plot_pacf import matplotlib.pyplot as plt plot_acf(df["ili_percent"].diff(1).dropna(), lags=40) plot_pacf(df["ili_percent"].diff(1).dropna(), lags=40) plt.savefig("acf_pacf.png")

看这两个图有个经验:ACF 在滞后 52 周附近出现显著峰值,说明年度季节性没被处理干净,这时候应该用 SARIMA 而不是继续加大 p。图看完后用auto_arima在小范围内扫描阶数,比手动试快得多。

from pmdarima import auto_arima model = auto_arima( df["ili_percent"], seasonal=True, m=52, start_p=0, max_p=3, start_q=0, max_q=3, start_P=0, max_P=1, start_Q=0, max_Q=1, d=1, D=1, trace=True, error_action="ignore", suppress_warnings=True, stepwise=True ) print(model.order, model.seasonal_order)

这里最关键的参数是m=52和D=1。m=52告诉模型周期是 52 周;D=1对季节分量做一次差分,把「去年同周」的水平差异抹掉。stepwise=True会先做条件搜索再用精确似然估计,速度比穷举快很多,流感这种小样本序列跑起来几秒就出结果。注意pmdarima不是statsmodels自带的,需要单独pip install pmdarima。

选完阶数后,我一般会再回到statsmodels重新拟合一次,目的是拿到标准残差诊断图,确认残差里没有明显的自相关。AIC 和 BIC 的选择其实是个小权衡:AIC 更偏向预测精度,BIC 更偏向模型简洁。流感样本量小,BIC 容易把模型压到过于简单,我通常 AIC 为主,两个候选 AIC 差不到 2 时选阶数低的那一个。

3.2 预测:滚动一步回填 vs 直接外推 14 周

ARIMA 的预测有两种模式。一种是直接predict(n_periods=14)外推未来 14 周,误差会随着步数快速变大;另一种是滚动一步预测——每预测完一周,就把这一周的真实值塞回历史,再预测下一周。后者在评估模型能力时更接近实战,因为流感监测的真实场景中,新一周的数据每周都会发布,你永远可以用最新真实值更新模型输入。

n_train = 400 history = list(df["ili_percent"].iloc[:n_train]) preds = [] for step in range(14): model = auto_arima( history, seasonal=True, m=52, d=1, D=1, stepwise=True, error_action="ignore", suppress_warnings=True ) pred = model.predict(n_periods=1)[0] preds.append(pred) actual = df["ili_percent"].iloc[n_train + step] history.append(actual) # 用真实值回填,模拟每周拿到新数据

上面这段是滚动一步预测的核心写法。注意history.append(actual)加的是真实值,不是预测值,这模拟的是「每周拿到最新周报后重新预测」的生产过程。如果你把actual换成pred,就是递归预测,误差会被自己一步步放大,预测 14 周后曲线常常直接飘飞。实际部署时真实值还没出,那就退化成直接外推,两种模式要清楚自己在评估什么。评估 ARIMA 时还有个加分项:它自带置信区间,预测未来第 4 周高峰时可以顺带给出上下界,这在医院备货场景里比点预测值实用得多。

4. LSTM 与 Transformer:同一套数据管线下的训练与对比

神经网络部分是这个流感时间序列预测源码里最容易让新手黑匣子化的地方。很多人直接把别人图像分类的模型结构抄过来,训几个 epoch 发现 loss 不降,就开始怀疑数据有问题。其实流感周报这种小样本单变量序列,用不到复杂网络,LSTM 两个隐藏层够用,Transformer 一个编码器也够用,重点全在防过拟合和调学习率上。

4.1 LSTM:52 周全窗口输入,两个隐藏层就够

LSTM 处理流感序列的优势是能记住「去年冬季的走势」,但记住太多就是过拟合。我用 PyTorch 写一个最小可跑的 LSTM 模型:

import torch import torch.nn as nn class LSTMFlu(nn.Module): def __init__(self, input_size=1, hidden_size=64, num_layers=2): super().__init__() self.lstm = nn.LSTM(input_size, hidden_size, num_layers, batch_first=True) self.fc = nn.Linear(hidden_size, 1) def forward(self, x): out, _ = self.lstm(x) return self.fc(out[:, -1, :]) model = LSTMFlu() optimizer = torch.optim.Adam(model.parameters(), lr=1e-3) loss_fn = nn.MSELoss() X_train_t = torch.tensor(X_train).unsqueeze(-1) y_train_t = torch.tensor(y_train) for epoch in range(80): model.train() optimizer.zero_grad() pred = model(X_train_t) loss = loss_fn(pred, y_train_t) loss.backward() optimizer.step() if epoch % 20 == 0: print(epoch, loss.item())

输入张量的形状是(样本数, 52, 1),batch_first=True把 batch 放到第一维;out[:, -1, :]取最后一个时间步的隐藏状态映射到预测值,这是序列到点的标准写法。hidden_size=64对一个 520 条左右的周序列足够了,调到 256 必过拟合;学习率 1e-3 是 Adam 的常用起点,如果 loss 在 0.001 量级震荡就降到 3e-4。80 轮是我习惯的起点,流感数据太少了,真正训练时用验证集 loss 做早停,连续 10 轮不降就停。

4.2 Transformer:单变量短序列要改位置编码和 dropout

transformer 在流感时间序列预测里不是拿来炫技的。很多人看 transformer 预测正弦数据的 demo 很漂亮,以为搬到流感周报上一样丝滑,结果训完画出来是一条贴近均值的直线。原因在于正弦数据低噪声、周期确定,流感周报信噪比低、高峰位置每年漂移。用 Transformer 做这类短序列,重点是让它不要过拟合到最近几周的历史模式上。

import torch.nn as nn class TransformerFlu(nn.Module): def __init__(self, d_model=64, nhead=4, num_layers=2, dropout=0.2): super().__init__() self.input_proj = nn.Linear(1, d_model) self.pos = nn.Parameter(torch.randn(1, 52, d_model) * 0.02) encoder_layer = nn.TransformerEncoderLayer( d_model=d_model, nhead=nhead, dim_feedforward=128, dropout=dropout, batch_first=True ) self.encoder = nn.TransformerEncoder(encoder_layer, num_layers=num_layers) self.fc = nn.Linear(d_model, 1) def forward(self, x): x = self.input_proj(x) x = x + self.pos out = self.encoder(x) return self.fc(out[:, -1, :])

和 NLP 里常见的 transformer 模型详解不同,这里只用编码器,没有解码器,因为流感预测是「历史 52 周 → 未来 1 个点」的序列到点回归,不是序列到序列生成。三个关键改法:self.pos用可学习位置编码而不是固定的 sin/cos,短序列上可学习编码更灵活;dropout=0.2是为小样本故意调大的,默认 0.1 在几百条样本上过拟合速度肉眼可见;nhead=4不要贪多,单变量序列里本来就没有多少独立信息可拆,8 个头纯粹增加过拟合风险。

4.3 对比评估:RMSE 和 MAPE 决定要不要上深度学习

三个模型跑完后,对比表按下面这个格式整理,横轴是模型,纵轴是测试集指标。常见的分布大致是:

模型训练耗时测试 RMSE 参考范围适用判断
SARIMA秒级0.10~0.20必选 baseline,小样本首选
LSTM分钟级0.15~0.35数据量 500+ 且 ARIMA 残差明显时用
Transformer分钟级0.20~0.40样本少于 500 不建议用

这个表里的数字是示意范围,不同地区流感强度差异很大,不要拿它当绝对值。我的判断逻辑是:如果 SARIMA 的测试 RMSE 已经低于 0.15,深度学习带来的提升很可能不足以抵消调参成本,那就不必上。只有当残差里明显还有周期性高峰没被 SARIMA 捕获时,才值得把 LSTM 作为残差修正器接在后面。

5. 实战避坑:流感预测最容易翻车的 5 个细节

这一章写的是我处理这类时序项目时真实踩过的坑,每条都按「现象 → 原因 → 解决」记录,比任何调参技巧都值钱。

5.1 随机切分数据集,测试集指标虚高到离谱

现象:测试集 RMSE 只有 0.03,画图一看预测曲线和真实曲线几乎重合,但换到下一个流感季的周报上预测立刻崩掉。

原因:时间序列不能像图像那样随机打乱切分。随机切分会把第 300 周的样本混进训练集、第 301 周混进测试集,模型在训练时已经见过「紧挨着」的未来信息,这属于标签泄漏。

解决:严格按时间顺序切分,训练集只能是时间上靠前的部分,测试集必须是时间上靠后的部分。用第 2.2 节的X_all[:split]和X_all[split:]写法,切完后再检查一次训练集最大日期是否早于测试集最小日期。

5.2 MinMaxScaler 用全量数据拟合,反归一化后预测值整体失真

现象:LSTM 训练时 loss 很低,反归一化后预测曲线形状对得上,但数值整体偏高或偏低 0.3 到 0.5 个百分点。

原因:先对全量序列fit_transform,再切训练测试集,scaler 已经偷看了测试集的取值范围。测试集的上下限被提前告知模型,反归一化时测试期的极值会整体拉偏数值。

解决:先按时间切分原始数值序列,再在训练集上scaler.fit(),然后用同一个 scaler 分别transform训练集和测试集。测试集的反归一化也只依靠训练时记录的 min 和 max,这才是无损的流程。

5.3 多步预测时递归把预测值当输入,误差逐周爆炸

现象:预测未来第 1 周很准,第 2 周开始偏,第 6 周以后直接变成一条缓坡直线,真实高峰完全看不到。

原因:LSTM 和 ARIMA 在递归预测时,每一步的输入里都混着上一步的预测误差,误差会像滚雪球一样累积。这不是模型坏了,是评估方式错了。

解决:评估阶段用第 3.2 节的滚动回填方式,每周用真实值更新历史。如果部署场景确实拿不到未来真实值,那就接受「递归预测的多周结果只有趋势参考价值」这个事实,别指望它精确复现高峰数值。

5.4 ARIMA 差分阶数设过头,周期信息被差没了

现象:auto_arima跑出来 d=2 甚至 d=3,预测结果是一根贴近均值的平线,置信区间宽到毫无意义。

原因:流感序列本身有年度周期,做一次差分去掉趋势就够了。差分阶数太多会把季节性周期差成白噪声,模型只学到「均值回归」,自然输出水平线。

解决:把d和D固定,不要放任自动搜索。我的习惯是 d=1、D=1,m=52,然后看残差 ACF 图确认 52 阶附近没有显著峰。如果仍有峰,优先调 P 和 Q,而不是继续加大差分阶数。

5.5 Transformer 在短序列上 loss 震荡,训练到 50 轮还降不下来

现象:Transformer 的 loss 曲线像锯齿,0.01 和 0.02 之间来回跳,学习率调小后反而完全不降了。

原因:流感数据量太少,Transformer 的加性位置编码和全连接层在样本不足时会互相干扰,常见学习率 1e-3 对它来说偏大,注意力层的梯度震荡剧烈。

解决:先用 3e-4 甚至 1e-4 的学习率,加 100 步 warmup(前 100 步学习率从 0 线性升到目标值)。另外把num_layers从默认 6 层压到 2 层,dim_feedforward从 2048 压到 128。Transformer 在小数据上根本不是「越大越好」,是越小越稳。

6. 把模型组合起来:残差修正与滚动验证

单模型跑通只是完成了这个流感时间序列预测源码的一半,它真正拿高分的地方在组合策略上。

6.1 残差组合:ARIMA 抓线性主峰,LSTM 修正非线性残差

我的做法是让 ARIMA 先预测流感序列的主趋势和年度季节峰,然后把真实值与 ARIMA 预测值的差作为残差序列,交给 LSTM 去拟合,最后把两者的预测结果相加。这样 ARIMA 负责解释「每年冬天都会涨」,LSTM 只负责解释「今年 12 月的峰为什么比去年提前两周」。

residuals = df["ili_percent"] - sarima_pred residual_scaled = scaler_resid.fit_transform(residuals.values.reshape(-1, 1)).flatten() X_resid, y_resid = make_windows(residual_scaled, seq_len=52, horizon=1)

残差序列的均值通常在 0 附近,LSTM 学起来比直接学原始峰值容易得多。组合预测值就等于 SARIMA 预测加 LSTM 的残差预测,训练目标也从「猜准 5.5% 的峰值」变成「猜准 0.3% 的偏差」。

6.2 滚动验证:用连续 4 周的预测窗口检验稳定性

验证组合模型时我不用单次切分,而是每移动 4 周重新训练一次,记录连续 4 个测试窗口的 RMSE。如果组合模型的 RMSE 比单独 SARIMA 的 RMSE 提升不到 5%,我一般直接放弃深度学习部分,保留 ARIMA 加置信区间作为最终交付。这是个血泪教训:别为了向别人展示技术栈而组合模型,不值得。行为准则就是「先跑基线,再加增量,没有增量就停」。希望帮到你。

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