纽约出租车流量预测实战:从数据清洗到LightGBM与LSTM模型全链路
2026/9/24 18:05:25 网站建设 项目流程

简介:这份资源是面向计算机相关专业学生、教师及科研人员的纽约出租车流量预测项目完整包,可作为毕业设计、课程作业或项目立项演示的参考方案。项目基于Python实现,围绕交通流量时序预测展开,涵盖数据加载、模型构建、训练评估与可视化全流程,适合具备一定深度学习基础、希望进阶实战的读者借鉴学习。压缩包共32个文件,约1.21MB,以py源码为主,辅以pyc缓存、xml配置、png训练指标图、npz数据文件及md说明文档,另附docx数据说明与设计报告,结构清晰便于按模块查阅。内容包含LSTM、GRU、CNN-LSTM、CNN-GRU等多种模型实现,以及损失曲线等评估图表,可帮助读者理解时序预测的建模思路与调参过程。目前已有67人学习,遇到配置或运行问题还可远程交流,适合在此基础上修改扩展,完成自己的毕设或课设任务。

1. 纽约出租车流量预测:从一份源代码包到能跑通的预测链路

纽约出租车流量预测这个题目,几乎成了时序预测入门到进阶的"标准靶场"。它同时踩中了几个刚需:数据量足够大(上亿条行程记录)、时间粒度清晰(按小时或按天聚合)、外部变量丰富(天气、节假日、机场客流)、业务价值直观(调度、定价、拥堵治理)。但真正让从业者头疼的,从来不是模型选型,而是从原始 parquet 文件到一条能复现的预测曲线之间,那一堆琐碎的工程细节。这份"基于 Python 的纽约出租车流量预测模型源代码 + 设计报告"要解决的,正是把数据清洗、特征工程、模型训练、评估可视化串成一条可交付的链路。它适合两类人:一类是刚学完 pandas 和 sklearn、想找一个真实规模数据集练手的新手;另一类是已经会调 XGBoost 或 LSTM、但没系统处理过时空特征和缺失值的熟手。下面我按自己落地时的顺序,把这条链路拆开讲清楚。

2. 数据准备与特征工程:把上亿条行程压成可训练的时间序列

2.1 纽约出租车数据的真实结构与你需要先搞清的字段

纽约出租车公开数据通常按月份发布,格式以 parquet 为主,单月文件在几百 MB 量级。核心字段包括tpep_pickup_datetimetpep_dropoff_datetimepassenger_counttrip_distancePULocationIDDOLocationIDfare_amounttip_amounttotal_amount等。做流量预测时,我们真正关心的是"某个区域在某个时间段内产生了多少条行程",所以第一步是把明细数据聚合成时间序列。

这里有个容易被忽略的点:PULocationID是纽约出租车分区编号,共 260 多个,直接对全部区域建模会导致序列过于稀疏。常见做法是选取 Top N 高流量区域(比如按总行程数排序取前 20),或者按行政区(Borough)粗粒度聚合。我一般会先做一次区域流量分布统计,再决定聚合粒度。

import pandas as pd import numpy as np # 读取单月数据,只保留建模需要的列以降低内存 cols = ['tpep_pickup_datetime', 'PULocationID', 'trip_distance', 'fare_amount'] df = pd.read_parquet('yellow_tripdata_2023-01.parquet', columns=cols) # 时间字段标准化,去掉时区信息避免后续聚合报错 df['pickup_hour'] = pd.to_datetime(df['tpep_pickup_datetime']).dt.floor('H') # 过滤异常行程:距离为负、金额为负、时间超出当月范围 df = df[(df['trip_distance'] >= 0) & (df['fare_amount'] >= 0)] df = df[(df['pickup_hour'] >= '2023-01-01') & (df['pickup_hour'] < '2023-02-01')] # 按小时 + 区域聚合,得到流量计数 flow = df.groupby(['pickup_hour', 'PULocationID']).size().reset_index(name='trip_count') print(flow.shape)

这段代码的逻辑是:先做列裁剪降低内存占用,再把时间戳对齐到整点,然后过滤掉明显异常的行,最后按"小时 + 区域"聚合出流量。参数上,dt.floor('H')决定时间粒度,如果你要做 15 分钟级预测就改成'15min';过滤条件里的金额和距离阈值可以根据数据质量报告调整,纽约数据里确实存在少量负值记录。

