特征工程:特征提取、特征选择、时间序列特征构造

特征工程这步,说白了就是给原始数据“化妆”——让模型能看清楚规律。我在早期做异常检测时,总以为把原始指标直接扔进模型就完事了,结果预测结果一塌糊涂。后来才明白,特征构造省掉的功夫,都会在后续调试中加倍还回来。下面我按三个核心环节来讲:提取、选择、时序构造。

特征工程核心流程 原始数据 特征提取 统计特征 · 频域特征 滑动窗口 · 平滑 特征选择 过滤 · 包裹 · 嵌入 重要性排序 时序特征构造 滞后 · 滚动统计 · 差分 季节分解 最终特征集

特征提取:从原始数据里“挖”出信号

特征提取的目的,是把原始时间点上的分散值,转化成能反映系统状态的高层特征。我在监控服务器 CPU 时,如果直接拿 1 秒粒度的 CPU 利用率做特征,模型根本抓不住慢速漂移。通常我会用以下几种方法。

方法说明典型场景
滑动窗口统计计算窗口内均值、方差、最大值、最小值、中位数检测均值漂移、方差突变
频域特征FFT 后提取主要频率、能量、功率谱密度周期性异常(如网络流量抖动)
差分与平滑一阶差分消除趋势,移动平均去噪非平稳时序预处理
自回归系数用 AR 模型系数代表过去对当前的影响有记忆性的传感器数据
我的一点经验:窗口大小别拍脑袋。之前做过一个磁盘 I/O 异常检测,窗口设 5 分钟漏报了一堆突发毛刺,后来用经验半周期法,设窗口为正常周期的 1.5 倍才稳定。建议先用领域知识给个初值,再通过网格搜索调整。
import pandas as pd
import numpy as np

def extract_features(series, window=50):
    df = pd.DataFrame({'raw': series})
    df['mean'] = df['raw'].rolling(window).mean()
    df['std'] = df['raw'].rolling(window).std()
    df['max'] = df['raw'].rolling(window).max()
    df['min'] = df['raw'].rolling(window).min()
    df['slope'] = df['raw'].diff()
    return df.dropna()

特征选择:别让垃圾特征污染模型

特征不是越多越好。我记得有一次给一个风电齿轮箱做异常检测,手工造了 200 多个特征,模型训练时间爆炸,过拟合还严重。特征选择就是做减法——留下真正有用的。常用的做法分三大类。

  • 过滤法(Filter):统计独立性或相关性筛选,例如 Pearson 相关系数、卡方检验、互信息。速度快,但忽略特征间组合效应。
  • 包裹法(Wrapper):用目标模型做搜索,例如递归特征消除(RFE)、前向/后向选择。效果准,但计算开销大。
  • 嵌入法(Embedded):在模型训练时内置选择,如 L1 正则化(Lasso)、树模型特征重要性。平衡效率与精度。
我曾踩过的坑:在过滤法里直接删除了与标签低相关的特征,结果把季节性弱信号也去掉了。后来改用互信息 + 包裹法混合,先用互信息砍掉一半冗余,再用 RFE 精调,效果好很多。别迷信单一种方法。
方法优点缺点
过滤法计算快,适合高维忽略特征交互,可能误删有用组合
包裹法考虑模型偏好,精度高计算量大,容易过拟合
嵌入法自动选择,兼顾效率依赖模型假设,解释性不如过滤

时间序列特征构造:让模型“看懂”时间依赖

普通特征构造只管数值,时间序列特征还要管顺序和周期。我在做服务器内存泄漏检测时,只用了原始内存使用量,模型死活分不清正常回收和泄漏趋势。后来加了差分和滚动分位数,效果才出来。

滞后特征(Lag Features)

就是拿过去时刻的值当当前特征。比如昨天下午 3 点的 CPU 利用率,可能对今天下午 3 点有参考意义。我用 shift() 生成滞后 1、2、3 步的特征。注意别把未来信息带进去,训练测试要严格对齐。

def add_lag_features(df, col, lags=[1,2,3,6]):
    for lag in lags:
        df[f'{col}_lag_{lag}'] = df[col].shift(lag)
    return df

滚动统计特征

滚动均值、滚动标准差、滚动偏度、滚动极差等。这些特征能捕捉局部趋势和波动性。举个例子:滚动方差突然增大,往往意味着异常开始。

差分与季节分解

一阶差分能消除线性趋势,使序列平稳。对于有强周期的数据(如每日 CPU 负载),我常用 seasonal_decompose 拆出趋势、季节、残差,然后把残差单独做特征。

重点:时间序列特征构造最容易犯的错——窗口数据泄漏。我有一个惨痛教训:用整个时间序列的滚动均值做训练,导致模型学到了未来信息,线上 AUC 暴跌 20%。正确做法:每个时间点的特征只能用该点之前的数据计算,不支持“双向滚动”。

周期性编码

对于小时、星期、月份等周期性变量,如果直接用 0-23 的数字编码,模型会误以为 23 和 0 差很远。我会同时生成 sin 和 cos 两个分量来平滑周期。

def cyclical_encode(hour, period=24):
    sin_feat = np.sin(2 * np.pi * hour / period)
    cos_feat = np.cos(2 * np.pi * hour / period)
    return sin_feat, cos_feat

实践要点:从项目里总结的几条建议

  • 先画时间线,再动代码:在异常检测中,特征必须保持时间因果关系。我每次都会先画出特征相对于标签的时间对齐图,确认没有超前数据。
  • 特征监控与漂移:部署后特征的分布会变(如均值偏移)。我习惯在特征流水线里加统计校验,一旦 KL 散度超过阈值就告警,提醒重新构造或选择特征。
  • 尽量保留可解释性:深层的自动特征提取(如自编码器)效果好但难 debug。初期先用统计特征打底,等到系统稳定了再考虑复杂特征。
一个小习惯:每次添加新特征,我会同步记下它的业务含义和期望效果。半年后再看代码,会发现这些笔记比任何注释都管用。毕竟你我不可能记住每个窗口大小的计算公式,对吧?