☰
基于Python的轨道交通客流预测系统:从数据清洗到XGBoost/LSTM模型实战
2026/10/3 4:27:18 网站建设 项目流程

简介:这份资源是面向高校计算机相关专业学生的轨道交通客流预测系统完整源码包,适用于毕业设计、期末大作业与课程设计等场景,难度适中,评审分达到98分,源码均经本地编译验证可运行。项目以Python为核心实现客流预测逻辑,同时包含前端可视化与数据交互模块,可帮助读者理解从数据预处理、模型构建到结果展示的完整链路。压缩包共1707个文件,约37.25MB,其中ts与js文件占主体,用于前端页面与交互逻辑;py文件承载预测算法与后端服务;json、xml、sqlite3与pkl等文件负责配置、数据存储与模型持久化,另有少量vue、html、md等辅助文件,目录结构清晰便于按模块查阅。目前已有360人学习关注,适合需要快速获取可运行项目、对照实现思路并完成论文或答辩准备的同学参考使用。

1. 从一份「毕业设计-基于Python的轨道交通客流预测系统源码」说起:它到底能跑出什么

每年毕业季,计算机和轨道交通相关专业的同学都会在选题上卡壳。有人想做点「看起来有技术含量、答辩时老师问不倒、简历上还能写一行」的东西,于是「基于 Python 的轨道交通客流预测系统」就成了一个高频选项。但真正动手时,问题立刻冒出来:数据从哪来?预测模型选哪个?系统是做成网页还是桌面程序?源码拿到手之后,为什么跑不起来?

这份标题里的关键词其实已经把范围框死了:Python 是技术栈,轨道交通是业务场景,客流预测是核心算法任务,系统说明它不只是个脚本,而是一个能演示、能交互、能答辩的完整工程。它解决的不是「预测明天全城地铁有多少人」这种宏大命题,而是一个更务实的落地问题——给定某个站点或某条线路的历史刷卡/闸机数据,预测未来一段时间(通常是小时级或日级)的进出站客流,并把结果用图表和界面呈现出来。

适合读这篇的人有三类:正在做类似毕设、需要一套能跑通的最小闭环的同学;已经拿到源码但被环境、数据格式、模型参数卡住的开发者;以及想把这个方向往实际工程项目上靠、需要知道边界和坑在哪的工程师。下面我不谈空泛的「智慧交通愿景」,只讲这套东西怎么从零搭起来、每一步的参数怎么定、哪些地方最容易翻车。

2. 客流预测系统的技术选型:为什么是 Python + 时序模型 + Web 演示

2.1 业务问题先拆清楚:预测粒度决定了后面所有选型

轨道交通客流预测不是一个单一任务。按时间粒度分,有短时预测(15 分钟、30 分钟、1 小时)和长时预测(日、周、月);按空间粒度分,有单站预测、线路预测、全网预测;按输入特征分,有纯历史客流序列,也有融合天气、节假日、周边 POI 的多变量输入。毕设场景下,最常见也最容易做出效果的是「单站或少量站点的短时进出站客流预测」,粒度取 1 小时,预测未来 1 到 6 个时间步。

为什么推荐这个粒度?因为再细到 15 分钟,数据噪声会急剧放大,闸机数据在非高峰时段经常出现大量零值,模型很难学到稳定模式;再粗到日级,样本量又太少,一个站点一年也就 365 条记录,训练集撑不起来。1 小时粒度在两者之间,既有足够的样本量(一年 8760 条),又能体现早晚高峰的规律性,答辩时画出来的曲线也好看。

确定粒度之后,特征工程的方向就清楚了:时间特征(小时、星期、是否节假日)、滞后特征(前 1 小时、前 2 小时、前 24 小时的客流)、滑动窗口统计(过去 3 小时均值、过去 24 小时最大值)。这些特征不需要外部数据源,从原始刷卡记录里就能算出来,对毕设来说是最稳妥的路线。

