目录
  1. 1. 一、概述
    1. 1.1. 1.1 特征工程的层次
    2. 1.2. 1.2 数据、模型与特征的关系
  2. 2. 二、基本概念
    1. 2.1. 2.1 数据
    2. 2.2. 2.2 模型
    3. 2.3. 2.3 特征
  3. 3. 三、数值型特征工程
    1. 3.1. 3.1 缺失值处理
    2. 3.2. 3.2 异常值处理
    3. 3.3. 3.3 缩放(Scaling)
    4. 3.4. 3.4 对数变换
    5. 3.5. 3.5 Box-Cox 变换
    6. 3.6. 3.6 分箱(Binning/Discretization)
    7. 3.7. 3.7 交互特征
  4. 4. 四、类别型特征工程
    1. 4.1. 4.1 编码方式对比
    2. 4.2. 4.2 One-Hot Encoding
    3. 4.3. 4.3 特征散列化(Feature Hashing)
    4. 4.4. 4.4 Target Encoding
  5. 5. 五、时间特征工程
    1. 5.1. 5.1 从时间戳提取特征
    2. 5.2. 5.2 时间窗口聚合
  6. 6. 六、文本特征工程
    1. 6.1. 6.1 词袋模型(Bag of Words)
    2. 6.2. 6.2 TF-IDF
    3. 6.3. 6.3 文本统计特征
    4. 6.4. 6.4 词嵌入(Word Embeddings)
  7. 7. 七、特征选择
    1. 7.1. 7.1 三大类方法
    2. 7.2. 7.2 Filter 方法
    3. 7.3. 7.3 Wrapper 方法
    4. 7.4. 7.4 Embedded 方法
  8. 8. 八、自动化特征工程
    1. 8.1. 8.1 Featuretools
    2. 8.2. 8.2 常见自动化工具
  9. 9. 九、竞赛中的特征工程经验
    1. 9.1. 9.1 Kaggle 大神的特征工程套路
    2. 9.2. 9.2 特征工程的投入配比
  10. 10. 十、面试高频问题
  11. 11. 十一、总结
【竞赛专题】特征工程

之所以把特征工程这一章放在Kaggle入门篇之后讲,想的是基于『先体验后付款』理念,因为只有在实战中才会知道

『More data beats clever algorithms, but better data beats more data』

—— Peter Norvig

本篇文章是一篇笔记,用于记录阅读《FEATURE ENGINEERING》一书,有关特征工程相关的重难点和自己的一些想法,如果有错误之处,还请不吝赐教。

一、概述

为了提取知识和做出预测,机器学习使用数学模型来拟合数据。这些模型将特征作为作为输入。特征就是一个客体或一组客体特性的抽象结果,它是原始数据在某个方面的数值表示。在机器学习流程中,特征是数据和模型之间的枢纽。特征工程是指从原始数据中提取特征并将其转换为适合机器学习模型的格式。它是机器学习流程中一个极其重要的环节,因为正确的特征可以减轻构建模型的难度,从而使机器学习流程输出更高质量的结果。正所谓”更多的数据可以击败聪明的算法,但更好的数据则会击败更多的数据”,也因此机器学习从业者都有一个共识,那就是建立机器学习流程的绝大部分时间都是耗在特征工程和数据清洗上的。下面正式开始学习。

1.1 特征工程的层次

特征工程可以分为四个层次:

层次 内容 示例 投入产出比
L1:数据清洗 处理缺失值、异常值、重复数据 填缺失、去重、纠错 极高
L2:特征变换 对原始特征做数学变换 归一化、对数变换、分箱
L3:特征构造 从原始特征中创建新特征 交互特征、聚合特征、时间特征
L4:特征选择 从大量特征中筛选出最重要的 Filter/Wrapper/Embedded

1.2 数据、模型与特征的关系

数据是原材料,特征是加工后的半成品,模型是最终产品。三者的关系可以类比为:

  • 数据 = 田里的麦子
  • 特征工程 = 磨面粉(清洗、研磨、筛选)
  • 模型训练 = 烤面包

