第三章 机器学习 -- 特征工程

第三章 特征工程

一、数据清洗

1.1 缺失值处理

缺失的来源与含义

数据缺失并非简单的"空"------它本身就是信息。常见原因:

  • 结构缺失:该字段对当前样本本就不适用(如儿童的"配偶收入"列)。
  • 随机缺失(MCAR):缺失与任何变量(观测或未观测)都无关。
  • 随机缺失(MAR):缺失与已观测变量有关,与未观测值无关。
  • 非随机缺失(MNAR):缺失与未观测值本身有关(如高收入者更可能拒绝填写收入)。

不同的缺失机制决定了处理策略:MNAR 直接用均值/中位数填补会引入严重偏差。

检测与可视化

python 复制代码
import pandas as pd
import numpy as np

df = pd.DataFrame({
    "a": [1, 2, np.nan, 4],
    "b": [np.nan, np.nan, np.nan, 1],
    "c": ["x", None, "z", "w"],
})

# 每列缺失率
df.isna().mean()

# 缺失模式热力图
import seaborn as sns
sns.heatmap(df.isna(), cbar=False)

常见处理策略

策略 描述 适用 注意点
直接删除 丢弃含缺失的样本或特征 缺失率 < 5% 且 MCAR 数据浪费,可能引入偏差
简单填充 均值 / 中位数 / 众数 MCAR(无偏但低估方差);MAR 需借助已观测变量做分层/回归插补 忽略特征间关系;降低方差
前向/后向填充 用时序相邻值填补 时序数据 强假设时序平稳
模型预测填充 用其他特征训练模型预测缺失值 特征间强相关 训练成本高;泄漏风险(见第九章 §3.1)
缺失指示特征 新增"是否缺失"二值列 MNAR 保留"缺失"信息;增加维度
多重插补(MICE) 多次插补建立分布 需不确定性量化 计算成本高

scikit-learn 实现

python 复制代码
from sklearn.impute import SimpleImputer, KNNImputer

# 中位数填充
imp_median = SimpleImputer(strategy="median")
X_imp = imp_median.fit_transform(X)

# KNN 填充(用 K 个最近邻的平均/中位数)
imp_knn = KNNImputer(n_neighbors=5)
X_imp = imp_knn.fit_transform(X)

# 缺失指示
from sklearn.impute import MissingIndicator
mask = MissingIndicator().fit_transform(X)  # 1 表示该位置原本缺失

与管道的衔接

预处理必须放进 Pipeline / ColumnTransformer,否则在交叉验证中会泄漏训练集的信息到验证集。详见第九章 §3.1。

易错点

  • 对测试集重新 fit :必须 fit 在训练集上,再 transform 验证/测试集。fit_transform 在训练集,transform 在测试集,顺序反了会引入数据泄漏。
  • 类别特征与数值特征混合时 :用 ColumnTransformer 分别填充(数值填中位数、类别填众数或专门类)。
  • 缺失值与 0、"" 的混淆 :pandas 中 0、""、pd.NA、np.nan 是不同的;先 df.replace(...) 统一再处理。

1.2 异常值处理

识别异常值

异常值(outlier)是显著偏离主体分布的样本。识别方法有两类:

  • 统计方法 :基于分布假设
    • Z-score:∣x−μ∣/σ>3|x - \mu| / \sigma > 3∣x−μ∣/σ>3(高斯假设)。
    • IQR:x<Q1−1.5IQRx < Q1 - 1.5 \text{IQR}x<Q1−1.5IQR 或 x>Q3+1.5IQRx > Q3 + 1.5 \text{IQR}x>Q3+1.5IQR(Tukey 围栏)。
    • 修正 Z-score(Modified Z-score):z~i=0.6745⋅(xi−x~)/MAD\tilde z_i = 0.6745 \cdot (x_i - \tilde x) / \text{MAD}z~i=0.6745⋅(xi−x~)/MAD,MAD=median(∣x−median(x)∣)\text{MAD} = \text{median}(|x - \text{median}(x)|)MAD=median(∣x−median(x)∣),0.67450.67450.6745 是高斯分布 MAD 与 σ\sigmaσ 的比值(使 MAD 在高斯下与 σ\sigmaσ 等价),常用阈值 ∣z~∣>3.5|\tilde z| > 3.5∣z~∣>3.5。对异常值更鲁棒。
  • 模型方法 :基于密度/距离
    • Isolation Forest:通过随机切分隔离点的难易度衡量异常。
    • Local Outlier Factor(LOF):样本的局部密度与邻居密度比值。
    • DBSCAN:将低密度区域点标为噪声(详见第七章 §3.2)。

