别再为METR-LA数据预处理头疼了!手把手教你用NumPy和Pandas搞定交通预测的输入输出格式
METR-LA数据预处理实战:从原始H5到时空预测模型的完美输入
第一次打开METR-LA的.h5文件时,那种面对34272×207的二维数据矩阵的茫然感至今记忆犹新。交通预测领域的论文总是优雅地展示着模型架构图,却很少提及数据预处理这个"脏活累活"。本文将带你一步步拆解这个看似复杂的数据转换过程,用NumPy和Pandas将原始数据转化为模型真正需要的N×T×12格式。
1. 理解METR-LA数据的基本结构
METR-LA数据集记录了洛杉矶207个传感器在4个月(2012年3月-6月)内每5分钟一次的交通速度数据。原始数据存储为HDF5格式,用Pandas读取后得到的是一个34272行×207列的DataFrame:
import pandas as pd df = pd.read_hdf('metr-la.h5') print(df.shape) # (34272, 207)关键特性解析:
- 行索引:时间戳(从2012-03-01 00:00:00开始,每5分钟一个间隔)
- 列索引:传感器ID(从0到206)
- 数值:交通速度(mph)
常见新手错误包括:
- 直接尝试reshape数据导致维度错乱
- 忽略时间戳的连续性检查
- 错误理解传感器ID的物理意义
提示:使用
df.head()和df.describe()快速检查数据分布,确保没有异常值或缺失值
2. 构建时间滑动窗口的核心逻辑
时空预测模型(如DCRNN、STGCN)通常采用滑动窗口机制,用过去12个时间步预测未来12个时间步。这需要精心设计offset策略:
import numpy as np # 输入窗口:过去12个时间点(包括当前时刻) x_offsets = np.arange(-11, 1, 1) # [-11, -10,..., 0] # 输出窗口:未来12个时间点(不包括当前时刻) y_offsets = np.arange(1, 13, 1) # [1, 2,..., 12]为什么这样设计offset?
- 输入窗口包含当前时刻(t=0)及其前11个时间点
- 输出窗口从t+1开始,避免信息泄露
- 这种设计确保模型只能基于历史数据预测未来
3. 时间特征工程实战技巧
许多模型会利用时间周期性特征(如一天中的相对时间)。以下是提取时间特征的实用方法:
# 计算当前时刻在一天中的相对位置(0-1之间) time_ind = (df.index.values - df.index.values.astype("datetime64[D]")) / np.timedelta64(1, "D") # 扩展为[34272, 207, 1]的张量 time_in_day = np.tile(time_ind, [1, num_nodes, 1]).transpose((2, 1, 0))维度变换详解:
time_ind初始形状:(34272,)- 经过
np.tile扩展:(1, 207, 34272) - 转置后得到:(34272, 207, 1)
注意:这一步不是所有模型都必需,但保持与主流实现一致有助于结果对比
4. 数据整合与滑动窗口生成
将交通数据和时间特征合并后,就可以生成最终的输入输出对:
# 原始交通数据增加维度 [34272, 207, 1] data = np.expand_dims(df.values, axis=-1) # 合并交通数据和时间特征 [34272, 207, 2] data = np.concatenate([data, time_in_day], axis=-1) # 滑动窗口生成 x, y = [], [] min_t = abs(min(x_offsets)) # 11 max_t = abs(data.shape[0] - abs(max(y_offsets))) # 34260 for t in range(min_t, max_t): x.append(data[t + x_offsets]) y.append(data[t + y_offsets]) x = np.stack(x) # [34249, 12, 207, 2] y = np.stack(y) # [34249, 12, 207, 2]窗口生成过程可视化:
| 窗口类型 | 时间点序列示例 | 实际索引范围 |
|---|---|---|
| 输入窗口 | t-11到t | [0:12] → [11:23] |
| 输出窗口 | t+1到t+12 | [12:24] → [23:35] |
5. 数据集划分与保存最佳实践
按照7:1:2的比例划分训练集、验证集和测试集:
num_samples = x.shape[0] num_test = round(num_samples * 0.2) num_train = round(num_samples * 0.7) num_val = num_samples - num_test - num_train # 切片获取各数据集 x_train, y_train = x[:num_train], y[:num_train] x_val, y_val = x[num_train:num_train+num_val], y[num_train:num_train+num_val] x_test, y_test = x[-num_test:], y[-num_test:] # 保存为压缩格式 np.savez_compressed( 'train.npz', x=x_train, y=y_train, x_offsets=x_offsets, y_offsets=y_offsets )数据集划分要点:
- 保持时间连续性,不要随机打乱
- 验证集用于早停(early stopping)
- 测试集只在最终评估时使用
6. 常见问题排查指南
问题1:维度不匹配错误
- 检查
data张量的形状是否为[时间步, 节点数, 特征数] - 确保
x_offsets和y_offsets长度相同
问题2:索引越界错误
- 确认
min_t和max_t计算正确 - 检查原始数据是否有缺失时间点
问题3:模型训练时loss不下降
- 验证输入输出对的对应关系是否正确
- 检查数据标准化是否合理
7. 高级技巧:内存优化策略
处理大规模时空数据时,内存管理至关重要:
# 使用生成器替代列表存储 def data_generator(data, x_offsets, y_offsets, batch_size=32): min_t = abs(min(x_offsets)) max_t = abs(data.shape[0] - abs(max(y_offsets))) while True: batch_x, batch_y = [], [] for t in np.random.choice(range(min_t, max_t), batch_size): batch_x.append(data[t + x_offsets]) batch_y.append(data[t + y_offsets]) yield np.stack(batch_x), np.stack(batch_y)优化方案对比:
| 方法 | 内存占用 | 适用场景 |
|---|---|---|
| 全量加载 | 高 | 小数据集快速实验 |
| 生成器 | 低 | 大数据集或有限内存 |
| 内存映射 | 中 | 频繁访问的中间数据 |
在实际项目中,我通常会先在小样本上验证流程正确性,再应用这些优化策略处理完整数据集。记得在处理完成后及时使用del释放大对象,特别是交互式开发时。