2.2 时间特征与滞后特征的构造方法

聚合出流量序列后,直接喂给模型效果通常很差,因为时序模型需要显式的周期信息。纽约出租车流量有非常强的日周期和周周期:工作日早高峰、晚高峰、周五夜间、周末凌晨,模式差异明显。所以特征工程至少要覆盖三类:时间戳派生特征、滞后特征、滑动窗口统计。

# 时间派生特征 flow['hour'] = flow['pickup_hour'].dt.hour flow['dayofweek'] = flow['pickup_hour'].dt.dayofweek flow['is_weekend'] = (flow['dayofweek'] >= 5).astype(int) flow['month'] = flow['pickup_hour'].dt.month # 对每个区域单独构造滞后特征,避免跨区域串数据 flow = flow.sort_values(['PULocationID', 'pickup_hour']) flow['lag_1'] = flow.groupby('PULocationID')['trip_count'].shift(1) flow['lag_24'] = flow.groupby('PULocationID')['trip_count'].shift(24) flow['lag_168'] = flow.groupby('PULocationID')['trip_count'].shift(168) # 滑动窗口均值与标准差 flow['rolling_mean_24'] = flow.groupby('PULocationID')['trip_count'].transform( lambda x: x.rolling(24, min_periods=1).mean()) flow['rolling_std_24'] = flow.groupby('PULocationID')['trip_count'].transform( lambda x: x.rolling(24, min_periods=1).std()) # 丢弃因 shift 产生的空值行 flow = flow.dropna().reset_index(drop=True)

lag_1捕捉上一小时的影响,lag_24捕捉昨天同一时刻的影响,lag_168捕捉上周同一时刻的影响,这三个滞后项基本覆盖了短、中、长周期。滑动窗口的 24 小时均值反映近期趋势,标准差反映波动性。注意groupby('PULocationID')这一步不能省,否则会把不同区域的流量混在一起,模型学到的就是噪声。

提示:滞后特征构造后一定要检查每个区域的时间序列是否连续。如果某个区域中间缺了几个小时,shift 出来的滞后值会错位,建议先做时间轴补全再构造特征。

2.3 训练集与验证集的时间切分原则

时序预测最忌讳随机切分。用train_test_split打乱顺序会让未来信息泄漏到训练集,验证指标虚高。正确做法是按时间先后切分:比如用前 10 个月训练,后 2 个月验证;或者用前 80% 时间点训练,后 20% 验证。

# 按时间排序后切分,保证验证集全在训练集之后 flow = flow.sort_values('pickup_hour').reset_index(drop=True) split_idx = int(len(flow) * 0.8) train = flow.iloc[:split_idx] valid = flow.iloc[split_idx:] feature_cols = ['hour', 'dayofweek', 'is_weekend', 'month', 'lag_1', 'lag_24', 'lag_168', 'rolling_mean_24', 'rolling_std_24'] X_train, y_train = train[feature_cols], train['trip_count'] X_valid, y_valid = valid[feature_cols], valid['trip_count']

切分比例 80/20 是常见起点,但如果你的数据有明显的季节性(比如冬季和夏季模式差异大),可以考虑按"最后 N 周"做验证,而不是按比例。特征列里没有放PULocationID本身,因为如果直接放进去,树模型会把它当成有序数值处理,产生误导;正确做法是对区域做 one-hot 或 target encoding,这部分在下一章展开。

3. 模型选型与训练:从 LightGBM 基线到 LSTM 的取舍

3.1 为什么先用梯度提升树而不是直接上深度学习

很多教程一上来就讲 LSTM 或 Transformer,但在纽约出租车流量这个场景里,梯度提升树(LightGBM、XGBoost)往往是更强的基线。原因有三:第一,表格型特征(时间派生 + 滞后 + 滑动窗口)对树模型非常友好,不需要归一化;第二,训练速度快,单机几分钟就能跑完,方便快速迭代特征;第三,可解释性强,特征重要性可以直接告诉你哪个滞后项贡献最大。

