简介这份资源是面向计算机相关专业学生、教师及科研人员的纽约出租车流量预测项目完整包可作为毕业设计、课程作业或项目立项演示的参考方案。项目基于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_datetime、tpep_dropoff_datetime、passenger_count、trip_distance、PULocationID、DOLocationID、fare_amount、tip_amount、total_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, columnscols) # 时间字段标准化去掉时区信息避免后续聚合报错 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(nametrip_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_periods1).mean()) flow[rolling_std_24] flow.groupby(PULocationID)[trip_count].transform( lambda x: x.rolling(24, min_periods1).std()) # 丢弃因 shift 产生的空值行 flow flow.dropna().reset_index(dropTrue)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(dropTrue) 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_estimators800, learning_rate0.05, num_leaves63, min_child_samples20, subsample0.8, colsample_bytree0.8, random_state42 ) model.fit(X_train, y_train, eval_set[(X_valid, y_valid)], eval_metricmae, 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_estimators800配合early_stopping(50)让模型自动决定何时停止learning_rate0.05是精度和速度的平衡点num_leaves63控制树的复杂度太大容易过拟合。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, window168): 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, dtypenp.float32), np.array(y, dtypenp.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_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): 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_dimX_tr.shape[2]).to(device) optimizer torch.optim.Adam(net.parameters(), lr1e-3) loss_fn nn.MSELoss() loader DataLoader(TensorDataset(torch.from_numpy(X_tr), torch.from_numpy(y_tr)), batch_size256, shuffleTrue) 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_dim64、num_layers2是中小规模数据的常用配置数据量大可以加到 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 后出现大量 NaNdropna 后数据量骤减原因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], labelActual, alpha0.8) plt.plot(sub[pickup_hour], model.predict(sub[feature_cols]), labelPredicted, alpha0.8) plt.legend() plt.title(fRegion {sample_region} Flow Prediction) plt.tight_layout() plt.savefig(prediction_curve.png, dpi150)调参方面我自己的习惯是先固定学习率 0.05用 early stopping 确定最佳迭代轮数然后调num_leaves32 到 127 之间试最后调min_child_samples和正则化参数。每次只调一个维度记录 MAE 变化避免同时改多个参数导致无法归因。LSTM 那边优先调窗口长度和 hidden_dim学习率用余弦退火比固定值更稳。还有一个容易被忽略的技巧把预测目标从原始流量改成相对上周同期的变化率有时候能显著降低误差因为这样模型学的是增量而不是绝对值对趋势变化的响应更快。这个思路在流量波动大的区域尤其有效。最后说个血泪经验纽约出租车数据每年都在更新字段名和分区编号偶尔会变拿到新月份数据时先跑一遍字段检查别直接套用旧脚本。我自己就因为没检查字段白跑了一晚上训练。希望帮到你。本文还有配套的精品资源点击获取