2.2 模型选型:从 ARIMA 到 LSTM,毕设到底该用哪个

这是被问得最多的问题。我的建议是:不要一上来就上深度学习,先用统计模型把 baseline 跑出来,再用机器学习模型对比,最后视时间和算力决定要不要上 LSTM 或 GRU。

ARIMA 的优势是解释性强、训练快、对小样本友好,缺点是只能处理线性关系,遇到早晚高峰这种强非线性模式容易欠拟合。实际做的时候,用statsmodels库的ARIMA或SARIMAX,把order参数设为(1,1,1),季节项seasonal_order设为(1,1,1,24),因为客流有明显的 24 小时周期。这个配置不是拍脑袋来的,(p,d,q)里的d=1表示做一阶差分让序列平稳,p和q从 1 开始试,用 AIC 准则选最优。

随机森林和 XGBoost 这类树模型,优势是能自动捕捉非线性关系和特征交互,对缺失值和异常值也更鲁棒。用sklearn的RandomForestRegressor,n_estimators设 100 到 200,max_depth控制在 8 到 12 之间防止过拟合。树模型在毕设里的性价比很高,训练几分钟就能出结果,特征重要性还能直接画图放进论文。

LSTM 的优势是能建模长序列依赖,适合捕捉「昨天同一时段客流对今天的影响」这类模式。但它的坑也最多:需要把数据归一化到 0 到 1 之间,需要把时间序列切成滑动窗口样本,需要调sequence_length、hidden_size、learning_rate等一堆参数。如果毕设时间紧张,我一般会建议用 LSTM 做对比实验,但主力模型用 XGBoost,这样既有深度学习的「亮点」,又有稳定的结果兜底。

模型训练速度数据量要求调参难度毕设推荐度
ARIMA/SARIMAX快低中作为 baseline
随机森林中中低主力可选
XGBoost中中中主力推荐
LSTM/GRU慢高高作为对比亮点

2.3 系统架构:Flask + ECharts 是毕设演示的最短路径

预测模型跑通之后,需要一个界面来展示结果。毕设答辩时,老师不会看你终端里打印的 RMSE,他们要看到一个能点、能切换、能出图的系统。最省事的方案是 Flask 做后端,前端用 ECharts 画图,数据库用 SQLite 存历史数据和预测结果。

Flask 的路由设计很简单:一个/路由返回主页,一个/api/predict接口接收站点 ID 和预测步数,调用模型返回 JSON 格式的预测结果,前端用 ECharts 的折线图把历史客流和预测客流画在一起。SQLite 不需要额外安装服务,一个.db文件就能搞定,适合毕设这种单机演示场景。

如果你想让系统看起来更「完整」,可以加一个数据管理页面,支持上传 CSV 文件、查看原始数据表格、导出预测结果。这些功能用 Flask 的render_template和pandas的to_csv就能实现,不需要引入前端框架。记住一个原则:毕设系统的核心是「能演示预测流程」,不是「做一个生产级平台」,把精力花在模型效果和图表呈现上,比花在页面美化上划算得多。

3. 从原始刷卡数据到模型输入:数据清洗与特征工程实操

3.1 拿到一份客流 CSV 之后,先做这四步清洗

假设你手里的原始数据是某站点一段时间的进出站刷卡记录,字段可能是card_id、timestamp、station_id、type(进/出)。第一步是按小时聚合,把每一条刷卡记录归到对应的小时区间,统计每个小时的进站量和出站量。第二步是处理缺失小时,有些时段可能因为设备故障或数据导出问题没有记录,需要用前向填充或线性插值补上,不能直接删掉,否则时间序列就断了。

第三步是异常值处理。客流数据里常见的异常是「某小时突然出现一个极大值」,可能是系统重复计数或特殊事件导致。我一般用 3 倍标准差法标记异常点,然后把它替换为前后两小时的平均值。第四步是检查时间连续性,确保聚合后的数据是按小时连续排列的,中间没有跳变。