import lightgbm as lgb from sklearn.metrics import mean_absolute_error, mean_squared_error # 区域做 target encoding,用训练集均值编码避免泄漏 region_mean = train.groupby('PULocationID')['trip_count'].mean() train['region_enc'] = train['PULocationID'].map(region_mean) valid['region_enc'] = valid['PULocationID'].map(region_mean) valid['region_enc'] = valid['region_enc'].fillna(region_mean.mean()) feature_cols = feature_cols + ['region_enc'] X_train, X_valid = train[feature_cols], valid[feature_cols] model = lgb.LGBMRegressor( n_estimators=800, learning_rate=0.05, num_leaves=63, min_child_samples=20, subsample=0.8, colsample_bytree=0.8, random_state=42 ) model.fit(X_train, y_train, eval_set=[(X_valid, y_valid)], eval_metric='mae', callbacks=[lgb.early_stopping(50)]) pred = model.predict(X_valid) print('MAE:', mean_absolute_error(y_valid, pred)) print('RMSE:', np.sqrt(mean_squared_error(y_valid, pred)))

参数上,n_estimators=800配合early_stopping(50)让模型自动决定何时停止;learning_rate=0.05是精度和速度的平衡点;num_leaves=63控制树的复杂度,太大容易过拟合。target encoding 这里用的是训练集区域均值,验证集用同样的映射,遇到训练集没出现过的区域就用全局均值兜底。

3.2 LSTM 版本的数据组织与训练要点

如果你确实需要上深度学习,LSTM 的输入组织方式和树模型完全不同。它需要三维张量:(样本数, 时间步长, 特征数)。也就是说,要把每个区域的序列切成固定长度的窗口,比如用过去 168 小时预测下一小时。

import torch import torch.nn as nn from torch.utils.data import DataLoader, TensorDataset def make_sequences(data, feature_cols, target_col, window=168): X, y = [], [] for region in data['PULocationID'].unique(): sub = data[data['PULocationID'] == region].sort_values('pickup_hour') vals = sub[feature_cols].values targets = sub[target_col].values for i in range(window, len(sub)): X.append(vals[i-window:i]) y.append(targets[i]) return np.array(X, dtype=np.float32), np.array(y, dtype=np.float32) X_tr, y_tr = make_sequences(train, feature_cols, 'trip_count') X_va, y_va = make_sequences(valid, feature_cols, 'trip_count') # 特征标准化,LSTM 对量纲敏感 mean, std = X_tr.mean(axis=(0, 1)), X_tr.std(axis=(0, 1)) + 1e-8 X_tr = (X_tr - mean) / std X_va = (X_va - mean) / std class FlowLSTM(nn.Module): def __init__(self, input_dim, hidden_dim=64, num_layers=2): super().__init__() self.lstm = nn.LSTM(input_dim, hidden_dim, num_layers, batch_first=True, dropout=0.2) self.fc = nn.Linear(hidden_dim, 1) def forward(self, x): out, _ = self.lstm(x) return self.fc(out[:, -1, :]).squeeze(-1) device = torch.device('cuda' if torch.cuda.is_available() else 'cpu') net = FlowLSTM(input_dim=X_tr.shape[2]).to(device) optimizer = torch.optim.Adam(net.parameters(), lr=1e-3) loss_fn = nn.MSELoss() loader = DataLoader(TensorDataset(torch.from_numpy(X_tr), torch.from_numpy(y_tr)), batch_size=256, shuffle=True) for epoch in range(20): net.train() for xb, yb in loader: xb, yb = xb.to(device), yb.to(device) optimizer.zero_grad() loss = loss_fn(net(xb), yb) loss.backward() optimizer.step()

窗口长度 168 对应一周的小时数,这是捕捉周周期的最小长度。hidden_dim=64num_layers=2是中小规模数据的常用配置,数据量大可以加到 128。标准化用训练集的均值和标准差,验证集复用同一组参数,这是防止信息泄漏的基本纪律。

3.3 树模型与深度模型的对比与选择建议

维度LightGBMLSTM
训练速度分钟级小时级(GPU)
特征工程依赖高,需手工构造滞后低,可自动学时序模式
可解释性特征重要性清晰弱,黑匣子
多区域联合建模需 target encoding天然支持多序列
小数据表现稳定容易过拟合

