4、用户行为特征工程:驾驶行为特征、交互操作特征、场景上下文特征、特征选择方法

好,咱们进入第四讲。说实话,特征工程这活儿,在车载HMI里比在互联网APP里要难搞得多。为什么?因为数据来源太杂了——CAN总线、车机日志、传感器、甚至还有语音助手那边的数据。我刚开始做这个方向时,光是把这些数据对齐到同一个时间轴上,就折腾了两周。

但话说回来,特征工程做得好,模型就成功了一半。今天咱们就聊聊,怎么从车载数据里挖出那些真正有用的特征。

4.1 驾驶行为特征:车怎么开,人怎么想

驾驶行为特征,说白了就是通过车辆的运动状态,反推驾驶员的意图和状态。我个人习惯把这部分特征分成三类:

  • 宏观驾驶风格:急加速频率、急刹车频率、平均车速、变道频率
  • 微观操作特征:油门踏板深度变化率、刹车踏板行程、方向盘转角速度
  • 疲劳/分心特征:车道偏离次数、跟车距离波动、反应延迟时间

举个例子,我在项目中遇到过这样一个场景:用户A在高速上频繁变道,但变道时方向盘转角很平滑;用户B也在变道,但每次都是猛打方向。这两个人虽然都在变道,但驾驶风格完全不同。前者可能是老司机,后者可能是新手或者情绪不稳定。嗯,这里要注意,同样的行为,背后的意图可能天差地别。

核心思路:驾驶行为特征不仅要看「做了什么」,还要看「怎么做」。

代码实现上,我一般会先做滑动窗口统计。比如计算过去30秒内的急加速次数:

def extract_aggressive_accel(df, window=30, threshold=2.5):
    """
    df: 包含加速度数据的DataFrame
    window: 滑动窗口大小(秒)
    threshold: 急加速阈值(m/s²)
    """
    df['accel_diff'] = df['speed'].diff() / 0.1  # 假设采样率10Hz
    df['is_aggressive'] = df['accel_diff'] > threshold
    
    # 滑动窗口统计
    df['aggressive_count'] = (
        df['is_aggressive']
        .rolling(window=window*10, min_periods=1)
        .sum()
    )
    return df

避坑指南:我曾经在提取急加速特征时,直接用原始加速度数据,结果发现很多误报。后来才意识到,车辆在上下坡时也会有较大的加速度变化。所以一定要结合坡度信息做修正,或者用纵向加速度减去重力分量。

4.2 交互操作特征:手指的每一次触碰都有意义

交互操作特征,就是用户在车机上的操作行为。这部分数据通常来自车机日志,包括点击、滑动、语音指令等。你想想看,用户点一个按钮,背后能挖出多少信息?

  • 操作频率:单位时间内点击次数、滑动次数
  • 操作序列:点击顺序、操作路径、回退次数
  • 操作耗时:页面停留时间、任务完成时间、响应等待时间
  • 错误操作:误触次数、无效点击、取消操作

我个人觉得,操作序列特征是最有价值的。比如用户想导航去一个地方,正常的操作路径是:打开地图 → 输入目的地 → 选择路线 → 开始导航。但如果用户反复在「输入目的地」和「选择路线」之间来回切换,说明他可能在犹豫,或者系统推荐的结果不理想。

这里分享一个我常用的特征提取方法——操作序列的N-gram编码:

def extract_operation_ngrams(seq, n=3):
    """
    seq: 操作ID序列,如 ['open_map', 'input_dest', 'select_route']
    n: N-gram长度
    """
    from collections import Counter
    ngrams = []
    for i in range(len(seq) - n + 1):
        ngram = tuple(seq[i:i+n])
        ngrams.append(ngram)
    return Counter(ngrams)

为什么要用N-gram?因为单个操作看不出问题,但连续几个操作组合在一起,就能反映出用户的真实意图。比如「打开音乐 → 搜索 → 播放」和「打开音乐 → 切换下一首 → 切换下一首」,前者是主动搜索,后者是随机播放,完全不同的行为模式。

注意:操作序列特征容易受到UI布局变化的影响。如果车机系统升级后按钮位置变了,用户的操作路径可能会发生很大变化。这时候需要做归一化处理,或者重新训练模型。

