
简介面向毕业设计、期末大作业及课程设计等实践场景这套围绕铁路货运量与客运量数据的时序建模预测方案提供完整的Python源代码、训练好的模型及配套数据集。资源共50个文件核心包括17个Python脚本、4个R脚本以及3个Excel和2个CSV数据表并配有19张用于展示序列分解、模型拟合与预测结果的PNG图表压缩包仅2.05MB目录层级清晰还包含说明文档与依赖清单部署门槛低。代码注释覆盖数据读取、平稳性检验、模型训练、误差评估、可视化输出等完整环节模块化设计便于二次开发与替换数据新手也能快速复现实验。方案出自CSDN用户qq_38140936属于个人手打的98分项目导师认可度高可作为毕业设计或课程设计的高分参考模板。目前已有269人学习下载适合需要构建运输量预测方案、撰写实验报告或搭建毕业设计Demo的读者参考。1. 铁路客货运量时序预测这份源码包能给你什么铁路货运量和客运量的时序建模预测是运输调度、统计分析、课题申报里反复出现的老需求货运要按大宗物资的运输节奏排车底客运要根据节假日和季节效应安排运力与售票计划。标题里这份“基于Python对铁路货运量-客运量进行时序建模预测源代码模型数据集.zip”拆开就是一条从原始台账到预测曲线的完整链路——清洗好的数据集、可跑的建模脚本、训练完的模型以及一套可以换数据复用的Python代码。它服务于三类人要做季度年度预测报告的分析岗、想拿真实数据练手时序建模的学生、要快速搭基线给领导汇报的技术人员。你不用从零造轮子关键是搞懂每一步代码在干什么、哪些参数要按你的数据改。这篇笔记就按“看数据—跑基线—上深度学习—踩坑—进阶”的顺序把这套包从黑匣子变成你顺手能改的工具。2. 拿到数据集先别建模把客货运量的脾气摸清楚2.1 铁路数据的两大特征量纲差一个数量级季节性藏在日历里铁路的货运量和客运量放在一起看至少有三个明显特征处理不好后面全是坑。第一是量纲差异。货运量常以“万吨”或“亿吨”计客运量常以“万人”或“亿人”计两列的数值可能差一到两个数量级。如果直接塞进同一个多变量模型数值大的那列会主导损失函数小量纲那列等于白给。第二是频率选择。月度数据是最常见的选择因为统计公报按月发布日频数据噪声大且缺数多季度数据又太粗月频既能看清趋势又有足够样本做季节性分析。第三是季节结构。客运量在春节前后有尖峰五一、十一有次峰货运量受煤炭冬储、春耕化肥、年底赶工影响也有周期性但相位和客运完全不同。多数人以为客运量受节假日影响更难预测实际做下来货运量更容易翻车——它跟煤炭、钢铁、粮食等大宗物资的运输需求绑定产业政策一调整趋势就拐弯而且拐了很难回头。客运量再波动春运的尖峰年年都在模型至少有规律可循。所以拿到数据集的第一步不是跑模型而是把两列分别画出来看趋势、看季节、看突变点把数据的脾气摸清楚再动手。2.2 数据清洗缺失值、异常值、日历对齐的代码与参数假设解压后 data/ 目录下有一个 railway_traffic.csv里面是月份、货运量、客运量三列货运量单位是万吨客运量单位是万人。先做基础清洗import pandas as pd import numpy as np df pd.read_csv(data/railway_traffic.csv, parse_dates[month]) df df.sort_values(month).set_index(month) # 缺失值月度统计里少量缺失优先用前向填充 df[freight] df[freight].ffill() df[passenger] df[passenger].ffill() # 异常值用滚动窗口的 3 倍标准差圈出来只标记不删除 window 12 mean df.rolling(window).mean() std df.rolling(window).std() outlier_mask (df - mean).abs() 3 * std print(疑似异常点, df[outlier_mask].index.tolist())逻辑说明先用前向填充处理缺失适合低频统计数据的缓慢变化特性再用滚动窗口识别异常。注意只标记不删除——客运量在春运月份和2020年初会出现“正常但剧烈”的跳变直接删会让模型丢掉重要信息。参数说明window12 表示以一年为窗口计算均值与标准差月度数据里一个窗口正好覆盖完整季节循环3 倍标准差是宽松阈值如果误报过多可以放宽到 3.5或改用中位数绝对偏差MAD。这套代码里ffill()是核心但如果你发现某个缺口横跨好几个月前向填充会把旧值一直沿用这时候要回头查原始台账别让填充掩盖了数据质量问题。如果你的原始数据是日频要先聚合到月频再走上面的流程。常见做法是用resample按自然月求和或求平均客运量用求和、货运量也用求和因为月累计量才是调度关心的口径。聚合时注意时区问题和跨月日期别把 1 月 31 日和 2 月 1 日的日数据算进同一个统计口径。清洗阶段还要做一件事加一列“春节虚拟变量”。春节在公历的 1 月或 2 月间移动月频模型的季节项固定为 12 个月抓不住这种漂移。先粗算一下每个月是否包含春节chinese_new_year pd.to_datetime([ 2014-01-31, 2015-02-19, 2016-02-08, 2017-01-28, 2018-02-16, 2019-02-05, 2020-01-25, 2021-02-12, 2022-02-01, 2023-01-22, 2024-02-10, 2025-01-29 ]) spring_festival pd.Series(1, indexchinese_new_year) df[is_spring_festival] df.index.isin(chinese_new_year.to_period(M).to_timestamp()).astype(int)这段代码的原理是按年份维护一个春节日期列表生成“当月是否有春节”的 0/1 特征。铁路客运量的春运峰值通常出现在春节前后 15 天光靠月份本身无法表达但这个虚拟变量能把“今年春节在 2 月”的信息直接喂给模型。数据量多的时候可以再细化成“春节前 7 天、春节后 15 天”这种分段特征对 SARIMA 的外生变量和 LSTM 的输入都有用。2.3 趋势分解一眼看出该用加法模型还是乘法模型数据清洗完下一步不是直接建模而是做趋势分解。用 statsmodels 的seasonal_decompose看两列的成分from statsmodels.tsa.seasonal import seasonal_decompose import matplotlib.pyplot as plt result seasonal_decompose(df[passenger], modeladditive, period12) result.plot() plt.tight_layout() plt.savefig(output/passenger_decompose.png, dpi150) trend result.trend.dropna() seasonal result.seasonal.dropna() # 看季节分量是否随趋势幅度变化决定用加法还是乘法模型 ratio seasonal.abs().mean() / trend.abs().mean() print(f季节成分与趋势幅值比{ratio:.3f})逻辑说明加法模型假设季节波动幅度固定乘法模型假设季节幅度随时间等比例放大。铁路客运量在春运月份的季节峰值基本固定在一个量级加法模型通常够用货运量随经济周期整体抬升时季节波幅也会放大这时候要在乘法模型下对比同样的分解图。判断依据很简单把趋势分量和季节分量画在同一张图里看季节分量的振幅是不是跟着趋势涨落如果是乘法模型更合适。参数说明period12对月度数据是第一直觉但你要知道这个参数的含义是“每 12 个月完成一次季节循环”。如果数据里有明显的春节漂移period12 的分解会把春节效应拆成一部分趋势、一部分噪声这是正常的后面 5.2 会专门讲处理方式。model参数的两种取值决定分解方式切换后只看季节分量和残差分量的形态变化不要盯着数值看。这一步做完你应该能回答三个问题有没有长期趋势季节周期是 12 个月还是别的长度残差里还有没有明显规律回答完这三个问题进建模才有底。如果残差图里还能看到明显的波峰说明季节项没提干净先回头解决别急着往下跑。3. 从 SARIMA 跑起基线模型的价值是让你知道下限在哪3.1 为什么铁路客货运量首选 SARIMA 做基线时序建模的开局选择通常是三选一统计模型ARIMA/SARIMA、树模型XGBoost/LightGBM、深度学习LSTM/Transformer。对铁路月度客货运量这种“长周期、中等样本、强季节”的数据SARIMA 是最可靠的开局原因有三条。样本量上如果只有十年左右的月频数据也就是 120 个点深度学习很难吃饱SARIMA 在 100 个点左右就能给出可解释的估计。可解释性上AR 项、MA 项、差分阶数和季节周期都有统计含义写报告时可以直接引用系数和置信区间这是领导最吃的一套。检验工具上残差的 Ljung-Box 检验、AIC/BIC 模型选择、平稳性 ADF 检验都是能被业务方和评审接受的依据比“神经网络效果不错”有说服力得多。这不是说 SARIMA 一定比 LSTM 准而是用它先跑通全流程把对数据的问题在统计框架里问清楚有没有趋势、季节周期是否稳定、是否需要干预变量。我一般叫它“先立一个木桩”后面不管换什么模型都有个对比坐标。如果你的数据里 2020 年的断崖还在SARIMA 也能更直观地暴露问题——它会直接把突变吃进趋势项让你一眼看到哪里不对劲。3.2 定阶ADF 检验、ACF/PACF、网格搜索三步走定阶分三步。第一步确认差分阶数用 ADF 检验from statsmodels.tsa.stattools import adfuller for col in [freight, passenger]: d 0 series df[col].copy() while d 2: p_val adfuller(series.dropna())[1] if p_val 0.05: break series series.diff().dropna() d 1 print(f{col}: ADF p{p_val:.4f}, d{d})逻辑说明ADF 检验的原假设是序列非平稳p 值小于 0.05 才拒绝原假设。循环里先测原始序列不平稳就做一阶差分再测。铁路月度数据大多一阶差分就平稳如果你发现需要二阶差分先回去检查是不是有突变点没处理而不是直接接受二阶差分的结果。参数说明循环上限设在 2 是经验值差分次数过大会把长期趋势信息也差掉预测结果会变成围绕均值震荡的“纯噪声”。adfuller默认回归项带常数项对多数业务序列够用如果序列明显有线性趋势可以加autolagAIC让函数自动选滞后阶数。第二步用网格搜索选 (p, q, P, Q)代码可以这样写from statsmodels.tsa.statespace.sarimax import SARIMAX from itertools import product import warnings warnings.filterwarnings(ignore) def grid_search_sarima(y, pdq_range, seasonal_range): best_aic float(inf) best_cfg None for pdq in product(*pdq_range): for s_cfg in seasonal_range: try: model SARIMAX(y, orderpdq, seasonal_orders_cfg, enforce_stationarityFalse, enforce_invertibilityFalse) res model.fit(dispFalse, maxiter200) if res.aic best_aic: best_aic, best_cfg res.aic, (pdq, s_cfg) except Exception: continue return best_cfg, best_aic pdq_range [range(0, 3), range(0, 2), range(0, 3)] seasonal_range [(0, 0, 0, 12), (0, 1, 1, 12), (1, 1, 0, 12), (1, 1, 1, 12)] best_cfg, best_aic grid_search_sarima( df[passenger].diff().dropna(), pdq_range, seasonal_range) print(最优阶数, best_cfg, AIC, best_aic)逻辑说明网格搜索的目标是找到 AIC 最小的参数组合。seasonal_range里的四元组是季节部分的 (P, D, Q, s)其中 s12 表示 12 个月一个季节周期D1 表示做一阶季节差分。p 和 q 的范围都放 0 到 2是因为对月度数据超过 2 的滞后阶数容易过拟合而且 AIC 本身会偏好复杂模型搜索空间开得太大选出来的模型换个验证集就崩。参数说明enforce_stationarityFalse和enforce_invertibilityFalse是让优化器在搜索中间状态时不因为参数越界直接报错把判断留给最终的 AIC 比较。maxiter200是迭代上限如果序列超过 200 个点建议提到 500不然有些组合还没收敛就被强制结束了。注意这里对 y 做了diff()也就是说你搜索的是差分后序列的阶数对应原序列其实是 (p, d1, q)。如果你不想手动差分直接传原始序列并把 d 的范围放进pdq_range里搜逻辑一样但搜索空间会变大。第三步是拟合最优模型并做残差检验from statsmodels.stats.diagnostic import acorr_ljungbox model SARIMAX(df[passenger], orderbest_cfg[0], seasonal_orderbest_cfg[1]) res model.fit(dispFalse) ljung acorr_ljungbox(res.resid.dropna(), lags[12], return_dfTrue) print(Ljung-Box p 值, ljung[lb_pvalue].values[0])逻辑说明Ljung-Box 检验残差是否还有自相关。p 值大于 0.05说明残差接近白噪声模型已经把信息提取干净了p 值小于 0.05说明季节结构或节假日效应还有残留。这时候不要急着扩大网格先检查春节漂移再看是不是需要加外生变量。参数说明lags[12]表示检查滞后 12 阶内是否还有自相关对月度数据这是标准做法。如果你同时检验滞后 6 和 12 阶p 值都得大于 0.05 才算过。残差检验通过后把 AIC、各系数估计、Ljung-Box p 值记下来这就是 SARIMA 基线的完整档案。3.3 滚动预测用真实世界的方式评估模型一次性把前 80% 当训练集、后 20% 当测试集对时序建模不成立。测试集在时间上晚于训练集真实使用中你根本看不到“未来”的任何信息所以评估必须用滚动预测也叫回溯测试from sklearn.metrics import mean_absolute_error, mean_absolute_percentage_error series df[passenger].astype(float) n_test 12 # 最后 12 个月做验证 rolling_preds [] history series.iloc[:-n_test].copy() for i in range(n_test): m SARIMAX(history, orderbest_cfg[0], seasonal_orderbest_cfg[1]) fit m.fit(dispFalse) yhat fit.forecast(steps1).iloc[0] rolling_preds.append(yhat) history pd.concat([history, pd.Series([series.iloc[-n_test i]])]) y_true series.iloc[-n_test:] mae mean_absolute_error(y_true, rolling_preds) mape mean_absolute_percentage_error(y_true, rolling_preds) * 100 print(f滚动 12 个月预测 MAE{mae:.1f}, MAPE{mape:.2f}%)逻辑说明每次只预测下一个月预测完立刻把真实值追加回历史序列再重新拟合模型预测下一个月这模拟的是“每个月重训一次”的真实用法。注意循环里每次都在重新拟合慢是慢了点但符合业务节奏。如果嫌慢可以改成每 3 个月重训一次中间两个月的预测直接用上次的模型误差会略有增加但工作量少一大截。参数说明n_test12表示验证集是最后 12 个月。对强季节序列验证集至少要覆盖一个完整季节周期否则你没法判断模型是真学会了季节模式还是只是运气好。MAPE 的计算里如果真实值接近 0会出现极大值铁路数据里不太会发生但如果是客运量在 2020 年某个月接近 0这一条的 MAPE 会异常高汇报时注明即可。这一章的产出物是三个数字MAE、MAPE、残差 Ljung-Box p 值。写进实验记录后面上任何高级模型唯一使命就是超过这三个数字。4. 上 LSTM滑窗切分、归一化与 PyTorch 最小实现4.1 什么时候值得从 SARIMA 升级到 LSTMSARIMA 的边界很清晰它是线性模型对非线性和多变量交互无能为力。当你手里有多年月度数据还想把货运量和客运量两列一起建模比如想捕捉“客运量变化带动货运结构调整”这类交互关系时LSTM 是性价比最高的下一步。它比 Transformer 轻在几百个点的小数据上不容易过拟合而且 PyTorch 生态成熟随便搜都是一大把可以改的源代码。但有个前提如果数据不到 80 个月别上 LSTM。同参数规模下线性统计模型在小样本上的泛化能力远比深度学习可靠。这也是为什么标题这套包里“模型”通常同时给统计模型和深度模型——先有基线才有对比。Transformer 这几年在时序领域讨论很多但它的注意力机制是为长序列设计的120 个月的月度数据对它来说太短效果反而不如 LSTM。真到了上千个点的日频粒度再考虑 Transformer 也不迟。4.2 滑窗切分把时序变成监督学习样本LSTM 不能直接吃“从 2014 年到 2024 年的月度值”它吃的是若干条(输入窗口, 目标)的样本对。滑窗切分的代码def make_windows(data, window12, horizon1): X, y [], [] for i in range(len(data) - window - horizon 1): X.append(data[i:i window]) y.append(data[i window:i window horizon]) return np.array(X), np.array(y) from sklearn.preprocessing import MinMaxScaler # 注意为了演示这里简化了真实使用时 scaler 只允许在训练段上 fit scaler MinMaxScaler() scaled scaler.fit_transform(df[passenger].values.reshape(-1, 1)).flatten() X, y make_windows(scaled, window12, horizon1) print(样本数, X.shape, 目标数, y.shape)逻辑说明对每个时间点 t用过去 12 个月的序列预测下 1 个月的值得到一个监督样本。window12 对应一年对月度季节数据是最低要求窗口再短模型看不到完整季节窗口越长样本量越少。样本之间有大量重叠这在时序建模里是正常的但不代表你可以随意 shuffle后面 5.5 会讲。这里有个非常容易翻车的细节fit_transform必须只在训练段上调用。上面这段为了演示用了全量数据真实使用时如果 scaler 的 min/max 包含了验证段的信息验证误差会虚低线上表现立刻现原形。正确做法是先切分 train/test再分别 fitting。参数说明horizon1表示单步预测。如果要预测未来 6 个月有两个方案把 horizon 改成 6 做多输出或者保持 1 做递归预测。后者更常见但误差会累积前者训练更复杂6.1 会展开对比。4.3 PyTorch 训练 LSTM网络结构、早停与超参数一个能跑的 LSTM 结构输入是 12 个月的窗口输出是下一个月import torch import torch.nn as nn from torch.utils.data import DataLoader, TensorDataset class LSTMForecast(nn.Module): def __init__(self, input_size1, hidden_size32, 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) return self.fc(out[:, -1, :])逻辑说明网络结构只有两层——LSTM 层把 12 个时间步编码成一个 hidden_state全连接层把它映射到预测值。out[:, -1, :]取最后一个时间步的输出因为预测目标只跟当前窗口的最新状态有关。如果只预测单步这个结构足够想预测多步可以在全连接层把输出维度改成 horizon 的长度或者接一个重复的 Decoder。训练循环里要做三件事划分验证集、设早停、每个 epoch 记录训练和验证损失。验证集必须取时间上靠后的段比如最后 12 个月。早停的触发条件是验证损失连续 N 轮不下降N 通常取 10 到 15。这里有个关键点训练集的 shuffle 要谨慎。样本之间本来就有重叠shuffle 后模型有机会从邻近时间段的样本里间接学到验证集的信息导致训练损失很低但滚动预测很差。我一般对时序训练集不用 shuffle或者用足够大的窗口减少重叠。超参数上我习惯这样起步hidden_size32num_layers1learning_rate0.001epochs100batch_size16早停 patience10。如果验证曲线一直不降先看归一化是不是写错了再看窗口是否太短最后才动网络结构。上来就堆 hidden_size 和层数只会让过拟合来得更快。4.4 预测与还原反归一化、滚动评估、和 SARIMA 对比预测结果要反归一化才和真实值同尺度。如果模型是在原始序列上训练的这一步就是单纯的反 scalermodel.eval() with torch.no_grad(): pred_scaled model(torch.tensor(X_test, dtypetorch.float32)).numpy() pred scaler.inverse_transform(pred_scaled)逻辑说明inverse_transform把 0-1 区间的预测值还原成“万人”单位的原始尺度。如果你在训练前对序列做过差分还要在反归一化之后做差分的逆运算——累加并补回初始值这一步漏掉预测曲线会整体偏移一个常数项肉眼很难发现但 MAPE 会异常大。LSTM 做验证集的滚动预测时不要每步都重新训练太慢。常见做法是每 3 步或每 6 步用新到的真实值更新一次训练集其余步直接用现有模型预测。这样速度可控也接近真实使用节奏。滚动评估的代码框架和 3.3 一样替换掉模型部分即可。对比 SARIMA 和 LSTM 时重点关注几个维度对比维度SARIMALSTMMAPE常见区间 5%-15%取决于数据质量数据量足且调参到位时可能接近或更低残差自相关Ljung-Box p0.05 可验证需要额外做残差诊断没有现成检验突发事件适应必须加干预变量也要加但可以靠近期窗口部分缓解可解释性强可以写进报告弱基本是黑匣子如果 LSTM 在验证集上比 SARIMA 差不要急着调参先检查三个事归一化是否用了全量数据、验证集是否被包含进训练过程、窗口是否覆盖季节周期。这三个问题排查完LSTM 的基线水平才算数。5. 时序预测避坑指南数据泄漏、春节漂移、断崖突变5.1 归一化用全量数据验证集损失好看是假的现象训练时损失下降正常验证集 MAPE 好看到感人但把模型放到真实滚动预测里误差直接翻倍。原因MinMaxScaler 在整段数据上做了 fit验证段的 min/max 信息混进了训练过程。模型在训练时偷看了未来的数值范围等于考试前看了答案。解决先切分训练段和验证段scaler 只在训练段上 fit再分别 transformtrain, test df[passenger].values[:-n_test], df[passenger].values[-n_test:] scaler MinMaxScaler().fit(train.reshape(-1, 1)) train_scaled scaler.transform(train.reshape(-1, 1)).flatten() test_scaled scaler.transform(test.reshape(-1, 1)).flatten()这条坑也是 LSTM 项目里最常见的“假分数”来源。任何能让未来信息影响训练过程的操作全都不允许不只归一化特征工程的每个步骤都要问一句这一步有没有用到验证段的数据。5.2 春节在 1 月和 2 月之间漂移周期固定为 12 抓不住春运现象客运量预测的残差在每年 1-2 月出现系统性偏差Ljung-Box 检查始终通不过春节早的年份高估 1 月春节晚的年份高估 2 月。原因月频数据的季节周期固定在 12 个月但春节在公历 1 月或 2 月之间移动。季节模型把春运峰值当成固定在某个月的事件可春节提前或延后时模型要么把峰值记在 1 月要么记在 2 月总有一年对不上。解决两个常用方案。方案一是加春节虚拟变量就是 2.2 里生成的那列is_spring_festival作为外生变量传进 SARIMAX或者作为额外特征拼到 LSTM 的输入里。方案二是按农历错位重排数据把每年春节对齐后再建模。方案一改动小我一般先做方案一如果虚拟变量加进去残差还是不行再考虑方案二。5.3 客运量在 2020 年出现断崖模型把突发事件当长期趋势现象用包含 2020 年的数据训练模型预测未来五年客运量都偏低不含 2020 年模型又高估近两年的恢复速度。两种切法都不对。原因2020 年初的断崖对模型来说是一个极端离群点。ARIMA 的差分项会把这段突变当成趋势的一部分吸收进去LSTM 则可能在窗口里学到“客运量可以瞬间减半”之后预测时动不动就给出过度悲观的估计。解决加干预变量。在数据里增加一列 0/1 变量标记 2020 年初的断崖区间SARIMAX 可以直接作为外生变量处理LSTM 把它作为额外的一个输入特征。训练阶段该变量为 1预测未来时该变量置 0模型就不会把突变外推。这个方法同样适用于货运量受政策影响出现的台阶式下降。5.4 差分后模型预测的是“差值”忘记还原直接对比真实值现象对序列先 diff 再建模预测结果画出来和真实值不在一个量级MAPE 大得离谱但看差值曲线又觉得挺准。原因把差分序列的预测值直接当成原始序列的预测值了。差分后的预测是“下月相对本月的增量”必须做累加还原还要补回被差掉的初始值。SARIMAX 的 forecast 方法内部会自动还原所以很多人没意识到这个问题一旦你手动 diff 后用别的模型这一步就得自己做。解决pred_diff model_fit.forecast(steps1).iloc[0] pred_orig last_original_value pred_diff这段代码短但忘了它的人不少。我习惯把它写成注释贴在模型封装函数的第一行永远确认模型输出的尺度是原始值还是差值。如果用了第三方的时序库多看文档里“forecast 返回的是否为原始尺度”这一句能省半天排查时间。5.5 早停的验证集随机切分把时间顺序打乱后模型未卜先知现象开了早停训练损失和验证损失都正常下降但滚动预测表现比不用早停还差。原因验证集从全量数据里随机抽了 10%而不是取时间上最后一段。随机切分让验证集里含有训练段“未来”的信息早停判定标准失真模型在“错误的时间点”停了。解决验证集必须从时间末尾截取样本生成后也不能整体 shuffle。如果担心最后一段数据不能代表整体分布可以用滚动验证——每次切掉前几个月逐步前移验证起点而不是随机打乱。时序数据的每一处随机化都要问一句这会泄露未来信息吗问了这一句能躲掉大部分深度学习在时序里的坑。6. 进阶从“预测下个月”到“能落地的预测系统”6.1 多步预测的两种策略递归、直接、多输出递归预测最简单用上一步的预测值作为下一步的输入一步步滚下去。问题是误差会累积预测 6 个月时第 6 个月的误差通常是第 1 个月的 2 到 3 倍时间越长越离谱。直接预测是训练 6 个模型分别预测 t1 到 t6误差不累积但训练成本高还要维护多个模型。折中的方案是多输出——LSTM 的输出层直接输出 6 个值在中小数据集上通常最可靠。我一般先用递归看趋势形状确认方向对了再上多输出精修。6.2 给预测加一个区间比单点数值有用得多单点预测最大的问题是没有不确定性信息调度不会因为一个数字就调整运力。SARIMA 的get_forecast自带置信区间直接取conf_int就是上下界。LSTM 没有现成区间可以用 Monte Carlo Dropout预测时开启 dropout多次前向取均值和标准差。实现改动极小预测时用model.train()替代model.eval()前向 50 次结果的方差就是不确定性估计。落地的报表里最好同时给出中位数、P10/P90 三个数字比只给一个期望值更有决策价值。6.3 把链路固定下来从 CSV 到图表的一键复现最后一步是工程化把数据清洗、参数搜索、模型重训、图表输出串成一个脚本固定随机种子把每次实验的验证集起始月份、模型参数、MAPE 写进输出文件名。我自己每次跑完一组实验都会把验证切分的起始月份和模型参数写进文件名比如passenger_sarima_202001_val_mape_6_3.csv三个月后回来看结果不用翻聊天记录。调参玄学少一点后悔药多一点。如果你要从这份源码包起步建议路线是先跑通数据清洗和趋势分解再用 SARIMA 把基线数字记下来之后再看要不要碰 LSTM。按这个顺序即使预测效果不理想你也知道问题出在数据还是模型而不是对着黑匣子瞎猜。希望帮到你。本文还有配套的精品资源点击获取