特征工程基础:特征选择、特征缩放、离散化与编码、特征交叉
做异常检测,很大程度上是在跟特征打交道。
很多人一上来就调模型参数,结果模型死活提不上来。为什么?
其实,特征没伺候好。模型再牛也白搭。
我个人习惯,拿到数据后先花一半时间在特征工程上。调参反而放到最后。你想想看,拿一堆噪声特征喂给模型,它能学到什么?
特征选择:做减法比做加法难
特征太多,模型容易过拟合,训练还慢。尤其异常检测里,正常样本和异常样本往往极度不平衡,冗余特征会拖后腿。
我记得有一次做信用卡欺诈检测,原始特征有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]}")
本章知识体系一览
我自己做异常检测项目时,特征工程从来不是一次性做完的。先快速尝试几种筛选和缩放,跑个基线;然后根据模型表现,回头再加交叉和编码。反复迭代三四轮,特征基本就定型了。