import pandas as pd import numpy as np # 读取原始刷卡记录 df = pd.read_csv('raw_card_data.csv', parse_dates=['timestamp']) # 按小时聚合进出站客流 df['hour'] = df['timestamp'].dt.floor('H') flow = df.groupby(['hour', 'type']).size().unstack(fill_value=0) flow.columns = ['in_flow', 'out_flow'] # 补齐缺失小时并插值 full_range = pd.date_range(flow.index.min(), flow.index.max(), freq='H') flow = flow.reindex(full_range) flow = flow.interpolate(method='linear') # 3倍标准差法处理异常值 for col in ['in_flow', 'out_flow']: mean, std = flow[col].mean(), flow[col].std() upper, lower = mean + 3 * std, mean - 3 * std flow[col] = np.where((flow[col] > upper) | (flow[col] < lower), flow[col].rolling(3, center=True, min_periods=1).mean(), flow[col]) flow.to_csv('hourly_flow.csv')

这段代码的逻辑是:先聚合再补全再清洗,顺序不能乱。dt.floor('H')把时间戳向下取整到小时,unstack把进站和出站拆成两列,reindex保证时间轴连续,interpolate用线性插值填补空缺。异常值处理用rolling均值替换,center=True表示取前后各一小时参与计算,min_periods=1保证边界小时也能算。

参数方面,freq='H'是小时频率,如果你的数据是 15 分钟粒度就改成'15T'。3 倍标准差是个经验值,数据波动大的线路可以放宽到 4 倍,波动小的可以收紧到 2.5 倍。清洗完的数据存成hourly_flow.csv,后面所有模型都从这个文件读。

3.2 构造时间特征和滞后特征:让模型「看见」早晚高峰

原始客流序列只有一列数值,模型从中能学到的信息有限。把时间信息拆成特征之后,模型才能区分「周一早上 8 点」和「周日早上 8 点」的差异。具体要构造的特征包括:小时(0 到 23)、星期(0 到 6)、是否周末(0/1)、是否节假日(0/1)。如果有条件拿到节假日表,直接 merge 进去;没有的话,至少把周末标出来。

滞后特征是客流预测里最有效的一类特征。lag_1表示前一小时的客流,lag_24表示前一天同一小时的客流,lag_168表示上周同一小时的客流。这三个滞后项基本能覆盖短期、日周期、周周期三种模式。滑动窗口统计也很有用,比如过去 3 小时的均值和标准差,能反映近期趋势和波动程度。

def build_features(flow_df): df = flow_df.copy() df['hour'] = df.index.hour df['weekday'] = df.index.weekday df['is_weekend'] = (df['weekday'] >= 5).astype(int) # 滞后特征 for lag in [1, 2, 3, 24, 168]: df[f'lag_{lag}'] = df['in_flow'].shift(lag) # 滑动窗口统计 df['rolling_mean_3'] = df['in_flow'].shift(1).rolling(3).mean() df['rolling_std_3'] = df['in_flow'].shift(1).rolling(3).std() df['rolling_max_24'] = df['in_flow'].shift(1).rolling(24).max() df = df.dropna() return df feature_df = build_features(flow) print(feature_df.shape)

这里有个关键细节:所有滞后和滑动窗口特征都要用shift(1)先错开一位,否则当前小时的客流会「泄露」到特征里,模型在训练集上表现很好,一到测试集就崩。这是时序预测里最经典的翻车点,很多人第一次做都会踩。dropna()会删掉因为 shift 产生的空值行,通常前 168 行会被删掉,这是正常的。

特征构造完之后,用train_test_split切分数据集时不能随机打乱,必须按时间顺序切。一般取前 80% 做训练,后 20% 做测试。如果用 XGBoost,直接把特征矩阵和标签传进去就行;如果用 LSTM,还需要额外做归一化和滑动窗口切分,这部分在下一章展开。