“有了好面粉,即使面包师傅不那么高超,也能烤出不错的面包。”同理,好的特征能让简单的模型(如逻辑回归)也表现优异。

二、基本概念

2.1 数据

数据是特征工程的原材料。常见的数据类型包括:

数据类型 描述 示例
数值型 连续或离散的数值 年龄、价格、温度
类别型 有限个离散取值 性别、城市、品牌
序数型 有顺序的类别 学历(高中<本科<硕士)
文本型 自然语言文字 评论、标题、邮件正文
时间型 时间戳或日期 2023-01-15 14:30:00
图像型 像素矩阵 照片、卫星图
空间型 地理坐标 经纬度、地址

2.2 模型

不同模型对特征有不同的”喜好”:

模型 对特征的要求 偏好
线性模型(LR, SVM) 需要特征缩放到相似范围 非线性变换(分箱、交互)有帮助
树模型(RF, GBDT, XGBoost) 对尺度不敏感 不需要 one-hot(可处理原始类别)
k-NN 强依赖距离度量,必须归一化 低维、连续特征效果好
神经网络 最好归一化到 [0,1] 或标准化 Embedding 处理类别
NB 离散/二值特征 需要分箱

2.3 特征

一个好的特征应该具备以下属性:

  • 有区分度:不同类别的样本在该特征上有显著差异
  • 与目标相关:特征与标签之间存在有意义的关联
  • 稳定:训练集和测试集上分布一致
  • 可解释:特征的物理/业务含义清晰

三、数值型特征工程

3.1 缺失值处理

缺失值处理是特征工程的第一个环节。需要先了解缺失机制

  • MCAR(完全随机缺失):缺失与任何变量无关
  • MAR(随机缺失):缺失与其他观测变量有关
  • MNAR(非随机缺失):缺失与缺失值本身有关

处理方法

import pandas as pd
import numpy as np

# 1. 简单填补
df['age'].fillna(df['age'].median(), inplace=True) # 中位数
df['category'].fillna('Unknown', inplace=True) # 新类别

# 2. 模型填补
from sklearn.impute import KNNImputer
imputer = KNNImputer(n_neighbors=5)
df_imputed = imputer.fit_transform(df)

# 3. 缺失指示器(有时缺失本身就是信息)
df['age_is_missing'] = df['age'].isna().astype(int)

# 4. Iterative Imputer (MICE)
from sklearn.experimental import enable_iterative_imputer
from sklearn.impute import IterativeImputer
imputer = IterativeImputer(max_iter=10, random_state=0)
df_imputed = imputer.fit_transform(df)

3.2 异常值处理

检测方法

# IQR 方法
Q1 = df['value'].quantile(0.25)
Q3 = df['value'].quantile(0.75)
IQR = Q3 - Q1
outliers = (df['value'] < Q1 - 1.5 * IQR) | (df['value'] > Q3 + 1.5 * IQR)

# Z-score 方法
from scipy import stats
z_scores = np.abs(stats.zscore(df['value']))
outliers = z_scores > 3

处理策略:截断(clipping)、Winsorize(替换为分位数边界值)、删除或单独建模。

3.3 缩放(Scaling)

方法 公式 输出范围 适用场景
Min-Max $x’ = \frac{x - \min}{\max - \min}$ [0, 1] 神经网络、需要限定范围
Standard (Z-score) $x’ = \frac{x - \mu}{\sigma}$ 无界(~N(0,1)) PCA、SVM、LR
Robust $x’ = \frac{x - \text{median}}{\text{IQR}}$ 无界 有离群点
MaxAbs $x’ = \frac{x}{\max(|x|)}$ [-1, 1] 稀疏数据
from sklearn.preprocessing import StandardScaler, MinMaxScaler, RobustScaler

scaler = StandardScaler()
X_scaled = scaler.fit_transform(X)

3.4 对数变换

对数变换用于处理重尾分布(长尾分布)或指数增长的数据:

$$ x' = \log(1 + x) $$

适用于:收入、价格、人口等大部分为正的偏态数据。

df['log_price'] = np.log1p(df['price'])  # log(1 + x)

3.5 Box-Cox 变换

Box-Cox 变换是对数变换的推广,将数据变换为正态分布:

$$ x' = \begin{cases} \frac{x^\lambda - 1}{\lambda} & \lambda \neq 0 \\ \log x & \lambda = 0 \end{cases} $$

from scipy import stats
df['boxcox_val'], lambda_ = stats.boxcox(df['value'] + 1)

3.6 分箱(Binning/Discretization)

将连续值转化为离散的”箱”:

方法 描述 适用场景
等宽分箱 每个箱的区间宽度相同 均匀分布的数据
等频分箱 每个箱包含相同数量的样本 偏态分布的数据
自定义分箱 根据业务知识设定边界 有领域知识的场景
from sklearn.preprocessing import KBinsDiscretizer

# 等频分箱
est = KBinsDiscretizer(n_bins=10, encode='onehot', strategy='quantile')
X_binned = est.fit_transform(X)

3.7 交互特征

将两个或多个特征组合成新的特征:

# 加减乘除
df['area'] = df['length'] * df['width'] # 面积
df['ratio'] = df['income'] / df['expenses'] # 比率
df['diff'] = df['revenue'] - df['cost'] # 差值

# 多项式特征
from sklearn.preprocessing import PolynomialFeatures
poly = PolynomialFeatures(degree=2, interaction_only=True)
X_poly = poly.fit_transform(X)

# 针对树模型的自定义交互
df['age_income_interaction'] = df['age'] * df['income']

交互特征的威力在 Kaggle 竞赛中屡见不鲜。尤其是在线性模型中,精心构造的交互项往往比盲目增加多项式次数更有效。

四、类别型特征工程

4.1 编码方式对比

方法 输出 维度 适合模型 缺点
Label Encoding 整数 1 树模型 线模误解序关系
One-Hot Encoding 稀疏二值 $C$ 线性模型 高基数爆炸
Target Encoding 目标均值 1 任何 需防过拟合
Count Encoding 出现次数 1 树模型 丢失类别语义
Binary Encoding 二进制位 $\lceil \log_2 C \rceil$ 线性模型 比 One-Hot 省空间
Hash Encoding 哈希值 固定 大型模型 有冲突风险

4.2 One-Hot Encoding

# pandas 方式
df_encoded = pd.get_dummies(df, columns=['city', 'brand'])

# sklearn 方式
from sklearn.preprocessing import OneHotEncoder
encoder = OneHotEncoder(sparse_output=True, handle_unknown='ignore')
X_encoded = encoder.fit_transform(df[['city', 'brand']])

注意:对于高基数(high cardinality)类别变量(如 user_id, product_id),one-hot 会导致维度爆炸。此时应该用 embedding 或 target encoding。

4.3 特征散列化(Feature Hashing)

当类别数量极多时,使用哈希函数将类别映射到固定数量的特征:

from sklearn.feature_extraction import FeatureHasher

hasher = FeatureHasher(n_features=100, input_type='string')
X_hashed = hasher.fit_transform(df['high_card_col'].astype(str))

4.4 Target Encoding

用目标变量的均值来编码每个类别。在 kaggle 中极为常用:

# 简单 target encoding(注意过拟合)
target_mean = df.groupby('category')['target'].mean()
df['category_encoded'] = df['category'].map(target_mean)

# 带平滑的 target encoding(防止过拟合)
def smooth_target_encoding(train, test, col, target, alpha=5):
global_mean = train[target].mean()
agg = train.groupby(col)[target].agg(['count', 'mean'])
counts = agg['count']
means = agg['mean']
smooth = (counts * means + alpha * global_mean) / (counts + alpha)
return smooth

# catboost 风格
from category_encoders import TargetEncoder
encoder = TargetEncoder(cols=['category'], smoothing=5)
X_encoded = encoder.fit_transform(X, y)