处理策略

策略 描述 注意
删除 直接移除异常样本 仅当确定是数据错误时使用
截尾(Winsorize) 把超过某分位的值替换为分位值 保留样本数;常用 1% / 99% 截尾
变换 取对数、Box-Cox、Yeo-Johnson 减轻右偏 有时比"删除"更安全
单独建模 把异常作为另一类/单独回归分支 适用于异常本身有意义(如欺诈)
鲁棒模型 使用 Huber 损失、RANSAC 等 让模型自身对异常鲁棒

箱线图快速诊断

python 复制代码
import seaborn as sns
sns.boxplot(x=df["income"])  # 须先去除 NaN

Isolation Forest 示例

python 复制代码
from sklearn.ensemble import IsolationForest

# contamination: 期望异常比例上界 ∈ (0, 0.5];scikit-learn 0.22+ 默认 'auto'(按原始 Liu et al. 2008 取阈值 -0.5)
clf = IsolationForest(contamination=0.05, random_state=0)
mask = clf.fit_predict(X)        # -1 表示异常
X_clean = X[mask == 1]

易错点

  • 不可逆删除:删除后无法恢复,应先备份原始数据或在 Pipeline 中按标记 mask 处理。
  • 异常 ≠ 错误:金融欺诈、罕见病阳性样本都是"异常"但有标签意义,直接删除会丢失关键信息。
  • 与缺失值的耦合:异常处理后缺失率可能变化(删除样本导致缺失),先处理缺失再处理异常更稳妥。

二、特征变换

2.1 标准化与归一化

为何需要缩放

许多模型对特征尺度敏感:

  • 基于距离的模型:KNN、K-Means、SVM(带 RBF 核)、PCA------大尺度特征主导距离。
  • 基于梯度的模型:线性回归、逻辑回归、神经网络------大尺度特征导致梯度方向偏斜、收敛慢。
  • 基于正则化的模型:L1/L2 正则对各特征同等惩罚,尺度差异会被放大。

主要方法对比

方法 公式 输出范围 适用
StandardScaler(z-score) x′=(x−μ)/σx' = (x - \mu) / \sigmax′=(x−μ)/σ μ=0,σ=1\mu=0, \sigma=1μ=0,σ=1 多数线性/核模型
MinMaxScaler x′=(x−xmin⁡)/(xmax⁡−xmin⁡)x' = (x - x_{\min}) / (x_{\max} - x_{\min})x′=(x−xmin)/(xmax−xmin) 0,10, 10,1 神经网络激活前
RobustScaler x′=(x−median)/IQRx' = (x - \text{median}) / \text{IQR}x′=(x−median)/IQR 中位数 0,IQR 1 含异常值
MaxAbsScaler $x' = x / \max_{\text{train}}( x )$
Normalizer(L2) $x' = x / x _2$

scikit-learn 标准做法

python 复制代码
from sklearn.preprocessing import StandardScaler
from sklearn.pipeline import Pipeline
from sklearn.linear_model import LogisticRegression

pipe = Pipeline([
    ("scale", StandardScaler()),
    ("clf", LogisticRegression(max_iter=1000)),
])
pipe.fit(X_train, y_train)

对异常值的差异

  • StandardScaler 用均值/标准差,对异常敏感。
  • RobustScaler 用中位数/IQR,对异常鲁棒。
  • MinMaxScaler 受极值影响严重,单一异常值可将所有值挤到相近区间。

树模型例外

随机森林、XGBoost、LightGBM 等树模型对特征缩放不敏感------它们只关心特征的排序而非绝对值。对树模型做 StandardScaler 不会改变结果,但也不会有害;为管道的统一性与未来切换模型的可能性,建议保留。

