
简介这份资源面向城市交通、智能交通方向的研究人员与数据分析学习者围绕地铁站AFC客流量预测这一典型时间序列问题提供从数据处理到多模型对比的完整代码与实验数据。压缩包共61个文件约7.59MB包含10个Python脚本、41张结果图、5个指标文本、2个Excel与1个CSV数据文件以及训练好的LSTM模型权重覆盖数据预处理、特征工程、模型训练与可视化全流程。代码实现了LSTM、RNN、GRU、DNN、SVM、KNN六类模型的统一对比并配有模型比较、论文图表生成与实验报告脚本输出损失曲线、预测对比、残差分析、雷达图与仪表盘等图表便于直接复现实验结论。目前已有48人学习适合希望掌握客流量预测建模思路、快速搭建对比实验或撰写相关论文的读者参考。1. 地铁站 AFC 客流量预测从刷卡流水到 15 分钟粒度进站人数早高峰的地铁站闸机每分钟吐出几百条刷卡记录这些记录背后藏着一个很实际的问题下一小时到底会有多少人进站AFC 系统天然记录了每一笔进出站交易客流量预测要做的就是把这些原始流水聚合成时间序列再喂给模型预测未来时段的人数。这件事的价值很直接——车站可以据此调整闸机开放数量、安排站务人员、联动限流措施。适合做这个方向的人包括轨道交通运营方的数据分析岗、做智慧交通项目的算法工程师以及手里有 AFC 数据想做预测练手的学生。完整代码和数据分享的意义在于AFC 数据不像公开数据集那样随手可得能拿到一份可运行的代码和脱敏样本比自己从零搭一套省太多时间。这一章先把问题边界划清楚后面几章再拆具体怎么做。2. 先搞清楚 AFC 数据长什么样字段、粒度和清洗逻辑2.1 AFC 交易记录的典型字段结构AFC 原始数据通常是一张交易明细表每行代表一次刷卡动作。不同城市的地铁系统字段命名有差异但核心字段大同小异。下面是一张典型的脱敏后 AFC 交易表结构字段名类型含义示例card_idstring卡号脱敏后C000****1234txn_timedatetime交易时间2024-03-15 08:23:41station_idstring车站编码ST0231txn_typeint交易类型1进站 2出站1line_idstring线路编码L04card_typeint卡类型普通/学生/老年0这张表最关键的三个字段是txn_time、station_id和txn_type。预测进站客流量时只需要筛选txn_type1的记录按车站和时间窗口做聚合。出站记录可以用来做OD分析但那是另一个问题本文聚焦进站量预测。2.2 从流水到时间序列聚合粒度怎么选原始流水是秒级的直接拿来建模没有意义。需要按固定时间窗口聚合。常见粒度有 5 分钟、15 分钟、30 分钟、1 小时。粒度选择直接影响预测难度和实用性5 分钟粒度波动极大噪声重适合做短时预警但模型很难压住误差15 分钟粒度兼顾波动特征和可预测性是多数运营场景的默认选择1 小时粒度平滑度高容易预测准但对现场调度的指导意义偏弱我一般先用 15 分钟粒度跑通全流程再根据实际需求调整。下面是把原始流水聚合成 15 分钟进站量的 Python 代码import pandas as pd # 读取原始 AFC 交易数据 df pd.read_csv(afc_transactions.csv, parse_dates[txn_time]) # 只保留进站记录 df_in df[df[txn_type] 1].copy() # 按车站和 15 分钟窗口聚合 df_in[time_window] df_in[txn_time].dt.floor(15min) flow ( df_in.groupby([station_id, time_window]) .size() .reset_index(nameinflow) ) # 补齐缺失时间窗口没有交易的时段补 0 full_range pd.date_range( flow[time_window].min(), flow[time_window].max(), freq15min ) stations flow[station_id].unique() idx pd.MultiIndex.from_product( [stations, full_range], names[station_id, time_window] ) flow flow.set_index([station_id, time_window]).reindex(idx, fill_value0).reset_index() flow.to_csv(afc_inflow_15min.csv, indexFalse) print(flow.head(10))这段代码的逻辑分三步先过滤进站记录再按车站和 15 分钟窗口计数最后用reindex补齐没有交易的时间段。补零这一步很容易被忽略但不补的话时间序列会断档后面做滑动窗口会出错。dt.floor(15min)是把时间戳向下取整到最近的 15 分钟边界比如 08:23:41 会归到 08:15 这个窗口。2.3 异常值处理哪些记录必须剔掉AFC 数据里有一类记录会严重干扰预测短时间内同一张卡在同一车站反复进出。这可能是设备故障、测试卡或者乘客误刷。处理方式是设定一个最小间隔阈值比如同一卡号 2 分钟内在同一车站有多次进站记录只保留第一条。# 剔除同一卡号短时间重复进站 df_in df_in.sort_values([card_id, txn_time]) df_in[gap] df_in.groupby(card_id)[txn_time].diff().dt.total_seconds() df_in df_in[(df_in[gap].isna()) | (df_in[gap] 120)]阈值 120 秒是经验值可以根据实际数据分布调整。如果发现某车站某时段流量异常高先查是不是设备重复上报而不是急着调模型。3. 特征工程把时间戳变成模型能吃的输入3.1 时间特征周期性和特殊日期的编码方式客流量预测的核心特征是时间。地铁客流有非常强的日周期和周周期规律。15 分钟粒度下一天有 96 个时间片一周有 672 个。特征构造要覆盖这几个维度时刻特征当前时间片在一天中的序号0-95星期特征星期几0-6是否高峰早高峰 7:00-9:00、晚高峰 17:00-19:00 标记为 1是否周末周六周日标记为 1是否节假日需要额外维护一张节假日表flow[hour] flow[time_window].dt.hour flow[minute] flow[time_window].dt.minute flow[slot_of_day] flow[hour] * 4 flow[minute] // 15 # 0-95 flow[day_of_week] flow[time_window].dt.dayofweek flow[is_weekend] (flow[day_of_week] 5).astype(int) flow[is_peak] ( ((flow[hour] 7) (flow[hour] 9)) | ((flow[hour] 17) (flow[hour] 19)) ).astype(int)slot_of_day这个特征比直接用小时更细能捕捉到 15 分钟级别的周期模式。is_peak和is_weekend是布尔特征用 0/1 编码即可不需要做独热编码因为树模型对这类二值特征处理得很好。3.2 滞后特征和滑动窗口让模型看到历史仅有时间特征不够模型需要看到过去几个时间片的流量才能预测下一个。滞后特征是最直接的做法# 按车站分组后构造滞后特征 flow flow.sort_values([station_id, time_window]) for lag in [1, 2, 4, 8, 96]: flow[flag_{lag}] flow.groupby(station_id)[inflow].shift(lag) # 滑动窗口统计 flow[rolling_mean_4] ( flow.groupby(station_id)[inflow] .transform(lambda x: x.shift(1).rolling(4).mean()) ) flow[rolling_std_4] ( flow.groupby(station_id)[inflow] .transform(lambda x: x.shift(1).rolling(4).std()) )lag_1是上一个 15 分钟的流量lag_96是前一天同一时间片的流量。rolling_mean_4是过去 1 小时的平均流量。注意shift(1)的位置——滑动窗口必须先 shift 再 rolling否则会把当前时刻的值也算进去造成数据泄漏。这个坑我踩过不止一次线下指标好看上线就崩。3.3 车站静态特征不同站的流量基线差异不同车站的客流量差异巨大换乘站和郊区站的日均进站量可能差几十倍。把车站的静态属性编码进去能帮模型区分基线station_info pd.read_csv(station_info.csv) # station_info 包含 station_id, line_count, is_transfer, area_type flow flow.merge(station_info, onstation_id, howleft)is_transfer标记是否换乘站area_type标记周边用地类型商业/住宅/混合。这些特征不需要实时更新一次关联即可。如果拿不到车站属性数据至少要把station_id做目标编码或者嵌入否则模型会把所有车站当成同一个站来学。4. 模型选型与训练从 LightGBM 到时序深度模型4.1 为什么先用 LightGBM 打底客流量预测这个任务特征工程做到位之后梯度提升树LightGBM/XGBoost的表现往往不输深度模型而且训练快、调参直观、可解释性好。我一般先用 LightGBM 跑一个基线确认特征和标签的管道没问题再考虑要不要上深度模型。import lightgbm as lgb from sklearn.model_selection import TimeSeriesSplit feature_cols [ slot_of_day, day_of_week, is_weekend, is_peak, lag_1, lag_2, lag_4, lag_8, lag_96, rolling_mean_4, rolling_std_4, line_count, is_transfer ] # 时间序列切分不能随机打乱 tscv TimeSeriesSplit(n_splits5) for train_idx, val_idx in tscv.split(flow): train flow.iloc[train_idx] val flow.iloc[val_idx] model lgb.LGBMRegressor( n_estimators800, learning_rate0.05, num_leaves63, min_child_samples20, subsample0.8, colsample_bytree0.8 ) model.fit( train[feature_cols], train[inflow], eval_set[(val[feature_cols], val[inflow])], eval_metricmae, callbacks[lgb.early_stopping(50)] )关键参数说明num_leaves63控制树的复杂度客流数据噪声大叶子太多容易过拟合min_child_samples20保证每个叶子至少有 20 个样本early_stopping(50)在验证集 50 轮不提升时停止。TimeSeriesSplit是必须的随机切分会让未来数据泄漏到训练集指标虚高。4.2 时序深度模型LSTM 和 TFT 的适用边界如果车站数量多、数据量大、且需要建模多站之间的空间关联可以考虑深度模型。LSTM 适合单站序列建模Temporal Fusion TransformerTFT适合多变量、多步预测场景。但深度模型对数据量和调参经验要求更高小数据集上很容易不如 LightGBM。import torch import torch.nn as nn class LSTMForecaster(nn.Module): def __init__(self, input_dim, hidden_dim64, num_layers2): super().__init__() self.lstm nn.LSTM( input_dim, hidden_dim, num_layers, batch_firstTrue, dropout0.2 ) self.fc nn.Linear(hidden_dim, 1) def forward(self, x): # x: (batch, seq_len, input_dim) out, _ self.lstm(x) return self.fc(out[:, -1, :]) # 取最后一个时间步LSTM 的输入需要构造成滑动窗口样本比如用过去 8 个时间片2 小时预测下一个。hidden_dim64和num_layers2是起步配置数据量大的话可以加到 128 和 3 层。dropout 设 0.2 是为了防止过拟合客流数据里噪声比例不低。4.3 评价指标MAE、RMSE 和 WMAPE 怎么选客流预测的评价指标不能只看 RMSE。RMSE 对大误差敏感但客流数据里高峰时段的绝对误差天然比平峰大RMSE 会被高峰主导。我一般同时看三个指标指标公式适用场景MAEmean(y - y_hatRMSEsqrt(mean((y - y_hat)^2))惩罚大误差看极端情况WMAPEsum(y - y_hatWMAPE 在客流预测里特别有用因为它用总流量做分母不同规模的车站可以直接比较。如果某个站的 WMAPE 明显高于其他站优先排查那个站的数据质量。5. 避坑与排查AFC 客流量预测里最容易翻车的 5 个地方5.1 现象线下验证 MAE 很低上线后误差翻倍原因最常见的是数据泄漏。滑动窗口特征没有 shift或者用随机切分代替时间序列切分导致模型在训练时看到了未来信息。另一个可能是训练数据和线上数据的聚合逻辑不一致比如线下用 15 分钟对齐线上用自然分钟对齐。解决检查所有滞后特征和滑动窗口特征是否都做了shift(1)切分必须用TimeSeriesSplit或按时间点硬切上线前用最近一周的数据做一次「模拟线上」验证确认管道一致。5.2 现象节假日预测误差暴增原因模型没见过节假日模式。训练数据里如果节假日样本很少树模型会把它当成普通工作日处理而节假日的客流模式可能完全不同——早高峰消失、晚高峰延后、总量下降。解决把节假日标记作为特征加入并且确保训练集覆盖至少一个完整年度的节假日。如果数据不足一年考虑用相似日匹配的方法做后处理修正。5.3 现象新开通车站没有历史数据预测完全不可用原因滞后特征和滑动窗口特征对新站全是空值。LightGBM 能处理缺失值但预测结果会退化成全局均值。解决对新站做冷启动处理。可以用同线路、同类型车站的流量模式做迁移或者先用车站属性特征周边用地、换乘线路数做一个粗粒度预测等积累几周数据后再切换到完整特征集。5.4 现象模型对突发大客流演唱会、赛事毫无反应原因突发客流的模式在历史数据里没有对应样本模型学不到。AFC 数据本身也不包含事件信息。解决接入外部事件日历把大型活动标记为特征。如果没有事件数据源至少要在预测后加一层规则当相邻车站或同线路其他站出现异常增长时触发人工复核。5.5 现象不同车站的预测误差差异巨大原因车站之间的流量量级和波动模式差异大统一模型可能对某些站欠拟合。另外某些站的 AFC 设备可能存在系统性偏差比如漏刷、重复上报。解决先按流量量级把车站分层分别训练模型或者做目标编码。对误差持续偏高的站单独排查 AFC 设备日志确认数据质量没问题再调模型。6. 把预测结果用起来滚动预测和误差监控的实操技巧模型训练完只是第一步真正落地要做滚动预测。每天固定时间用最新数据重新生成未来 96 个时间片一天的预测值写入数据库供调度系统读取。下面是一个滚动预测的骨架def rolling_forecast(model, flow, station_id, horizon96): 用最新数据滚动预测未来 horizon 个时间片 station_data flow[flow[station_id] station_id].copy() station_data station_data.sort_values(time_window) predictions [] for step in range(horizon): latest station_data.iloc[-1:].copy() # 构造特征与训练时一致 features build_features(latest, station_data) pred model.predict(features)[0] predictions.append(pred) # 把预测值追加到序列末尾供下一步使用 new_row latest.copy() new_row[time_window] new_row[time_window] pd.Timedelta(15min) new_row[inflow] pred station_data pd.concat([station_data, new_row], ignore_indexTrue) return predictions这段代码的核心逻辑是「预测一步、追加一步、再预测下一步」。注意build_features必须和训练时的特征构造逻辑完全一致否则会出现训练-推理偏差。滚动预测的误差会逐步累积预测步长越长越不准所以实际使用中一般只取前 4 到 8 个时间片1 到 2 小时作为有效预测。误差监控同样重要。我习惯每天记录实际值和预测值的偏差按车站和时段汇总。如果某个车站连续三天 WMAPE 超过 15%就触发告警去查数据管道。监控指标不用太复杂一个简单的偏差表就够日期车站时段实际值预测值绝对误差WMAPE03-15ST023108:00-08:15342318247.0%03-15ST023108:15-08:30401365369.0%这张表每天自动生成异常行标红。时间久了会发现一些规律比如某站在雨天误差偏大那就把天气特征加进去。模型不是一次训练就完事持续监控和迭代才是常态。最后说一个我自己的习惯每次重新训练模型之前先把上一版模型在最近一周数据上的表现跑一遍作为基线。新模型如果在这个基线之上没有明显提升就不替换。这个「后悔药」机制帮我避免了好几次因为数据管道变动导致的模型退化。希望帮到你。本文还有配套的精品资源点击获取