3.3 训练集和测试集怎么切:时间序列不能随机打乱

很多人习惯用sklearn的train_test_split默认参数,随机抽 20% 做测试集。这在时间序列任务里是错的。随机打乱会让模型「看到未来」,测试集里的某些时间点可能在训练集之后,但模型已经通过相邻样本间接学到了未来的信息,导致评估结果虚高。

正确的做法是按时间切:假设有 8760 条小时数据,取前 7000 条做训练,后 1760 条做测试。如果要做交叉验证,用TimeSeriesSplit,它保证每一折的训练集都在测试集之前。评估指标用 MAE、RMSE 和 MAPE,其中 MAPE 要注意分母为零的情况,客流为零的小时会导致除零错误,实际计算时加一个极小值或者过滤掉零值样本。

from sklearn.model_selection import TimeSeriesSplit from sklearn.metrics import mean_absolute_error, mean_squared_error X = feature_df.drop(columns=['in_flow', 'out_flow']) y = feature_df['in_flow'] split_idx = int(len(X) * 0.8) X_train, X_test = X.iloc[:split_idx], X.iloc[split_idx:] y_train, y_test = y.iloc[:split_idx], y.iloc[split_idx:] tscv = TimeSeriesSplit(n_splits=5) for train_idx, val_idx in tscv.split(X_train): print(f"Train: {len(train_idx)}, Val: {len(val_idx)}")

TimeSeriesSplit的n_splits=5表示做 5 折,每一折的训练集大小递增。这个类在sklearn里直接可用,不需要额外安装。切分完之后,训练模型时把X_train和y_train传进去,预测时用X_test,算出来的 MAE 和 RMSE 才是可信的。如果 MAE 在测试集上比训练集高很多,说明过拟合了,需要降低模型复杂度或增加正则化。

4. 模型训练与预测:XGBoost 和 LSTM 两条路的具体参数

4.1 XGBoost 版本:十分钟跑完训练和评估

XGBoost 是我在毕设场景下最推荐的模型,原因是训练快、调参少、效果稳定。用xgboost库的XGBRegressor,核心参数有五个:n_estimators控制树的数量,max_depth控制每棵树的深度,learning_rate控制每棵树的贡献权重,subsample控制每棵树用的样本比例,colsample_bytree控制每棵树用的特征比例。

import xgboost as xgb from sklearn.metrics import mean_absolute_error, mean_squared_error import numpy as np model = xgb.XGBRegressor( n_estimators=300, max_depth=6, learning_rate=0.05, subsample=0.8, colsample_bytree=0.8, random_state=42 ) model.fit(X_train, y_train, eval_set=[(X_test, y_test)], verbose=False) preds = model.predict(X_test) mae = mean_absolute_error(y_test, preds) rmse = np.sqrt(mean_squared_error(y_test, preds)) print(f"MAE: {mae:.2f}, RMSE: {rmse:.2f}")

参数解释:n_estimators=300配合learning_rate=0.05是一个比较稳的组合,树多但每棵树学得慢,不容易过拟合。max_depth=6是经验值,再深容易记住训练集噪声,再浅可能欠拟合。subsample=0.8和colsample_bytree=0.8是行采样和列采样,相当于给模型加了两层随机性,能提升泛化能力。eval_set传测试集进去是为了在训练过程中观察验证误差,如果开了early_stopping_rounds,可以在验证误差不再下降时提前停止。

训练完之后,用model.feature_importances_看特征重要性,通常lag_1、lag_24、hour这三个特征排在最前面,这符合客流预测的直觉:最近一小时的客流、昨天同一小时的客流、当前是几点,这三个信息对预测贡献最大。把特征重要性画成条形图放进论文,是一个很好的加分项。

4.2 LSTM 版本:归一化、滑动窗口和三个必调参数