易错点

  • fit / transform 顺序 :训练集 fit_transform、测试集只 transform。Pipeline 自动处理,但手工写代码时易出错。
  • 稀疏矩阵 :StandardScaler 会破坏稀疏结构(减均值 ≠ 0)。应使用 MaxAbsScaler 或不缩放。
  • 目标变量 y 也要缩放吗 :回归目标可缩放以提升数值稳定性,但最终预测必须逆变换回原尺度 才能与业务指标对齐。对数目标推荐 sklearn.compose.TransformedTargetRegressor(regressor=..., func=np.log, inverse_func=np.exp),训练与逆变换自动衔接。

2.2 类别特征编码

机器学习模型需要数值输入。类别特征必须转换为数值。编码方式的选择会显著影响模型表现与距离度量。

常用方法

方法 描述 优 劣
标签编码(Label Encoding) 每个类别映射为一个整数 简单,保留序关系(若类别有序) 对无序类别引入伪序
独热编码(One-Hot) 每个类别一个二值列 不引入伪序 高基数时维度爆炸
频次编码(Frequency) 用出现频次替代类别 单列、抗高基数 频次相同的不同类别被合并
目标编码(Target Encoding) 用目标变量在该类别的均值替代 信息丰富;抗高基数 严重的数据泄漏风险
嵌入编码(Embedding) 学习低维稠密向量 表达力强 需要训练;通常用于深度学习
二进制编码(Binary) 类别号转二进制,每位一列 维度 log⁡2K\log_2 Klog2K 信息密度低

独热编码的实现

python 复制代码
import pandas as pd
df = pd.DataFrame({"city": ["BJ", "SH", "GZ", "BJ"]})

# pandas
pd.get_dummies(df, columns=["city"])

# scikit-learn
from sklearn.preprocessing import OneHotEncoder
ohe = OneHotEncoder(sparse_output=False, handle_unknown="ignore")
ohe.fit_transform(df[["city"]])

handle_unknown="ignore" 让模型在推理时遇到训练集未见的类别返回全 0 向量,而非报错。

目标编码的正确姿势

直接用目标均值替换类别会导致严重的数据泄漏 (同一类别在训练时已"看"到自己的标签)。正确做法依赖双重防护:

  1. 外层 CV 隔离 :在交叉验证的每个 fold 内,仅用训练折样本计算 yˉc\bar{y}_cyˉc 与 ncn_cnc,再用此编码训练模型。
  2. 折内贝叶斯平滑 :即使单折内 ncn_cnc 极小也要平滑,避免小样本类别取极端均值:

enc(c)=nc⋅yˉc+α⋅yˉglobalnc+α\text{enc}(c) = \frac{n_c \cdot \bar{y}c + \alpha \cdot \bar{y}{\text{global}}}{n_c + \alpha}enc(c)=nc+αnc⋅yˉc+α⋅yˉglobal

其中 ncn_cnc 是类别 ccc 的样本数,α\alphaα 是平滑强度(先验样本数)。推理时用全量训练集重新计算 yˉc(all)\bar{y}_c^{(all)}yˉc(all) 与 nc(all)n_c^{(all)}nc(all) 后编码测试样本。

category_encoders.TargetEncoder 默认含 smoothing 与 CV 防护;手写实现务必保持"训练折计算、推理全量"的非对称。

高基数类别

类别数 KKK 很大时(城市 ID、商品 SKU、URL 域名):

  • 独热编码不可行(维度爆炸)。
  • 频次编码、目标编码、哈希编码(Hashing Trick)是常用替代。
  • 第三方库 category_encoders.HashingEncoder(非 scikit-learn 内置)将类别哈希到 nnn 个桶内,冲突概率可控、维度固定。

有序类别

教育程度(小学/中学/大学)、评分(差/中/好)天然有序。标签编码 0/1/2 即可,模型会学到序关系。不应对有序类别做独热------会破坏顺序信息。

与管道的衔接

类别编码必须用 ColumnTransformer 区分数值与类别列:

python 复制代码
from sklearn.compose import ColumnTransformer

pre = ColumnTransformer([
    ("num", StandardScaler(), num_cols),
    ("cat", OneHotEncoder(handle_unknown="ignore"), cat_cols),
])

易错点

  • 对训练集用全部样本计算编码(如目标编码用全量均值)→ 数据泄漏,模型评估虚高。
  • 测试集出现训练集未见的类别 → 独热编码会报"未注册";用 handle_unknown="ignore" 或频次编码兜底。
  • 类别字符串大小写/拼写不一致 ("Beijing" vs "beijing")→ 编码前先 str.lower().str.strip() 统一。