KFold Target Encoding(竞赛常用技巧):

from sklearn.model_selection import KFold

def kfold_target_encoding(train, col, target, n_folds=5):
train[col + '_encoded'] = np.nan
kf = KFold(n_splits=n_folds, shuffle=True, random_state=42)

for tr_idx, val_idx in kf.split(train):
X_tr, X_val = train.iloc[tr_idx], train.iloc[val_idx]
means = X_tr.groupby(col)[target].mean()
train.loc[val_idx, col + '_encoded'] = X_val[col].map(means)

# 用全部训练数据为测试集编码
full_means = train.groupby(col)[target].mean()
train[col + '_encoded'].fillna(full_means, inplace=True)

return full_means # 用于测试集

五、时间特征工程

5.1 从时间戳提取特征

df['hour'] = df['timestamp'].dt.hour
df['dayofweek'] = df['timestamp'].dt.dayofweek # 0=Mon
df['is_weekend'] = df['dayofweek'].isin([5, 6]).astype(int)
df['month'] = df['timestamp'].dt.month
df['quarter'] = df['timestamp'].dt.quarter
df['day_of_year'] = df['timestamp'].dt.dayofyear

# 循环编码(处理周期性)
df['hour_sin'] = np.sin(2 * np.pi * df['hour'] / 24)
df['hour_cos'] = np.cos(2 * np.pi * df['hour'] / 24)
df['month_sin'] = np.sin(2 * np.pi * df['month'] / 12)
df['month_cos'] = np.cos(2 * np.pi * df['month'] / 12)

5.2 时间窗口聚合

# 过去 N 天的聚合统计
df['rolling_mean_7d'] = df.groupby('user_id')['amount'].transform(
lambda x: x.rolling(7, min_periods=1).mean()
)
df['rolling_std_7d'] = df.groupby('user_id')['amount'].transform(
lambda x: x.rolling(7, min_periods=1).std()
)

# 自上次事件的时间差
df['days_since_last_purchase'] = df.groupby('user_id')['date'].diff().dt.days

六、文本特征工程

6.1 词袋模型(Bag of Words)

from sklearn.feature_extraction.text import CountVectorizer

vectorizer = CountVectorizer(
max_features=10000,
stop_words='english',
ngram_range=(1, 2) # unigrams + bigrams
)
X_bow = vectorizer.fit_transform(documents)

6.2 TF-IDF

from sklearn.feature_extraction.text import TfidfVectorizer

tfidf = TfidfVectorizer(
max_features=10000,
stop_words='english',
ngram_range=(1, 2),
sublinear_tf=True # 用 1+log(tf) 来抑制高频词汇
)
X_tfidf = tfidf.fit_transform(documents)

6.3 文本统计特征

df['char_count'] = df['text'].str.len()
df['word_count'] = df['text'].str.split().str.len()
df['avg_word_length'] = df['char_count'] / df['word_count']
df['unique_word_ratio'] = df['text'].apply(
lambda x: len(set(x.split())) / len(x.split())
)
df['uppercase_ratio'] = df['text'].apply(
lambda x: sum(1 for c in x if c.isupper()) / len(x)
)
df['punctuation_count'] = df['text'].apply(
lambda x: sum(1 for c in x if c in '!?.,;:')
)

6.4 词嵌入(Word Embeddings)

# 用预训练的 FastText 或 GloVe 获取词向量
# 文档向量 = 词向量的平均
import numpy as np

def get_doc_vector(text, word2vec_model):
words = text.split()
word_vectors = [word2vec_model[w] for w in words if w in word2vec_model]
if len(word_vectors) == 0:
return np.zeros(300) # 假设 300 维
return np.mean(word_vectors, axis=0)

七、特征选择

7.1 三大类方法

类别 方法 原理 速度 质量
Filter 方差、相关系数、卡方检验、互信息 独立于模型评估
Wrapper RFE、前向/后向搜索 用模型评估子集
Embedded LASSO、特征重要性 模型自带选择

7.2 Filter 方法

