简介:这是一套基于机器学习的重庆轨道交通客流量时空分析预测本科毕业设计项目,面向需要完成毕设或希望实践客流预测的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, dtype=int) 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[i+1])] = 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 季度,这种硬编码映射逻辑很简单,也方便在答辩时解释。
特征构造完成后,数据集长这样:
| 字段 | 类型 | 取值示例 | 说明 |
|---|---|---|---|
| date | datetime | 2023-04-15 | 原始日期,仅用于排序 |
| day | int | 1 | 工作日/周六/周天 |
| han_shu_jia | int | 0/1/2 | 非假期/寒假/暑假 |
| week_of_month | int | 3 | 月中第几周 |
| season_of_year | int | 2 | 春季/夏季/秋季/冬季 |
| flow | float | 45210.5 | 当日客流量(预测目标) |
3.3 归一化与时间序列切分:两个不能省的动作
特征构造完之后,进入机器学习中最关键的预处理环节:归一化和训练集切分。BP 神经网络基于梯度下降训练,特征数值范围不统一时,梯度会来回震荡,收敛极慢甚至发散。day的取值是 0 到 2,season_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(drop=True) 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(drop=True) 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), activation='relu', solver='adam', learning_rate_init=0.001, max_iter=500, random_state=42 ) clf.fit(X_train, y_train) models[station] = clf scalers[station] = (sx, sy)模型的参数不是随便填的,我逐个说明。hidden_layer_sizes=(64, 32, 16)是三层隐藏层,神经元逐层递减,第一层 64 个神经元负责放大输入维度,第三层 16 个神经元负责压缩特征。对 4 个输入特征来说,这个结构已经偏深,但不会过拟合,因为数据量虽然不大,特征维度更低。activation='relu'是目前 MLP 的默认标配,比 sigmoid 和 tanh 收敛更快,梯度消失问题也轻得多。solver='adam'是自适应学习率优化器,对学习率的初始值不敏感,省去手动调参的麻烦,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(drop=True) 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 的波动——这种小实验写进论文里很有说服力。从那以后我每次复现这类客流项目,都会强制走一遍流程:先验证建图边的权值来源,再确认切分顺序,最后才看模型指标。这三个动作能挡掉八成以上的翻车现场,希望帮到你。
本文还有配套的精品资源,点击获取