2.3 特征构造

特征构造是利用业务知识、领域直觉、数据本身的结构 ,从原始字段衍生出新特征。往往是表格数据建模中收益最大的环节。

常见构造思路

  1. 代数变换 :对偏态特征取对数、平方根、Box-Cox;对周期性特征用 sin⁡/cos⁡\sin/\cossin/cos 编码。
  2. 比例与差分:A/B、A-B、A/(A+B)。
  3. 聚合:按某分组键聚合,例如"用户近 7 天的下单金额均值"。
  4. 时间特征:从时间戳提取 hour、dayofweek、is_weekend、距离节假日的天数。
  5. 文本特征:TF-IDF、词袋、句向量(详见 NLP 资料)。
  6. 交叉特征 :离散特征的两两组合(如 城市 × 星期 独热交叉)。
  7. 离散化(分箱):连续特征分桶(年龄→年龄段),便于树模型与线性模型。
  8. 目标相关变换:在对数尺度上的目标(销售、点击)通常比原尺度更易建模。

示例:时间与聚合

python 复制代码
df["order_ts"] = pd.to_datetime(df["order_ts"])
df["hour"] = df["order_ts"].dt.hour
df["dayofweek"] = df["order_ts"].dt.dayofweek
df["is_weekend"] = df["dayofweek"].isin([5, 6]).astype(int)

# 用户近 7 天订单总额
df["user_7d_sum"] = (
    df.sort_values("order_ts")
      .groupby("user_id")["amount"]
      .rolling("7D", on="order_ts")
      .sum()
      .reset_index(level=0, drop=True)
)

周期性编码

对 hour、dayofweek 等环形特征,简单的 0--23 整数会让模型以为 23 与 0 相距很远。更合理的做法是用 sin⁡\sinsin、cos⁡\coscos 编码:

python 复制代码
df["hour_sin"] = np.sin(2 * np.pi * df["hour"] / 24)
df["hour_cos"] = np.cos(2 * np.pi * df["hour"] / 24)
# 通用公式:sin(2π·x/T)、cos(2π·x/T),T 为周期长度
# hour → T=24, dayofweek → T=7, minute → T=60, month → T=12(但月有年内强弱周期,需另行考虑)

离散化(分箱)

  • 等宽分箱 :pd.cut,按值域均匀分。
  • 等频分箱 :pd.qcut,按分位数分。
  • 基于决策树的分箱 :用决策树自动找最优切分点(sklearn.tree.DecisionTreeRegressor)。
  • WOE 编码 (Weight of Evidence):金融风控经典,将分箱映射为 log⁡(P(bad)/P(good))\log(P(\text{bad})/P(\text{good}))log(P(bad)/P(good)),直接对接逻辑回归。

特征构造的常见陷阱

  • 时间泄漏:聚合时如果包含未来数据(如用未来 7 天数据构造今天特征)→ 训练与测试分布不一致。
  • 过拟合风险:构造了过多与训练集噪声相关的特征,模型在训练集表现好但泛化差。
  • 可解释性 vs 表现:高度交叉的特征会显著提升模型表现,但同时让解释与调试变难。

易错点

  • 离散化后再做线性模型:分箱会把连续特征变成有序整数,线性模型可以学;如果直接独热每个分箱,会引入大量列。一般决策树无需分箱。
  • 构造的特征未在测试集做同处理 :训练/推理代码必须共享特征构造函数;用函数封装 + apply 是常见做法。
  • 与目标强相关但实际是泄漏:例如用"用户最近一次购买距今天数"预测"明天是否购买"------若该特征在数据生成时已用到了"明天"信息,则必须重做。

三、特征选择

3.1 过滤式与包裹式

特征选择在数据维度高、冗余多的场景下至关重要。它能:

  • 降低过拟合风险(减少模型可用的自由度)。
  • 提升训练与推理速度。
  • 改善可解释性。

三大类方法对比

类别 思想 与模型关系 优 劣
过滤式(Filter) 按统计量对特征打分 模型无关 快、可解释 不考虑特征组合
包裹式(Wrapper) 用模型性能挑选子集 模型相关 考虑特征交互 计算昂贵、过拟合风险
嵌入式(Embedded) 训练过程中自动选择 模型内嵌 计算与效果平衡 模型特定