LSTM 的流程比 XGBoost 多三步:归一化、滑动窗口切分、维度变换。归一化用MinMaxScaler把客流缩放到 0 到 1 之间,因为 LSTM 的激活函数对输入范围敏感,不归一化会导致梯度爆炸或消失。滑动窗口是把连续的时间序列切成固定长度的样本,比如用过去 24 小时预测下一小时,sequence_length就设为 24。

import torch import torch.nn as nn from sklearn.preprocessing import MinMaxScaler scaler = MinMaxScaler() scaled = scaler.fit_transform(feature_df[['in_flow']]) def create_sequences(data, seq_len): xs, ys = [], [] for i in range(len(data) - seq_len): xs.append(data[i:i+seq_len]) ys.append(data[i+seq_len]) return np.array(xs), np.array(ys) SEQ_LEN = 24 X_seq, y_seq = create_sequences(scaled, SEQ_LEN) class LSTMModel(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 = LSTMModel(input_size=1, hidden_size=64, num_layers=2) criterion = nn.MSELoss() optimizer = torch.optim.Adam(model.parameters(), lr=0.001)

三个必调参数:hidden_size控制 LSTM 内部状态的维度,64 是一个常用起点,数据量大可以加到 128,数据量小就降到 32。num_layers=2表示两层 LSTM 堆叠,层数越多表达能力越强但越容易过拟合,毕设场景下 1 到 2 层足够。lr=0.001是 Adam 优化器的经典学习率,如果训练损失震荡就降到 0.0005,如果下降太慢就升到 0.002。

训练循环里要注意把数据转成torch.Tensor,并且用DataLoader分批送进去。每个 epoch 结束后在验证集上算一次损失,如果连续 10 个 epoch 验证损失不下降就提前停止。训练完之后,预测结果要用scaler.inverse_transform反归一化回原始量级,否则画出来的图和实际客流对不上。

4.3 预测结果怎么评估:MAE、RMSE 和 MAPE 各看什么

MAE 是平均绝对误差,单位和人流量一致,解释起来最直观:MAE 等于 50 就是说平均每个小时的预测偏差是 50 人。RMSE 是均方根误差,对大误差更敏感,如果 RMSE 比 MAE 大很多,说明存在少数预测偏差特别大的样本,需要去检查那些时间点是不是有异常事件。

MAPE 是平均绝对百分比误差,适合比较不同站点的预测效果,因为大站和小站的绝对误差不可比,但百分比误差可以。计算 MAPE 时要过滤掉真实值为零的样本,或者给分母加一个平滑项。毕设论文里通常三个指标都报,MAE 和 RMSE 放主表,MAPE 作为补充。

指标公式含义适用场景注意事项
MAE绝对误差的平均值同站点不同模型对比单位与客流一致
RMSE误差平方均值的开方关注大误差场景受异常值影响大
MAPE百分比误差的平均值不同站点横向对比需处理零值

如果 XGBoost 的 MAE 比 LSTM 低,不要觉得 LSTM「白做了」。在毕设里,两个模型对比本身就是一种贡献,说明你做了实验、有数据支撑、能分析原因。通常 XGBoost 在短时预测上不输 LSTM,因为树模型对特征工程的质量更敏感,而滞后特征已经捕捉了大部分时序模式。

5. 避坑与排查:这套系统最容易翻车的五个地方

5.1 现象:模型在训练集上 MAE 很低,测试集上翻倍

原因几乎总是数据泄露。最常见的是构造滞后特征时忘了shift,当前小时的客流被当成特征喂进去了。另一个可能是归一化时用了全量数据的均值和方差,而不是只用训练集的统计量。解决方法是检查特征矩阵里有没有和标签高度相关的列,归一化时先fit训练集再transform测试集。

5.2 现象:Flask 接口返回的预测结果全是同一个值

这通常是模型加载失败或者输入特征维度不对。检查model.predict的输入 DataFrame 列顺序是否和训练时一致,XGBoost 对列顺序敏感,列顺序错了不会报错但结果会乱。另一个可能是前端传过来的站点 ID 没有匹配到对应的模型文件,导致用了默认模型。在接口里加一行print(X.columns.tolist())对比训练时的列名,能快速定位。

5.3 现象:ECharts 折线图画出来是断的或者时间轴错乱

原因是传给前端的 JSON 里时间字段格式不统一。pandas的Timestamp直接转 JSON 会变成时间戳数字,ECharts 需要的是'2024-01-01 08:00:00'这种字符串。解决方法是导出前用dt.strftime('%Y-%m-%d %H:%M:%S')格式化,并且确保历史数据和预测数据的时间轴是连续的,预测部分的起始时间要接在历史数据最后一个时间点之后。

5.4 现象:LSTM 训练损失不下降,一直卡在某个值

先检查归一化有没有做,没归一化的客流数据范围可能在几百到几万之间,LSTM 根本学不动。如果归一化做了还是不降,检查sequence_length是不是太长,24 小时窗口对某些站点可能太长,改成 12 试试。再检查学习率,0.001不奏效就试0.0001和0.01,学习率对 LSTM 的影响比 XGBoost 大得多。

5.5 现象:换一台电脑跑源码就报错,依赖装不上

毕设源码最常见的环境问题是 Python 版本和库版本不匹配。requirements.txt里如果写的是pandas而不带版本号,在新环境里可能装到不兼容的新版本。建议在源码里固定主要库的版本,比如pandas==2.0.3、xgboost==2.0.0、torch==2.0.1。另外,Windows 和 Linux 下torch的安装命令不同,源码里最好分别注明,或者用pip install torch --index-url指定 CPU 版本,避免下载几个 G 的 CUDA 包。

提示:如果拿到源码后第一步就报ModuleNotFoundError,先看requirements.txt是否存在,不存在就根据import语句手动装。不要一上来就升级所有库到最新版,版本兼容性在毕设环境里比新特性重要得多。

6. 让预测结果更可信:残差分析和答辩时的一个小技巧

模型跑通、指标算完,很多人就停在这里了。但答辩时老师最容易追问的是:「你怎么知道模型学到的是真实规律,而不是碰巧拟合了数据?」这时候残差分析就是你的后悔药。把测试集的预测值和真实值相减得到残差序列,画成折线图,如果残差在零附近随机波动、没有明显趋势,说明模型没有系统性偏差;如果残差在某些时段持续为正或为负,说明模型在那个时段有系统性高估或低估,需要针对性调整。

具体操作:用residuals = y_test - preds算出残差,然后用matplotlib画residuals.plot(),再加一条plt.axhline(0, color='red', linestyle='--')的零线。如果发现残差在早晚高峰时段偏大,说明模型对高峰的捕捉不够,可以尝试增加高峰时段的样本权重,或者在特征里加入「是否高峰时段」的标记。这个分析过程写进论文,比单纯报一个 MAE 数字有说服力得多。

另一个答辩时很实用的小技巧:准备一个「极端场景」的演示。比如选一个节假日或者暴雨天,展示模型在那一天的预测曲线和真实曲线的对比。如果预测偏差明显变大,不要藏起来,主动分析原因——节假日客流模式和平日不同,模型训练时这类样本少,所以预测不准。这种「知道模型边界在哪」的态度,比声称「我的模型准确率 95%」更能打动答辩老师。

我自己做这类项目最大的教训是:不要等到系统全部做完才开始写论文。每跑通一个模块,就把对应的代码片段、参数配置、结果图表整理成文档。否则到最后一周,你会发现模型调参的记录找不到了,特征重要性的图忘了保存,只能重新跑一遍。把「可复现」当成第一优先级,比追求模型精度多涨一个点重要得多。希望帮到你。

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

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询