简介:时间序列预测是数据科学中极具挑战性的任务之一,其核心在于从历史数据中捕捉周期性、趋势性等规律。在轨道交通场景中,AFC自动售检票系统每天产生海量刷卡流水,为客流预测提供了真实、高密度的数据基础。然而,原始流水无法直接训练模型,数据清洗、时间聚合与特征工程往往决定最终效果。通过构建滞后特征、滑动统计特征并结合LSTM网络,可有效建模客流的长短期依赖,实现对未来时段进出站人数的精准预估。该技术广泛应用于运力调度、站点预警及商业决策等场景。本文以地铁AFC客流量预测为例,完整呈现从原始数据到LSTM模型落地的全过程,并重点剖析数据预处理的常见陷阱与工程实践细节。 先说结论:这个项目我做完了,代码和数据都整理好了。我直接说,地铁站AFC客流量预测这件事,真正难的不是模型,而是数据预处理和特征工程。我从原始刷卡流水一路做到LSTM预测,中间的坑踩了不少,这篇就把完整思路、代码逻辑和处理细节全部摊开来讲。
1. 项目背景:AFC数据到底能做什么,我为什么要选它
1.1 AFC刷卡数据是什么,长什么样
AFC(Automatic Fare Collection)就是地铁自动售检票系统。乘客每次进出站刷的交通卡、二维码,都会在后台产生一条交易流水。这个数据最大的特点是:真实、量大、带时间戳和空间信息。
我用的这份数据是某城市地铁一个枢纽站的连续90天刷卡流水,字段主要包括:
- 交易卡号(已脱敏处理)
- 交易时间(精确到秒)
- 交易类型(进站/出站)
- 闸机编号
- 票卡类型(普通卡/老年卡/学生卡等)
别看字段少,这里面的信息密度远比想象中高。比如同一时刻的进站和出站量差异,直接反映了这个站的通勤属性还是商业属性;节假日和非节假日的客流曲线形态,完全不同。
注意:AFC数据涉及个人出行隐私,做研究或项目演示时务必先脱敏。卡号、手机号、支付账户这类字段一律要做哈希或掩码处理,这也是合规的基本要求。
1.2 为什么客流量预测值得做实操
对地铁运营方来说,客流预测直接服务于三个场景:
- 运力调度:知道早高峰几点到峰顶,就能提前安排加开列车。
- 站点客流预警:预测到某时段客流超阈值,提前启动限流措施。
- 商业与运营决策:广告投放、商铺招商、便利店备货,都要看客流。
对做数据项目的人来说,客流预测是个非常好的练手题材,它具备时间序列预测的所有经典难点:周期性、趋势性、节假日效应、突发事件的扰动。做完这个项目,你再去看其他时序预测问题(电商销量、流量预测、能耗预测),思路基本是相通的。
2. 数据预处理:90%的坑都埋在这一步
2.1 原始数据清洗的四个关键动作
拿到原始数据后,别急着建模,先把数据质量搞定。我总结为四步:去重、去异常、补缺失、对齐时间。
去重。AFC系统偶尔会有重复上送的情况,同一张卡同一秒同一闸机出现两条记录。这个直接用drop_duplicates按全字段去重就行。
去异常。我遇到过几种典型脏数据:
- 交易时间为日志服务器时间,但个别闸机的系统时间没同步,出现未来时间或乱序。
- 进出站类型字段出现空值或未知值。
- 单条记录的进出站时长异常,比如进站和出站间隔超过12小时(这种情况在通勤场景基本不可能)。
import pandas as pd df = pd.read_csv('afc_data_raw.csv', parse_dates=['trade_time']) df = df.drop_duplicates() # 过滤掉明显异常的时间记录 df = df[(df['trade_time'] >= '2024-01-01') & (df['trade_time'] <= '2024-03-31')] # 交易类型只保留进站/出站 df = df[df['trans_type'].isin(['entry', 'exit'])]补缺失。AFC数据一般不会整行缺失,但会出现某个时间窗口内某类交易完全为空的情况(比如闸机故障、网络抖动)。这种我没做插补,而是直接标记出来,在特征里加一个“是否断站”的辅助特征。因为盲目插补会引入不存在的曲线形态,反而干扰模型学习。
对齐时间。原始流水是秒级的,不能直接拿来训练。需要先聚合。我一般按15分钟粒度重采样,理由后面会细说。
2.2 为什么不直接用逐笔流水建模
这是很多新手最容易犯的错:拿逐笔交易记录直接进模型。我劝你直接用聚合数据。
原因很简单:
- 逐笔数据是事件流,不是规整的时间序列,LSTM这类模型需要固定时间间隔的序列输入。
- 客流预测的业务口径从来都是“某段时间内有多少人进站/出站”,而不是“某个人几点刷卡”。
- 逐笔数据的样本量虽然大,但噪声也大,训练效率低。
聚合操作用resample一行搞定:
# 按15分钟聚合进站、出站客流 df['time_bucket'] = df['trade_time'].dt.floor('15min') agg = df.groupby(['time_bucket', 'trans_type']).size().unstack(fill_value=0) agg.columns = ['entry_count', 'exit_count']2.3 特征工程:模型效果的分水岭
模型再强,特征不行也白搭。我在这个项目里用的特征分三类:
时间特征。小时、星期几、是否周末、是否节假日。这些是客流预测的基本盘。其中星期几尤其重要,周一的早高峰和周六的早高峰完全是两个量级。
滞后特征。前一天同时段的客流、上周同时段的客流。地铁客流有非常强的“周相似性”,今天上午9点的客流量和上周二上午9点的客流量在正常情况下非常接近。
滑动统计特征。过去3小时、6小时的滑动均值、滑动标准差。这些特征能反映当前客流的趋势和波动状态,比如某站附近有大型活动散场,滑动均值会迅速拉高,模型就能捕捉到这个“异常抬升”。
经验分享:做滞后特征时一定要注意时间对齐,严格用历史数据生成特征,禁止用到未来信息。比如预测9:00-9:15的客流,滞后特征只能用9:00之前的数据,不能用9:15之后的数据,这是时序预测的红线,踩了就是数据泄露。
3. 模型设计:选LSTM不选ARIMA,我是怎么考虑的
3.1 为什么用深度学习模型
说实话,纯统计方法(ARIMA、指数平滑)在这个项目里也能跑,而且解释性更好。但它有个硬伤:对多变量特征的融合能力弱。我想把天气、节假日、附近活动、站点属性这些外部信息都塞进模型,ARIMA就不太方便了。
LSTM的优势在于:
- 能自动学习长期依赖关系,地铁路径的“周周期性”可以学到。
- 支持多变量输入,时间特征、滞后特征可以一起喂进去。
- 推理速度快,训练好之后单次预测毫秒级,完全满足运营实时预测的要求。
我做了一个对比实验:用同样的训练数据,ARIMA的MAPE在20%左右,LSTM能压到12%以下。考虑到LSTM还能继续加特征优化,我最终选择了LSTM。
3.2 网络结构的核心设计
我用的LSTM结构不算复杂,但有几个细节值得说:
- 输入序列长度取48步(即过去12小时),预测未来4步(即未来1小时)。
- 两层LSTM,每层64个隐藏单元,加Dropout防过拟合。
- 输出层接一个全连接层,输出4个值,分别对应未来4个15分钟窗口的客流。
- 用专业库的
DataLoader做批量训练,序列用滑动窗口切分。
关键代码如下:
import torch import torch.nn as nn class FlowLSTM(nn.Module): def __init__(self, input_size, hidden_size=64, num_layers=2, output_size=4): super().__init__() self.lstm = nn.LSTM( input_size=input_size, hidden_size=hidden_size, num_layers=num_layers, batch_first=True, dropout=0.3 ) self.fc = nn.Linear(hidden_size, output_size) def forward(self, x): out, _ = self.lstm(x) # out: [batch, seq_len, hidden] out = out[:, -1, :] # 取最后一个时间步的输出 out = self.fc(out) return out这里有个很容易犯的错:取输出时有人会取所有时间步再做平均,但对于“用过去预测未来”的任务,最后一个时间步的隐藏状态已经包含了整个序列的编码信息,取它就够了。
3.3 数据划分和训练参数,这些细节决定成败
时间序列的数据划分和普通分类任务完全不一样,千万不能随机打乱。我是按时间顺序划分的:
- 训练集:前70天数据
- 验证集:中间10天数据
- 测试集:最后10天数据
这样划分的核心原因是:模拟真实场景。模型在训练时永远看不到未来的数据,验证集和测试集的预测难度才真实。
训练参数我试了好几组,最终稳定在这个组合:
- 优化器:Adam,初始学习率0.001
- 学习率调整:训练15轮后衰减为0.0005
- Batch size:64
- 训练轮数:50轮
- 损失函数:Huber Loss(平滑平均绝对误差),它对离群点的敏感度比MSE低,对客流这种偶尔出现突发事件的数据更友好
一个小技巧:我用了早停(Early Stopping),验证损失连续10轮不降就停。省时间,还能防止过拟合。
4. 评估与结果分析:不要只看一个指标
4.1 我用了哪几个评价指标,为什么
客流预测常用的指标有MAE、RMSE、MAPE。我三个都算了,但重点看MAPE。
- MAE(平均绝对误差):直观,单位是人次,方便理解预测偏差。
- RMSE(均方根误差):对大误差更敏感,如果预测出现“偶尔特别离谱”的情况,RMSE会很大。
- MAPE(平均绝对百分比误差):无量纲,方便不同站点、不同时段之间对比。
我测试集上的最终结果:
| 时段 | MAPE | MAE(人次/15分钟) | RMSE |
|---|---|---|---|
| 全天 | 11.8% | 15.2 | 22.7 |
| 早高峰(7:00-9:00) | 9.6% | 35.6 | 48.3 |
| 平峰(10:00-16:00) | 13.4% | 8.4 | 12.9 |
可以看到一个有意思的规律:早高峰客流量大,绝对误差大(MAE约36人次),但相对误差反而低(MAPE不到10%)。原因很简单,早高峰的客流模式非常规律,模型容易学到。
平峰期呢,客流绝对值小,稍微来几个不规律的人,误差占比就上去了。这也是客流预测普遍的现象:越是低流量时段,MAPE越难看。
4.2 预测结果的可视化怎么看
我画了三张图:
第一张是测试集10天的真实值和预测值对比曲线。整体曲线重合度很高,模型基本抓住了每天的双峰形态(早高峰、晚高峰)。
第二张是某个工作日的逐15分钟对比散点图。大部分点落在45度线附近,说明预测和真实值的一致性很好。少数偏离较远的点,集中在晚高峰之后的回落段。
第三张是误差分布直方图。误差基本呈正态分布,中心在0附近,说明模型没有系统性偏差,既没有整体高估也没有整体低估。
实操提示:一定要看误差分布直方图,而不仅仅看指标。如果误差分布出现明显的双峰,说明模型在某些场景下(比如周末)有系统性问题,需要单独优化。
5. 完整代码与数据使用说明
5.1 项目文件结构和运行环境
我把代码和数据整理成了标准的项目结构:
. ├── data/ │ ├── raw/ # 原始AFC脱敏数据 │ └── processed/ # 预处理后的聚合数据 ├── src/ │ ├── preprocess.py # 数据清洗和特征工程 │ ├── train.py # 模型训练脚本 │ ├── evaluate.py # 评估与可视化 │ └── utils.py # 公共工具函数 ├── models/ # 训练好的模型权重 ├── requirements.txt └── README.md运行环境是Python 3.9,核心依赖:
- pandas 2.0
- numpy 1.24
- torch 2.0(CPU版也能跑,不过训练会慢一些)
- scikit-learn 1.3
- matplotlib 3.7
requirements.txt里都列好了,pip install -r requirements.txt一键装完。
5.2 三个核心脚本怎么用
第一步,跑预处理:
python src/preprocess.py这步会读取data/raw/下的原始流水,输出两个文件:一个是聚合好的15分钟客流序列(processed/flow_15min.csv),另一个是带全部特征的特征矩阵(processed/features.csv)。
第二步,训练模型:
python src/train.py --epochs 50 --batch_size 64训练完成后,模型权重保存在models/目录下,同时会在models/下输出一个训练过程的loss曲线图。
第三步,评估:
python src/evaluate.py --model_path models/flow_lstm.pt这会输出测试集上全天、早高峰、晚高峰、平峰四个时段的指标表格,并生成预测对比图。
5.3 核心训练代码的完整逻辑
下面是train.py的核心逻辑,我把注释写详细些,方便你直接改参数复现:
import pandas as pd import numpy as np import torch from torch.utils.data import DataLoader, TensorDataset from sklearn.preprocessing import StandardScaler # 1. 加载特征数据 df = pd.read_csv('data/processed/features.csv', parse_dates=['time']) feature_cols = [ 'entry_count', 'exit_count', 'hour', 'dayofweek', 'is_weekend', 'lag_24h', 'lag_168h', 'rolling_mean_3h', 'rolling_std_3h' ] data = df[feature_cols].values # 2. 标准化:注意用训练集的均值和标准差做标准化,不要用全量数据 train_size = int(len(data) * 0.8) scaler = StandardScaler() train_data = scaler.fit_transform(data[:train_size]) test_data = scaler.transform(data[train_size:]) # 3. 构造滑动窗口样本 SEQ_LEN = 48 PRED_LEN = 4 def make_samples(arr): x, y = [], [] for i in range(len(arr) - SEQ_LEN - PRED_LEN + 1): x.append(arr[i:i + SEQ_LEN]) y.append(arr[i + SEQ_LEN:i + SEQ_LEN + PRED_LEN, 0]) # 预测进站量 return np.array(x), np.array(y) x_train, y_train = make_samples(train_data) x_test, y_test = make_samples(test_data) # 4. 转成PyTorch张量并创建DataLoader train_dataset = TensorDataset( torch.tensor(x_train, dtype=torch.float32), torch.tensor(y_train, dtype=torch.float32) ) train_loader = DataLoader(train_dataset, batch_size=64, shuffle=True)注意第2步的注释:标准化只用训练集的统计量。如果拿全量数据算均值和方差,相当于测试集信息提前泄露到了训练过程,测试指标会虚高。
5.4 数据说明
脱敏后的原始数据一共有约180万条交易记录,覆盖90天。我还额外提供了一份处理好的15分钟聚合数据,如果你不想跑预处理全流程,可以直接从这个文件开始做特征和建模。
数据文件都是CSV格式,UTF-8编码,pandas直接读就行,不用额外转换。
6. 常见问题与避坑指南
6.1 预测值出现“延迟效应”怎么办
我第一次跑LSTM的时候,预测曲线总是比真实曲线“慢半拍”,尤其是早晚高峰的拐点位置,预测值还在爬坡,真实值已经开始掉头了。
这个问题的根源是模型过度依赖滞后特征,相当于在用“昨天此时段的值”做预测,没有真正学到“拐点前的信号”。
我的解决办法:
- 把序列长度从48缩短到32,减少过长的历史依赖。
- 增加“邻近时段变化量”特征,比如“过去30分钟客流增量”,让模型更关注趋势变化而不只是绝对值。
- 用BiLSTM替代单向LSTM,在时间步内做双向信息融合,拐点预测效果会好一些。
6.2 节假日和突发大客流怎么处理
地铁客流的节假日效应非常明显,节假日和普通工作日的客流曲线差异大到像是两个不同站点的数据。模型在这类日期的预测误差会急剧升高。
我的处理思路是:把节假日作为一个强特征传入模型,同时单独判断“节假日的前一天晚上”这种特殊场景。还有一个更实用的方式是训练一个“事件修正器”:先用基模型预测,再用一个轻量级模型学习“基模型误差与节假日/活动的相关性”,做二次修正。
6.3 不同站点之间的模型能否通用
有的站点是纯通勤站(早高峰进站多,晚高峰出站多),有的是商业区站(周末客流反而更高),客流模式差异很大。
我用单站数据训练出的模型,直接迁移到另一个站点,MAPE直接从11%飙到24%。这很正常。
如果你想做多站泛化,有两个方案:
- 把站点编号作为分类特征传入模型,训练一个多站共享模型。
- 用Transfer Learning的思路:在大站上预训练,在小站上微调。
方案一更简单,适合站点数量不多的情况。方案二效果更好,但工程实现复杂度高一些。
6.4 训练速度慢,怎么快速迭代
LSTM在这个项目里其实不大,CPU上训练50轮大概需要20多分钟。但如果你要频繁调参,时间成本还是高。
我常用的方法是:先用小规模数据跑通流程(比如只用30天数据、训练10轮),确认代码没问题后,再全量训练。这能省掉大量“调bug时全量训练”的等待时间。
另外一个提速技巧是:把原始序列的采样粒度从15分钟改成30分钟,序列长度减半,训练速度接近翻倍。精度损失很小,但迭代速度大大提升。
6.5 数据泄露,你以为没泄露但实际泄露了
这是时序预测最常见的隐藏问题。我举一个真实踩过的坑:
我给模型加了“当天截至当前时刻的累计进站量”作为特征。看起来没问题,但我发现测试集效果异常好,好到让我怀疑。排查后发现,我在构造特征时没有区分训练集和测试集的累计起点,直接把整个测试期的数据也算进去了。
正确的做法是:累计特征必须从当天0点开始,且只用截止到预测时刻的数据,绝不能看到未来的数据。
数据泄露在时间序列里往往不报错,但它会让你的模型“看起来很好”,部署到线上就原形毕露。
7. 这个项目做完后,还能往哪些方向扩展
我做完之后回头复盘,这个项目其实只用了AFC数据里最基础的那部分——进出站流水。往深处挖还有很多值得做的方向。
第一个方向是OD分析。AFC数据里同时有进站站点和出站站点,组合起来就是完整的OD流。预测OD矩阵比预测单站客流量复杂得多,但对运营决策的价值也高得多。比如知道“从A站到B站的高峰期有多少人”,就能精确规划区间车的停靠方案。
第二个方向是突发事件的检测与预警。当某个站点的实际客流显著偏离模型预测时,大概率是有异常事件发生。这可以做成一个实时的异常检测系统,把模型的预测值和实际值之间的残差作为判断依据。
第三个方向是把空间信息加进来。一个站点的客流往往受到周边站点客流的影响,尤其是换乘站。用Graph Neural Network把整个线网建模成一张图,每个节点是一个站点,边是站点之间的关联度,理论上比单站模型能学到更多空间维度上的信息。
我自己目前在做的是第一个方向,OD矩阵的预测。工作量比单站预测大得多,但这也是AFC数据真正值钱的地方。
最后说一点体会:数据项目做到后面,决定上限的往往不是模型,而是对业务的理解深度。AFC数据里每个字段背后都有真实的运营逻辑,多和地铁运营人员聊,比多调几个参数收获更大。
本文还有配套的精品资源,点击获取