ARTICLE · INTELLIGENCE

战地情报 · 详情页

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

Python时序建模实战:铁路运量预测从数据对齐到ARIMA与LightGBM

Python时序建模实战:铁路运量预测从数据对齐到ARIMA与LightGBM 简介一份基于Python的铁路货运量/客运量时序建模预测项目资源定位为毕业设计、期末大作业或课程设计的高分参考。项目采用时间序列建模思路覆盖数据读取、特征处理、模型构建与预测展示等完整环节附带代码注释新手也能快速理解与部署。压缩包共50个文件大小2.05MB其中包含17个Python脚本与4个R脚本用于核心建模分析19个PNG图展示预测结果与评估曲线3个Excel与2个CSV文件为货运量、客运量及运输量等数据集2个Markdown文档和txt说明辅助上手。已有269人学习下载项目整体结构清晰无需复杂配置即可复现。对于需要用Python完成时序预测课题的学生这套资料能节省大量查阅资料与调试时间尤其适合作为期末答辩和毕设演示的支撑项目。1. 从曲线能看出趋势到能拿去答辩的预测结论这套Python时序建模资源到底装了什么如果你正在做毕业设计或期末大作业题目撞上铁路货运量/客运量预测最尴尬的不是不会建模而是手里只有一份Excel画完折线图就不知道下一步该干嘛。趋势图谁都会看但导师要的是基于Python对铁路货运量/客运量进行时序建模预测的完整链路——数据对齐、特征构造、模型训练、效果评估、可视化每一步都要能拿出来讲。这套资源装的正是这条链路源代码里把时序建模预测从数据读取写到结果输出模型文件和数据集打包在一起运输量.xlsx、ads.csv、currency.csv、时序建模.xlsx这些文件对应不同的输入角色requirements.txt 帮你把环境一次拉齐。适合三类人毕设选了预测题但没跑通完整流程的课程设计想要一份能改能讲的项目骨架的以及想看看一份被导师认可的高分项目到底写到什么颗粒度的。2. 数据装进来先别建模先把时间对齐这件事做扎实时序项目里翻车最多的不是模型是数据准备。这个资源里同时存在多张表运输量.xlsx 大概率是主序列客运量/货运量月度值ads.csv 和 currency.csv 这类文件通常扮演伴随变量角色——广告投入、汇率等外生序列时序建模.xlsx 则可能是把多路数据整合之后的建模宽表。我拿到这种压缩包的第一件事不是跑模型而是先确认所有表的时间轴是不是同一个频率、有没有缺月、索引是不是有序。因为后面 ARIMA 也好、LightGBM 也好都默认你喂进去的是一串按时间排好序且频率一致的值顺序一乱滞后特征和差分全是错的。2.1 读入路径与时间戳对齐月频数据的隐藏坑主序列和外部变量分布在不同的 xlsx/csv 里第一步是把它们统一读进 DataFrame并且让日期列变成索引。这一步最常踩的坑是日期被读成字符串——字符串索引没法做 resample排序也是按字典序排的2023-01 会排在 2023-02 前面但 2023-10 会排错位置。import pandas as pd import numpy as np # 主序列运输量.xlsx 里通常是日期列 客运量 货运量 df pd.read_excel(运输量.xlsx, parse_dates[日期], index_col日期) # 统一到月频如果源数据里混了周/日粒度先采样成月度再往下走 df df.resample(MS).sum() # 外部变量ads.csv、currency.csv 这类文件按同一时间轴合并进来 ads pd.read_csv(ads.csv, parse_dates[时间], index_col时间) df df.join(ads.resample(MS).mean(), howleft) print(df.head())先解释两个关键参数parse_dates[日期]让 pandas 在读取时就把这一列转成 datetime 类型而不是等到后面再pd.to_datetime()补救index_col日期把日期设为索引这是所有时序操作的前提——resample、shift、rolling都要求时间在索引上。resample(MS)里的MS表示月初Month Start它会强制把整张表对齐到每个自然月的第一天这样春运月份、数据空缺月份都会在时间轴上留下明确位置。总和还是均值取决于变量语义客运量、货运量这种流量指标用sum()汇总汇率、价格这类比率指标用mean()更合理。join(..., howleft)表示以主序列为基准外部变量缺的月份不补行只留 NaN 等下一步处理。2.2 缺失值与量纲处理插值、差分和标准化的取舍月度数据最常见的缺失有两种某几个月完全没有记录resample 之后变成整行 NaN以及单个月份是空值。处理策略不能一刀切。连续缺 12 个月线性插值就够缺一长段先用前向填充把趋势延续住再做插值。这里给一套我常用的顺序# 连续缺失较多的列先 ffill 再线性插值避免插值函数在长空洞里乱飘 df[客运量] df[客运量].ffill().interpolate(methodlinear) # 切分建模区间最后 12 个月当测试集前面全部做训练 train df.iloc[:-12] test df.iloc[-12:] # 量纲处理ARIMA 不需要标准化LightGBM 这类树模型其实也不强制 # 但如果你后面想接神经网络或者想让 loss 曲线更稳定就统一缩放到 [0,1] from sklearn.preprocessing import MinMaxScaler scaler MinMaxScaler() for col in [客运量, 货运量]: df[col _scaled] scaler.fit_transform(df[[col]])要说明的是ffill()是拿上一个非空值填当前空值它对趋势有滞后性适合短期补缺interpolate(methodlinear)是按缺失位置前后两端的值拉一条直线中间结果不会保留原序列的波动形态。对月度铁路运量这种强季节性的序列一年缺了 3 个月以上线性插值会严重低估旺季峰值这时候更好的做法是去年同期值 × 最近月份同比增速——先用df[客运量].pct_change(12)算出同比增速再乘到去年的基期上。差分和标准化不要混着用ARIMA 里处理的是平稳性用diff()消趋势LightGBM 这类树模型对单调变换不敏感标准化主要价值是让回归目标量级变得可读不影响分裂点。另一个隐藏点做完 resample 之后一定要检查索引是否严格单调递增我在实际项目里经常遇到合并外部变量后索引乱序的情况后续所有shift()特征全错排查起来极其费时。3. 从基线到回归ARIMA和LightGBM在同一份数据里的配合方式这个资源的建模思路大概率是经典时序模型 机器学习回归双线并行这也是目前时序预测任务的标准打法。ARIMA 负责给出一个可解释的基线它假设序列由自回归项、差分项和滑动平均项组成适合捕捉线性趋势和自相关结构LightGBM 则用滞后值和滚动统计量做特征回归能捕捉到 ARIMA 看不上的非线性交互比如6 个月前的客运量 本月是否旺季这种组合模式。两条线都跑一遍不是为了炫技而是让答辩时有对照ARIMA 的残差如果明显带季节性说明线性模型不够LightGBM 的测试集误差如果明显低于 ARIMA说明特征窗口的构造是有效信息。3.1 先用ARIMA定基线平稳性检验、ACF/PACF与AIC选阶ARIMA 有三个阶数要定p自回归阶数、d差分次数、q滑动平均阶数。d 不是猜的用 ADF 检验看 p 值p 和 q 先用 ACF/PACF 图粗看再用 AIC 在小区间里网格搜索。月度铁路运量这类数据通常有明显趋势和 12 个月季节周期直接套 ARIMA 往往需要先差一次必要的时候季节差分。from statsmodels.tsa.stattools import adfuller from statsmodels.graphics.tsaplots import plot_acf, plot_pacf from statsmodels.tsa.arima.model import ARIMA import itertools # 1) 平稳性检验p 0.05 认为序列平稳否则做一阶差分 result adfuller(train[客运量].dropna()) print(ADF p值:, result[1]) d 0 if result[1] 0.05: d 1 # 2) 看差分后的 ACF/PACF粗略框定 p、q 的搜索范围 diff_series train[客运量].diff().dropna() plot_acf(diff_series, lags24) plot_pacf(diff_series, lags24) # 3) 小范围网格搜索按 AIC 选最小阶数 best_aic float(inf) best_order None for p, q in itertools.product(range(0, 5), range(0, 5)): try: model ARIMA(train[客运量].dropna(), order(p, d, q)) fit model.fit() if fit.aic best_aic: best_aic, best_order fit.aic, (p, d, q) except Exception: continue print(AIC最小阶数:, best_order)adfuller返回的 p 值大于 0.05 就拒绝序列平稳这个结论典型处理是差分一次取d1。plot_acf看的是滑动平均的影子ACF 在某阶之后突然截尾对应 q 的候选值plot_pacf看自回归的影子PACF 截尾处对应 p。这两个图在月度数据上经常拖尾交错肉眼分不清所以交给 AIC 穷举更稳。p、q 搜索范围给到 04 就够月度样本量一般一两百条阶数再高就是过拟合。那层try/except是必须的——有些 (p, q) 组合在最大似然估计时不收敛statsmodels 会直接抛异常。3.2 LightGBM做特征回归滑动窗口与滞后值怎么组织把时间序列喂给树模型第一步是把过去的数值重组成特征矩阵。这里最常见的问题是特征窗口怎么定。我的习惯是同时放短期lag1、同季度lag3/lag6、跨年同期lag12再配一个滚动均值让模型能看到短期波动、中期趋势和年度季节性三层信号。月份这一列直接作为整数特征给进去等于让模型自己学淡旺季分布。import lightgbm as lgb from sklearn.model_selection import train_test_split # 构造滞后特征与滚动统计量 df[lag1] df[客运量].shift(1) df[lag6] df[客运量].shift(6) df[lag12] df[客运量].shift(12) df[roll3_mean] df[客运量].rolling(3, min_periods1).mean() df[month] df.index.month # 建模用的特征列去掉冷启动期的 NaN 行 feat_cols [lag1, lag6, lag12, roll3_mean, month] df_ml df.dropna(subsetfeat_cols [客运量]).copy() X df_ml[feat_cols] y df_ml[客运量] # 时序数据切分顺序切分绝对不能用默认的随机打乱 X_train, X_test, y_train, y_test train_test_split( X, y, test_size0.2, shuffleFalse ) params { objective: regression, learning_rate: 0.05, num_leaves: 31, max_depth: 5, seed: 42, verbose: -1, } model lgb.train( params, lgb.Dataset(X_train, y_train), num_boost_round500, valid_sets[lgb.Dataset(X_test, y_test)], callbacks[lgb.early_stopping(stopping_rounds50)], )代码里有几个值得展开的参数。shift(n)是把整列往下移动 n 行shift(1)就是上个月的客运量shift(12)是去年同月客运量——这是时序特征构造的基石但也意味着前 12 行必然产生 NaN所以后面要用dropna(subset...)把冷启动段的行删掉。rolling(3, min_periods1).mean()是三个月移动均值min_periods1保证窗口期不足 3 个月时也不产生 NaN这个参数在数据短的时候特别重要。train_test_split(..., shuffleFalse)是关键中的关键随机打乱会让前 80% 的训练集里混进未来的测试数据模型验证分数虚高到离谱后面避坑章专门说。LightGBM 参数里num_leaves31对应树模型容量月度数据通常几百个样本叶子数超过 50 就开始记忆噪声learning_rate0.05配合early_stopping(stopping_rounds50)验证集连续 50 轮不下降就提前停止比固定 500 轮更稳。参数推荐值说明num_leaves31树复杂度上限小样本别超过 50max_depth5防止过深配合 num_leaves 一起约束learning_rate0.05小学习率 early stopping 更稳seed42固定随机种子保证可复现test_size0.2时序切分时对应最后 20% 的时间段stopping_rounds50验证集连续 50 轮不提升即早停4. 避坑时序预测里最容易翻车的五个实操问题做这类题目模型跑通只是开始真正花时间的是排错。下面五条是我在时序项目里反复踩过的坑每条都按现象 → 原因 → 解决拆开你可以当排查清单用。4.1 预测曲线总是慢半拍滞后特征带来的惯性偏移现象预测值和真实值画在一起形状很像但整体往右偏移了一两个月就像复制粘贴后平移了一段距离。原因以 lag1 为首的自变量本质上就是拿上个月的数预测这个月当序列本身有强惯性时模型学到的最优策略就是预测值 ≈ 最近几期观测值的加权平均于是预测曲线天然落后于真实曲线。解决不要只盯预测曲线形状去看残差分布如果滞后偏移动机是模型过度依赖 lag1就把 lag1 从特征里拿掉强迫模型用 lag6、lag12 和滚动均值做判断。这个坑在 ARIMA 里同样存在区别是 ARIMA 的偏移会体现在残差自相关上。4.2 切分训练集时顺手shuffle验证集分数虚高到不敢信现象LightGBM 跑完测试集 RMSE 比训练集还低或者验证集分数好得不像话换真实外推立刻翻车。原因train_test_split默认shuffleTrue数据一打乱训练集里混进了预测目标前后的真实观测值模型等于开卷考试。解决加shuffleFalse只是第一步更严谨的做法是按时间切分——X_train X.iloc[:int(len(X) * 0.8)]并且训练集和测试集之间留出至少 12 个月的空隙避免预测月份和训练月份首尾相连产生泄漏。4.3 resample之后出现成片NaN月度数据缺档位的锅现象resample(MS)之后数据中间出现连续 36 个月的 NaN画图直接断档。原因原始记录里某几个月根本没有业务发生比如某些年份的 2 月、春节前后数据缺失resample 会把没有记录的月份强行补成一行空值而不是自动跳过。解决先用df.index.min()和df.index.max()确认起止年份然后按业务经验判断缺口是正常淡季没记录还是数据没导全。前者用 ffill 填平后者需要回到源头补数据不要指望插值函数造出一段没有信息量的值。4.4 客运量的春节脉冲事件型冲击被模型当成噪声现象每年 12 月客运量出现明显尖峰但模型预测结果把尖峰削平了整体像一个被磨圆了的均值曲线。原因铁路客运量受春运这类事件强影响而月度哑变量112 月只能表达季节性均值表达不了某一年政策、票价、极端天气带来的额外脉冲。解决把 12 月单独做成一个脉冲特征df[spring_festival] df.index.month.isin([1, 2]).astype(int)同时构造上年同月同比增速作为回归特征。极端情况下直接对春运月份单独建一个小模型和主模型的分月预测拼起来。4.5 每次运行结果不一样random_state没有固定现象同一份代码跑两遍验证集指标差 10% 以上模型预测结果也不同答辩时无法自圆其说。原因LightGBM 内部有特征列采样、样本采样等随机过程train_test_split也有随机切分只要有一个种子没固定结果就不可复现。解决代码顶部统一设seed 42LightGBM 的 params 里加seed: 42, bagging_seed: 42, feature_fraction_seed: 42交叉验证的StratifiedKFold也要传random_state。养成习惯之后所有刚才还能跑出 0.03 的 loss现在变 0.05的灵异事件基本消失。5. 把预测结果拿给导师看之前滚动回测与残差检查单次训练集/测试集切分只能给出一个点估计无法回答模型在不同时间段的稳定性如何这个答辩必问的问题。我在交付这类资源时一定会附加一次滚动回测从第 36 个月开始每前进 1 个月重新训练一次向外预测 6 个月把每一轮的误差收集起来算平均值。这样最后的误差数字不是一个运气指标而是覆盖了多个时间窗口的稳定表现。from sklearn.metrics import mean_squared_error import numpy as np # 滚动回测expanding 窗口最小训练量 36 个月外推 6 个月 min_train 36 horizon 6 errors [] for start in range(min_train, len(df_ml) - horizon): train_part df_ml.iloc[:start] test_part df_ml.iloc[start:start horizon] m lgb.train(params, lgb.Dataset(train_part[feat_cols], train_part[客运量])) pred m.predict(test_part[feat_cols]) # sMAPE对称平均绝对百分比误差对量级不敏感 errors.append( np.mean(2 * np.abs(pred - test_part[客运量].values) / (np.abs(pred) np.abs(test_part[客运量].values) 1e-6)) ) print(平均sMAPE:, np.mean(errors)) print(sMAPE标准差:, np.std(errors))代码里min_train36是经验值月度数据至少 3 年才能让模型见过完整的季节周期horizon6是外推长度你可以按题目要求改成 3 或 12。误差函数选 sMAPE 而不是 RMSE是因为客运量量级从几十万到几百万波动绝对值误差在旺季和淡季不可比sMAPE 对分子分母做对称归一跨时间段对比更公平。那层1e-6是防止预测值或真实值恰好为 0 时除零报错。标准差的意义在于如果 sMAPE 均值 0.08 但标准差 0.12说明模型在某些年份段极不稳报告里要把这条写清楚。答辩或期末报告里优先放这三个输出物输出物用途做法预测 vs 实际折线图直观展示趋势拟合和外推效果按时间轴画两条折线标注测试集起点误差指标表答辩时的硬指标列出 RMSE、MAE、sMAPE附带滚动回测均值/标准差残差散点图检查是否存在系统性偏移横轴为时间或预测值纵轴为真实值减预测值残差图是一个黑匣子检验手段如果残差点基本均匀散布在 0 轴上下说明模型没有系统性低估或高估如果残差连续 6 个月同号说明某段趋势没被模型捕捉到返工的方向就是加滞后特征或增大差分阶数。从那以后我每次跑时序项目都强制走一遍滚动回测先看误差分布再看残差图两个都不离谱才敢把结论往外拿。模型变体天天翻新但数据对齐和验证这两件事始终躲不掉。希望帮到你。本文还有配套的精品资源点击获取
RELATED READING

延伸阅读

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