资讯动态

Python为何成为轨道交通客流预测的首选?从AFC数据到LSTM实战

发布时间:2026/9/23 20:52:17 来源:尧图企业网站定制
简介一套面向轨道交通客流预测场景的Python/Django示例项目适合交通数据分析初学者、毕业设计或课程实践参考。项目以地铁ACC用户行程和站点数据为基础搭建了B/S架构的完整Web系统后端采用Django前端使用Bootstrap、jQuery与Echarts可对线路级和站点级客流进行分析与预测。资源包共26个文件压缩后仅38KB其中22个Python文件构成核心业务逻辑2个Markdown文档用于辅助说明并携带gitignore配置整体目录简洁、易于快速上手。目前已有365人在线学习浏览。通过源码可学习ACC行程数据的整理建模思路、Django框架的接口组织方式以及Echarts可视化客流趋势的常见做法还可在现有源码基础上扩展模型算法易读的代码结构降低了二次开发门槛尤其适合作为地铁客流预测小项目的起步模板。1. 为什么轨道交通客流预测值得用 Python 自己搭一套系统武汉轨道交通 12 号线江北段工程环境影响报告书公示的时候沿线每个站点的进出站客流估算直接决定出入口宽度、换乘通道数量和楼扶梯配置——这是建设期的客流预测。但运营侧的客流预测是另一件事:早高峰从光谷涌向中心城区的乘客有多少哪一个车站在下一小时会触发大客流预警这是排班、调图、应急处置每天都要面对的问题。Python 轨道交通客流预测系统源码做的就是这件事:从闸机刷卡数据出发利用 Python 生态的数据处理、时序模型和 Web 服务把昨天每个站进出多少人变成未来一小时每个站进出多少人。这套系统的价值不在于把模型跑通而在于把一个数据管道完整建起来。我见过太多项目卡在数据清洗和特征对齐上,模型反而不是瓶颈。适合读这篇文章的人是手里已经能拿到 AFC 刷卡数据、想从零搭一套可运行客流预测系统的工程师或者是想用这个标题的源码做课程设计、毕设但不知道从哪下手的学生。下面按真实项目的落地顺序讲:先做数据,再选模型,然后排坑,最后上线。2. 数据与特征工程AFC 刷卡数据里藏着预测的天花板2.1 AFC 原始数据长什么样轨道交通客流预测的原料是 AFCAutomatic Fare Collection系统产生的刷卡记录。一条原始记录通常包含这些字段:卡号、交易类型进站/出站、交易时间、线路编号、车站编号、闸机编号、票卡类型普通卡/老年卡/员工卡。实际拿到的数据不是整洁的表格常见格式是 CSV 或 Parquet 文件按天分目录存储一个中等城市地铁一天的记录量在千万行级别武汉这种规模的城市高峰期单日能到 800 万笔以上。第一步不是建模是先把数据读进来并做基础清洗。我一般会用 Polars 或 Pandas 先做一次探查看看字段是否有缺失、时间戳是否跨时区。下面这段代码完成的是:读取单日原始记录过滤掉无效记录然后聚合到车站-小时粒度得到每个站每个小时的进站量——这是后面所有模型训练的基本样本。import pandas as pd df pd.read_csv( afc_records/20250310.csv, dtype{ card_id: string, station_id: int16, trade_type: int8, # 1-进站 2-出站 3-换乘 card_type: int8, }, parse_dates[trade_time], ) # 只保留进站记录换乘记录不重复计数 entry df[(df[trade_type] 1) df[station_id].notna()].copy() # 聚合到车站-小时粒度hourly_in 就是该站该小时进站人数 station_hourly ( entry.groupby([entry[station_id], entry[trade_time].dt.floor(h)]) .size() .reset_index(namehourly_in) ) station_hourly.columns [station_id, hour, hourly_in]逻辑说明:先按 card_id 去重吗?不能去重。一张卡可以在同一天多次进出站每笔交易都是独立的客流事件去重反而会低估真实流量。过滤条件只保留进站记录是因为后面要预测的是进站量出站量可以在同一套框架下换成 trade_type 2 单独建目标。trade_time.dt.floor(h) 是把秒级时间戳规整到小时边界这样 09:23:17 和 09:47:52 都归入 9 点这一个桶。参数说明:station_id 用 int16 而不是 int64 可以省内存千万行数据下这个差别能省出几百 MB。实际项目中如果内存还是吃紧把 groupby 的 key 换成 pandas.Categorical再用 observedTrue 聚合。到这里离建模还差得远因为模型不能光靠过去一小时是多少来预测未来一小时是多少真正的信息藏在时间特征和外部特征里。2.2 时间特征、天气特征和节假日特征怎么构造客流预测的特征工程遵循一个铁律:在预测截止时刻 T只能用 T 之前已经发生的信息以及 T 时刻确定知道的信息。天气算哪一类?决策的时候用的是天气预报而不是实测天气,因为实测天气在 T 时刻还没发生。这点在代码里要用注释标清楚,不然几个月后你自己都会看错。时间特征的核心是周期性。地铁客流有极强的每日周期早高峰 7-9 点、晚高峰 17-19 点、每周周期工作日与周末结构完全不同和年度周期春节前后断崖式下跌。把时间戳拆成 hour、weekday、is_weekend、is_holiday 四个原始特征还不够,关键是构造滞后特征和滚动统计特征,让树模型和神经网络都能直接利用这些历史模式。import numpy as np from chinese_calendar import is_holiday def build_features(station_df): 输入:按小时排序的车站流量表;输出:带特征列的训练样本 df station_df.sort_values(hour).copy() # 基础时间特征 df[hour] df[hour].dt.hour df[weekday] df[hour].dt.weekday # 周一0 df[is_weekend] df[weekday].isin([5, 6]).astype(int) df[is_holiday] df[hour].apply( lambda x: int(is_holiday(x)) ) # 滞后特征: 昨天同一小时、上周同一天同一小时 df[lag_24h] df[hourly_in].shift(24) df[lag_168h] df[hourly_in].shift(168) # 滚动统计: 过去 3 小时均值用于捕捉短时趋势 df[rolling_3h_mean] ( df[hourly_in].rolling(3, min_periods1).mean() ) # 滚动统计的 shift避免用当小时数据预测当小时 df[rolling_3h_mean] df[rolling_3h_mean].shift(1) return df.dropna(subset[lag_24h])参数说明:shift(24) 是昨天同一小时,shift(168) 是上周同一天同一小时。这两个滞后特征对客流预测至关重要——工作日的客流曲线和上周同一天高度相似模型从这两个特征里能学到周模式。rolling(3).mean() 再 shift(1) 是为了防止数据泄漏:如果直接用包含当前小时的均值去预测当前小时模型学到的是答案本身,上线后这个特征不存在,预测就崩了。需要注意:is_holiday 这个特征要用 chinese_calendar 库它能处理中国法定节假日的调休安排。不要自己手写节假日表——清明节、国庆节的调休规则每年变手写表一定漏。如果目标客户是武汉这种城市还要额外加一个是否为大型活动日的特征比如体育场有演唱会、会展中心有展会时临近两个站的客流会异常抬升。2.3 样本组织:单步预测还是多步预测接下来决定预测目标。运营侧最关心的是未来一小时每个站进站量——提前一小时知道哪些站会触发大客流预警就可以安排加车、限流和人员引导。这个目标对应单步预测:给定过去 24 小时的时间序列和特征输出未来 1 小时的值。另一种需求是未来 3 小时的客流曲线,用于排班计划对应多步预测。单步和多步在实现上有本质区别。单步预测是回归问题直接训练模型输出一个值;多步预测有两种路线:递归预测把上一步的预测值当作下一步的输入和直接预测模型一次输出未来 3 个值。递归预测误差会累积早上 8 点预测 9 点偏差 5%,到预测 11 点可能偏差到 15%。直接预测训练更稳定,输出层改成 3 个神经元即可。实际项目中,我更推荐单步预测 每隔 15 分钟滚动重预测,这样既覆盖了未来 1-3 小时的信息,又不用承担递归误差。# 构造训练样本: X 过去 24 小时的流量 特征; y 未来 1 小时流量 WINDOW 24 def make_samples(station_df, feature_cols, target_colhourly_in): samples_x, samples_y [], [] data station_df[feature_cols [target_col]].values for i in range(WINDOW, len(data) - 1): x data[i - WINDOW:i, :] # 过去 24 小时的全部特征 y data[i 1, -1] # 未来 1 小时的真实流量 samples_x.append(x) samples_y.append(y) return np.array(samples_x), np.array(samples_y)逻辑说明:这里的 X 形状是 (样本数, 24, 特征数)y 形状是 (样本数,)。因为数据是按时间排序的,第 i 行代表第 i 个小时,所以 i - WINDOW:i 这一段就是过去 24 小时的完整特征序列,y 取第 i 1 行的目标列。这个结构直接喂给 LSTM 不需要再 reshape喂给 XGBoost 则需要把 24×特征数摊平成一条一维向量。特征数和窗口大小是超参,窗口取 24 是因为客流日周期是 24 小时,取太短丢失周期信息,取太长增加计算量但对精度提升有限。3. 模型选型与训练从基线模型到 LSTM 的落地路径3.1 Prophet、XGBoost、LSTM 怎么选客流预测的模型选型没有银弹,核心看数据量和特征复杂度。下表是我在多个项目里的实际对比结论:模型优势劣势适用场景Prophet自动处理周期性和缺失值;调参简单;适合快速出基线难以融合天气、活动等外部特征;对突发客流不敏感数据量小、周期稳定、快速验证XGBoost / LightGBM特征工程灵活;解释性强;能捕捉非线性关系需要手工构造滞后特征;时间顺序信息利用较弱特征丰富、需要看到特征重要度排名的场景LSTM直接建模序列依赖;能从原始序列中学习模式需要大量数据;调参成本高;对异常值敏感数据量百万级以上、需要捕捉复杂时序模式节奏建议:先做 5 分钟跑通的 Prophe t基线用 MAPE 衡量误差;然后上 XGBoost 加滞后特征;最后如果精度还不够再上 LSTM。不要一上来就训练 LSTM——你需要在基线模型上确认数据管道是通的,再引入深度模型的复杂度,否则出了问题你分不清是数据错了还是模型错了。实际数据里,工作日和周末的客流模式差异大到无法用同一个模型同时精确拟合。两条曲线放在同一张图上,工作日是双驼峰(早高峰 晚高峰),周末是单驼峰(午后)。如果你的数据量足够,按工作日/周末/节假日分组训练三个模型,比一个大模型的精度高 10%-15%。我在后面第五章会展开讲这个分桶思路。3.2 PyTorch 实现 LSTM:网络结构与训练参数LSTM 适合这个任务的本质原因是:客流序列是强自相关的。今天 9 点的客流量和昨天 9 点、上周 9 点高度相关,而且这种相关是长距离的(跨 24 小时、跨 168 小时)。LSTM 的门控机制天然适合学习这种长距离依赖。下面是可复现的模型定义:import torch import torch.nn as nn class TrafficLSTM(nn.Module): def __init__(self, input_dim, hidden_dim64, num_layers2, output_dim1): super().__init__() self.lstm nn.LSTM( input_sizeinput_dim, hidden_sizehidden_dim, num_layersnum_layers, batch_firstTrue, dropout0.2 if num_layers 1 else 0.0, ) self.fc nn.Sequential( nn.Linear(hidden_dim, 32), nn.ReLU(), nn.Linear(32, output_dim), ) def forward(self, x): # x 形状: (batch, seq_len, input_dim) out, _ self.lstm(x) # 取最后一个时间步的输出 last_hidden out[:, -1, :] return self.fc(last_hidden)参数说明:input_dim 是特征数,按 2.2 节的特征列表算大概是 10 个左右(小时、星期、周末、假日、滞后 24h、滞后 168h、滚动均值、天气、活动标记等)。hidden_dim64 是一个平衡点:太小欠拟合,太大过拟合且训练变慢——百万级样本下 64 或 128 即可。num_layers2 足够捕捉复杂的时序层级,层数再多在这个任务上收益极小。dropout0.2 只在层数大于 1 时生效,防止两层 LSTM 之间过拟合。训练循环里有两个关键选择:损失函数用 HuberLoss 而不是 MSELoss;验证集必须按时间顺序切分,不能随机打乱。def train_model(model, train_loader, val_loader, epochs40, lr1e-3): optimizer torch.optim.Adam(model.parameters(), lrlr) scheduler torch.optim.lr_scheduler.StepLR(optimizer, step_size10, gamma0.5) criterion nn.HuberLoss(delta1.0) # 对异常尖峰更鲁棒 best_val_loss float(inf) patience 0 for epoch in range(epochs): model.train() for x_batch, y_batch in train_loader: optimizer.zero_grad() pred model(x_batch) loss criterion(pred.squeeze(), y_batch) loss.backward() torch.nn.utils.clip_grad_norm_(model.parameters(), max_norm5.0) optimizer.step() # 验证 model.eval() val_loss 0.0 with torch.no_grad(): for x_batch, y_batch in val_loader: pred model(x_batch) val_loss criterion(pred.squeeze(), y_batch).item() val_loss / len(val_loader) print(fEpoch {epoch1}: val_loss{val_loss:.4f}) if val_loss best_val_loss: best_val_loss val_loss patience 0 torch.save(model.state_dict(), best_model.pt) else: patience 1 if patience 5: print(早停:验证集损失连续 5 轮未下降) break scheduler.step()损失函数选 HuberLoss 的原因:客流的真实分布带有尖峰比如一场演唱会让某站 22:00-23:00 的进站量从平时 800 暴增到 5000。MSE 对这样的离群值施加平方惩罚模型会疯狂调整权重去拟合这些罕见尖峰导致正常时段精度下降。HuberLoss 在 delta1.0 以内是平方损失以外是线性损失对尖峰更宽容。梯度裁剪 clip_grad_norm_ 设 5.0 是为了防止 LSTM 训练中梯度爆炸——这个问题在使用 long 序列时会遇到属于必加的防护。3.3 训练数据的时间切分法与验证指标时序预测最忌讳随机切分。假设你的数据覆盖 2024 年 1 月到 2025 年 3 月,用 train_test_split(random_state42) 会把 1 月和 12 月的样本混进同一个集合模型偷看了未来,验证指标虚高。正确做法是按时间顺序切分,前 80% 训练,中间 10% 验证,最后 10% 测试。这里有个隐藏陷阱:客流数据存在年度周期性,如果测试集只覆盖 2025 年 1 月(春运前),预测误差会偏大,因为 1 月的客流结构和其他月份差异很大。稳妥做法是测试集覆盖至少一个完整自然月,且包含工作日和周末。我在实际项目中一般这样切:训练集 2024 年整年、验证集 2025 年 1 月、测试集 2025 年 2 月——覆盖春节前后,考验模型对极限场景的泛化能力。评估指标建议同时看两个:MAE(平均绝对误差)和 MAPE(平均绝对百分比误差)。MAE 的数值直观,比如全站平均每小时误差 180 人;MAPE 反映相对误差,大站客流量大而 MAE 高,但 MAPE 可能只有 5%,小站反之。只看 MAE 你会误以为小站预测效果好——绝对误差低是因为基数小。要特别警惕小站在夜间时段的 MAPE:凌晨 2 点客流量只有个位数,预测偏差 2 个人,MAPE 就是 200%,这个数字没有参考意义,评估时应过滤掉流量低于 20 人/小时的时段。4. 避坑排查时序预测最容易翻车的 5 个真实问题4.1 数据泄漏:随机切分导致验证集指标虚高现象:模型在验证集上 MAPE 只有 4.3%,看起来非常理想,但上线后真实预测的 MAPE 高达 18%,折线图上的预测值比实际值明显脱相。原因:训练代码里用了 train_test_split 默认的随机切分,同一周的客流样本被同时分进训练集和验证集。模型在训练时见过验证集里同一时间段的相邻样本,相当于开卷考试。解决:严格按时间顺序切分,并且保证验证集的时间段晚于训练集。用下面这段代码强制时间窗口切分:def temporal_split(df, train_ratio0.8, val_ratio0.1): n len(df) train_end int(n * train_ratio) val_end int(n * (train_ratio val_ratio)) return ( df.iloc[:train_end], # 训练集 df.iloc[train_end:val_end], # 验证集 df.iloc[val_end:], # 测试集 )4.2 特征工程中的未来信息泄漏现象:某个车站的预测曲线在 9:00-10:00 准确率极高,但在 22:00 之后误差突然变大。排查特征后发现,22 点以后客流的昨天同一时刻特征 lag_24h 恰好是前一天 22 点——一天中最不规律的时段。原因:其实这不是逻辑 bug,而是特征本身的规律性差异。但还有更隐蔽的泄漏点:如果构建滚动均值时未 shift,训练时模型见到了当前小时的均值这个特征,而当前小时的真实值正是预测目标。解决:所有滚动统计量计算完必须 shift(1);所有与时间相关的特征统一用截止到 T-1 时刻的数据构建;保存特征工厂的每个中间结果,输出到 CSV 检查一遍,确认没有未来数据混入。这个步骤我用一个_check_leakage() 函数,对每个特征列做计算该特征所用的时间戳是否都小于等于样本时间戳的断言,一劳永逸。4.3 节假日大客流日预测系统性低估现象:周五晚高峰的预测误差正常,但五一假期前一天(4 月 30 日)晚高峰,预测值普遍低于实际值 30% 以上,尤其交通枢纽站和商圈站。原因:训练集中这样的节前高峰日每年只有几天,LSTM 和 XGBoost 对低频但高影响的模式学习不足,损失函数把这类样本当作噪声。模型学的是一种平均客流形态,而节前一天的形态是独特的陡升。解决:双轨办法。第一,样本加权:对节假日及其前后一天的训练样本提高 loss 权重,比如 weight3,让模型对这类样本更敏感。第二,独立节假日模型:收集近两年的节假日样本单独训练一个 LightGBM,日常预测走主模型,当运营日历标记节假日时走节假日模型。武汉轨道交通的 AFC 数据里有完整的节假日记录,按这个方案,4 月 30 日的 MAPE 通常能从 20% 以上降到 10% 以内。4.4 AFC 数据延迟 30 分钟导致线上特征错位现象:模型白天预测准确,17:00 左右预测的 18:00 晚高峰值系统性偏低。查看线上日志发现,特征计算用的是 16:00-17:00 的 AFC 数据,但 AFC 系统在高峰期有批量上传延迟,实际拿到这批数据的时间是 17:25。原因:模型训练时用的是干净的实时数据——每条记录的时间戳就是采集时间。但线上环境里,数据到达特征工厂的时间比事件发生时间晚 20-40 分钟。训练与推理的数据分布不一致,模型看到的是 16:00 的滞后数据,所以预测偏低。解决:训练时就模拟线上延迟。把所有 AFC 记录的时间戳统一加 30 分钟,用这个延迟后的时间重新做特征聚合。这样模型在训练时就见过数据不完整的状态,而不是假设特征总是完美到位的。代码上加一个 paramet er lantency_min30,在读取数据后统一执行 df[event_time] df[event_time] pd.Timedelta(minuteslatency_min)。这是时序系统最容易忽略的坑,遇到训练好、上线差先查数据对齐。4.5 新线开通导致旧模型漂移现象:某新城区的换乘站开通后,周边三个站点的预测误差在一周内从 6% 恶化到 22%,且换乘站本身是 0 客流。原因:新线改变了乘客的出行路径选择。原来在 A 站换乘的乘客改到新开的 B 站换乘,A 站的客流结构骤然变化,旧模型对 A 站的权重是基于旧路径学出来的,自然失效。解决:部署一个漂移监测任务,每天计算近 7 日预测误差移动平均与基线误差 3 倍标准差的比较,超过阈值自动触发告警并自动重训。重训时对新线开通后的数据加倍采样,让模型快速适应新客流结构。这里的关键是自动化,不要靠人肉盯指标——地铁运营的变量太多,事故高发期往往你还在手动跑脚本。5. 把模型变成系统推理服务、重训节奏与量化评估5.1 多站点分桶训练:一个模型不要打天下所有站共用一个 LSTM 模型,训练简单但精度上限低。枢纽站(如汉口火车站)客流有显著的铁路到发耦合,早高峰比普通站提前 1 小时;商圈站(如江汉路)周末午后是高峰,工作日反而平缓;居住区站(如金银湖)早晚双峰明显。这三类站放在同一个模型里,模型会被迫学习一个平均形态,对每类站都不是最优的。更好的做法是按站点的客流形态聚类,每类训练一个模型。聚类特征取每个站一周的逐小时平均客流曲线,用 KMeans 聚成 3-5 类,然后每类单独训练。实际效果:枢纽站 MAPE 改善 8%,商圈站改善 12%——因为商圈站的曲线形态和其他站差异最大,分桶后模型不用再折中。分组还可以进一步细化:大客流站单独成组,因为它们的绝对误差对运营决策影响最大;中小站合并成组,因为它们客流结构相似且数据量不足,单独建模会过拟合。5.2 推理服务的延迟与吞吐预测模型本身只是一个 .pt 文件,要让运营人员真正使用,需要包一层轻量推理服务。模型推理单次耗时在一个 LSTM 50ms 以内,批量预测全网 200 个站在 2 秒内可以完成。瓶颈在特征工厂:如果按时从数据仓库拉取昨日数据和今日实时数据,再聚合特征,整个过程需要 5-10 秒。对于未来 15 分钟才需要一次预测的场景,这个延迟完全可接受。# 伪代码:FastAPI 推理接口核心逻辑 from fastapi import FastAPI import torch app FastAPI() model TrafficLSTM(input_dimFEATURE_DIM) model.load_state_dict(torch.load(best_model.pt, map_locationcpu)) model.eval() app.post(/predict) def predict(station_id: int, timestamp: str): # 1. 从特征库构造该站过去 24 小时的特征序列 x build_features_from_store(station_id, timestamp) # 2. 转换为模型输入张量并推理 x_tensor torch.tensor(x, dtypetorch.float32).unsqueeze(0) with torch.no_grad(): pred model(x_tensor).item() return {station_id: station_id, prediction: round(pred, 1)}服务上线前要做的压测:用 wrk 或 locust 压 200 个并发请求,确认 P99 延迟小于 1 秒。预测值是给调度大屏和应急系统用的,如果接口超时,运营人员会直接放弃使用这个系统,技术指标再高也没用。5.3 重训节奏与量化收益评估模型训好不是终点,客流的底层结构会变。票价调整、新线开通、疫情后的出行习惯变化,都会让模型偏差逐渐扩大。我建议的节奏是:每周自动重训一次,用最近 90 天数据;每天做一次误差监控,计算近 24 小时实际值与预测值的 MAE,连续 3 天超过历史基线的 1.5 倍就手动检查。重训后不要直接切换,先做影子模式:新模型和旧模型并行跑一周,对比两者的 MAE 和峰值误差,确认新模型全面占优再切换。这个习惯避免了下线一个稳定的模型、上线一个在训练集上表现好但在真实环境翻车的新模型。最后一个值得投入的技巧是预测残差分析。每次预测后记录(真实值 - 预测值),按小时维度画残差热力图。你会发现残差在特定时段和特定站点有聚集模式——比如某站残差总是集中在下雨的工作日。发现这种模式后,给特征工厂加一个过去 3 小时累计降雨量特征,往往一次迭代就能让 MAE 降 5% 以上。这个方法比盲目调 LSTM 的超参数有效得多。我习惯把每个版本的模型、特征签名和验证指标一起存档。下次有人问这个预测为什么比上周准,直接翻存档对比特征列表,而不是对着黑匣子猜。这个习惯救过我多次,也推荐给你。希望帮到你。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价