简介:针对地铁站点日常客流预测需求,这份基于LSTM的交通客流预测压缩包给出了从数据处理到模型训练与可视化的完整方案。项目以2019年某地铁站平日客流量及天气因素为输入,按8:2划分训练集与测试集,借助numpy、pandas完成预处理,再通过sklearn、LSTM神经网络建模预测,并利用matplotlib、seaborn输出结果,适合机器学习初学者与交通数据分析人员学习参考。压缩包共17个文件,含5个csv原始数据、2个h5训练模型、1个py预测脚本,还有doc/ppt/xlsx等说明与展示材料,整体3.95MB。目前已有1292人浏览学习。通过该资源可系统掌握客流量预测的完整流程,获得可直接运行的代码、预训练模型及答辩PPT,能够快速复现实验并延伸应用到同类场景。
1. 基于 LSTM 的交通客流预测资源包:一个能直接跑通的时间序列项目
如果你做过客流预测,一定见过这种场景:早高峰地铁闸机的出站量、节假日前一天的高速收费站车流、商场促销时的进店人数,波动大、周期强、突发情况多,用传统 ARIMA 或移动平均很难追上突变。这份《基于LSTM交通客流预测.zip》正是一个把 LSTM 应用到短时客流预测的完整资源,核心链路是「时序数据 → 滑窗构造样本 → LSTM 训练 → 反归一化评估」,不依赖第三方付费数据源,用公开的客流序列就能复现。适合三类人:刚入门时间序列预测的学生,想把 LSTM 落到实际业务但不想从零搭框架的数据工程师,以及需要一份基线模型做对比的实验人员。
2. 数据预处理:把原始客流序列变成模型能吃的时间窗
LSTM 吃的是「序列」,不是单条记录。原始数据通常是按 15 分钟或 1 小时间隔记录的客流数值,比如[2024-01-01 08:00, 352]。这个结构离模型需要的输入形状还差两步:构造滑窗、做归一化。顺序不能反,先滑窗再归一化是常规做法,但更稳的是先归一化再滑窗,理由后面第五章节会说。
2.1 拿到数据先别急着建模型:先做缺失值与异常值
客流数据最常见的脏数据有两种。第一种是设备断传导致的整段缺失,常见于闸机或摄像头采集链路,表现为连续数小时数值为 0 或直接没有记录;第二种是瞬时尖峰,比如某天早高峰因为列车晚点导致出站量突然翻倍,这类点不一定是噪声,但对训练影响很大。我的处理习惯是先做探索性可视化,把序列画出来,用肉眼确认缺失段和异常段的位置,再决定填充策略。
处理缺失值时,客流序列按周和按天都有明显周期性,所以用前一周同一天同时段的均值填充,比用线性插值更符合业务规律。异常值用滚动中位数检测,以 24 个点为一个窗口,如果某个点的值超过窗口内中位数的 3 倍或低于其 1/3,就标记为异常,然后用前后两个正常点的均值替换。注意这里不要用整体均值替换,节假日或促销日的高峰值一旦被整体均值拉平,模型训练出来会系统性低估峰期客流。
import pandas as pd import numpy as np def load_and_clean(df): # df 必须包含两列: timestamp 和 flow df['timestamp'] = pd.to_datetime(df['timestamp']) df = df.set_index('timestamp').sort_index() # 缺失填充: 前一周同一时刻的均值 df['flow'] = df['flow'].replace(0, np.nan) # 0 值当作缺失处理 for idx in df[df['flow'].isna()].index: week_ago = idx - pd.Timedelta(days=7) df.loc[idx, 'flow'] = df.loc[week_ago, 'flow'] # 异常值替换: 滚动中位数 + 3 倍阈值 median = df['flow'].rolling(24, min_periods=1).median() diff = (df['flow'] - median).abs() threshold = median * 3 df.loc[diff > threshold, 'flow'] = median.loc[diff > threshold] return df这段代码两条核心逻辑:缺失值用「上周同期」语义填充,而不是简单的前向填充,这是考虑到了客流数据的周期属性;异常值检测用的是滚动中位数而非滚动均值,因为中位数对尖峰本身不敏感,不会出现「均值被尖峰拉高、结果尖峰反而看起来正常」的自我矛盾。rolling(24)里的 24 对应小时粒度下的一天,如果你数据是 15 分钟粒度,就填 96。
2.2 滑窗构造样本:window 大小和步长的取舍逻辑
滑窗是把连续序列切成「过去 N 个时间点预测未来 M 个时间点」的样本对。窗口大小是这份资源里最值得调的第一个参数。我的经验法则:窗口至少覆盖一个完整的日周期(小时粒度至少 24,15 分钟粒度至少 96),如果数据包含周末和工作日两种模式,可以考虑把窗口拉到 48 小时让模型看到跨天规律。但窗口不是越大越好,LSTM 对这种长序列的记忆能力有限,超长窗口反而会引入大量无关历史信息,把训练时间拉长的同时还不见得提升精度。
步长(step)控制样本的重叠程度。步长等于 1 时,相邻两个样本只错开一个时间点,数据量最大,但相邻样本几乎一样,训练耗时高;步长等于预测长度时,样本完全不重叠,数据量小但能覆盖更长的时间跨度。我的默认配置是:窗口 48、步长 24,这样既保留了日周期特征,又不会让训练集膨胀到十万级。下面是滑窗构造的核心代码。
def create_sequences(data, window=48, step=24, pred_len=1): X, y = [], [] # 按时间顺序滑动,不能打乱 for i in range(0, len(data) - window - pred_len + 1, step): X.append(data[i : i + window]) y.append(data[i + window : i + window + pred_len]) return np.array(X), np.array(y) # 归一化必须在滑窗之前,且 fit 只能用在训练集上 from sklearn.preprocessing import MinMaxScaler scaler = MinMaxScaler(feature_range=(0, 1)) train_scaled = scaler.fit_transform(train_raw.reshape(-1, 1)) val_scaled = scaler.transform(val_raw.reshape(-1, 1)) X_train, y_train = create_sequences(train_scaled.flatten(), window=48, step=24) X_val, y_val = create_sequences(val_scaled.flatten(), window=48, step=24)create_sequences返回的X形状是(样本数, window, 1),第三个维度是特征数,单变量预测时就是 1。这里有个新手最容易忽略的点:scaler.fit_transform只对训练集做,验证集和测试集用transform,让验证集的数据分布「模型没见过」,评估结果才可信。归一化到 [0,1] 是 LSTM 这类梯度敏感模型的标准做法,客流数值动辄几千上万的量级如果直接喂给模型,激活函数的梯度会迅速饱和。
3. 模型训练:搭 LSTM 网络与调参的落地路径
数据处理完,下一步就是把滑窗样本喂进 PyTorch 的nn.LSTM。这章先说网络结构怎么定,再说训练循环怎么写,全程按能跑出合理结果的标准来配置,不给花活。
3.1 网络结构:nn.LSTM 四个关键参数的选型
nn.LSTM里有四个参数直接决定模型容量:input_size、hidden_size、num_layers、batch_first。input_size等于每个时间步的特征维度,单变量客流预测就是 1,如果你加了天气、节假日特征,这里就变成特征总数。hidden_size是隐状态维度,客流预测任务 64 到 128 之间是甜点区间,太小拟合不了周期性波动,太大容易过拟合且训练时间成倍上涨。num_layers一般取 1 或 2,客流序列的规律还没复杂到需要三层以上堆叠。batch_first=True让输入形状变为(batch, seq_len, input_size),符合直觉,代码不容易绕晕。
需要注意nn.LSTM的输出是一个元组(output, (h_n, c_n)),做单步回归时,常规做法是取最后一层在最后一个时间步的隐状态h_n[-1],再过一个全连接层把维度压到预测长度。下面是我在这类任务上常用的一种结构。
import torch import torch.nn as nn class LSTMPredictor(nn.Module): def __init__(self, input_size=1, hidden_size=64, num_layers=2, output_size=1, dropout=0.2): super().__init__() self.lstm = nn.LSTM(input_size=input_size, hidden_size=hidden_size, num_layers=num_layers, batch_first=True, dropout=dropout) self.fc = nn.Linear(hidden_size, output_size) def forward(self, x): # x: (batch, window, input_size) out, (h_n, c_n) = self.lstm(x) # h_n[-1]: 最后一层最后一个时间步的隐状态 last_hidden = h_n[-1] # (batch, hidden_size) pred = self.fc(last_hidden) # (batch, output_size) return prednum_layers=2时如果dropout传 0.2,PyTorch 只会在两个 LSTM 层之间丢,不会作用于输出层,这个dropout是层间随机失活,不是给输入数据加噪声,理解错容易误调。last_hidden = h_n[-1]这个写法的含义是取最深那一层在所有 batch 样本上的最后时刻隐状态,它保留了整个输入序列压缩后的信息,是序列到单点回归的标准取法。
3.2 训练流程:损失函数、优化器与早停
客流预测本质是回归任务,损失函数用均方误差nn.MSELoss()即可,它对大误差值(比如峰期预测偏差几百人)的惩罚呈平方级放大,倒逼模型把峰期拟合得更准。优化器选 Adam,学习率默认 0.001,这是时间序列任务里最不容易翻车的起手配置。训练循环里比「前向传播」更需要留意的是数据形状要对齐,X_train的形状是(样本数, 窗口, 特征数),但到了模型里 PyTorch 自动把第一维当 batch,这正好对应设计。
早停(early stopping)建议自己写一个最简版本,盯验证集损失,连续 20 个 epoch 没有下降就保存历史最优权重并终止训练。客流数据本身噪声大,训练集损失降到很低也不代表泛化,验证集才是模型真实能力的照妖镜。
def train_model(model, X_train, y_train, X_val, y_val, epochs=100, lr=0.001, patience=20): optimizer = torch.optim.Adam(model.parameters(), lr=lr) criterion = nn.MSELoss() best_val_loss = float('inf') wait = 0 for epoch in range(epochs): model.train() optimizer.zero_grad() pred = model(X_train) loss = criterion(pred, y_train) loss.backward() optimizer.step() model.eval() with torch.no_grad(): val_pred = model(X_val) val_loss = criterion(val_pred, y_val).item() # 早停: 验证损失连续 patience 轮不降则终止 if val_loss < best_val_loss: best_val_loss = val_loss wait = 0 torch.save(model.state_dict(), 'best_lstm.pt') else: wait += 1 if wait >= patience: break model.load_state_dict(torch.load('best_lstm.pt', weights_only=True)) return modeloptimizer.zero_grad()必须在loss.backward()之前,漏掉这行梯度就会跨 batch 累积,训练损失曲线会呈现不规则的锯齿。每个 epoch 结束切到model.eval()再算验证损失,是为了让 dropout 层在验证阶段不生效,否则验证指标每次前向传播都带随机性,没法做早停判断。torch.save保存的是网络参数,训练完用load_state_dict恢复到验证集最优状态,避免因最后几轮过拟合把模型带偏。
4. 评估验证:RMSE、滞后效应与预测结果的可信度
训练跑通只是第一步,模型到底能不能用,要看它在测试集上的表现。这章讲三件事:指标怎么算、预测曲线怎么看、以及评估里最容易被忽略的滞后效应。
4.1 评估指标:RMSE、MAE 与 MAPE 各看什么
评估 LSTM 客流预测推荐同时算三个指标,各有侧重点,只看一个容易自欺欺人。RMSE 对大幅偏差敏感,能反映峰期预测失误的严重程度;MAE 是平均绝对误差,单位与人次一致,最直观;MAPE 是相对误差,但由于客流分母可能接近 0,深夜低谷时段会导致 MAPE 虚高,所以建议只算白天时段的 MAPE。
计算指标前必须先把预测结果做反归一化(inverse_transform),直接在 [0,1] 尺度上算误差得到的数值没有任何业务含义。常见错误是把验证集的预测值和真实值各自inverse_transform,但注意真实值在归一化时是按「整体训练集最大最小值」缩放的,反变换时也要用同一个 scaler,不能重新 fit。
def evaluate(y_true, y_pred): # y_true, y_pred 传入时是原始人次尺度 rmse = np.sqrt(np.mean((y_true - y_pred) ** 2)) mae = np.mean(np.abs(y_true - y_pred)) mask = y_true > 0 mape = np.mean(np.abs((y_true[mask] - y_pred[mask]) / y_true[mask])) * 100 return {'RMSE': round(rmse, 2), 'MAE': round(mae, 2), 'MAPE': round(mape, 2)} # 反归一化示例 pred_org = scaler.inverse_transform(pred_scaled.reshape(-1, 1)).flatten() true_org = scaler.inverse_transform(y_val_scaled.reshape(-1, 1)).flatten() print(evaluate(true_org, pred_org))mask = y_true > 0这行是为了剔除深夜零客流的时段,否则这些时段真实值可能是 5、10,预测值即使只差 3 人次,MAPE 也会暴涨到 60% 以上,直接把整体指标搞失真。RMSE、MAE、MAPE 三个值建议同时看,如果 MAE 不大但 RMSE 明显偏高,说明存在少数预测极差的时段,这些时段大概率集中在早晚高峰,需要单独拿出来分析。
4.2 滞后效应:评估图里最容易被忽视的黑匣子
训练完成、指标也打印了,但如果你只是埋头看数字,很可能漏掉最严重的问题——预测曲线滞后。把预测值和真实值画在同一张图上,如果预测曲线整体比真实曲线「晚了一两个时间点」,像贴着真实曲线平移了一段距离,说明模型学到的是「复制前一个时刻的值」,而不是真正预测未来。这种现象在客流预测里极其常见,尤其是单一客流序列输入时,模型发现最优策略就是把最后的观测值原样输出,因为客流本身有强自相关性,这么做损失函数已经很低了。
判断方法很简单:计算预测序列和真实序列的交叉相关性,看峰值出现在哪个 lag 上;更直接的办法是把预测结果整体向前平移一个时间点,再算一次 RMSE。如果平移后误差显著变小,基本可以确认模型在偷懒。解决滞后效应有两个方向:一是把窗口拉开到 48 甚至 72,让模型有更多历史信息去做模态判断而不是贴身复制;二是加入多步预测损失,让模型同时输出未来 1、2、3 步的预测值,从训练信号上逼它学到趋势而非复制。这段属于我自己的血泪经验,早先做第一个客流模型时指标好看到不行,画图才发现全滞后了一拍。
5. 避坑指南:客流序列在 LSTM 里的五个翻车点
这章集中写我复现这类资源时踩过、以及周围同事踩过的具体坑。每一条都是「现象 → 原因 → 解决」三步走,建议在自己机器上跑的时候逐条对照。
5.1 数据泄漏:归一化 fit 到了全量数据
现象:验证集指标极好,RMSE 低得离谱,但一到上线或者换新数据预测就崩盘。
原因:写代码时图省事,先对整个数据集做了MinMaxScaler.fit_transform,再接滑窗和划分训练验证。这样验证集的最小最大值已经被模型在归一化阶段「见过」了,数据分布的边界信息泄漏到了训练流程里,评估结果虚高。
解决:严格按「先划分训练/验证/测试,再在训练子集上fit,验证集和测试集只做transform」。我在第 2.2 节的代码里就是这个顺序,照抄就不会踩。数据泄漏是这类时序项目里最隐蔽的坑,因为指标不会报错,只会给一个不真实的好结果。
5.2 验证集划分用了随机打乱
现象:训练损失正常下降,验证损失也正常,但模型在测试集上一塌糊涂,而且没有滞后问题的曲线也看不出明显原因。
原因:train_test_split默认shuffle=True,把时间序列随机抽成了训练集和验证集。LSTM 训练时,验证集里混着训练集时刻前后的样本,相当于让模型提前「看了看」未来数据,周期规律被完整泄露。
解决:时序项目的划分强制按时间序,训练集取前 70%~80%,验证集取再往后的 10%,测试集取最后 10%。如果担心季节覆盖不全,可以按「滚动划分」做多轮评估,但每轮内部依然严格按时间切分,不能用随机抽样的思路。
5.3 训练损失降了,验证 MAPE 却极高
现象:训练集 RMSE 很小,验证集 RMSE 也不大,但业务方问「平均误差百分之几」时,MAPE 一算 30% 以上,直接没法看。
原因:前面提过,夜间低谷时段的真实客流可能只有 20 人次,预测值 35 人次,绝对误差只有 15,RMSE 贡献微乎其微,但 MAPE 是 75%。如果按全天 24 小时包含低谷一起算,MAPE 被几个深夜点拉爆。
解决:评估指标按业务时段区分。做交通客流运营预测,重点关注早高峰 7:00~9:00、晚高峰 17:00~19:00 和全天非低谷时段,分组计算 MAPE。深夜段用 MAE 衡量即可,不要混在一个数里。
5.4 LSTM 输入形状 shape 报错:三维变二维的维度错位
现象:model(X_train)报错Expected 3D input, got 2D,或者到了nn.Linear时报 hidden_size 和输入维度对不上。
原因:滑窗构造时X_train是(样本数, window)的二维数组,而nn.LSTM要求(batch, seq_len, input_size)三维输入。另一种情况是取隐状态时用了h_n而不是h_n[-1],导致 fc 层的输入维度变成了(batch, num_layers, hidden_size)的展平值。
解决:构造完序列后做一次X_train = X_train.reshape(-1, window, 1)确认形状;取最后隐状态写h_n[-1],这是 last layer 的 hidden state,维度正好是(batch, hidden_size),和前面的nn.Linear(hidden_size, output_size)能对上。
5.5 多特征拼接时忘了对齐时间戳
现象:加了天气温度、是否是节假日这些特征后,训练直接崩,或者 Loss 变成 NaN。
原因:特征表和客流表的索引没有对齐,比如客流是 15 分钟粒度,天气是小时粒度,两者直接concat后产生了时间偏移,模型读到的「当前时刻温度」其实是 45 分钟前的。
解决:先统一时间粒度,客流 15 分钟粒度时,天气特征按小时向前填充(forward fill)到 15 分钟粒度;节假日特征要保证按天对齐,不要用未来日期的标签。拼接前打印df.shape和df.isna().sum()确认没有形状错位。
6. 多步预测与多特征融合:把单步预报扩展成实用方案
前面整条链路做的是「输入 48 个小时预测未来 1 个小时」,但实际业务里更常见的问题是「现在下午 4 点,想预测今晚 6 点到 8 点的出站客流」。这就涉及多步预测,有两条路线:递归预测和直接多输出。
递归预测的策略是把上一步的预测值当作下一步的输入,循环预测到目标步数;实现简单,但误差会逐步累积,预测到第 6 步以后曲线会趋于平滑,因为模型把不确定性平均化了。直接多输出则是修改模型最后一层,把output_size设为待预测步数,一次输出多步结果。这两种方案各有场景,做未来 1~3 小时的客流调配建议用直接多输出,模型一层输出就能拿到完整预测曲线。注意直接多输出时前面滑窗构造的pred_len要相应调大,训练标签y的形状变为(样本数, pred_len)。
多特征融合是把预测质量往上提一档的关键。单用客流序列训练,模型本质上只能学「过去的客流模式在未来重演」,遇到节假日、暴雨天气这些外部扰动必然失效。常见做法是加入 4 个特征:是否为工作日、是否为节假日、小时序号(0~23 的正弦/余弦编码)、天气等级(0~3 映射)。特征拼接的时机是在归一化之后,把所有特征与客流序列一起构成input_size=5的输入。加入外部特征后,滞后效应通常会减轻,因为模型不再只依赖历史客流做贴身复制,而是能感知「今天是工作日且下雨」这种全局状态。
在那之后我跑客流预测养成了一个习惯:每换一个数据集或改一个特征,都强制先看一眼预测对齐图,再算指标。图和数字一起通过,才敢把结果拿出去。模型的结构、参数、数据处理全都可以在这一份资源包里直接改,希望这份拆解能帮你在自己的客流数据上少走几段弯路。
# 直接多输出 + 多特征的训练入口示意 # 假设 feature_cols 已与客流序列按时间对齐并归一化 X_multi, y_multi = create_sequences(multi_feature_data, window=48, step=24, pred_len=4) model = LSTMPredictor(input_size=X_multi.shape[2], hidden_size=128, num_layers=2, output_size=4) model = train_model(model, X_multi, y_multi, X_val_multi, y_val_multi, epochs=80, lr=0.001, patience=15)这份资源包里附带的数据集结构、模型初始参数和训练脚本,都是以此为基准写的。把input_size、hidden_size、window三项按你的数据量做加减法,就能从一个「能跑的 demo」变成一个「业务上敢用的预测模块」。希望帮到你。
本文还有配套的精品资源,点击获取