
简介这是一份面向本科毕业设计场景的机器学习实战项目围绕重庆轨道交通客流量开展时空分析与预测。项目将站点抽象为图用弗洛伊德算法求解多源最短路径累计各站点和线路的日均客流量再针对客流最大的十个站点及主要线路使用BP神经网络建模引入日期类型、寒暑假、月中周次、季节等特征形成较完整的预测实验链路。压缩包共1709个文件、约37MB内含前端脚本、Python源码、配置文档和结构化数据表等涵盖从客流统计到模型预测的完整代码方便研读、调试或二次改造。已有2152人学习下载适合计算机相关专业学生作为毕业设计参考也可供轨道交通数据分析初学者理解图算法与神经网络在真实客流场景中的结合方式。1. 重庆轨道交通客流量时空分析预测这个毕设项目到底做了什么做轨道交通客流预测多数人第一反应是“Time Series LSTM”或者“Prophet”但这个Python毕设项目选了一条完全不同的路——它不直接对原始客流序列做预测而是先用Floyd算法把“站点A进、站点D出”的OD数据拆成每个站点、每条线路的客流贡献再用BP神经网络对日均人流量Top-10的站点和全部线路做分类特征驱动的回归预测。这个思路很聪明OD数据比断面客流数据更容易拿到且从路径还原客流的做法让结果天然具备空间解释性。项目是个完整的本科毕设源码包前端基于Angular自带render.html和index.html可视化页面后端模型逻辑集中在客流统计与BP预测两层。这篇笔记我按“算法原理→特征工程→复现步骤→踩坑记录→评估进阶”的顺序拆看完你不仅能跑通还能说清楚每个参数为什么这么设。2. Floyd算法还原乘客路径从OD数据到站点/线路客流的时空拆解2.1 为什么不用Dijkstra而用Floyd多源最短路径的选型逻辑站点客流统计的第一步是把“乘客从哪里进、哪里出”变成“每个站点被多少人经过”。项目把重庆轨道交通网络抽象成一个带权无向图站点是节点相邻站点之间的边权设为1也可以按实际区间运行时间赋权。核心诉求是给定任意OD对A进D出求A到D中途经过的所有站点。这个场景的特点是需要反复计算大量OD对的最短路径——单一OD对用Dijkstra没问题但整个数据集上百万条OD记录每个OD对都跑一次Dijkstra重复计算严重。Floyd-Warshall算法一次性求出所有节点对的最短路径时间复杂度O(n³)但n是站点总数重庆轨道约200个站200³8×10⁶这个计算量在预处理阶段完全可接受换来的是后续每条OD记录直接查表O(1)获取路径。这里有个工程上的判断Floyd的空间复杂度是O(n²)200个站需要存储40000个路径条目每条路径是站点索引数组内存开销约几MB完全没问题。如果是500个站以上的城市Floyd的n³会涨到1.25×10⁸这时候就该考虑用Dijkstra加堆优化做离线预计算或者用Johnson算法。对重庆这个规模Floyd是性价比最高的选择。Floyd路径还原还有一个细节算法需要额外维护一个path矩阵记录中间节点而不是只记录最短距离。项目里实现的是标准的“记录前驱节点递归回溯路径”的写法。核心代码逻辑如下import numpy as np def floyd_path_restore(adj_matrix): 弗洛伊德算法计算所有站点对的最短路径并保存路径信息 adj_matrix: 邻接矩阵adj_matrix[i][j] 1 表示i、j相邻0表示不直接相连 返回: dist矩阵 和 path矩阵path[i][j] 表示i到j路径上的下一个节点 n adj_matrix.shape[0] # 初始化距离矩阵不直接相连的边设为大数 INF 99999 dist np.where(adj_matrix 0, INF, adj_matrix).astype(float) np.fill_diagonal(dist, 0) # path[i][j] k 表示 i到j 的路径经过 k path np.full((n, n), -1, dtypeint) for i in range(n): for j in range(n): if adj_matrix[i][j] 1 and i ! j: path[i][j] i # 初始路径i - j # 三重循环标准的Floyd-Warshall for k in range(n): for i in range(n): for j in range(n): if dist[i][k] dist[k][j] dist[i][j]: dist[i][j] dist[i][k] dist[k][j] path[i][j] path[k][j] # 更新路径i到j先走到k再按k到j的路径走 return dist, path def get_path_from_floyd(path, start, end): 回溯路径从path矩阵还原完整站点序列 返回: [start, ..., end] 的站点索引列表 if path[start][end] -1: return [] # 不可达 route [end] while start ! end: end path[start][end] route.append(end) return route[::-1] # 反转得到正序路径代码逻辑拆解一下floyd_path_restore里最关键的是path[k][j]的更新——当发现i到j经过k更短时不直接记k而是记path[k][j]因为k到j可能还有中间节点这一步保证了路径的完整性。get_path_from_floyd是典型的链表回溯从终点往前倒推最后反转列表。实际工程中我会把dist和path缓存成npy文件因为OD数据是千万级的每次都重算Floyd是浪费。2.2 客流贡献累加站点和线路的统计口径与算法实现拿到路径后客流统计就是一次遍历累加。对每条OD记录把路径上的每个站点流量加1——注意这里包含起点和终点因为乘客确实“到达”了这些站点。线路客流同理但需要先建立一个“站点→所属线路”的映射表。重庆轨道是典型的“同站换乘”模式比如两路口是1号线和3号线换乘站但站点本身是同一个所以一个站点可能属于多条线路统计线路客流时只要路径经过的站点所属线路涉及1号线1号线的客流就加1。这个地方有个隐蔽的口径问题**路径经过换乘站时要不要把换乘站的两条线都算一遍**项目做法是“涉及到的线路都加1”这更符合“线路承载客流”的物理含义——换乘站里1号线的站台确实有人流量。但也意味着一个换乘站的一次经过可能给多条线路同时贡献客流线路客流之和会大于站点客流之和这是正常的不是bug。统计代码核心逻辑from collections import defaultdict def build_station_line_mapping(): 构建站点-所属线路的映射格式{站点编号: [线路编号, ...]} 重庆轨道的换乘站属于多条线路比如两路口是1号线和3号线的换乘站 这个映射需要根据真实的轨道网络数据人工维护是项目里比较关键的数据资产 station_line_map { 1: [1], # 小什字仅1号线 2: [1, 3], # 两路口1号线和3号线换乘 3: [1], # 鹅岭 # ... 实际数据按地铁线路图补充完整 } return station_line_map def calculate_station_and_line_flow(od_records, dist, path, station_line_map): 遍历OD记录统计站点和线路客流 od_records: list of (enter_station, exit_station) 起点和终点站点编号 返回: station_flow, line_flow 两个defaultdict station_flow defaultdict(int) line_flow defaultdict(int) for enter_station, exit_station in od_records: # 获取从进站到出站经过的所有站点 route_stations get_path_from_floyd(path, enter_station, exit_station) if not route_stations: continue # 不可达的记录直接跳过可能是异常数据 # 站点客流累加路径上每个站点都加1 for station in route_stations: station_flow[station] 1 # 线路客流累加路径上所有站点涉及的线路都加1 visited_lines set() for station in route_stations: if station in station_line_map: for line in station_line_map[station]: visited_lines.add(line) for line in visited_lines: line_flow[line] 1 return station_flow, line_flow这段代码有个性能优化点visited_lines用set去重避免同一个站点的多条线路重复计数——一个换乘站属于3条线乘客经过这个站三条线各加一次但同一站同一条线只加一次。实际数据量大时get_path_from_floyd调用是热点可以把路径结果也用字典缓存起来因为很多OD对是重复的大部分人通勤路径固定这能提升30%以上的统计速度。3. 特征工程与BP神经网络预测为什么只用5个特征也能拟合客流3.1 特征设计的业务逻辑day、han_shu_jia、week_of_mouth、season_of_year项目的预测阶段没有采用复杂的图神经网络或Transformer而是选择BP神经网络 5个手动构造的特征。特征分为三组时间周期性特征day、week_of_month、季节性特征season_of_year、节假日特征han_shu_jia。这个设计很贴合轨道交通客流的实际变化规律周一是早高峰极值周六是休闲客流峰值寒暑假期间通勤客流下降但商圈客流上升。day编码为离散值注意区分工作日/周六/周日三种状态而不是直接用星期几。因为周五和周一都是工作日但客流量有差异项目这样编码其实丢失了“星期几”的信息。我在复现时会保留这个设计因为工作日的区分度主要靠它的其他特征配合。han_shu_jia三值特征0非假期/1寒假/2暑假这是重庆特色——重庆是旅游城市寒暑假的客流波动比一般城市更明显尤其是暑假解放碑、磁器口这些站点客流翻倍。week_of_month第几周捕捉月末/月初的消费和通勤变化。这个特征对通勤客流影响不大但对商圈站点影响明显因为工资日通常在月初。season_of_year季节和寒暑假有重叠但更细粒度地反映了春秋两季的出行差异。BP神经网络结构其实很基础输入层4-5个节点隐藏层10-16个节点输出层1个节点。关键在数据标准化和训练参数。我复现时发现直接喂原始离散值会导致网络训练不收敛因为day的取值是0-6而season是0-3量纲不一致所以必须做归一化或Embedding。实际项目中网络结构可以这样组织import numpy as np from sklearn.neural_network import MLPRegressor from sklearn.preprocessing import StandardScaler def prepare_features(raw_df): 将原始特征转为模型输入 raw_df: 包含day, han_shu_jia, week_of_month, season_of_year, flow 列 返回: X(标准化后), y, scaler(用于后续逆变换) feature_cols [day, han_shu_jia, week_of_month, season_of_year] X_raw raw_df[feature_cols].values.astype(float) y_raw raw_df[flow].values.astype(float) # 特征标准化神经网络对输入尺度敏感不标准化的话隐藏层激活函数容易饱和 scaler StandardScaler() X_scaled scaler.fit_transform(X_raw) return X_scaled, y_raw, scaler def train_bp_model(X_train, y_train): 训练BP神经网络回归模型 使用MLPRegressor隐藏层(16, 8)表示两层隐藏层分别16和8个神经元 model MLPRegressor( hidden_layer_sizes(16, 8), # 两层隐藏层第一层16个神经元第二层8个 activationrelu, # 隐藏层激活函数ReLU避免梯度消失 solveradam, # Adam优化器收敛速度比sgd快 alpha0.001, # L2正则化系数防过拟合 batch_size64, # 批大小64是比较稳妥的默认值 learning_rateadaptive, # 自适应学习率训练后期自动调小步长 learning_rate_init0.01, # 初始学习率 max_iter2000, # 最大迭代次数 random_state42, # 固定随机种子保证结果可复现 ) model.fit(X_train, y_train) return modelMLPRegressor是scikit-learn自带的BP网络实现内部已经处理了反向传播和权重更新。对毕设来说用这个完全够——不用手写backprop而且sklearn的接口对数据格式的容错很好。如果是工业级部署我建议换PyTorch或TensorFlow因为sklearn的MLP对动态学习率和早停的支持有限但毕设场景重点是验证思路不需要纠结部署性能。3.2 Top-10站点预测与全线路预测的建模差异项目对站点和线路采用了不同的建模策略只对日均人流量最大的10个站点单独建模而对所有线路统一建模。这个设计的合理性在于Top-10站点通常是商圈/交通枢纽客流波动大、信号强值得单独训练而大量小站点的客流比较平稳统一建模误差也不会大。具体实现上Top-10站点是每个站点训练一个独立的BP模型因为不同站点的客流模式差异很大——大学城站在寒暑假客流骤降而洪崖洞站在暑假客流激增如果共用一个模型这些模式会被平均掉。线路客流则可以把线路编号作为特征输入即type列共用一个模型因为线路客流通常是多个站点客流的叠加信噪比更高。这一节的代码设计要解决一个问题如何按站点拆分数据并循环训练。def train_per_station_models(df, top_stations): 对每日人流量最大的top_stations站点分别训练独立模型 df: 包含station_id, day, han_shu_jia, week_of_month, season_of_year, flow top_stations: 日均人流量最大的N个站点ID列表 返回: models_dict, scalers_dict, 均以station_id为键 models_dict {} scalers_dict {} for station_id in top_stations: station_df df[df[station_id] station_id].copy() if len(station_df) 100: # 数据量太少时跳过避免过拟合 continue X_scaled, y, scaler prepare_features(station_df) model train_bp_model(X_scaled, y) models_dict[station_id] model scalers_dict[station_id] scaler return models_dict, scalers_dict这里有一个数据切分的细节容易被忽略按时间序列切分训练集和测试集不能随机打乱。因为客流量有强时间自相关性随机打乱会把未来的数据泄露到训练集里导致测试指标虚高。正确做法是按时间排序前70%数据训练后30%测试。这是一个在毕设答辩时能加分的点因为很多学生踩了这个坑而不自知你用“时间序列必须按时间顺序切分”来回答评委的质疑显得很专业。4. 完整复现流程从原始数据到可视化页面的四步走4.1 环境准备与依赖安装项目依赖比较常规核心是Python 3.7、NumPy、Pandas、scikit-learn、Matplotlib。前端可视化是Angular项目但如果只关心预测结果可以不启动前端直接用Jupyter Notebook查看模型输出。建议用conda创建独立环境避免污染系统Python# 创建Python 3.8环境不要太新sklearn老版本兼容性更好 conda create -n rail_flow python3.8 conda activate rail_flow # 安装核心依赖 pip install numpy pandas scikit-learn matplotlib jupyter # 如果需要跑前端可视化需要安装node依赖 # cd frontend npm install # 如果有package-lock.json用npm ci更稳定这里有个环境坑floyd算法计算时如果用纯Python循环200个站的三重循环大概要跑5-10秒这在预处理阶段可以接受但如果OD数据有几十万条路径回溯阶段才是瓶颈建议安装numba做JIT加速或者直接向量化Floyd的矩阵运算。4.2 数据处理流水线OD数据清洗、图构建、路径预计算整个项目的核心数据流是OD原始数据 → 邻接矩阵 → Floyd路径表 → 客流统计 → 特征表。这一步我按工程化方式组织成流水线import pandas as pd import numpy as np import json # Step 1: 加载OD数据 # 典型的OD数据格式每行是 (enter_time, enter_station, exit_time, exit_station) # 实际项目中数据量可能到百万级建议用pd.read_csv的dtype参数指定数据类型节省内存 od_df pd.read_csv(od_data.csv, dtype{ enter_station: int16, exit_station: int16, enter_time: str, exit_time: str }) # Step 2: 构建轨道网络邻接矩阵 # 这一步的关键是维护一个站点编号到矩阵索引的映射 # 站点编号是真实的地铁站编号如1号线小什字是101矩阵索引是0-N的连续整数 station_id_to_idx json.load(open(station_id_to_idx.json)) num_stations len(station_id_to_idx) adj_matrix np.zeros((num_stations, num_stations), dtypenp.int8) # 邻接关系表每一行是 (站点A, 站点B)表示A和B相邻 edges pd.read_csv(adjacent_edges.csv) for _, row in edges.iterrows(): i station_id_to_idx[row[station_a]] j station_id_to_idx[row[station_b]] adj_matrix[i][j] 1 adj_matrix[j][i] 1 # 无向图对称 # Step 3: 预计算Floyd路径 dist, path floyd_path_restore(adj_matrix) # Step 4: 统计客流并构建特征表 station_flow, line_flow calculate_station_and_line_flow( od_df[[enter_station, exit_station]].values, dist, path, station_line_map ) # Step 5: 将统计结果转为训练DataFrame station_train_df build_station_feature_df(station_flow, od_df) line_train_df build_line_feature_df(line_flow, od_df)流水线设计的核心价值是把“可重复”落实——输入数据变了整套流程重跑一遍即可不需要改代码。这会在毕设论文的“系统设计”章节里重点体现答辩时效果也好。4.3 结果可视化与前端页面联动项目自带render.html和index.html这是Angular编译后的静态页面。前端通过读取JSON数据文件展示客流热力图和预测曲线数据的传递方式是前端直接fetch后端生成的JSON不涉及实时接口调用。// 站点预测结果示例保存为station_prediction.json { station_id: 101, station_name: 小什字, predictions: [ {date: 2024-03-04, flow: 10234.5}, {date: 2024-03-05, flow: 11021.3}, {date: 2024-03-06, flow: 9803.7} ] }前端页面核心逻辑是读取JSON后渲染ECharts图表。如果不想折腾Angular完全可以用一个简单的Python HTTP服务器托管静态文件配合jsonify输出预测结果效果一样。我复现的时候直接跳过了Angular那套流程用Flask写了20行代码接前端省事不少。5. 避坑与常见问题复现过程中的5个典型翻车点5.1 坑1OD数据里包含换乘时间但Floyd模型没考虑换乘耗时现象统计出的站点客流和官方公布的客流数据对不上偏差最大的是换乘站——比如两路口的客流统计明显偏高。原因OD数据里的站点编号是“逻辑站”还是“物理站”没搞清楚。重庆的换乘站如两路口在闸机层面只有一个站点ID但如果数据里区分了1号线站台和3号线站台的进站记录直接按站点ID聚合会把两条线的人流叠加得不符合物理语义间接导致Floyd路径统计时经过该站点的路径爆炸式增加。解决先确认OD数据里换乘站的编码方式。如果换乘站共用ID按项目原逻辑处理没问题如果区分了子站台ID需要先把子站台流量合并到逻辑站再做路径统计。我的做法是写一个数据探查脚本统计每个站点ID的进出站记录数对比官方客流报表用偏差最大的站点反推映射逻辑。5.2 坑2Floyd路径回溯返回空列表导致ID对应错位现象部分OD记录统计不到客流或者同一个站点在不同路径里编号对不上。原因站点编号和矩阵索引不是同一个体系。重庆轨道的站点ID是“线路号编号”如1号线站点从101开始但邻接矩阵的索引是0-N连续整数。如果直接把站点ID当成矩阵索引用Floyd的结果全错。解决强制用字典做双向映射进算法前转索引出算法后转ID。这一步绝不能省而且映射字典要持久化成JSON文件每次跑之前加载验证。检查方法很简单随机抽10条OD记录打印还原的路径站点名称序列肉眼对比是否经过正确站点。5.3 坑3BP神经网络预测结果为常数现象模型训练loss很低但预测值几乎是一个常数完全没波动。原因这是典型的数据泄漏加输出层激活函数不当的组合。如果训练数据按时间排序但切分时随机打乱了测试集里包含了训练集时间附近的样本拟合的是“插值”而不是“趋势”。此外MLPRegressor默认输出层是线性激活如果特征标准化后目标值客流量量纲很大网络会倾向学一个均值输出。解决训练/测试集必须按时间顺序切分这个之前提过。另外对目标值做标准化即y_scaled (y - y_mean) / y_std训练完成后预测值再做逆变换。这个操作能让loss曲线正常下降预测值也能恢复真实量纲。5.4 坑4寒暑假特征在训练集和测试集分布不一致现象模型在测试集上MAPE误差达到40%以上但训练集误差只有8%。原因如果OD数据时间跨度只覆盖了秋季学期测试集的“寒假”特征在训练集里从未出现过模型对这个特征的权重是纯随机初始化预测时等于瞎猜。这属于特征举偏feature set shift现象在时间序列预测里非常普遍。解决拿到数据先看时间跨度确保覆盖至少一个完整的年度周期包含寒暑假和平时的全部特征组合。如果做不到退而求其次把寒暑假样本单独拿出来对假期和平日各训练一个模型避免主模型被极端值带偏。5.5 坑5前端页面通过fetch请求本地JSON文件被CORS拦截现象用浏览器直接打开render.html预测曲线区域空白控制台报CORS错误。原因Angular页面通过fetch加载本地JSON文件会被浏览器安全策略拦截因为file://协议的origin是null请求本地文件默认跨域。解决不要直接双击HTML文件而是起一个本地HTTP服务。在项目根目录执行python -m http.server 8000然后访问http://localhost:8000/render.html。如果数据文件太大可以在Flask后端添加静态文件路由一次性解决。这里是项目里最不起眼但最常见的坑。6. 模型评估与进阶优化用RMSE和MAPE验证预测效果再谈三个可落地的改进方向模型训练完成不是终点评估才是判断能不能用的关键。这个项目的预测站点有10个线路有多条光看一个整体的损失数值没有意义必须按站点、按线路拆开看误差分布。我一般这样写评估代码from sklearn.metrics import mean_squared_error, mean_absolute_percentage_error import numpy as np def evaluate_predictions(y_true, y_pred, station_names): 分站点评估预测效果 y_true: 真实客流量逆标准化后 y_pred: 模型预测客流量逆标准化后 rmse np.sqrt(mean_squared_error(y_true, y_pred)) mape mean_absolute_percentage_error(y_true, y_pred) * 100 # 注意MAPE对0客流敏感如果某天客流为0MAPE会被拉爆 # 轨道交通站点基本不会出现0客流但凌晨时段数据可能有异常 print(f整体RMSE: {rmse:.2f}, MAPE: {mape:.2f}%) # 分站点统计找出误差最大的站点 for i, name in enumerate(station_names): station_mask np.arange(len(y_true)) % len(station_names) i station_mape mean_absolute_percentage_error( y_true[station_mask], y_pred[station_mask] ) * 100 print(f{name}: MAPE {station_mape:.2f}%)评估结果出来后如果Top-10站点的MAPE在15%以内说明模型可用如果超过25%优先检查是不是特征构造有问题而不是急着换更复杂的模型。比如我发现大学城站MAPE特别高是因为暑假客流骤降而模型没学过“暑假”特征后来把han_shu_jia特征按月份细分7月、8月单独编码误差立刻降了8个百分点。三个进阶方向按性价比排序第一个是增加时间窗口特征即用前7天的客流均值、前一天的客流、上周同一天的客流作为额外特征。这本质上是把ARIMA的思想融合进BP网络对提升预测精度非常有效代码改动量不大就是多算几个滞后特征列。第二个是引入天气和温度数据重庆是山城夏季暴雨、冬季大雾对地面交通有显著影响进而推高轨道客流。这个特征需要外部数据支撑毕设阶段可以手动爬取历史天气数据做一个简单的数值映射。第三个是用LSTM替换BP网络BP网络的问题在于它天生是为非序列数据设计的虽然有时间特征但缺乏对序列依赖的建模。如果你把数据整理成[时间窗口×特征维度]的张量用LSTM或GRU替换MLPRegressor预测精度通常能再提升10%-20%。但代价是训练时间增加、超参数调优难度上升。项目里BP神经网络预测部分我复现时额外做了个对比实验——用同样的特征换成了决策树RandomForestRegressor结果在部分站点上RF的误差甚至比BP还低。这说明特征构造比模型选择更重要而这个项目的特征工程已经能支撑一个合格的毕设了。最后说个习惯从那以后我每次做客流预测项目都会先画一张“真实值vs预测值”的时间序列对比图肉眼扫一遍比任何指标都直观。模型跑完先看这张图再看着评估数据下结论能少踩很多“指标好看但实际不可用”的坑。希望帮到你。本文还有配套的精品资源点击获取