from sklearn.feature_selection import VarianceThreshold, mutual_info_classif

# 方差阈值(移除常数特征)
sel = VarianceThreshold(threshold=0.01)
X_filtered = sel.fit_transform(X)

# 互信息
mi = mutual_info_classif(X, y)
selected_features = np.argsort(mi)[::-1][:k]

7.3 Wrapper 方法

from sklearn.feature_selection import RFE
from sklearn.linear_model import LogisticRegression

# 递归特征消除
lr = LogisticRegression()
rfe = RFE(estimator=lr, n_features_to_select=20)
X_rfe = rfe.fit_transform(X, y)

7.4 Embedded 方法

from sklearn.feature_selection import SelectFromModel
from sklearn.ensemble import RandomForestClassifier

# 基于树模型的特征重要性
rf = RandomForestClassifier(n_estimators=100)
rf.fit(X, y)
selector = SelectFromModel(rf, threshold='median')
X_embedded = selector.fit_transform(X, y)

八、自动化特征工程

8.1 Featuretools

import featuretools as ft

# 定义实体和关系
es = ft.EntitySet(id='customers')
es = es.add_dataframe(dataframe_name='transactions',
dataframe=df, index='transaction_id')

# 深度特征合成 (DFS)
feature_matrix, feature_defs = ft.dfs(
entityset=es,
target_dataframe_name='customers',
max_depth=2,
agg_primitives=['mean', 'sum', 'std', 'min', 'max'],
trans_primitives=['add_numeric', 'multiply_numeric']
)

8.2 常见自动化工具

工具 特点 适用场景
Featuretools DFS 自动生成特征 关系型数据
tsfresh 时间序列特征提取 时序数据
AutoFeat 自动化特征工程 + 选择 通用
Boruta 全相关特征选择 高维数据
SHAP 特征重要性 + 解释 模型解释

九、竞赛中的特征工程经验

9.1 Kaggle 大神的特征工程套路

  1. 先做基础特征,再上模型看 baseline:不要在特征工程上投入无限时间而不看反馈
  2. “target encoding + frequency encoding”是最常用的组合拳:尤其对高基数类别变量
  3. 聚合统计特征:按类别聚合(mean, std, min, max, count)几乎是标配
  4. 时间滞后特征:对时序列预测至关重要
  5. 模型融合:不同特征子集训练不同模型再做 ensemble

9.2 特征工程的投入配比

在典型的数据竞赛中:

  • 数据清洗:20% 时间
  • 特征构造:40% 时间
  • 特征选择:10% 时间
  • 模型训练与调参:20% 时间
  • 模型融合:10% 时间

可以看出特征构造占据了最大份额——这正是”特征工程是 ML 的核心”的最好佐证。

十、面试高频问题

Q1:数值特征为什么需要缩放?如果把未缩放的特征直接给 k-NN 或 SVM 会怎样?

k-NN 和 SVM(线性核)都依赖距离计算。如果特征尺度不一致(如特征 A 范围 [0,1],特征 B 范围 [0,100000]),那么在欧氏距离中,特征 B 会完全主导距离计算,特征 A 几乎被忽略。

例如,k-NN 中两个样本的距离 $d = \sqrt{(A_1-A_2)^2 + (B_1-B_2)^2}$,如果 $B$ 的变化是 $A$ 的 100000 倍,$B$ 的项将完全掩盖 $A$ 的信息。

树模型(决策树、随机森林、GBDT)则不受影响,因为它们的分裂只依赖特征值的相对顺序,与绝对尺度无关。

Q2:One-hot 编码和 Label 编码如何选择?Label 编码给线性模型会有什么问题?

  • Label Encoding:将类别映射为整数(如 Red=1, Blue=2, Green=3)。适合树模型,因为它们天然能处理这种编码(只需找分裂点)。但对线性模型,Label Encoding 会强制引入一个虚假的序关系(Green > Blue > Red),模型可能学到”颜色越大越重要”这样的伪模式。
  • One-Hot Encoding:每个类别一个独立的二值列,没有序关系假设。适合线性模型神经网络