过滤式:单变量统计

  • 方差阈值 :去掉方差为 0 的特征(所有样本同一值)。VarianceThreshold(threshold=0)。
  • Pearson 相关系数 :仅衡量线性关系,范围 −1,1-1, 1−1,1。
  • 互信息:可衡量任意依赖,详见第二章 §4.2。
  • 卡方检验:分类特征与分类标签的独立性。
  • ANOVA F 值:数值特征与分类标签的组间均值差异。
python 复制代码
from sklearn.feature_selection import (
    VarianceThreshold, SelectKBest, f_classif, mutual_info_classif
)

# 去掉方差 < 0.01 的特征
vt = VarianceThreshold(threshold=0.01)
X_v = vt.fit_transform(X)

# F 值选 top 20 特征
skb = SelectKBest(f_classif, k=20)
X_k = skb.fit_transform(X, y)

过滤式:多变量

  • 相关矩阵去冗余:若两特征相关系数 > 0.95,删其一。
  • VIF(方差膨胀因子):检测多重共线性。VIF > 5 或 10 视为强共线性。
  • mRMR(最小冗余最大相关):选择与目标高相关、彼此低冗余的特征。

包裹式:搜索策略

  • 前向选择(Forward Selection):从空集开始,每次加入使评估指标提升最大的特征。
  • 后向消除(Backward Elimination):从全集开始,每次移除影响最小的特征。
  • 穷举 / 双向:计算量爆炸,仅在特征数极少(<20)时可行。
  • 递归特征消除(RFE) :用模型系数或特征重要性递归剔除。sklearn.feature_selection.RFE。
  • RFECV:RFE + 交叉验证自动确定最佳特征数。
python 复制代码
from sklearn.feature_selection import RFE
from sklearn.linear_model import LogisticRegression

rfe = RFE(estimator=LogisticRegression(max_iter=1000),
          n_features_to_select=15,
          step=0.1)  # step 可为整数或 (0,1] 比例;step=0.1 表示每轮剔除 10% 特征
X_rfe = rfe.fit_transform(X, y)

过滤式 vs 包裹式的工程取舍

  • 过滤式快、稳,但忽略特征组合------两个弱特征组合后可能很强。
  • 包裹式考虑组合,但计算昂贵、易过拟合(选出的子集对训练数据敏感)。
  • 实操建议:先用过滤式砍掉 80% 冗余特征,再用包裹式精修。

易错点

  • 数据泄漏 :用全量数据计算方差/相关系数再划分训练/测试,泄漏了测试集信息。必须 fit 在训练集,transform 在测试集。
  • 包裹式对评估的依赖:包裹式直接优化某个评估指标,所以评估指标的选取决定了选出的特征集。指标与业务目标不一致时,会选出"指标好但业务差"的特征。

3.2 嵌入式方法

嵌入式方法在模型训练过程中自动完成特征选择,把选择与建模融为一体。

L1 正则化(稀疏解)

Lasso 回归在线性回归损失上加 L1 范数惩罚:

min⁡w∣Xw−y∣22+λ∣w∣1\min_\mathbf{w} | \mathbf{X}\mathbf{w} - \mathbf{y} |_2^2 + \lambda |\mathbf{w}|_1wmin∣Xw−y∣22+λ∣w∣1

L1 的几何特性使最优解常落在坐标轴上------某些系数严格为 0,对应特征被"自动剔除"。

  • sklearn.linear_model.Lasso 调大 alpha → 更多系数为 0。
  • LogisticRegression(penalty="l1", solver="liblinear") 同样有 L1 效应。
  • 适用:特征数远大于样本数、需要可解释的稀疏模型。

L2 正则化(收缩但不稀疏)

Ridge 回归加 L2 范数惩罚:

min⁡w∣Xw−y∣22+λ∣w∣22\min_\mathbf{w} | \mathbf{X}\mathbf{w} - \mathbf{y} |_2^2 + \lambda |\mathbf{w}|_2^2wmin∣Xw−y∣22+λ∣w∣22

L2 不会让系数严格为 0,但会缩小系数的绝对值,从而降低方差。L2 是特征"正则化"而非"选择"------所有特征都保留,只是权重被压小。

