简介:加拿大Whitemud Drive高速公路数据集,是阿尔伯塔省埃德蒙顿市一条被用作试验道路的市内高速公路的实测数据,全程28公里,限速80公里/小时,主干道与闸道均部署地感线圈,可同时观测车流量、车速与车辆密度,是交通流预测、出行时间预测及可变限速(VSL)控制研究的关键实证数据。打包为rar压缩包,约138MB,包含748个文件,以692个xls原始检测数据为主,辅以51个jpg速度等高线图、少量xlsx汇总表、txt说明、m脚本及png图片,便于快速理解数据结构并开展可视化分析。目前已有2492人学习下载,尤其适合智能交通方向的研究生、工程师用于短期出行需求预测、瓶颈路段检测、天气对可变限速策略影响分析以及高速公路通行能力评估。利用该数据集可直接开展交通流模型验证、出行时间预测建模,量化评估可变限速控制对缓解拥堵、降低事故率的作用,为相关科研与工程实践提供有力支撑。
1. 加拿大Whitemud Drive高速公路数据集:一份被低估的交通流样本
第一次打开加拿大Whitemud Drive高速公路数据集时,我原以为拿到的是干净整齐的表格,结果却是几十万行带着坐标噪声、时间戳错位和车辆ID断裂的原始轨迹记录。这份数据集本质上是某条城市快速路在一段连续时间内的车辆移动轨迹或断面流量记录,常被用于交通流预测、跟车行为建模、轨迹平滑与地图匹配等任务。它和常见的仿真交通数据最大的区别是:真实、琐碎、有大量测量噪声,而这恰恰是它最有价值的地方。适合的人群很明确——做智能交通算法落地、时序预测或者轨迹挖掘的工程师和研究者。新手可以拿它练数据清洗基本功,熟手可以用它验证模型在真实噪声下的鲁棒性。它不是什么标准答案,而是一块需要自己动手打磨的原材料。
2. 从压缩包到结构化数据:Whitemud Drive 文件解析与格式确认
拿到.rar压缩包后,很多人第一件事就是解压然后直接写代码读数据,结果被字段名、分隔符和编码问题折腾半天。我的习惯是先做一次“文件体检”,把数据集的物理形态摸清楚,再决定用哪套解析方案。这一步做扎实了,后面所有清洗和建模工作才会顺。
2.1 解压先别急着跑代码:文件类型识别与快速预览
无论你在哪台机器上工作,解压只是第一步。真正要关心的是解压后目录里到底装了什么——是单个大 CSV,还是分了多个子目录的原始采集片段。常见做法是先用系统命令做最基本的探查,不要急着打开 Python。
# 解压到独立目录,避免污染工作区 mkdir -p whitemud_raw && unzip -o whitemud_dataset.rar -d whitemud_raw # 或者用 unrar 处理 .rar 格式 unrar x whitemud_dataset.rar whitemud_raw/ # 看目录结构,判断是单文件还是多文件分片 find whitemud_raw -type f | head -20 # 对疑似数据文件做类型识别 file whitemud_raw/*.csv whitemud_raw/*.txt这段命令的核心价值是让你在写任何解析代码之前,先确认三个关键信息:文件数量、文件格式、字符编码。unrar x和unzip -o的区别在于前者保留目录结构,后者把文件解到指定目录;如果你在 Windows 上工作,可以用 WinRAR 或 7-Zip 的图形界面,效果一样。file命令会告诉你文件到底是纯文本 CSV、制表符分隔的 TSV,还是某种二进制格式——很多交通采集设备会输出带 BOM 头的 UTF-8 文件,肉眼看不出来,但file能显示出来。
拿到文件清单后,我一般会再抽几行数据看一眼:
# 看第一行,确认是表头还是直接是数据 head -5 whitemud_raw/trajectories.csv # 统计行数和列数,建立初步规模认知 wc -l whitemud_raw/trajectories.csv这一步看起来基础,但能避免一个非常常见的翻车场景:直接按表头名取列,结果发现这个文件根本没有表头,字段顺序全靠猜。如果head显示第一行是时间戳和数字,说明没有表头,你就得在解析时手动指定列名。
2.2 字段级拆解:时间戳、位置坐标与车辆ID的对应关系
这类高速公路轨迹数据集的字段设计大同小异,核心信息通常是车辆ID、时间戳、位置坐标(经纬度或平面坐标)、瞬时速度、车道编号和目标车距。Whitemud Drive 这类城市快速路数据集里,因为采集设备可能是路侧单元或车载 GPS,字段的命名和单位会存在差异,最常见的问题是坐标到底走的是 WGS84 经纬度还是某种投影坐标。
我一般会在解析脚本里加一个“schema 探针”,把每一列的类型、缺失比例和样例值打出来,而不是凭感觉写死。
import pandas as pd # 先只读前 5000 行做快速探查,避免一次性载入大文件卡死 df_sample = pd.read_csv( "whitemud_raw/trajectories.csv", nrows=5000, sep="," ) print(df_sample.dtypes) print(df_sample.head(3)) print("缺失比例:") print(df_sample.isnull().mean().round(4))这段代码有三个关键设计:第一,nrows=5000限定了读取行数,几百 MB 的文件也能秒开,先看清楚结构再决定要不要全量加载。第二,dtypes能直接暴露字段类型问题,比如时间戳被读成了object而不是datetime64,说明原始格式不是标准 ISO 字符串。第三,缺失比例是第一个需要记录的数据质量指标,如果某列缺失达到 30%,下游做特征工程时就得考虑是插值还是丢弃。
拿到 schema 之后,重点确认三件事。时间戳字段要统一成 UTC 还是本地时间,坐标字段是经纬度还是 UTM 投影坐标,车辆 ID 是唯一标识还是存在复用。其中车辆 ID 复用是这类数据集的普遍坑点——某个 ID 可能代表一段连续轨迹,但也可能在同一时间片内出现两次,说明采集端做了轨迹拼接或重识别。我会用分组统计的方式快速验证:
# 同一辆车是否出现在多个不连续时间段 grp = df_sample.groupby("vehicle_id")["timestamp"] stats = pd.DataFrame({ "count": grp.count(), "time_span": grp.max() - grp.min(), }) print(stats.describe())逻辑上,如果某辆车的time_span超过一个合理的行程时长(比如 2 小时),或者count出现极端峰值,基本可以确定这个车辆 ID 不是一次完整的跟车记录,而是被多个传感器拼接出来的。这个判断直接影响后续轨迹切片方式,千万别把两段不同时间、不同位置的轨迹当成一辆车的连续行为来建模。
3. 把原始轨迹变成可训练样本:清洗、切片与特征工程
结构摸清之后,下一步就是让数据变得“可用”。原始轨迹数据直接喂给模型基本不可能收敛,因为里面充满了传感器噪声、时间戳抖动和无效定位。这个阶段的工作决定了模型效果的天花板,投入产出比远高于调参。
3.1 轨迹清洗四板斧:去漂移、补缺失、对齐时间、过滤静置
我处理轨迹数据有一个固定流程:先去坐标漂移,再补时间缺口,然后统一时间基准,最后把原地不动的车辆过滤掉。四步做完,数据质量会有一个质的提升。
漂移是 GPS 轨迹最常见的噪声,表现是车辆位置在极短时间内跳出去几百米又跳回来。判断标准可以用“连续两点间的位移速度是否超过物理极限”,比如一辆车在 1 秒内位移了 300 米,那瞬时速度就是 300 m/s,这在城市快速路上是不可能的。
import numpy as np import pandas as pd def haversine_distance(lat1, lon1, lat2, lon2): """计算两个经纬度点的球面距离(米)""" R = 6371000.0 phi1, phi2 = np.radians(lat1), np.radians(lat2) dphi = np.radians(lat2 - lat1) dlambda = np.radians(lon2 - lon1) a = np.sin(dphi/2)**2 + np.cos(phi1)*np.cos(phi2)*np.sin(dlambda/2)**2 return 2 * R * np.arcsin(np.sqrt(a)) df["prev_lat"] = df.groupby("vehicle_id")["lat"].shift(1) df["prev_lon"] = df.groupby("vehicle_id")["lon"].shift(1) df["dist"] = haversine_distance(df["lat"], df["lon"], df["prev_lat"], df["prev_lon"]) df["dt"] = df.groupby("vehicle_id")["timestamp"].diff().dt.total_seconds() # 瞬时速度超过 60 m/s 视为漂移点(约 216 km/h,超过快速路限速) df["speed_estimate"] = df["dist"] / df["dt"].replace(0, np.nan) drift_mask = df["speed_estimate"] > 60.0 print(f"漂移点数量: {drift_mask.sum()}")这里的关键参数是速度阈值 60 m/s,你可以根据道路实际限速调整。Whitemud Drive 这类城市快速路限速一般在 80-100 km/h,取 60 m/s 已经是相当宽松的上限,能保住正常超速车辆,同时把绝大多数定位跳变过滤掉。groupby("vehicle_id").shift(1)保证了计算相邻点距离时不会跨车辆计算,避免把前一辆车的终点和后一辆车的起点连到一起。
时间对齐解决的是另一个问题:不同采集设备的时间戳精度可能不一致,有的精确到秒,有的精确到毫秒。如果模型输入需要固定时间间隔,就得做重采样。
# 把每辆车的轨迹按 1 秒间隔重采样,使用线性插值填补缺失 def resample_trajectory(group): group = group.set_index("timestamp").sort_index() new_index = pd.date_range(group.index.min(), group.index.max(), freq="1S") return group.reindex(new_index).interpolate(method="linear") df_aligned = df.groupby("vehicle_id", group_keys=False).apply(resample_trajectory)重采样的代价是会产生一部分插值点,插值点的可信度低于原始观测点。我一般在重采样后保留一列is_interpolated标记,方便下游做训练时筛选或加权。如果原始数据采样频率本身是 10Hz,就没必要强行重采样到 1Hz,保持原始频率反而更好。
最后是静置过滤。城市快速路虽然通畅,但某些时段的拥堵可能让车辆在原地停留。静止点本身对速度预测模型是有效训练样本,但对轨迹平滑和跟车模型来说,长时间静置会让数据分布偏斜。我会把连续静止超过 60 秒的片段单独切掉。
3.2 用滑动窗口切出模型输入:序列构造与目标字段设计
清洗完的数据还是表格形态,需要把它切成“序列样本”才能喂给时序模型。这里最常用的做法是固定窗口滑动切片——用过去 N 秒的位置和速度预测未来 M 秒的速度或位置。窗口长度和预测步长是模型效果最敏感的两个超参数。
def create_sequence_samples(df, window=10, horizon=5, step=2): """从轨迹表构造 (输入序列, 预测目标) 样本对 - window: 输入序列长度,单位秒 - horizon: 预测未来多少秒 - step: 滑动步长,控制样本重叠度 """ samples, targets = [], [] for veh_id, group in df.groupby("vehicle_id"): group = group.sort_values("timestamp") if len(group) < window + horizon: continue values = group[["speed", "acceleration", "lane"]].values for start in range(0, len(group) - window - horizon + 1, step): x = values[start:start + window] y = group["speed"].values[start + window:start + window + horizon] samples.append(x) targets.append(y.mean()) return np.array(samples), np.array(targets)三个参数需要重点说明。window=10表示用过去 10 个观测点做输入,在 1Hz 重采样下就是 10 秒的历史信息;horizon=5表示预测未来 5 秒的平均速度;step=2控制相邻样本的时间重叠度,step 越小样本越多但相关性越高,训练时需要用 shuffle 打散。一个血泪经验是:如果 step 设成了 1,相邻样本几乎完全相同,模型会严重过拟合,验证集上表现很好,但换到新一天的数据上直接崩掉。
目标字段的设计要和任务对齐。如果目标是预测平均速度,用y.mean()取未来 5 秒的均值是合理的;如果目标是预测轨迹坐标,就得把未来的经纬度序列作为目标,那y就应该是二维数组,而不是标量。这个选择必须在建模前敲定,因为不同目标定义的模型能力边界完全不同。
3.3 落盘为 parquet:按时间段分片,避免全量读内存
清洗和切片完成之后,数据量通常会膨胀不少,尤其是做了重采样和滑动窗口之后。几十万行原始数据可能变成几百万个样本,全部塞进 CSV 会让后续每次训练都卡在 I/O 上。我习惯在这一步把中间结果转成列式存储格式,按天或按小时分片,训练时按需加载。
import pyarrow.parquet as pq import pyarrow as pa # 按日期列分片,每个分片包含一个完整时间段的数据 df["date"] = df["timestamp"].dt.date for date, group in df.groupby("date"): table = pa.Table.from_pandas(group) pq.write_table(table, f"whitemud_processed/{date}.parquet") print(f"已写入 {date}: {len(group)} 行")这里按日期分片的逻辑是:交通流数据有极强的昼夜周期特性,按天切分能方便你做训练集/验证集的时间隔离,避免数据穿越。如果用随机切分,某天的早高峰数据可能同时出现在训练集和验证集里,模型评估结果会虚高。parquet 的另一个优势是压缩率高,同样的数据比 CSV 通常小 60%-70%,而且读取时可以只选择需要的列,内存压力小很多。
4. Whitemud Drive 数据集实战中的 5 个坑:现象、原因与解决
这是整个流程里最值得写下来的部分。真实数据集的坑往往不在模型复杂度上,而在数据本身的隐蔽问题。以下 5 个坑我基本每个都踩过,写出现象、原因和解决方式,帮你绕开。
4.1 坐标跳变:一辆车 1 秒内位移了 300 米
现象:清洗前直接可视化轨迹,发现某些车辆的路径上出现脱离道路的“飞点”,位置直接跳到几公里外的街区,下一秒又回到道路上。
原因:这是 GPS 定位漂移或地图匹配失败,在城市场景中尤其常见,高楼反射和多路径效应会严重干扰定位。
解决:按速度阈值过滤,计算相邻点距离和间隔时间,瞬时速度超过 60 m/s 的点直接剔除,然后用前后有效点做插值修复。阈值设置要留足余量——高速环境下车辆也可能短时间达到 130 km/h 约 36 m/s,所以 60 m/s 能保留真实高速行驶记录,只去掉明显不可能的点。
4.2 时间戳格式混乱:一会是本地时间,一会是 UTC
现象:把数据按时间排序后,发现同一辆车的轨迹时间出现“倒流”,或者相邻时间段之间的间隔忽大忽小。
原因:采集设备在数据传输过程中,一部分设备用了本地时区,一部分用了 UTC,写入数据集时没有统一转换。还有可能是跨了夏令时切换节点。
解决:统一转成 UTC 时间戳再做排序和重采样。具体做法是,先识别字段里的时区偏移量,再用tz_localize和tz_convert统一到 UTC,宁可全部转成一个基准,也不要保留混合时区。处理完后做一次时间单调性校验,确保每个vehicle_id内部的时间戳严格递增。
4.3 车辆 ID 复用:同一个 ID 出现在两小时后完全不同的位置
现象:某辆车的轨迹时间跨度长达 3 小时,行驶里程超过 200 公里,但该快速路全长通常不到几十公里,数据存在明显的不合理。
原因:采集系统对不同传感器段的车辆做了重识别,一旦车辆在某个断面被跟丢又在新断面重新出现,系统可能沿用旧 ID,导致同一 ID 下拼接了两段不同行程。
解决:按时间间隔切分轨迹。检查相邻点的时间差,如果超过一个阈值(我一般用 120 秒),认为车辆轨迹断开,把同一 ID 切成两条独立轨迹。切分后分别生成新的轨迹 ID,避免把不连续的时间段当成一个序列建模。
4.4 特征泄漏:样本构造时不小心把未来信息放进输入
现象:模型在验证集上指标极好,但部署到新数据上预测效果大幅下降,速度预测偏差超过 30%。
原因:滑动窗口切片时,用了包含未来点的行做当前样本的输入特征,最常见的是在特征工程阶段计算了“整段轨迹的平均速度”作为特征,但这个统计量是在样本构造后算的,包含了未来信息。
解决:严格按时间顺序构造样本,所有特征只允许使用当前窗口之前的数据。一个实用的自检方法是:把样本的输入特征矩阵打乱后重新训练,如果效果几乎不变,说明特征本身就带着泄漏;如果效果大幅下降,才说明时间依赖是真的有效。
4.5 数据不平衡:白天黑夜的样本量差距悬殊
现象:数据集里白天样本占 80% 以上,夜晚样本稀疏,模型在夜间场景下的预测误差明显偏高。
原因:城市快速路的车流量本身就有强时段性,夜晚车流少是客观规律,采集到的轨迹数量自然少。
解决:不要盲目对少数类做重采样,而是先按小时统计样本分布,再决定是否按时间段分层采样。我的做法是:训练时按小时分层采样,保证每个小时的样本量被压到相近水平;或者在评估指标上分开看白天和夜间的表现,接受夜间数据不足的现实,不做过度增强。
5. 用按天留出法验证模型:从随机切分翻车到时间感知评估
最后一个章节想分享一个具体技巧:交通流数据的模型验证必须用时间隔离的方式,而不是随机切分。这是我最惨痛的一次翻车经历——当时做速度预测,按随机 8:2 切分训练集和验证集,验证集误差 4%,感觉很完美。结果模型被放到新一周的数据上测试,误差直接飙到 21%。
原因很简单:高速公路数据是强时间序列,随机切分会让同一辆车的轨迹片段同时出现在训练集和验证集里,模型实际上记住了车辆个体的行为模式,而不是学到了通用的交通流规律。要验证真实泛化能力,必须在时间维度上做隔离。
做法是按天留出,比如把前 6 天做训练,第 7 天做验证。如果数据跨度更长,可以用滑动的时序交叉验证,每次换不同的天做验证集。
days = sorted(df["date"].unique()) train_days = days[:-1] # 前 N-1 天 test_day = days[-1] # 最后一天做验证 train_df = df[df["date"].isin(train_days)] test_df = df[df["date"] == test_day] # 训练集和验证集之间没有任何时间重叠 print(f"训练集天数: {len(train_days)}, 验证集: {test_day}")这个验证方式的附加好处是能观察模型在不同日期类型上的表现差距。比如验证集落在周一,早高峰的预测误差明显高于周二周三,说明模型还没有学好周周期特征。我会再用一个分组指标来定位问题是否出在特定时段:
test_df["hour"] = test_df["timestamp"].dt.hour hourly_error = test_df.groupby("hour").apply( lambda g: np.abs(g["pred"] - g["true"]).mean() ) print(hourly_error)看到误差高峰出现在清晨 6-9 点时,我通常选择在训练数据里加一周同小时的历史统计特征,而不是盲目把模型加宽加深。这种做法在 Whitemud Drive 这类真实交通数据集上非常管用——它逼你从“模型结构”的执念里走出来,正视数据分布本身的时间结构。
现在做数据项目,我再也不会用随机切分来评估模型了。时间感知的评估方式虽然让验证集上的数字没那么好看,但它骗不了人,模型上线后的表现心里有底得多。这个习惯是从一次次的翻车里换来的,希望帮到你。
本文还有配套的精品资源,点击获取