4. 特征工程(上)
在ETA预测模型中,原始数据(订单时间、起终点坐标、路径长度)无法直接用于机器学习算法。特征工程是将原始数据转化为模型可理解、可学习的数值化特征的过程。本章聚焦于三大核心特征体系:时间特征、距离特征与路况特征,并提供可直接复用的Python代码实现。
工程提示:特征工程的质量直接决定模型上限。在ETA任务中,时间与距离的交互特征(如“高峰时段下的长距离订单”)往往比单一特征更具预测力。建议在完成基础特征提取后,立即进行特征交叉实验。
4.1 时间特征提取:从时间戳到周期性编码
原始时间戳(如 2025-03-15 08:30:00)对模型是离散字符串。我们需要将其分解为具有物理意义和周期性的数值特征。
4.1.1 基础时间切片
| 特征名称 | 提取逻辑 | 数据类型 | 示例值 |
|---|---|---|---|
hour |
从时间戳提取小时数 | int (0-23) | 8 |
day_of_week |
星期几(周一=0, 周日=6) | int (0-6) | 2(周三) |
is_weekend |
是否周末(周六/日) | bool (0/1) | 0 |
month |
月份 | int (1-12) | 3 |
is_peak_hour |
是否早晚高峰(7-9点或17-19点) | bool (0/1) | 1 |
4.1.2 周期性时间编码(Sin/Cos变换)
小时、星期、月份等特征具有周期性。例如,23点与0点虽然数值相差23,但在时间意义上仅相隔1小时。直接使用原始数值会误导模型认为它们差异巨大。解决方案是使用正弦/余弦变换将时间映射到单位圆上。
import pandas as pd
import numpy as np
def add_cyclical_time_features(df, time_col='pickup_time'):
"""
对时间列添加周期性编码特征。
参数:
df: 包含时间戳的DataFrame
time_col: 时间戳列名
返回:
添加了sin/cos特征的DataFrame
"""
df[time_col] = pd.to_datetime(df[time_col])
# 提取小时并编码
df['hour'] = df[time_col].dt.hour
df['hour_sin'] = np.sin(2 * np.pi * df['hour'] / 24)
df['hour_cos'] = np.cos(2 * np.pi * df['hour'] / 24)
# 提取星期并编码
df['day_of_week'] = df[time_col].dt.dayofweek
df['dow_sin'] = np.sin(2 * np.pi * df['day_of_week'] / 7)
df['dow_cos'] = np.cos(2 * np.pi * df['day_of_week'] / 7)
# 提取月份并编码
df['month'] = df[time_col].dt.month
df['month_sin'] = np.sin(2 * np.pi * df['month'] / 12)
df['month_cos'] = np.cos(2 * np.pi * df['month'] / 12)
return df
# 使用示例
df = pd.DataFrame({'pickup_time': ['2025-03-15 08:30:00', '2025-03-15 23:45:00']})
df = add_cyclical_time_features(df)
print(df[['hour', 'hour_sin', 'hour_cos']].head())
关键理解:Sin/Cos编码将一维时间值扩展为二维坐标。例如,23点(sin≈-0.5, cos≈0.87)与0点(sin=0, cos=1)在二维空间中的欧氏距离很小,正确反映了时间的邻近性。模型可以同时学习两个分量,从而理解周期的连续性。
4.2 距离特征提取:从经纬度到路网距离
ETA预测中最核心的距离特征包括:直线距离(Haversine)、路网距离(OSRM/Dijkstra)以及距离衍生特征。
4.2.1 Haversine直线距离
作为基准特征,Haversine公式计算地球表面两点间的最短弧线距离。虽然实际行驶路径更长,但它是路网距离的强相关代理,且计算成本极低。
import numpy as np
def haversine_distance(lat1, lon1, lat2, lon2):
"""
计算两点间的Haversine距离(单位:公里)。
"""
R = 6371 # 地球平均半径(公里)
dlat = np.radians(lat2 - lat1)
dlon = np.radians(lon2 - lon1)
a = np.sin(dlat/2)**2 + np.cos(np.radians(lat1)) * np.cos(np.radians(lat2)) * np.sin(dlon/2)**2
c = 2 * np.arctan2(np.sqrt(a), np.sqrt(1-a))
return R * c
# 批量计算示例
df['straight_distance_km'] = haversine_distance(
df['pickup_lat'], df['pickup_lng'],
df['dropoff_lat'], df['dropoff_lng']
)
4.2.2 路网距离与距离比
通过地图API(如OSRM、Google Maps)获取实际行驶距离。由于API调用有成本与延迟,通常在生产环境中使用预计算缓存或离线路网数据。
| 特征名称 | 定义 | ETA预测价值 |
|---|---|---|
straight_distance |
Haversine直线距离 | 基础距离基准,低计算成本 |
road_distance |
实际路网行驶距离 | 真实路径长度,高精度但高成本 |
distance_ratio |
road_distance / straight_distance | 反映道路曲折程度,值越大说明绕路越严重 |
log_distance |
log(road_distance + 1) | 压缩长尾分布,提升模型对短距离的敏感度 |
工程避坑:路网距离API调用可能失败(如网络超时、坐标无效)。务必实现降级策略:当API返回空值时,使用Haversine距离乘以区域平均绕路系数(如1.3~1.5)作为替代值,避免特征缺失导致模型预测崩溃。
4.3 路况特征提取:从静态到动态
路况是影响ETA精度的最大变量。路况特征可分为静态路况(道路等级、限速)与动态路况(实时拥堵指数、历史平均速度)。
4.3.1 静态路况特征
- 道路等级:将路径按高速公路、主干道、次干道、支路分类,统计各等级道路占比。
- 红绿灯密度:路径上每公里红绿灯数量,影响停车等待时间。
- 限速特征:路径平均限速、最低限速段长度。
4.3.2 动态路况特征
| 特征名称 | 数据来源 | 提取方法 |
|---|---|---|
实时拥堵指数 |
地图API(高德/百度) | 路径分段拥堵等级(畅通/缓行/拥堵)的加权平均 |
历史平均速度 |
历史轨迹数据 | 按路段+时段(如“周一早8点”)聚合历史平均速度 |
速度波动率 |
历史轨迹数据 | 同一路段同一时段速度的标准差/均值 |
天气影响因子 |
气象API | 根据雨雪强度对速度进行折扣(如大雨×0.8) |
def extract_traffic_features(row, historical_speed_lookup):
"""
提取路况特征。
参数:
row: 单行数据,包含路径分段信息
historical_speed_lookup: 历史速度查找表 {(road_id, hour, day_of_week): avg_speed}
返回:
路况特征字典
"""
features = {}
road_id = row['road_id']
hour = row['hour']
dow = row['day_of_week']
# 历史平均速度
key = (road_id, hour, dow)
if key in historical_speed_lookup:
features['historical_avg_speed'] = historical_speed_lookup[key]
else:
features['historical_avg_speed'] = 40.0 # 默认值(km/h)
# 实时拥堵指数(假设已从API获取)
features['congestion_index'] = row.get('realtime_congestion', 1.0)
# 速度折扣因子
features['speed_factor'] = features['historical_avg_speed'] / features['congestion_index']
return features
4.4 特征组合与工程落地要点
实战建议:单一特征往往存在信息盲区。例如,仅使用“距离”无法区分高速路段与城市拥堵路段。推荐构建以下组合特征:
- 高峰×距离:若is_peak_hour=1且distance>10km,则peak_long_distance=1。
- 路况×时间:congestion_index × hour_sin,捕捉不同时段拥堵差异。
- 距离比×道路等级:distance_ratio × highway_ratio,识别绕路是否由高速引起。
4.4.1 特征工程流水线代码框架
def build_eta_feature_pipeline(df):
"""
完整的ETA特征工程流水线。
输入: 原始DataFrame(包含时间戳、经纬度、路径信息)
输出: 特征工程后的DataFrame
"""
# 1. 时间特征
df = add_cyclical_time_features(df)
df['is_weekend'] = df['day_of_week'].isin([5, 6]).astype(int)
df['is_peak_hour'] = ((df['hour'] >= 7) & (df['hour'] <= 9) |
(df['hour'] >= 17) & (df['hour'] <= 19)).astype(int)
# 2. 距离特征
df['straight_distance_km'] = haversine_distance(
df['pickup_lat'], df['pickup_lng'],
df['dropoff_lat'], df['dropoff_lng']
)
# 假设road_distance已通过API获取
df['distance_ratio'] = df['road_distance'] / (df['straight_distance_km'] + 0.1)
df['log_distance'] = np.log1p(df['road_distance'])
# 3. 路况特征(简化示例)
df['congestion_level'] = pd.cut(df['realtime_congestion'],
bins=[0, 1.5, 2.5, 10],
labels=[0, 1, 2]) # 0:畅通, 1:缓行, 2:拥堵
# 4. 组合特征
df['peak_long_distance'] = ((df['is_peak_hour'] == 1) &
(df['straight_distance_km'] > 10)).astype(int)
df['congestion_time_interaction'] = df['congestion_level'] * df['hour_sin']
return df
4.4.2 特征重要性快速验证
在进入模型训练前,建议使用LightGBM或随机森林进行快速特征重要性排序,剔除无效特征。以下为典型特征重要性分布(基于历史项目经验):
| 特征 | 重要性权重 | 说明 |
|---|---|---|
road_distance |
0.35 | 最核心特征,直接决定行驶时间基数 |
congestion_index |
0.25 | 动态路况的实时影响 |
hour_sin + hour_cos |
0.15 | 时间周期性的综合贡献 |
distance_ratio |
0.10 | 反映道路曲折与绕路程度 |
is_peak_hour |
0.08 | 高峰时段标记 |
其他特征 |
0.07 | 天气、道路等级等 |
注意:特征重要性会因城市、时间段、数据质量而显著变化。例如,在路网稳定的郊区,
distance_ratio的重要性可能下降;而在交通灯密集的老城区,traffic_light_density的重要性会上升。务必基于本地数据进行验证。