高基数类别(如 user_id 有 100 万种),两种方案都不好——one-hot 维度爆炸,label encoding 序关系无意义。此时应用 Target Encoding 或 Embedding。

Q3:Target Encoding 容易过拟合,如何防范?

  1. Smoothing:将编码值向全局均值”拉回”,$\text{enc} = \frac{n \cdot \text{mean} + \alpha \cdot \text{global_mean}}{n + \alpha}$
  2. KFold Target Encoding:用其他折的数据计算当前折的编码,杜绝数据泄露
  3. Add Noise:在编码中加入少量噪声 $\text{enc} + \epsilon$
  4. Regularization:限制编码值的范围

Q4:特征构造中的”数据泄露”(data leakage)指什么?

数据泄露是指:在构造特征时(无意中)使用了测试集的信息,导致模型在测试集上表现虚高,但上线后表现差。

常见泄露场景:

  • 用 full dataset 做标准化(应该只用训练集的 mean/std 对测试集做 transform)
  • 用整个数据集做 target encoding
  • 用未来信息预测过去(时间序列中)
  • 用测试集的标签分布做特征选择

防范:始终在 pipeline 中用 fit_transform() 于训练集、transform() 于测试集;时序问题中按时间严格切分。

Q5:特征工程的 ROI 如何评估?怎样才能不花太多时间在无效特征上?

特征工程的投入产出评估:

  1. 每次只加一个特征(或一组相关特征),观察 CV 分数的变化。无提升就丢弃。
  2. 做特征重要性分析:训练一个简单模型(如随机森林),看各特征的重要性。重要性极低的特征可以删除。
  3. 使用 permutation importance:打乱特征的值,看 CV 分数下降多少。下降越多越重要。
  4. 定期精简:每积累一组新特征后,做一次全面的特征选择,清除冗余特征。

实践中,”feature hashing + 随机特征”往往比精心构造的特征更弱——Good features are born from domain understanding, not from random combinations.

十一、总结

特征工程是机器学习的”最后一公里”——它连接着原始数据和模型之间最后的鸿沟。在深度学习时代,虽然有”端到端学习免特征工程”的说法,但实际上,特征工程的价值丝毫不减,只是从手工构造转向了更智能的方式(如 Entity Embedding、自动化特征工程)。

对于 Kaggle 竞赛参与者而言,特征工程往往是银牌和金牌的分水岭——同样的模型,不同的特征构造,排名差出几十位是常事。对于工业界而言,一个好特征投入生产后,其价值可以持续数年,比不断替换模型更加稳定。

本文梳理了从数值特征到文本特征、从特征编码到特征选择的完整方法论。核心建议是:先跑一个 baseline,然后用特征工程一小步一小步地提升,每次改动都要看 CV 反馈。特征工程不是玄学,而是一门可以通过反复实践精进的手艺。

参考文献

  1. Zheng, A., & Casari, A. (2018). Feature Engineering for Machine Learning. O’Reilly Media.
  2. Kuhn, M., & Johnson, K. (2019). Feature Engineering and Selection: A Practical Approach for Predictive Models. CRC Press.
  3. “Approaching (Almost) Any Machine Learning Problem” by Abhishek Thakur.
  4. Prokhorenkova, L., Gusev, G., Vorobev, A., Dorogush, A. V., & Gulin, A. (2018). CatBoost: unbiased boosting with categorical features. NIPS.
  5. Kanter, J. M., & Veeramachaneni, K. (2015). Deep feature synthesis: Towards automating data science endeavors. DSAA.
文章作者: Leo·Cheung
文章链接: http://tufusi.com/2021/09/20/%E3%80%90%E7%AB%9E%E8%B5%9B%E4%B8%93%E9%A2%98%E3%80%91%E7%89%B9%E5%BE%81%E5%B7%A5%E7%A8%8B/
版权声明: 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来自 ONE·PIECE
打赏
  • 微信
  • 支付宝

评论