简介这是一套基于机器学习的重庆轨道交通客流量时空分析预测本科毕业设计项目面向需要完成毕设或希望实践客流预测的Python开发者。项目将地铁站点抽象为图利用弗洛伊德算法求解多源最短路径以此统计每位乘客途经站点与线路的客流量再对日均客流最大的10个站点建立BP神经网络预测模型引入工作日、周末、寒暑假、月中周次与季节等特征形成从数据清洗、特征工程到模型评估的完整链路。资源包共1709个文件包含Python源码、PyCharm工程配置、前端Vue与页面组件、JSON配置文件、Markdown说明文档、地图与可视化图片、客流XLS表格以及sqlite数据库等压缩包约37MB目录划分清晰便于按模块阅读与二次开发。项目还附带预测模型文件与可视化页面可辅助理解站点客流规律。已有2153人学习下载适合作为毕业设计参考、课程项目复现或机器学习应用研究的起点。1. 重庆轨道交通客流量时空分析预测一份能直接跑的毕设源码车站客流预测听起来像是个偏研究的方向但拿到这份 Python 毕业设计源码后你会发现整条链路是能落地的先把重庆轨道交通网络抽象成图用弗洛伊德算法算出任意两站之间的最短路径再拿 AFC 刷卡数据去还原每个乘客实际经过哪些站、哪些线路进而统计站点和线路的日均客流量最后把时间特征喂给 BP 神经网络对日均流量最大的 10 个站点和每条线路做预测。它适合正在做公共交通相关毕设或课设的学生也适合想快速搭建一套客流统计与预测流程的从业者。与其从零造轮子不如直接在这个项目骨架上改自己的数据下面我把统计口径、特征构造、模型参数和几个坑一次性说清。2. 客流统计弗洛伊德算法把“谁经过哪些站”算清楚2.1 建图与最短路径为什么这里选弗洛伊德算法客流量统计的核心难点不是“有多少人刷卡进站”而是“这个人中途经过了哪些站”。重庆轨道交通是典型的网状线路1 号线、2 号线、3 号线之间通过换乘站相连乘客从 A 站进、D 站出中间可能跨线、换乘途径站点是一整条路径。这个项目把每个站点抽象成图的一个节点相邻两个站点之间连一条带权值的边整个地铁网络就变成了一个邻接矩阵。然后需要解决的是多源最短路径问题任意两个站点之间走哪条路线经过的站点数最少、代价最小。Dijkstra 算法跑单源没问题但这里是全站点对之间的路径弗洛伊德算法一次跑完所有节点对三重循环实现简单逻辑也容易在答辩时讲清楚。对重庆这种站点数在 200 个左右规模的网络弗洛伊德算法的耗时完全可接受这是毕业设计场景里最稳的选型。建图代码常见做法如下import numpy as np INF float(inf) def build_adj_matrix(stations, edges): n len(stations) idx {s: i for i, s in enumerate(stations)} adj np.full((n, n), INF) np.fill_diagonal(adj, 0) for u, v, weight in edges: i, j idx[u], idx[v] adj[i][j] weight adj[j][i] weight return adj, idx这里edges是相邻站点列表每条记录包含起点站、终点站和边权。边权我建议直接用两站之间的运行时间而不是直线距离因为乘客换乘时选的是“时间最短”而不是“距离最短”。无向图要做双向赋值否则弗洛伊德算法在回溯路径时会断链。核心的弗洛伊德算法实现如下def floyd(adj): n adj.shape[0] dist adj.copy() nxt np.full((n, n), -1, dtypeint) for i in range(n): for j in range(n): if adj[i][j] ! INF: nxt[i][j] j 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] nxt[i][j] nxt[i][k] return dist, nxt def extract_path(nxt, u, v): if nxt[u][v] -1: return [] path [u] while u ! v: u nxt[u][v] path.append(u) return pathnxt矩阵是弗洛伊德算法的关键副产品它记录的是 i 到 j 最短路径上经过的第一个中间节点。有了它提取完整路径时不用递归一个 while 循环就能回溯出来。这个nxt矩阵到底有什么用答案是它让后面的客流累计变得极其廉价每一条 OD 记录只需要调用一次extract_path就拿到了完整的途经站点序列。2.2 站点客流统计途经一个站就累加一次站点客流统计的思路很直接每条乘客记录从起点站 A 到终点站 D 的最短路径已经算出来了路径上经过的每个站点日均人流量都加 1。station_flow {s: 0 for s in stations} for a, d in od_records: path extract_path(nxt, idx[a], idx[d]) for s in path: station_flow[stations[s]] 1 # 日均客流量 总流量 / 统计天数 daily_station_flow {s: v / total_days for s, v in station_flow.items()}代码逻辑很直白外层循环遍历每一条 AFC 刷卡记录内层循环把路径上每个站点的计数器加 1。这个统计口径的本质是“客流贡献分摊”——一个人从 A 到 D他在路径上的每个站点都产生了“存在性影响”所以每个站都计一次。日均值的计算要注意如果od_records覆盖了完整时间段需要除以统计天数如果只是样本抽样则要除以样本比例这两者的口径要在论文里写清楚不然答辩时容易被追问。这里有一个需要留意的地方换乘站在同一路径中只出现一次因为图里的节点是唯一的path不会重复包含同一个站点。也就是说从 1 号线换到 3 号线时换乘站本身被计了一次客流它承担了换乘压力但不会被重复计数。这是这个算法的优点但也带来一个口径上的取舍它无法区分换乘客流和进出站客流只能统计“经过量”。对站点日常拥堵评估来说这个量已经够用了。2.3 线路客流统计按边归属线路去重累加线路级客流量统计稍微复杂一点因为路径上相邻两个站点构成的边可能属于不同的线路。比如乘客从 1 号线的 A 站坐到换乘站 B再换到 3 号线坐到 D这条路径经过了 1 号线和 3 号线两条线路。线路客流统计要解决的问题是这条 OD 记录对哪几条线路产生了贡献。line_of_edge {} for line_name, station_list in lines.items(): for i in range(len(station_list) - 1): line_of_edge[(station_list[i], station_list[i1])] line_name line_flow {line: 0 for line in lines} for a, d in od_records: path extract_path(nxt, idx[a], idx[d]) path_lines set() for u, v in zip(path[:-1], path[1:]): line line_of_edge.get((u, v)) if line: path_lines.add(line) for line in path_lines: line_flow[line] 1这段代码的关键是用set()对线路去重。原因很现实重庆轨道交通有共线运营区间某些路段会有两条线路并行同一段路径在line_of_edge里可能映射到同一条线路的多个连续边不去重就会把同一条 OD 记录的贡献重复累加导致 1 号线客流直接翻倍。去重之后一条记录对某条线路最多只贡献 1 次这个口径和官方统计的“线路客运量”更接近。提示line_of_edge的构造依赖线路站点列表如果原始数据里只有线路和站点名称没有现成的相邻关系需要先按顺序生成邻接关系再建映射。这一步做错了后面所有线路预测都会失真。2.4 统计结果怎么落盘输出成时间序列才能喂给模型客流统计的最终产出不仅仅是一张汇总表而是带时间维度的序列数据。因为后续要做预测必须知道每一天、每一个站点/每一条线路的客流量是多少才能构造出时间特征去训练模型。常见做法是把统计结果输出成 CSV字段包括日期、站点编号、线路编号、客流量按日期排序。这样从数据清洗到模型训练一条 pandas 链路直接贯穿不用反复改格式。数据的时间跨度建议至少 3 个月以上否则周末和工作日的样本量太少BP 神经网络很难学到稳定的周期性规律。3. 机器学习中的数据处理是什么特征构造、编码与归一化3.1 预测目标与可用特征客流量预测的机器学习建模首先要明确“用什么预测什么”。这个项目的预测目标是给定某一天的时间属性预测某个站点或某条线路当天的客流量。所以特征全部来自日期本身没有引入天气、节假日营销等外部数据这是毕业设计里最常见也最稳妥的做法。原始客流表经过上一章的统计后基本包含这些字段日期、站点、线路、客流量。要把它变成可训练的数据集核心工作是把日期字段拆解成模型能理解的数值特征。项目正文里给出的特征有四个day工作日、周六、周天、han_shu_jia寒假、暑假、不是假期、week_of_month该周是一个月中的第几周、season_of_year一年中的哪个季节。这四个特征描述的是客流波动的核心周期性一天内的类型差异、一周内的位置差异、寒暑假的长期偏移、季节性的趋势差异。对应到机器学习的数据处理流程这里的核心问题就是类别特征如何编码、日期字段如何转换、数值范围如何统一。下面一步步拆开。3.2 日期字段转特征编码方式决定模型下限特征构造的代码通常是这个样子import pandas as pd def classify_holiday(d): if d.month in (7, 8): return 2 # 暑假 if d.month in (1, 2): return 1 # 寒假 return 0 # 非假期 def to_week_of_month(d): return (d.day - 1) // 7 1 season_map {1: 1, 2: 1, 3: 1, 4: 2, 5: 2, 6: 2, 7: 3, 8: 3, 9: 3, 10: 4, 11: 4, 12: 4} def build_time_features(df): df[date] pd.to_datetime(df[date]) df[day] df[date].dt.dayofweek.apply( lambda x: 0 if x 5 else (1 if x 5 else 2) ) df[han_shu_jia] df[date].apply(classify_holiday) df[week_of_month] df[date].apply(to_week_of_month) df[season_of_year] df[month].map(season_map) return df编码规则需要仔细说明day用 0 表示工作日1 表示周六2 表示周天。为什么要分周六和周天而不是合并成“周末”因为重庆轨道交通周六和周天的客流形态有明显差异周六全天客流峰值更高周天晚间返程客流集中合并会丢信息。han_shu_jia是三级分类0 非假期、1 寒假、2 暑假。寒假和暑假对轨道交通的影响幅度不同寒假有春节返乡暑假有旅游和学生出行分开编码更合理。这里直接用数值编码而不是独热编码是因为三者存在有序关系——寒暑假对客流的冲击都大于平时数值编码保留了这种强度差异。week_of_month按(day - 1) // 7 1计算结果是 1 到 5 之间的整数。这是近似处理因为每个月天数不同最后一周可能只有 2 到 3 天但对模型来说它捕捉的是“月初、月中、月末”的位置信息近似的代价可控。season_of_year是季度编号1 到 4。注意重庆的 12 月我归到了第 4 季度这种硬编码映射逻辑很简单也方便在答辩时解释。特征构造完成后数据集长这样字段类型取值示例说明datedatetime2023-04-15原始日期仅用于排序dayint1工作日/周六/周天han_shu_jiaint0/1/2非假期/寒假/暑假week_of_monthint3月中第几周season_of_yearint2春季/夏季/秋季/冬季flowfloat45210.5当日客流量预测目标3.3 归一化与时间序列切分两个不能省的动作特征构造完之后进入机器学习中最关键的预处理环节归一化和训练集切分。BP 神经网络基于梯度下降训练特征数值范围不统一时梯度会来回震荡收敛极慢甚至发散。day的取值是 0 到 2season_of_year是 1 到 4量级尚可但客流量flow动辄几万差别巨大所以目标值也必须归一化。from sklearn.preprocessing import MinMaxScaler feature_cols [day, han_shu_jia, week_of_month, season_of_year] df df.sort_values(date).reset_index(dropTrue) X df[feature_cols].values y df[flow].values.reshape(-1, 1) split_idx int(len(df) * 0.8) X_train, X_test X[:split_idx], X[split_idx:] y_train, y_test y[:split_idx], y[split_idx:] scaler_x MinMaxScaler() scaler_y MinMaxScaler() X_train scaler_x.fit_transform(X_train) X_test scaler_x.transform(X_test) y_train scaler_y.fit_transform(y_train).ravel() y_test scaler_y.transform(y_test).ravel()这段代码里有三个容易被忽略的细节。第一切分前必须sort_values(date)因为客流是时间序列绝不能随机打乱后切分这个问题在避坑章会展开说。第二scaler_x只在训练集上fit测试集只用transform如果把全量数据拿去fit测试集的信息就泄漏到了训练过程中模型评估结果会虚高。第三scaler_y的inverse_transform要在预测后调用否则你得到的预测值停留在 0 到 1 的区间无法和真实客流量对比。提示MinMaxScaler 的默认范围是 0 到 1它对异常值敏感。如果某一天因突发活动导致客流量特别大归一化后其他日期的数值会被压缩到很小的区间。遇到这种情况可以先做分位数截断或者改用 RobustScaler。4. BP 神经网络预测网络结构、训练参数与双分支建模4.1 为什么选 BP 神经网络预测任务到这里已经变成了一个标准的回归问题4 个时间特征输入1 个客流值输出。可选模型有线性回归、随机森林、XGBoost 和支持向量回归但本科毕设选 BP 神经网络有三个现实理由一是训练数据量不大特征只有 4 个全连接网络足够表达非线性关系二是 BP 神经网络的结构在论文里好展开隐藏层层数、神经元数量、激活函数、学习率都可以逐个解释三是 Keras 或 scikit-learn 的实现代码非常简洁不需要手动推导梯度。特征少不代表线性可分。客流与工作日、寒暑假的关系存在明显的非线性交互比如暑假的工作日客流和平时的工作日客流变化趋势完全不一样线性模型对这种交互无能为力BP 神经网络的隐藏层可以自动学习这些组合特征。4.2 逐站点建模10 个站点各训练一个模型项目正文明确说“只对日均人流量最大的 10 个站点进行人流预测”。为什么要限定 10 个站点而不是全量因为站点之间的客流形态差异很大重庆北站是交通枢纽工作日和节假日客流都高李子坝是网红打卡点周末客流明显上涨纯办公区站点则相反。把所有站点塞进一个模型模型会被差异大的样本搞糊涂。按站点分别建模每个模型学到的是该站点自身的客流规律这是更合理的工程做法。from sklearn.neural_network import MLPRegressor from sklearn.preprocessing import MinMaxScaler top10_stations [...] # 按日均客流量排序取前 10 feature_cols [day, han_shu_jia, week_of_month, season_of_year] models {} scalers {} for station in top10_stations: sub df[df[station] station].sort_values(date).reset_index(dropTrue) X sub[feature_cols].values y sub[flow].values.reshape(-1, 1) split_idx int(len(sub) * 0.8) X_train, X_test X[:split_idx], X[split_idx:] y_train, y_test y[:split_idx], y[split_idx:] sx MinMaxScaler() sy MinMaxScaler() X_train sx.fit_transform(X_train) X_test sx.transform(X_test) y_train sy.fit_transform(y_train).ravel() y_test sy.transform(y_test).ravel() clf MLPRegressor( hidden_layer_sizes(64, 32, 16), activationrelu, solveradam, learning_rate_init0.001, max_iter500, random_state42 ) clf.fit(X_train, y_train) models[station] clf scalers[station] (sx, sy)模型的参数不是随便填的我逐个说明。hidden_layer_sizes(64, 32, 16)是三层隐藏层神经元逐层递减第一层 64 个神经元负责放大输入维度第三层 16 个神经元负责压缩特征。对 4 个输入特征来说这个结构已经偏深但不会过拟合因为数据量虽然不大特征维度更低。activationrelu是目前 MLP 的默认标配比 sigmoid 和 tanh 收敛更快梯度消失问题也轻得多。solveradam是自适应学习率优化器对学习率的初始值不敏感省去手动调参的麻烦500 次迭代足以让这个小网络收敛。训练完成后测试集的预测要走一遍完整的反归一化流程pred sy.inverse_transform(clf.predict(X_test).reshape(-1, 1)) y_true sy.inverse_transform(y_test.reshape(-1, 1))这一步漏掉的话你拿到的预测值会被压缩在 0 到 1 之间和真实客流完全无法对比。这个错误在实操中出现频率极高属于做完归一化最容易忘的收尾动作。4.3 评估指标MAE 和 MAPE 怎么读模型训练得好不好不能只看 loss 曲线对客流预测项目我一般看两个回归指标MAE平均绝对误差和 MAPE平均绝对百分比误差。from sklearn.metrics import mean_absolute_error import numpy as np for station, clf in models.items(): sub df[df[station] station].sort_values(date).reset_index(dropTrue) X sub[feature_cols].values y sub[flow].values.reshape(-1, 1) split_idx int(len(sub) * 0.8) X_test X[split_idx:] y_test y[split_idx:] sx, sy scalers[station] X_test sx.transform(X_test) pred sy.inverse_transform(clf.predict(X_test).reshape(-1, 1)) y_true sy.inverse_transform(y_test.reshape(-1, 1)).ravel() mae mean_absolute_error(y_true, pred) mape np.mean(np.abs((y_true - pred) / (y_true 1e-6))) * 100 print(f{station}: MAE{mae:.1f}人, MAPE{mape:.2f}%)y_true 1e-6是防止除零的常见技巧因为某些小站点的客流可能在某些极端日期为 0。MAE 的值要看站点量级日均 5 万人的站点 MAE 三千到四千算正常日均 1 万人的站点 MAE 一千以内才算合格。MAPE 是相对指标10% 以内在这个特征条件下已经不错超过 20% 就要检查特征或训练集切分是否有问题。4.4 线路预测分支把 type 加进特征站点预测和线路预测的结构几乎一样唯一的差别是线路预测多了一个type特征——所属线路编号。重庆轨道交通 1 号线、2 号线、3 号线的客流形态差异明显1 号线贯穿核心城区2 号线是沿江景观线3 号线连接北部新区所以线路编号本身是强特征。feature_cols_line [type, day, han_shu_jia, week_of_month, season_of_year]注意type在这里是类别特征直接传入数值编码即可因为编号 1、2、3 本身携带了线路的相对位置信息不需要独热展开。训练流程和站点预测完全一致只是样本换成了按线路聚合后的日客流序列。到这里整个项目的机器学习部分就完整了客流统计产出序列数据特征工程构造输入BP 神经网络完成预测评估指标验证效果。5. 避坑记录复现这份项目最容易踩的六个坑5.1 最短路径算出来的路径和实际出行不符现象客流统计结果里某些站点的客流量明显高出认知比如原本冷门的站点日均客流上万。对比地图后发现模型算出的路径和乘客实际常走的路线完全不是一回事。原因建图时边的权重用了两站之间的直线距离而不是运行时间。重庆是山城两站直线距离很近但可能没有直达列车弗洛伊德算法会选一条在空间上“看起来近”但实际需要绕行的错误路径。解决权重统一改成两站之间的运行时间这个数据在轨道交通官方线路图和时刻表里都能查到。改动只涉及build_adj_matrix的edges输入代码层面不需要动。5.2 换乘站被重复计数现象统计出的全网站点客流总量比 AFC 进出站记录总和还高部分换乘站客流量异常突出。原因OD 路径里换乘站确实只出现一次但如果统计代码里把每个“经过的区间”也去计入站客流换乘站就会被重复累加。客流统计的口径必须统一项目原意的“途经站点加 1”并不包含区间重复计数。解决严格按项目摘要的口径执行对路径经过去重后再累加站点客流。换乘站的压力通过线路客流统计来体现不参与站点重复计数。5.3 寒暑假特征写成文本模型直接报错现象训练时 sklearn 报could not convert string to float或者模型训练完准确率极低。原因不少人在构造han_shu_jia特征时直接填了“寒假”“暑假”“不是假期”这样的中文字符串而 MLPRegressor 只接受数值输入。日期字段也常见这个问题to_datetime转换时格式不一致产生 NaT。解决han_shu_jia统一编码成 0、1、2 三个整数日期字段先pd.to_datetime再dt.month提取月份所有特征列在训练前检查一次dtypes确保都是数值类型。5.4 随机切分训练集预测结果好到不真实现象测试集 MAPE 只有 2% 左右模型看起来完美但画真实值和预测值曲线时发现预测曲线像是真实曲线向右平移了一天。原因用了train_test_split默认的随机切分测试集里混入了训练集时间范围之后的样本。客流数据是强时间序列随机切分让测试样本和训练样本高度相似模型相当于提前看到了答案。解决切分前必须按日期排序前 80% 作为训练集后 20% 作为测试集中间不插入随机操作。验证时把预测曲线和真实曲线画在同一个时间轴上出现“平移感”就说明序列切分逻辑有问题。5.5 归一化在数据集上整体 fit测试信息泄漏现象训练集 MAE 和测试集 MAE 差距极小模型泛化能力看起来异常优秀但换一批数据就崩溃。原因对全量数据fit了MinMaxScaler测试集的数值范围已经被训练过程看过。这和数据泄露的严重性一样会让评估指标失真。解决严格按训练集和测试集分开处理scaler.fit(X_train)之后测试集只调用transform。这段代码在第三节已经示范过照抄即可。5.6 render.html 双击打开后地图空白现象项目里的可视化页面render.html或index.html用浏览器直接打开图表区域一片空白控制台报跨域或资源加载失败。原因HTML 文件里引用了本地 JS 资源浏览器file://协议下会有严格的安全限制本地脚本加载被拦截。如果文件路径带中文或空格问题更明显。解决在项目目录下开一个本地静态服务终端执行python -m http.server 8000然后浏览器访问http://localhost:8000/render.html所有图表资源就能正常加载。这是验证可视化部分的固定套路不要直接双击 HTML 文件。6. 从复现到改造换城市、换特征、换模型时该动哪些代码这套项目骨架的价值在于重庆只是第一个落地场景换成其他城市只需要改四个地方stations站点列表、edges边权数据、line_of_edge线路映射、od_records刷卡记录的来源。代码的建模路径完全不用动。改造项改动位置说明换城市stations和edges按新城市线路图重新建图换线路line_of_edge更新线路站点顺序和共线区间换数据源od_records保持字段名和格式一致即可加特征feature_cols天气、节假日列表都可以往里加我每次换城市都会把这些配置单独抽成一个字典避免硬编码散落在代码各处city_config { stations: [A, B, C], edges: [(A, B, 3), (B, C, 4)], line_of_edge: {(A, B): 1号线, (B, C): 1号线}, od_file: data/od_records.csv }如果你想验证模型能力边界可以把MLPRegressor换成随机森林或 XGBoost在同一个训练集和测试集上对比 MAE。我实际跑下来随机森林在小样本上的表现通常不输 BP但 BP 的可解释性对毕设答辩更友好。还可以试试把week_of_month换成“本月第几日”或“距最近节假日的天数”观察 MAPE 的波动——这种小实验写进论文里很有说服力。从那以后我每次复现这类客流项目都会强制走一遍流程先验证建图边的权值来源再确认切分顺序最后才看模型指标。这三个动作能挡掉八成以上的翻车现场希望帮到你。本文还有配套的精品资源点击获取