轨道交通客流预测Python实战:AFC数据清洗、特征工程与LightGBM建模
发布时间:2026/10/1 3:07:44来源:尧图网络
简介面向城市轨道交通客流预测场景的这套Django项目源码以地铁自动售检票系统清分中心的用户行程和站点数据为基础提供线路级与站点级客流分析与预测的完整实现。项目采用B/S架构后端基于Django框架前端使用Bootstrap、jQuery和Echarts完成交互式可视化整体结构清晰适合从事数据挖掘的开发者、交通专业学生以及准备课程设计或毕业设计的读者参考学习。压缩包共含26个文件其中22个Python源文件是核心代码负责模型构建、业务逻辑及数据处理另有2个Markdown文档用于说明项目结构与部署用法2个Git忽略文件用于版本管理整个包仅38KB体积小巧且层次清楚便于快速查看和改造。当前已有365人学习下载。通过这份源码可以较快掌握从数据读取、特征加工、客流统计到图形报表输出的整套思路并能直接运行或修改后复用到类似场景作为地铁大数据分析的入门实践也很合适。1. 轨道交通客流预测为什么难先别急着上LSTM数据口径才是关键轨道交通客流预测系统要解决的核心问题是把“明天早高峰这个站会有多少人进站”从拍脑袋变成可量化的判断。很多拿到这个题目的开发者第一反应是上 LSTM、Transformer结果往往是花了大量时间调参上线后一遇节假日就翻车。我见过不少类似案例最后定位到的问题都不在模型而在数据口径AFC 刷卡记录怎么按车站和时间对齐、缺失值怎么补、节假日和调休怎么区分、验证集怎么切。写一套能跑的 Python 源码并不难难的是把数据链路捋顺。这篇文章从数据、特征、模型、踩坑到落地带你把客流预测系统的最小实现完整走一遍。适合正在用 Python 做交通数据分析的从业者也适合刚做完 Python 入门、想找个真项目练手的开发者。2. 客流预测系统的数据与模型选型AFC刷卡记录如何变成预测结果2.1 AFC刷卡数据怎么聚合车站、时间粒度与换乘站口径客流预测的数据源头通常是 AFC 自动售检票系统。每一次过闸机会留一条记录字段大致包括卡号、进站或出站标识、车站编码、交易时间。注意进站和出站是两条独立记录原始表是千万行级别的明细数据第一步永远是聚合。聚合粒度一般选“车站 小时”。分钟级数据噪声太大AFC 传输和处理有时延分钟级序列里全是尖刺日级数据又太粗早上 7 点和晚上 9 点的客流形态完全不一样对运营调度没有指导意义。按小时聚合正好兼顾精度和可用性。每个车站每天产生 24 条样本一年就是 8760 条一个中等城市的线网有几十个车站样本量足以训练一个像样的模型。这里有个非常容易踩坑的口径问题换乘站。换乘站的“进站量”只是进闸机的人数还有大量乘客从其他线路换乘经过这部分人虽然也会在站台候车但不会出现在“进站”数据里。所以换乘站要想预测真实站台负荷必须叠加换乘通道的到达客流这通常依赖 OD 矩阵反推。第一版系统可以先只预测进站量但你要清楚模型给出的是“进站人数”不等于“车站客流强度”。很多项目正是在这一步把概念混了导致后面所有分析跟着偏。做系统前先把预测目标定义清楚比选模型更重要。2.2 特征工程是精度上限四类特征缺一类效果就差一截把原始数据聚合成序列后下一步是构造特征。我做过多个预测项目统一感受是特征工程决定精度上限模型只是在逼近这个上限。第一类时间结构特征。包含小时、星期几、是否周末、是否法定节假日、是否调休补班。这些字段看似基础却有讲究。调休补班日要单独处理周六补班那天客流形态接近工作日五一放假期间的工作日客流形态又接近周末。只用一个“工作日/周末”布尔值模型会在这两类日子上糊涂。正确做法是用三值标签正常工作日为 0、法定假日为 1、调休补班为 -1。第二类历史滞后特征。也就是前 1 天、前 7 天、前 14 天同一小时的进出站量。这一组特征对客流预测的作用最大尤其是对早高峰的预测周周期效应非常强。为什么用 7 和 14轨道客流基本以周为周期7 天是主周期14 天给模型一个“上上周同期”的记忆能帮模型区分长期趋势还是偶然波动。如果你写过 Python 量化交易策略代码这套滞后特征和滚动窗口的处理思路会非常眼熟本质都是“只用过去的信息预测未来”。第三类外部环境特征。主要是天气和气温。天气数据通常通过开放接口或脚本每天拉一次落到本地 CSV。雨水对地面出入口排队和公交换乘有影响但影响幅度不大且不稳定反而是冬夏极端气温对客流的影响更稳定。这部分特征做辅助可以别指望靠它翻盘。第四类事件标签。大型活动、体育赛事、演唱会散场会造成某个站晚间客流出现不正常的尖峰。这类事件有明确的时间和地点应该用标签显式告诉模型而不是让模型自己从历史数据里猜。把“今晚 22 点附近有活动散场”变成一个 0/1 特征模型对尖峰时段的预测能力会明显改善。2.3 模型怎么选为什么 LightGBM 比 LSTM 更适合当主力先把结论放前面我一般把 LightGBM 当默认模型ARIMA 和 Prophet 拿来做基线对比LSTM 留到有充足数据量和专门调优资源时再考虑。这不是说 LSTM 不好而是它在真实客流场景里的收效往往不成正比。模型对比可以从几个维度看模型训练速度多站点支持节假日处理调参成本实际稳定性ARIMA快差每站一个模型需额外外生变量低中等遇节假日明显偏差Prophet快差每站一个模型内置节假日参数低中等LightGBM快好站点作为类别特征特征里加标签即可中高LSTM慢好但需构造序列输入需额外特征对齐高数据不足时偏低重点说 LightGBM 的工程优势。它天然支持类别特征可以把车站 ID、小时、星期几直接当类别喂进去几十个车站共享一个模型不必每站单独训练。这对大规模线网预测很关键。再加上它对缺失值鲁棒、训练快迭代一个特征只需要几分钟非常适合工程环境。我的建议是就算你最终打算上 LSTM也先把 LightGBM 基线跑通。用基线误差做参照才能判断复杂模型到底有没有带来真实提升。我见过好几个项目LSTM 调了一周还不如 LightGBM 基线最后又退回 GBDT。先有基线再谈先进是工程里的基本原则。3. 用Python搭一套最小客流预测系统数据清洗、特征构建与训练代码3.1 环境准备Python版本、依赖与项目目录先用最小依赖搭环境。这里假设你已经装好了 Python 3.9 以上版本如果还在配置环境优先用 Anaconda 或者 Visual Studio Code 的 Python 插件把解释器指到同一个虚拟环境即可。如果你刚照着 Python 安装教程装完环境下一步就是确认python --version能在终端里跑出结果。pip install pandas numpy lightgbm scikit-learn joblib openpyxlpandas 负责数据聚合numpy 做数组运算lightgbm 是主模型scikit-learn 用来算评估指标joblib 保存模型openpyxl 让 pandas 能写 Excel 格式的结果文件给运营同事看。这几个库里 pandas 和 lightgbm 最核心其余都是配套。项目目录建议保持扁平方便快速跑通traffic_forecast/ ├── data/ │ ├── afc_raw.csv # AFC原始刷卡记录必填 │ ├── holiday_calendar.csv # 节假日与调休日历手工维护 │ └── weather.csv # 天气历史数据可先不填 ├── features.py # 特征工程模块 ├── train.py # 训练入口 ├── predict.py # 预测入口 └── config.py # 全局参数这是最小结构。第一版保持这样足够等要上生产再考虑拆分配置和加模型版本管理。3.2 数据清洗聚合、补缺失与车站口径AFC 原始数据读进来后第一步是聚合。下面代码把每次刷卡的明细记录聚合成“站 天 小时”的进站量与出站量import pandas as pd # 读AFC原始数据trans_time是交易时间trans_type为in/out df pd.read_csv(data/afc_raw.csv, parse_dates[trans_time]) # 剔除无效记录 df df[df[station_id].notna()] # 提取日期与小时作为聚合键 df[date] df[trans_time].dt.date.astype(str) df[hour] df[trans_time].dt.hour # 进站、出站分别计数再按站/日期/小时合并成宽表 entry (df[df[trans_type] in] .groupby([station_id, date, hour]) .size().rename(entry).reset_index()) exit_ (df[df[trans_type] out] .groupby([station_id, date, hour]) .size().rename(exit).reset_index()) flow entry.merge(exit_, on[station_id, date, hour], howouter) flow flow.fillna(0)这段代码逻辑直白但有一个容易出错的点进站和出站必须分开计数。高峰时段某些闸机会出现感应失败、人工放行的情况混合计数会把两类数据搅在一起。合并时用howouter而不是inner可以避免某一方向计数缺失时把另一方向的数据也丢掉。fillna(0)会把缺失小时补成 0这里埋了一个雷夜间停运时段本来就没车补 0 合理但如果白天某一小时 AFC 系统故障全表缺失补 0 会严重拉低均值。严谨做法是分时段处理夜间 0-5 点补 0白天缺失用前 7 天同一小时的中位数填充。这个坑我放到第 4 章详细展开。车站口径问题也要在这一步解决。不同年份的 AFC 数据里车站编码可能因为换乘站开通、站名变更而不一致。常见做法是维护一张 station_map.csv把旧编码映射到新编码然后在聚合前替换# station_map.csv: old_code,new_code,station_name station_map pd.read_csv(data/station_map.csv) df[station_id] (df[station_id] .map(station_map.set_index(old_code)[new_code]) .fillna(df[station_id]))这里用map而不是replace原因是map在遇到未收录的旧编码时会保留原值不会因为一个笔误把整列编码清掉。映射完后跑一次df[station_id].nunique()确认合并后的车站数量与预期一致。3.3 特征构建时间、滞后项与节假日标签数据清洗出干净的 flow 表后构造模型特征。这是整个系统里最值得花时间的部分def build_features(flow, holiday_cal): df flow.copy() # 把日期和小时拼成标准时间列用于排序 df[dt] pd.to_datetime(df[date] df[hour].astype(str) :00) df df.sort_values([station_id, dt]).reset_index(dropTrue) # 时间结构特征 df[weekday] df[dt].dt.weekday # 0周一6周日 df[hour] df[dt].dt.hour df[is_weekend] (df[weekday] 5).astype(int) # 合并节假日日历holiday_type1法定假-1调休补班0普通日 df df.merge(holiday_cal, ondate, howleft) df[is_holiday] df[holiday_type].fillna(0) # 滞后特征按站、按小时分组往前推 N 天 df[entry_lag_1d] ( df.groupby([station_id, hour])[entry].shift(24)) df[entry_lag_7d] ( df.groupby([station_id, hour])[entry].shift(24 * 7)) df[entry_lag_14d] ( df.groupby([station_id, hour])[entry].shift(24 * 14)) # 近期趋势前3小时滚动均值排除当前时刻 df[entry_rolling_3h] ( df.groupby(station_id)[entry] .transform(lambda x: x.rolling(3, min_periods1).mean().shift(1))) # 前推14天导致的开头缺失行直接删除 df df.dropna(subset[entry_lag_14d]) return df这段代码有几个细节要重点说。第一groupby的键是[station_id, hour]保证滞后特征不会跨站错位。第二shift(24)推的是 24 行也就是“昨天同一小时”。第三滚动均值后面接的shift(1)是为了排除当前小时自身否则当前值混进特征就是数据泄漏。前面提到过 Python 量化交易策略代码里的滞后特征和滚动窗口处理逻辑几乎一样核心原则只有一条特征里绝不能出现预测目标本身。节假日日历是整个系统里最手工的部分。我一般每年初从官方发布的节假日安排里整理一张表包含日期和 holiday_type然后全系统共用。调休补班日必须标记成 -1不能用 0 表示否则模型无法区分“周六补班”和“正常周六”。3.4 训练与预测LightGBM 的最小可用代码特征准备好后模型训练很短。下面是训练入口核心代码import lightgbm as lgb FEATS [station_id, weekday, hour, is_weekend, is_holiday, entry_lag_1d, entry_lag_7d, entry_lag_14d, entry_rolling_3h] # 按时间切分前80%训练后20%验证绝不随机打乱 split_idx int(len(df) * 0.8) train_df df.iloc[:split_idx] val_df df.iloc[split_idx:] model lgb.LGBMRegressor( n_estimators500, learning_rate0.05, num_leaves63, min_child_samples30, subsample0.8, colsample_bytree0.8, random_state42, verbose50 ) model.fit( train_df[FEATS], train_df[entry], categorical_feature[station_id, weekday, hour, is_weekend, is_holiday], eval_set[(val_df[FEATS], val_df[entry])], eval_metricmae, callbacks[lgb.early_stopping(50)] )训练参数说明如下。n_estimators500是树数量上限实际在 500 轮内验证集 MAE 连续 50 轮不下降就会触发 early stopping 自动截断。learning_rate0.05是步长一般取 0.03-0.1 之间调小会更稳但更慢。num_leaves63控制树的复杂度和表达能力客流预测特征维度不高63 个叶子足够表达交叉效应。min_child_samples30限制叶子节点最少样本数防止过拟合。subsample0.8和colsample_bytree0.8分别是行采样和列采样同样是防过拟合。验证切分这里用了“按时间长度切 80%”而不是按车站抽样本或随机打乱。原因是客流预测天然具有时序性只有用未来数据验证评估出的误差才是上线后真实能期望的水平。随机切分会把未来信息泄漏进训练集验证指标虚高这个坑我在第 4 章详细说。训练完成后保存模型并单步预测import joblib # 保存模型供预测服务加载 joblib.dump(model, models/lgbm_forecast.joblib) # 单步预测构造一个目标时段的行 def predict_one(model, row, featsFEATS): row是构造好的未来特征行返回进站人数预测值 pred model.predict(row[feats].values.reshape(1, -1)) return float(pred[0])单步预测很好理解只要未来某一天的滞后特征能从历史表里取到比如预测明天早上 8 点我们知道昨天、上周同时间的进站量就可以直接喂给模型算出一个数。要做连续 6 小时或 24 小时的滚动预测需要把预测结果按顺序回填到滞后特征里这个放到第 5 章说。4. 客流预测最容易踩的5个坑从数据泄漏到换乘站口径4.1 坑1滞后特征没按站分组整个序列错位现象是训练集误差很低但验证集误差忽高忽低某些站点的预测值整体滞后了一天。原因是写特征时图省事把groupby([station_id, hour])写成了groupby(station_id)。这样 shift 操作不是在“同一小时”内滑动而是把整个车站序列按行数硬推结果“昨天早上 8 点”的标签被当成了“今天早上 7 点”的特征。解决方法是严格按[station_id, hour]分组并且每个分组内部按时间排序。要验证特征是否错位可以临时打印某一天、某一小时的特征行人工核对滞后值与真实历史值是否一致。这一步值得做因为一旦错位后面所有模型迭代都是在错误地基上盖楼。4.2 坑2白天缺失补0客流被凭空砍掉一大截现象是某个雨天的下午预测值突然比实际值低 30%。原因是 AFC 系统某小时故障记录全丢聚合后该小时被fillna(0)补成了 0。模型看到这个 0把它当成真实客流低点后续几天的滞后特征也被带偏。解决方法是分时段补缺失夜间 0 到 5 点补 0 合理白天 6 到 23 点的缺失用前 7 天同一小时的中位数填充。中位数比均值更稳因为不会被补班日、节假日等特殊日干扰。代码实现就是在聚合后加一个条件判断白天缺失行走groupby([station_id, hour])[entry].transform(lambda x: x.median())夜间直接填 0。4.3 坑3随机切分训练集模型被“剧透”未来现象是验证集 MAE 好得惊人但一旦拿真实未来数据做回测误差立刻翻倍。原因是很多人习惯用train_test_split的默认参数随机打乱再切。客流数据里有大量滞后特征随机切分会让训练集里混进未来日期的滞后值模型实际上看到了不该看到的“答案”。解决方法是严格按时间顺序切分最前面 80% 做训练最后 20% 做验证。如果担心 20% 时间跨度不够覆盖节假日可以改用滚动验证先训练到 6 月预测 7 月再加进 7 月数据预测 8 月逐段评估。这样得到的是模型在真实上线场景里的可靠误差估计。4.4 坑4节假日与调休日混成一锅粥现象是长假前后那几天误差特别大尤其是国庆前一周和五一后一周。原因是只给了一个布尔字段“是否法定节假日”模型无法区分真正的放假和调休补班。国庆假期的前两天很多人已经在放假而国庆前的那个周六很多人还在上班两者的客流形态完全不同一个布尔值表达不了三种状态。解决方法是引入三值标签普通工作日为 0法定节假日为 1调休补班日为 -1。这个标签放在特征里LightGBM 能直接学到三种状态的不同客流模式。还要注意把“节前一天”单独作为一个特征因为节前一天下午很多人提前下班晚高峰形态和普通工作日明显不同。4.5 坑5换乘站只预测进站量站台负荷被低估现象是大型活动散场后换乘站的预测值显示“客流平缓”但现场站台已经挤得水泄不通。原因是模型预测的是进站量换乘站的换乘客流根本不经过进站闸机进站量再准也无法代表站台真实负荷。解决方法是区分站点类型。普通站用进站量做预测目标换乘站要把换乘通道的估计客流叠加进去。实际工程里换乘客流可以从 OD 矩阵反推或者用换乘站历史刷卡数据的结构比例估算。第一版可以在特征里加一个“是否换乘站”的标识更进一步则对换乘站单独建模把进出站量、换乘通道监测数据一起纳入。做站台负荷预警类应用这一步绕不开。5. 把预测结果用起来分时段验证、滚动预测与输出细节5.1 验证结果要分时段看别被总MAPE骗了客流预测的常见错误是只报一个整体 MAE 或 MAPE。整体指标会被平峰时段稀释早高峰几站误差大晚间平峰几十站的误差小一平均看起来还不错但真正需要预警的高峰时段恰恰是最不准的。我习惯把验证集按小时分组分早高峰、平峰、晚高峰分别计算指标test[hour_group] pd.cut( test[hour], bins[-1, 6, 10, 16, 19, 23], labels[深夜, 早高峰, 平峰, 晚高峰, 夜间] ) for group, part in test.groupby(hour_group, observedTrue): mape (abs(part[entry] - part[pred]) / part[entry]).mean() print(f{group}: MAPE{mape:.2%})这样的分组评估能快速定位问题早高峰 MAPE 偏大通常说明早高峰特征没有做透晚高峰偏大多半是节假日或事件标签缺失。光看总指标很容易把问题掩盖掉。5.2 滚动预测每15分钟跑一次比一次预测24小时更稳提前 24 小时一次性预测未来一天看起来很美实际误差会随着预测距离拉长快速放大。轨道运营真正需要的是“未来 1 到 6 小时”的预警。常见做法是每 15 分钟重跑一次预测窗口覆盖未来 6 小时每次预测都基于最新刷卡的 AFC 数据把滞后特征更新到当前时刻。这样做的好处是误差可控。提前 1 小时预测前 1 天的滞后特征基本准确预测误差主要来自随机波动提前 6 小时预测误差会大一些但还在可接受范围。如果直接预测 24 小时前 14 天的滞后特征全部来自历史现值等于放弃了最近 14 天的信息误差会明显恶化。客流预测系统上线时我一般先做 6 小时滚动窗口稳定后再考虑向 24 小时延伸。5.3 预测值输出前的两个细节取整与置信区间模型输出的预测值是一个浮点数比如 2134.876。直接把这个数给调度系统会造成“假精确”的错觉。实际运营只需要 5 人或 10 人级别的粒度。我会在输出前取整到 5 的倍数比如 2135同时附上 90% 置信区间。LightGBM 本身不直接给区间可以用历史预测误差的标准差来估算如果过去 30 天早上 8 点的预测误差标准差是 120 人那 90% 区间大约是预测值上下各 200 人。运营同事看到的不再是一个孤立数字而是“预计 2100 到 2300 人”这样的判断依据。做这套系统给我的一个深刻教训是模型精度提升 1%不如把输出格式和验证口径做好。我们曾经为了把 MAE 压下去投入了大量时间调参后来发现调度人员真正依赖的只是“是否超过阈值、是否提前预警”这两个信号。把预测结果按时段拆开、按滚动窗口更新、输出附上误差范围这三个习惯比换任何复杂模型都更能提升系统的实际价值。希望帮到你。本文还有配套的精品资源点击获取
网站建设高端定制企业官网