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)在二维空间中的欧氏距离很小,正确反映了时间的邻近性。模型可以同时学习两个分量,从而理解周期的连续性。
时间特征工程流程与周期性编码 原始时间戳 基础切片 周期编码 布尔标记 输出特征集 hour, day_of_week hour_sin, hour_cos dow_sin, dow_cos month_sin, month_cos is_weekend, is_peak_hour is_holiday 周期性编码原理: 原始值:hour=23 与 hour=0 在数值上差异巨大,但时间上仅相隔1小时。 编码后:hour_sin(23)≈-0.5, hour_cos(23)≈0.87 与 hour_sin(0)=0, hour_cos(0)=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
路况特征体系与数据流架构 数据源层 地图API 历史轨迹 特征提取层 静态特征 动态特征 特征存储层 在线缓存 离线表 最终路况特征向量: [road_level_ratio_highway, road_level_ratio_main, traffic_light_density, avg_speed_limit] [realtime_congestion_index, historical_avg_speed, speed_volatility, weather_factor] [speed_factor, distance_ratio, log_distance]

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的重要性会上升。务必基于本地数据进行验证。