我的建议是:先用 LightGBM 跑出一个可信基线,把 MAE 和 RMSE 记下来。如果 LSTM 没有明显超过基线(比如 MAE 降低不到 5%),就不值得为它付出额外的工程复杂度。很多实际项目里,特征工程做得好,树模型就能打平甚至超过深度模型。

4. 避坑与排查:纽约出租车流量预测里最容易翻车的五件事

4.1 现象:验证集 MAE 很低,上线后预测全偏

原因:特征里用了全局统计量(比如全局均值、全局分位数),这些统计量在训练时包含了验证集和未来数据,造成信息泄漏。解决:所有统计类特征必须只用训练集计算,验证集和测试集复用训练集的映射表。检查方法是把训练集和验证集的时间范围打印出来,确认没有重叠。

4.2 现象:某些区域预测值恒为 0 或恒定常数

原因:这些区域在训练集中样本极少,模型没有学到有效模式,或者 target encoding 时被全局均值覆盖。解决:对低流量区域做合并,或者设置最小样本阈值,低于阈值的区域归入"其他"类别。也可以对目标做 log1p 变换,缓解长尾分布。

4.3 现象:滞后特征 shift 后出现大量 NaN,dropna 后数据量骤减

原因:shift(168)会让每个区域前 168 行变成 NaN,如果区域数量多、序列短,dropna 后可能损失大半数据。解决:要么缩短最大滞后阶数,要么对 NaN 做填充(比如用该区域的历史均值),要么在构造特征前先确认每个区域的序列长度是否足够。

4.4 现象:LSTM 训练 loss 不下降或震荡严重

原因:特征未标准化、学习率过大、batch size 太小、序列窗口内存在大量缺失值。解决:先做标准化,学习率从 1e-3 开始试,batch size 不低于 64,检查序列里是否有 NaN 或 inf。另外,LSTM 对异常值敏感,建议对目标做截断或 log 变换。

4.5 现象:模型在节假日预测误差暴增

原因:训练数据中节假日样本占比低,模型没有学到节假日的特殊模式。解决:加入节假日标记特征(is_holiday),或者对节假日单独建模。纽约数据里,感恩节、圣诞节、独立日的流量模式和普通工作日差异极大,不做特殊处理几乎必然翻车。

5. 评估、可视化与一个能复用的调参习惯

评估环节,MAE 和 RMSE 是最常用的两个指标,但光看数字不够,必须画图。我一般会画三张图:第一张是验证集上预测值和真实值的时间序列对比,看整体趋势是否跟上;第二张是按小时的误差分布,看哪个时段误差最大;第三张是散点图,看是否存在系统性高估或低估。

import matplotlib.pyplot as plt # 取单个区域做可视化,避免全量图太乱 sample_region = valid['PULocationID'].value_counts().index[0] sub = valid[valid['PULocationID'] == sample_region].sort_values('pickup_hour') plt.figure(figsize=(14, 4)) plt.plot(sub['pickup_hour'], sub['trip_count'], label='Actual', alpha=0.8) plt.plot(sub['pickup_hour'], model.predict(sub[feature_cols]), label='Predicted', alpha=0.8) plt.legend() plt.title(f'Region {sample_region} Flow Prediction') plt.tight_layout() plt.savefig('prediction_curve.png', dpi=150)

调参方面,我自己的习惯是:先固定学习率 0.05,用 early stopping 确定最佳迭代轮数;然后调num_leaves(32 到 127 之间试);最后调min_child_samples和正则化参数。每次只调一个维度,记录 MAE 变化,避免同时改多个参数导致无法归因。LSTM 那边,优先调窗口长度和 hidden_dim,学习率用余弦退火比固定值更稳。

还有一个容易被忽略的技巧:把预测目标从原始流量改成"相对上周同期的变化率",有时候能显著降低误差,因为这样模型学的是增量而不是绝对值,对趋势变化的响应更快。这个思路在流量波动大的区域尤其有效。

最后说个血泪经验:纽约出租车数据每年都在更新,字段名和分区编号偶尔会变,拿到新月份数据时先跑一遍字段检查,别直接套用旧脚本。我自己就因为没检查字段,白跑了一晚上训练。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询