树模型特征重要性

  • 不纯度减少(Mean Decrease Impurity, MDI) :训练时该特征带来的不纯度(基尼/熵)下降总和。RandomForestClassifier().feature_importances_ 即此值。MDI 对高基数特征(独热编码后的多列类别)有偏,会过度重视。
  • 置换重要性(Permutation Importance,又称 Mean Decrease Accuracy, MDA) :把单列打乱后看模型性能下降多少。sklearn.inspection.permutation_importance。MDA 比 MDI 更稳健,且不依赖具体模型。
  • SHAP :基于 Shapley 值,可解释单个预测的每个特征贡献。shap 库。
python 复制代码
from sklearn.linear_model import Lasso
from sklearn.ensemble import RandomForestClassifier
import numpy as np

lasso = Lasso(alpha=0.1)
lasso.fit(X_train, y_train)
selected = np.where(lasso.coef_ != 0)[0]
print("Lasso 选中的特征数:", len(selected))

rf = RandomForestClassifier(n_estimators=200, random_state=0)
rf.fit(X_train, y_train)
importances = rf.feature_importances_
top10 = np.argsort(importances)[-10:]

基于梯度提升的特征选择

XGBoost / LightGBM 提供 plot_importance、get_score(importance_type="gain"),返回特征对总损失的贡献。与 SHAP 结合可解释每个特征在不同样本上的贡献分布。

递归特征消除 + 交叉验证(RFECV,嵌入式与包裹式的混合)

严格说,RFE/RFECV 属于 §3.1 介绍的包裹式 方法(它通过反复训练模型 + 评估来挑子集)。但因为它常与 L1 / 树模型重要性等嵌入式打分结合使用("嵌入式打分 + 包裹式消除"),工程上常归到嵌入式讨论。sklearn.feature_selection.RFECV 把两者合一:

python 复制代码
from sklearn.feature_selection import RFECV
from sklearn.linear_model import LogisticRegression

rfecv = RFECV(
    estimator=LogisticRegression(max_iter=1000),
    step=1,
    cv=5,
    scoring="roc_auc",
)
rfecv.fit(X_train, y_train)
print("最佳特征数:", rfecv.n_features_)

嵌入式 vs 过滤式/包裹式

  • 嵌入式与模型耦合,选出的特征只在当前模型下最优。换模型后可能需要重新选择。
  • 嵌入式在训练中完成选择,不增加额外计算(Lasso 训练成本约略高于线性回归)。
  • 包裹式可发现更优子集但昂贵;过滤式快但粗。实操上三者常组合使用:先用过滤式粗筛 → 再用嵌入式精筛 → 必要时用包裹式终审。

易错点

  • Lasso 在高度相关特征上不稳定:若两特征高度相关,Lasso 会随机选一个、另一个置 0;改用 ElasticNet(L1 + L2)更稳定。
  • 树模型重要性的高基数偏置:类别独热后的高基数特征(独热多列)会被过度重视;优先用目标编码 / 频次编码合并。
  • 嵌入式选择的特征不通用:用 Lasso 选出的特征在 XGBoost 上未必是最佳------两者优化的目标函数不同。
相关推荐
GIS数据转换器3 小时前
地下空间综合管廊运营管理平台
大数据·开发语言·人工智能·经验分享·perl
dozenyaoyida8 小时前
AI与大模型新闻日报 | 2026-10-17
人工智能·ai·chatgpt·新闻
遇码8 小时前
流式调用中途断连别直接报错:瞬态故障识别、退避重试与中文根因诊断的搭建方法
大数据·人工智能·ai·rust·数据分析·大模型·agent
救救孩子把9 小时前
AI面试八股文-21_21-多模态Agent面试题-Vision-Language-Agent
人工智能·面试·职场和发展·llava·多模态agent
今年下半年9 小时前
AI智能体项目实战记录(02):初始化 Python 项目
开发语言·人工智能·python·fastapi
Jlzn88889 小时前
华东电芯分选通道数怎么配才不堵线
java·人工智能·架构·自动化·制造
u1301309 小时前
GitHub 热榜项目:日榜(2026-10-17)
人工智能·github
流烟默9 小时前
MCP第 5 篇:用 Spring AI 写 MCP:注解、自动配置、ChatClient 集成
人工智能·spring·mcp封装