4.3 场景上下文特征:时间、地点、天气,一个都不能少

场景上下文特征,说白了就是「用户在什么环境下开车」。这部分特征往往被忽视,但实际效果非常好。我做过一个实验,加入场景特征后,推荐准确率提升了12%。

常见的场景上下文特征包括:

特征类别 具体特征 数据来源
时间特征 时段(早高峰/晚高峰/深夜)、星期几、是否节假日 系统时间
位置特征 GPS坐标、POI类型(家/公司/商场)、道路类型(高速/市区/乡村) GPS模块、地图数据
天气特征 温度、降雨量、能见度、是否雾天 车载传感器、网络API
车辆状态 电量/油量、剩余续航、胎压、空调状态 CAN总线

举个例子,用户在下雨天开车时,更倾向于听舒缓的音乐,导航偏好也会选择更安全的路线。如果系统能感知到这些上下文信息,推荐的内容就会更贴合用户需求。

特征工程中,我建议把场景上下文特征做成交叉特征。比如「时段 × 道路类型」可以区分出「晚高峰在市区堵车」和「深夜在高速巡航」两种完全不同的场景。代码实现很简单:

def create_context_cross_features(df):
    # 时段编码:0-凌晨,1-早高峰,2-白天,3-晚高峰,4-深夜
    df['time_period'] = pd.cut(df['hour'], 
                               bins=[0, 5, 9, 17, 20, 24],
                               labels=[0, 1, 2, 3, 4])
    
    # 道路类型编码:0-市区,1-高速,2-乡村
    df['road_type'] = df['road_type'].map({'urban':0, 'highway':1, 'rural':2})
    
    # 交叉特征
    df['time_road_cross'] = df['time_period'].astype(str) + '_' + df['road_type'].astype(str)
    return df

避坑指南:我曾经在提取天气特征时,直接用了网络API返回的「晴/雨/雪」文本标签。后来发现,同样的「雨」,小雨和大雨对驾驶行为的影响完全不同。所以建议用降雨量(mm/h)这样的连续值,而不是离散标签。

4.4 特征选择方法:别让垃圾特征拖后腿

特征造了一大堆,但并不是所有特征都有用。我见过有人一口气造了200多个特征,结果模型训练时间翻了三倍,效果反而下降了。特征选择,说白了就是「去粗取精」。

常用的特征选择方法有三种:

  • 过滤法:先算特征和目标变量的相关性,把相关性低的去掉。比如用皮尔逊相关系数、卡方检验。
  • 包裹法:把特征子集扔进模型里跑,看效果。比如递归特征消除(RFE)。
  • 嵌入法:在模型训练过程中自动选择特征。比如L1正则化、树模型的特征重要性。

我个人习惯先用过滤法快速筛一遍,再用嵌入法精挑细选。举个例子,用随机森林的特征重要性来筛选:

from sklearn.ensemble import RandomForestClassifier

def select_features_by_importance(X, y, threshold=0.01):
    rf = RandomForestClassifier(n_estimators=100, random_state=42)
    rf.fit(X, y)
    
    # 获取特征重要性
    importance = pd.DataFrame({
        'feature': X.columns,
        'importance': rf.feature_importances_
    }).sort_values('importance', ascending=False)
    
    # 保留重要性大于阈值的特征
    selected = importance[importance['importance'] > threshold]['feature'].tolist()
    return selected

你想想看,如果某个特征的重要性只有0.001,那它对模型的贡献几乎可以忽略不计。留着它,只会增加过拟合的风险。

注意:特征选择时,一定要考虑特征之间的多重共线性。比如「急加速次数」和「平均加速度」这两个特征高度相关,同时保留它们不仅没有额外信息,还会干扰模型。我建议用VIF(方差膨胀因子)来检测,VIF大于10的特征可以考虑剔除。

嗯,最后总结一下。特征工程没有银弹,每个项目都需要根据实际情况来调整。但有一点是确定的:好的特征工程,能让模型事半功倍。下一讲咱们聊聊推荐系统的核心算法,到时候见。