
简介基于Python的铁路货运量/客运量时序建模预测完整项目适合毕业设计、课程设计与期末大作业使用。内含源代码、模型与运输量数据集代码注释详尽新手也能看懂部署后即可复现预测流程。压缩包共50个文件包括17个Python脚本和4个R脚本覆盖数据预处理、时序建模与预测19张png展示序列特征与预测效果xlsx/csv为原始运输量数据md/txt为使用说明pptx可用于答辩汇报整体仅2.05MB。已有269人学习下载这套高分项目提供了从数据到模型再到可视化的完整参考能帮助读者快速掌握时序建模关键步骤并节省项目搭建时间。1. 铁路客货运量时序预测先想清楚“预测给谁用”再打开代码打开一个命名为“基于Python对铁路货运量-客运量进行时序建模预测源代码模型数据集.zip”的压缩包时多数人第一反应是解压、装依赖、跑训练脚本然后盯着一张 loss 曲线等结果。但这类项目真正值钱的部分反而不是源代码而是“输入数据怎么清洗、模型怎么评估、预测结果怎么被业务采信”。如果跳过这些demo 跑通后大概率会卡在同一个问题上模型在历史数据上拟合得很好一到未来就翻车。这篇文章想讲清楚三件事——这类时序建模项目里源代码、模型和数据集各自扮演什么角色怎么用最小成本把它跑通并验证以及哪几个坑会让你白干一场。适合刚接触时序预测、想把客货运量这类“慢变量”预测做成可用成果的 Python 从业者也适合正在选型、不确定该用 ARIMA 还是 LSTM 的算法工程师。2. 时序建模的选型逻辑统计模型、机器学习与深度网络各自的边界2.1 铁路客货运量数据的三重属性趋势、季节性与事件脉冲铁路货运量和客运量是我个人认为最适合练手时序建模的一类数据——它不像股票那样信噪比极低也不像气象数据那样依赖高密度观测站。它具备清晰的三重时间结构。第一重是趋势项。货运量与宏观经济景气度、大宗商品运输需求强相关长期看会随产业结构调整和政策导向缓慢爬升或下行客运量则与人口流动、城镇化率、高铁网络扩张速度绑定。趋势项决定了你的模型必须有能力捕捉“缓慢变化的均值”而不是把序列当作平稳随机过程硬拟合。第二重是季节项。这里要特别小心铁路客运量的季节性不只是“夏季高、冬季低”这种温度驱动更关键的是中国特有的春运和暑运脉冲。货运量虽然季节性弱一些但也会受年末抢运、农资运输、煤炭冬储影响形成每年固定月份的形状。时序建模里最常见的错误就是拿一个没有季节性组件的模型去硬拟合这种数据结果在春节前后误差大到离谱。第三重是事件脉冲。调图、新线开通、极端天气、突发公共卫生事件都会在月度序列上砸出一个“凹坑”或“尖峰”。这类信号无法被任何周期性组件解释只能通过外生变量或干预分析intervention analysis处理。对普通从业者来说最务实的做法不是让模型学会所有突发事件而是在预测区间内明确标注“事件风险不可控”。除了这三重结构还要看数据粒度。月度数据适合做中长期运力规划日度数据适合做节假日应急预案周度数据则处于尴尬位置——季节性不够干净又没细到能捕捉日内峰谷。粒度选择直接决定模型复杂度不要一上来就追日度。2.2 三条建模路线对比ARIMA、Prophet、LSTM 怎么选选型没有银弹但有清晰的决策路径。我一般会把候选模型压缩到三条路线然后用一张表把边界画清楚。维度ARIMA/SARIMAstatsmodelsProphetMetaLSTMKeras/PyTorch数据量要求100 个观测点起步200 个观测点较稳需要足够多的周期重复月度数据最好 5 年以上趋势建模差分 漂移项分段线性趋势自动断点靠网络容量隐式学习季节性需手动指定周期与阶数自动拟合年/周/日季节支持自定义节假日需构造滞后特征周期越长越难外生变量支持但扩展性一般支持额外回归器可加支持特征拼接到输入向量预测区间有理论置信区间有基于 MCMC 采样无原生区间需自己 bootstrap 或分位数回归可解释性强参数有统计含义中组件可视化清晰弱黑匣子调参成本中需做平稳性检验低默认参数就可跑高网络结构、窗口长度、学习率都要试铁路客货运量这类月度数据我通常的建议是样本量在 5 年以内、业务方要求能讲清楚“为什么预测值是这个数”时优先走 SARIMA 或 Prophet样本量超过 8 年、且你有新线开通里程、GDP、人口流动等外生变量可以喂给模型时才值得上 LSTM。要清醒认识到LSTM 不会因为“更高级”就天然更好它只是把“特征工程”换成了“架构工程”——前者你花一天能做完后者可能要花一周而且结果不一定更好。还有一个容易被忽略的选型条件团队后续维护能力。如果代码要交给运营团队在 Excel 里核对预测结果ARIMA 的输出格式最友好如果最后要接进实时数据管道Prophet 的 Python 接口和序列化模型更方便如果是为了写论文或者做技术预研LSTM 才有不可替代的叙事空间。模型选型本质是资源约束下的决策不是精度竞赛。3. 把预测流程跑通的四个步骤数据读取、特征构造、建模、评估3.1 数据读取与重采样把原始表格变成可建模的时间序列拿到这类资源包第一步永远是检查数据集长什么样。常见做法是里面有两张表一张是货运量月度数据字段类似month, freight_volume一张是客运量月度数据字段类似month, passenger_volume。但实际数据往往没这么干净——日期可能带着“2023年1月”这种中文格式也可能有缺失月份还有可能客货运量放在同一张宽表里。我习惯先写一个通用读取函数把两列时间序列各自抽出来。import pandas as pd import numpy as np # 读取原始表格兼容常见的 Excel 和 CSV 两种格式 def load_volume_data(file_path: str, date_col: str, value_col: str) - pd.Series: if file_path.endswith(.xlsx) or file_path.endswith(.xls): df pd.read_excel(file_path) else: df pd.read_csv(file_path, encodingutf-8-sig) # 把日期列统一成 datetime 类型并设置为索引 df[date_col] pd.to_datetime(df[date_col], formatmixed, errorscoerce) df df.dropna(subset[date_col, value_col]) df df.set_index(date_col).sort_index() # 重采样到月粒度处理重复日期与缺失值 series df[value_col].astype(float).resample(MS).sum() series series.replace(0, np.nan) series series.interpolate(methodlinear, limit3) return series freight load_volume_data(freight_data.xlsx, date, freight_volume) passenger load_volume_data(passenger_data.csv, date, passenger_volume) print(freight.head(), passenger.head())逻辑说明这个函数做了三件事——解析日期、按月度重采样、线性插值补缺失。resample(MS)表示按“每月第一天”为刻度重采样sum是聚合方式如果你的原始数据本身就是月度值改成first或last更安全避免把同月多条记录重复累加。interpolate最多连续补 3 个点超过 3 个缺失说明数据源质量太差此时宁可砍掉那段历史也不要硬补否则会造出假的趋势形态。参数说明formatmixed是 pandas 2.0 之后的推荐写法能同时兼容“2023-01-01”和“2023/1/1”这类混排格式errorscoerce会把解析失败的日期置为 NaT随后被dropna清掉。limit3这个值不是随便定的——铁路月度数据连续丢失 3 个月以上的情况极少除非源头统计口径变更而那种问题靠插值是救不回来的。3.2 基于 statsmodels 的 SARIMA 实现定阶、拟合与滚动预测对月度客货运量数据纯 ARIMA 基本不够用必须带上季节性分量也就是 SARIMA。它的参数记作SARIMA(p,d,q)(P,D,Q,s)其中s是季节周期长度月度数据取 12。定阶是最容易劝退新手的一步——p和q的取值范围一旦放开网格搜索的组数会爆炸。我的做法是先做平稳性检验再用auto_arima给一组初始值最后人工微调。import warnings warnings.filterwarnings(ignore) from statsmodels.tsa.stattools import adfuller from statsmodels.graphics.tsaplots import plot_acf, plot_pacf from statsmodels.tsa.statespace.sarimax import SARIMAX # 1. 平稳性检验原序列大概率非平稳看差分后的结果 adf_raw adfuller(freight.dropna()) adf_diff adfuller(freight.diff().dropna()) print(Raw ADF p-value:, round(adf_raw[1], 4)) print(Diff ADF p-value:, round(adf_diff[1], 4)) # 2. 用 pmdarima 的 auto_arima 快速定阶seasonalTrue 表示启用季节分量 from pmdarima import auto_arima auto_model auto_arima( freight.dropna(), start_p0, max_p3, start_q0, max_q3, d1, seasonalTrue, m12, D1, traceFalse, error_actionignore, suppress_warningsTrue, stepwiseTrue, ) print(auto_model.order, auto_model.seasonal_order)逻辑说明adfuller的 p 值大于 0.05 说明序列非平稳这时先做一阶差分再看。auto_arima会在给定范围内搜索 AIC 最小的参数组合stepwiseTrue表示用贪心搜索而不是全组合网格速度会快一个数量级。这里的m12是季节周期的关键设定——如果你把月度数据当成无季节序列后面所有预测都会在春运所在的 1、2 月集体翻车。参数说明差分阶数d和季节差分阶数D一般取 0 或 1 就够了取 2 以上会过度差分把趋势信息也抹掉。traceTrue可以打印搜索过程初次跑建议打开能看到模型在哪些参数上收敛、哪些方向被放弃这比只看最终结果有用得多。拿到定阶结果后下一步是真正拟合和预测。这里有个必须养成的习惯不要只做一次性预测要做滚动预测rolling forecast也就是每一步只预测未来一个月然后把真实值并回训练集再预测下一个月。这更接近业务实际用法也能暴露模型在多步外推时的真实水平。from statsmodels.tsa.statespace.sarimax import SARIMAX import numpy as np from sklearn.metrics import mean_absolute_error # 按自动定阶结果实例化模型手工加上季节性设定 # 这里以 auto_arima 返回的 order 和 seasonal_order 为准 order auto_model.order # 例如 (1, 1, 1) seasonal_order auto_model.seasonal_order # 例如 (1, 1, 1, 12) history list(freight.dropna().values) n_test 12 # 最后 12 个月作为测试集 test freight.dropna().values[-n_test:] predictions [] for t in range(n_test): model SARIMAX( history, orderorder, seasonal_orderseasonal_order, enforce_stationarityFalse, enforce_invertibilityFalse, ) fitted model.fit(dispFalse, max_iter200) # 只预测下一个时间点 pred fitted.forecast(steps1)[0] predictions.append(pred) # 把真实观测值追加到历史里模拟真实生产环境 history.append(test[t]) mae mean_absolute_error(test, predictions) print(Test MAE:, round(mae, 2))逻辑说明滚动预测的循环里每次都用全部历史数据重新fit一次模型。这种做法在数据量只有几百个月时计算成本完全可接受但能显著减少“一个模型定终身”带来的误差。forecast(steps1)只预测一步然后立刻把真实值推进历史——这个过程模拟的是业务上“每个月月初重新跑一次预测”的节奏。参数说明enforce_stationarity和enforce_invertibility设成False是给 SARIMA 减负的常用技巧避免模型因为数值问题无法收敛而抛出异常。max_iter200是上限保护如果数据量小、模型复杂度较高50 次迭代也可能收敛设大一点只是买个保险。3.3 用 Prophet 处理节假日效应把春运写进模型如果数据里有明显的春节、国庆脉冲SARIMA 的季节项只能捕捉“每年固定月份的形状”但它无法区分“今年春节在 2 月”和“去年春节在 1 月”这种农历差异。这时候 Prophet 的自定义节假日功能就派上用场了。Prophet 允许你传入一个holidays数据框里面指定每年春节、国庆、暑运高峰的具体起止日期模型会为每个节假日学习一个独立的脉冲效应。from prophet import Prophet import pandas as pd # 构造节假日表春节前 7 天到正月十五国庆 7 天暑运 7-8 月 # lower_window 和 upper_window 控制节日前后的影响区间 holidays_df pd.DataFrame([ {holiday: spring_festival, ds: 2021-02-12, lower_window: -7, upper_window: 14}, {holiday: spring_festival, ds: 2022-02-01, lower_window: -7, upper_window: 14}, {holiday: spring_festival, ds: 2023-01-22, lower_window: -7, upper_window: 14}, {holiday: national_day, ds: 2021-10-01, lower_window: 0, upper_window: 6}, {holiday: national_day, ds: 2022-10-01, lower_window: 0, upper_window: 6}, {holiday: national_day, ds: 2023-10-01, lower_window: 0, upper_window: 6}, ]) # Prophet 要求输入列名必须是 ds 和 y passenger_df passenger.reset_index() passenger_df.columns [ds, y] passenger_df[y] passenger_df[y].astype(float) model Prophet( yearly_seasonalityTrue, weekly_seasonalityFalse, daily_seasonalityFalse, seasonality_modemultiplicative, # 客流量季节波动随规模增大而放大 changepoint_prior_scale0.05, ) model.add_country_holidays(country_nameCN) # 注入法定节假日 model.fit(passenger_df) # 预测未来 12 个月并把节假日表透传给预测周期 future model.make_future_dataframe(periods12, freqMS) forecast model.predict(future)逻辑说明holidays_df里的ds必须是节假日当天日期lower_window-7表示春节前 7 天就开始有影响upper_window14表示春节后 14 天仍在影响窗口内。这里有个细节春运影响不是对称的节前客流集中返程、节后分散返工所以用不对称窗口来拟合会更准。add_country_holidays(country_nameCN)会注入官方法定节假日但中国的调休安排会让这个内置列表和实际客流不完全一致所以核心节假日还是建议手动维护。参数说明seasonality_modemultiplicative是客运量预测里比较关键的选择——暑期和春运的绝对增量比平季大得多用加法季节项会低估高峰。changepoint_prior_scale控制趋势拐点的敏感度调大模型会更激进地捕捉趋势变化但也更容易过拟合默认 0.05 对货运量这种慢变量偏保守可以试着调到 0.1 看验证集误差变化。这里也要提醒一句Prophet 的预测区间是基于历史残差的模拟不是理论置信区间。它的区间宽度在节假日附近会明显变宽——这其实是好事说明模型意识到“节假日效应有不确定性”但业务方如果只看点预测值可能会忽略这个信号。3.4 LSTM 最小管道从滑动窗口到模型训练用 LSTM 做月度客货运量预测最大的误区是把整条序列直接丢进网络。LSTM 需要的是“用过去 N 个月预测下一个月”的监督学习格式。N 的取值直接影响模型表现——月度数据里 N 太小捕捉不到年周期N 太大训练效率暴跌。我一般先设 24 个月两年窗口起步保证输入里至少包含一个完整的年度季节周期。import numpy as np from tensorflow.keras.models import Sequential from tensorflow.keras.layers import LSTM, Dense, Dropout from sklearn.preprocessing import MinMaxScaler from sklearn.metrics import mean_absolute_error # 1. 构造监督学习样本用过去 window 个月的量预测下一个月 def make_supervised(series, window24): X, y [], [] for i in range(len(series) - window): X.append(series[i:iwindow]) y.append(series[iwindow]) return np.array(X), np.array(y) # 2. 标准化注意 fit 必须在训练集上做测试集只能 transform scaler MinMaxScaler(feature_range(0, 1)) data passenger.dropna().values.reshape(-1, 1) scaled scaler.fit_transform(data).flatten() window 24 X, y make_supervised(scaled, window) # 按时间顺序切分最后 12 个月做测试 split_idx len(X) - 12 X_train, X_test X[:split_idx], X[split_idx:] y_train, y_test y[:split_idx], y[split_idx:] # 3. 输入 shape 要求 [samples, timesteps, features] X_train X_train.reshape(X_train.shape[0], X_train.shape[1], 1) X_test X_test.reshape(X_test.shape[0], X_test.shape[1], 1) # 4. 一个浅层 LSTM 就够不要在数据量不大时盲目堆层 model Sequential([ LSTM(32, activationtanh, return_sequencesTrue, input_shape(window, 1)), Dropout(0.2), LSTM(16, activationtanh), Dense(1), ]) model.compile(optimizeradam, lossmse) model.summary()这段代码的关键点有两个。第一MinMaxScaler必须只对训练部分fit再对整个数据集transform——如果先全量fit再切分测试集的尺度信息就已经参与训练了算数据泄漏验证效果会被系统性高估。第二make_supervised里窗口滑动生成的样本是高度重叠的这意味着相邻样本之间存在极强的自相关性模型很容易死记硬背训练时务必设置验证集来观测真实的泛化表现。参数说明LSTM(32)第一个 LSTM 层的隐藏单元数月度数据 1664 之间都算合理区间超过 64 就需要大量数据否则必过拟合。Dropout(0.2)是防止小样本过拟合的标配一般取 0.10.3 之间设太高会把记忆能力也丢干净。return_sequencesTrue的作用是让第一个 LSTM 层输出完整序列给第二层如果只有一个 LSTM 层这个参数要设成False。训练时还要做一步早停用验证集监控 loss连续若干轮不下降就停止这一步能省下大量调参时间。from tensorflow.keras.callbacks import EarlyStopping early_stop EarlyStopping( monitorval_loss, patience15, restore_best_weightsTrue, ) history model.fit( X_train, y_train, validation_data(X_test, y_test), epochs200, batch_size16, callbacks[early_stop], verbose1, ) # 预测并反标准化 pred_scaled model.predict(X_test).flatten() pred scaler.inverse_transform(pred_scaled.reshape(-1, 1)).flatten() true scaler.inverse_transform(y_test.reshape(-1, 1)).flatten() print(LSTM MAE:, round(mean_absolute_error(true, pred), 2))逻辑说明EarlyStopping(monitorval_loss, patience15)的意思是验证集 loss 连续 15 轮没有刷新最低记录就停止训练restore_best_weightsTrue确保保存的是验证集最优那轮权重而不是最后一轮。这里有个常见的翻车点——validation_data直接用了测试集这在探索阶段可以容忍但正式评估时最好再切一段不参与任何决策的最终测试集否则你根据验证集调参调多了验证集本身也会被污染。参数说明batch_size16对几十到几百条的时序数据足够小太小会让训练震荡太大会让梯度更新次数过少。epochs200只是上限有早停机制在实际训练轮数通常在 50100 之间。LSTM 的训练噪声比较大不同随机种子跑出来的结果可能有明显差异正式场景下最好固定tf.random.set_seed()保证可复现。4. 避坑指南时序预测最容易翻车的 5 个细节4.1 交叉验证方式错误导致未来数据泄漏现象用KFold或train_test_split随机切分数据验证集上 MAE 漂亮得不得了但一上真实数据预测就崩盘。原因时间序列的样本不是独立的。2021 年 3 月的客流量和 2021 年 4 月、5 月强相关把 5 月放进训练集、4 月放进验证集模型等于提前看到了答案。标准的 KFold 随机打乱在这里是致命的。解决改用时间序列专用的切分方式。最简单的是TimeSeriesSplit严格按时间先后顺序切分更严谨的是 Rolling Window Cross-Validation每次都把训练窗口向右平移。记住一条铁律在任何时候训练集的最后时间点都必须早于验证集的最早时间点。4.2 春运被当成“异常值”清洗掉现象数据预处理阶段发现 1 月或 2 月的客运量陡增觉得是噪声用 3 倍标准差或者分位数法把它拉平或删掉。结果模型在春运月份误差爆表业务方直接打回。原因春运是铁路客运量最强的周期性信号不是噪声。异常值检测算法只认识统计分布不认识“中国农历新年前后客流天然翻倍”这个业务常识。解决把春运当成一种规律性事件来建模。在 Prophet 里显式配置holidays数据框在 SARIMA 里把seasonal_order的周期设对在 LSTM 里保证滑动窗口跨过完整年度。同时在评估指标上对春运月份单独统计 MAE不要让它被全年平均成绩掩盖。4.3 用全局归一化做训练/测试切分现象LSTM 训练时 loss 下降很漂亮测试集预测曲线和真实值几乎重合于是你信心满满地上线。一个月后真实值出来预测偏离得非常离谱。原因你在切分训练集和测试集之前对全量数据做了MinMaxScaler.fit_transform()。测试集的数值范围已经参与计算了全局最小值和最大值模型在训练时就“见过”未来数据的分布信息。这属于数据泄漏的一种表现就是离线验证虚高、在线预测打回原形。解决先把数据按时间切分再在训练集上单独fit标准化器测试集只调用transform。代码上就是scaler.fit(X_train)和X_test_scaled scaler.transform(X_test)的顺序问题。这个坑在时序预测里出现频率极高几乎每一个刚上手 LSTM 的人都会踩一次。4.4 多步预测误差累积却用单步预测评估现象测试时每次只预测下一个月误差看起来还行但业务要的是未来 6 个月或 12 个月的预测曲线生成时只能用预测值迭代输入模型结果越往后越歪最后几个月的预测值直接变成了水平线。原因模型是用“真实历史值”训练出来的但多步预测时输入窗口里开始出现模型自己的预测值误差会逐级累积。这不是模型 bug而是时序预测的固有属性——任何递归式多步预测都无法避免误差累积差别只是快慢。解决评估时必须区分单步one-step和多步multi-step。多步预测的迭代评估方法叫动态验证dynamic validation每一步都 feed 预测值继续往后走产出未来 12 个月的连续预测线。SARIMA 的forecast(steps12)就是这种模式。如果业务方只要未来一个月的预算单步评估就够如果要一条全年曲线必须接受远期误差放大并在汇报时明确标注置信区间。4.5 把客运量和货运量混在一个模型里现象数据表里既有客运量又有货运量图省事把两列拼成一个多维序列直接喂给模型输出的预测曲线形态怪怪的两头都不对。原因客运量和货运量的驱动因子完全不同。客运量受节假日、人口流动、高铁网络影响货运量受工业生产和原材料运输影响两者季节相位、趋势斜率都不一致。强行塞进同一个多维序列让模型用一个隐状态去拟合两组特征结果就是两者的模式互相干扰。解决把客运量和货运量拆成两个独立模型。公共的部分可以共享数据预处理代码和评估函数但模型参数、特征集合和预测结果必须分离。如果实在要在一个脚本里管理写成两个类或两个函数各自建模最后在汇报时并排展示即可。5. 进阶验证与模型落地从“预测曲线好看”到“决策能信”到了最后这一步模型已经能跑通避坑点也摸了一遍但距离真正被业务采用还有一段路。我自己的经验是能让你在评审会上不翻车的从来不是模型在训练集上的精度而是三件看起来“不够技术”的事。第一件事是滚动回测的长期验证。不要拿最近 12 个月做一次测试就下结论把过去 3 到 5 年每个月都做一次“训练到该月、预测下月”的回测累积出几十个误差样本然后按年度和月份分别统计误差分布。这样你能回答两个业务必问的问题旺季和淡季预测误差差多少最近一年误差是收敛还是恶化我见过太多项目死在“平均误差 5%”这句话上因为春运月份的误差可能高达 25%。第二件事是预测区间的可视化。点预测是给机器看的区间预测才是给人看的。ARIMA 的get_forecast().conf_int()、Prophet 的yhat_lower/yhat_upper都能直接给出区间LSTM 则可以通过多次对不同窗口做预测取分位数来近似。把区间画成阴影区域业务方才能理解“预测 1200 万人”和“预测 1100 万到 1300 万人”之间的差别——后者给了决策留余地。第三件事是模型与业务指标的对齐。客运量预测的最终用途是排图、候补购票策略和车站人力安排货运量预测的用途是车皮调度和场站吞吐规划。因此预测结果的输出格式应该直接服务于这些场景输出月度总量之外还要有同比、环比增量和当前运力对比后要能提示“下月供需缺口约 X 万人次”。我自己的习惯是固定每周五下午跑一次全量回测把三个模型的结果并排展示在同一个表格里。这个动作坚持两个月你会自然积累一套“哪种数据状态下哪个模型更稳”的经验这比任何理论分析都有效。最后想说的是模型选型也好、调参也罢无非是拿数据在试错中找规律——踩过坑、记下来、再优化这就是工程。希望这些经验能帮你在客货运量预测这条路上少走几步弯路把时间留给更值得做的业务问题。本文还有配套的精品资源点击获取