特征工程基础:特征选择、特征缩放、离散化与编码、特征交叉

做异常检测,很大程度上是在跟特征打交道。

很多人一上来就调模型参数,结果模型死活提不上来。为什么?

其实,特征没伺候好。模型再牛也白搭。

我个人习惯,拿到数据后先花一半时间在特征工程上。调参反而放到最后。你想想看,拿一堆噪声特征喂给模型,它能学到什么?

特征选择:做减法比做加法难

特征太多,模型容易过拟合,训练还慢。尤其异常检测里,正常样本和异常样本往往极度不平衡,冗余特征会拖后腿。

我记得有一次做信用卡欺诈检测,原始特征有200多个。我一股脑全扔进孤立森林,结果AUC只有0.65。后来砍到30个核心特征,AUC直接跳到0.82。怎么回事?

多出来的那些特征大多是噪声,模型被它们带偏了。

三种主流筛选方法

方法核心思路适用场景
过滤法用统计指标(方差、卡方、互信息)给特征打分,排名靠后的扔掉数据量大、特征多时快速筛选
包裹法用目标模型反复训练,每次增删特征,找到最优子集特征数少、计算资源充裕时精确搜索
嵌入法模型训练时自动选出重要特征(如L1正则、树模型特征重要性)兼顾效果和效率,我最常用

避坑指南:我刚开始做特征选择时,习惯只跑一遍过滤法就完事。后来发现,某些单变量不显著的特征,组合起来其实能揪出异常模式。所以我现在会先过滤法粗筛,再嵌入法精调。

# 过滤法示例:用方差选择移除低方差特征
from sklearn.feature_selection import VarianceThreshold
selector = VarianceThreshold(threshold=0.01)  # 移除方差小于0.01的特征
X_reduced = selector.fit_transform(X)
print(f"原特征数: {X.shape[1]}, 保留: {X_reduced.shape[1]}")

特征缩放:让量纲不再打架

异常检测里,特征尺度的差异经常把人坑惨。

比如交易金额(几万块)和交易次数(个位数)放在一起,欧氏距离直接被金额主导。KNN、PCA、聚类这些方法全废了。

我曾经有个项目,用One-Class SVM检测异常,一开始召回率不到10%。研究了半天,发现有个特征范围是0~1,另一个是0~100000,SVM的核函数完全被大尺度特征绑架了。标准化之后,召回率飙升到65%。

常用方法对比

方法公式特点
标准化 (Z-score)\(z = (x - \mu) / \sigma\)假设数据近似正态;对异常值敏感
归一化 (Min-Max)\(x' = (x - min) / (max - min)\)将数据映射到[0,1];受极端值影响大
鲁棒缩放 (RobustScaler)\(x' = (x - median) / IQR\)用中位数和四分位距,抗异常值干扰

我的建议:如果特征里明显有离群点(异常值本身可能就是你要找的目标),别用标准归一化。用RobustScaler更稳妥。我自己做异常检测时,默认先试RobustScaler。

from sklearn.preprocessing import StandardScaler, MinMaxScaler, RobustScaler

scaler = RobustScaler()
X_scaled = scaler.fit_transform(X)
print(f"缩放后特征均值: {X_scaled.mean(axis=0).round(2)}")

离散化与编码:让模型更懂类别特征

模型不认识字符串,只认数字。但直接把类别特征编码成整数(比如城市A=1, B=2, C=3)会引入虚假的序关系——模型会以为城市B比A大,这不对。

另一方面,连续特征强行分箱有时能帮模型找到更鲁棒的边界。比如把年龄分桶成[0-18, 19-35, 36-60, 60+],异常检测更容易发现某年龄段特有的异常行为。

编码方式速览

方法做法适合场景
独热编码每个类别创建一个0/1列类别少(<10种),树模型和线性模型都适用
标签编码类别映射为连续整数有序类别(如学历);树模型能用,线性模型慎用
目标编码用目标变量(如是否异常)的均值替换类别高基数类别(如邮政编码),但需防止过拟合

嗯,这里要注意:目标编码需要在训练集上做交叉验证统计,不能直接拿全量目标均值去编,否则会严重泄漏未来信息。

import pandas as pd
from sklearn.preprocessing import OneHotEncoder

# 假设 df 有一列 'category'
encoder = OneHotEncoder(sparse_output=False, handle_unknown='ignore')
encoded = encoder.fit_transform(df[['category']])

特征交叉:组合出隐藏模式

单个特征可能看不出什么,但两个特征一结合,异常就现形了。

举个例子:你单独看“登录IP”和“交易时间”没什么,但把IP+时间段交叉成一个新特征,就能发现某IP在凌晨3点的交易异常频繁。

我做过一个游戏反作弊项目,单看“击杀数”和“等级”都正常,但把“击杀数/等级”做成比值特征,外挂玩家这个比值异常高。这就是特征交叉的力量。

怎么做交叉

  • 人工组合:凭业务直觉,比如加减乘除、取平均、取差值
  • 多项式特征:用sklearn的PolynomialFeatures自动生成两两乘积
  • 树模型派生的交叉:比如用GBDT的叶子节点索引作为新特征,再喂给线性模型(Facebook的经典做法)

避坑指南:我曾经一次性生成上百个多项式交叉特征,结果维度过高,模型训不动还过拟合。后来我学乖了:先用业务规则挑出10个以内有意义的交叉,再决定是否加机器学习生成的交叉。

from sklearn.preprocessing import PolynomialFeatures
poly = PolynomialFeatures(degree=2, interaction_only=True, include_bias=False)
X_poly = poly.fit_transform(X[['age', 'income']])
print(f"交叉后特征数: {X_poly.shape[1]}")

本章知识体系一览

特征工程知识体系 特征工程核心流程 特征工程 特征选择 过滤法 包裹法 嵌入法 特征缩放 标准化 归一化 鲁棒缩放 离散化与编码 独热编码 标签编码 目标编码 特征交叉 人工组合 多项式特征 树模型派生 实际工作中四者常结合使用

我自己做异常检测项目时,特征工程从来不是一次性做完的。先快速尝试几种筛选和缩放,跑个基线;然后根据模型表现,回头再加交叉和编码。反复迭代三四轮,特征基本就定型了。