特征工程(Feature Engineering)是将原始数据转化为更能代表问题本质、更能提升机器学习模型性能的特征的过程。它被认为是机器学习中最重要也是最耗时的环节之一——有研究表明,数据科学家和机器学习工程师 60%-80% 的时间都花在特征工程和数据准备上。在金融量化、推荐系统、自然语言处理等实际应用中,好的特征工程往往比复杂的模型更能带来性能提升。
"Coming up with features is difficult, time-consuming, requires expert knowledge. 'Applied machine learning' is basically feature engineering."
— Andrew Ng (吴恩达)
为什么特征工程如此重要?我们从模型信息论的视角来看:
一个机器学习模型本质上是一个信息处理系统。根据信息瓶颈(Information Bottleneck)理论,模型的性能受限于输入特征中关于目标变量的信息量 。特征工程的目标就是最大化这个互信息:
其中 是原始特征, 是工程化后的特征, 是目标变量。好的特征工程能提取原始数据中隐藏的信号,压缩噪声,使模型能更高效地学习。
假设我们要预测房价,原始数据中只有"建筑面积"和"房间数"两个特征:
| 原始特征 | 值 |
|---|---|
| 建筑面积 | 120 m² |
| 房间数 | 3 |
通过特征工程,我们可以构造:
这些新特征大大提升了模型的表达能力和预测精度。
数据预处理是特征工程的基础步骤,包括清洗、缩放和异常值处理。
缺失值处理是特征工程的第一步。常见方法包括:
| 方法 | 适用场景 | 优点 | 缺点 |
|---|---|---|---|
| 删除缺失行 | 缺失比例 < 5% | 简单直接 | 可能丢失有效数据 |
| 均值/中位数填充 | 数值型特征 | 保留样本量 | 降低方差 |
| 众数填充 | 类别型特征 | 保留样本量 | 可能引入偏差 |
| KNN 插补 | 中小规模数据 | 利用特征间相关性 | 计算成本高 |
| 模型预测填充 | 有缺失的模式明显 | 精度高 | 可能过拟合 |
| 标记 + 填充 | 缺失本身有信息 | 不丢失缺失信息 | 增加维度 |
一个实用的技巧是:对于缺失值本身可能包含信息的场景(如"是否回答了某个问题"),可以同时添加一个布尔指示特征 is_missing:
# 缺失值标记法示例
import pandas as pd
import numpy as np
df['income_missing'] = df['income'].isna().astype(int)
df['income'] = df['income'].fillna(df['income'].median())
异常值(Outliers)会严重影响模型的训练,特别是对基于距离的模型(如 KNN、SVM)和线性模型。
识别方法:
Z-score 方法:假设数据服从正态分布, 的点视为异常
IQR 方法:基于四分位数,超出 的点视为异常
孤立森林(Isolation Forest):基于树的集成异常检测方法
DBSCAN 聚类:将不在任何高密度区域的点标记为异常
处理策略:
| 策略 | 方法 | 适用场景 |
|---|---|---|
| 截断(Clipping) | 极端值明显是噪声 | |
| 变换 | 对数变换、Box-Cox | 长尾分布 |
| 分箱 | 离散化为分类特征 | 特征实际是分级的 |
| 保留但标记 | 添加 is_outlier 特征 | 异常值本身可能是信号(如欺诈检测) |
特征缩放使不同量纲的特征处于相近的数值范围,这对梯度下降优化的模型(神经网络、SVM、逻辑回归)至关重要。
| 方法 | 公式 | 输出范围 | 适用场景 |
|---|---|---|---|
| 标准化(Z-score) | 近似 | 通用,不假设分布 | |
| Min-Max 缩放 | 特征有明确边界 | ||
| 稳健缩放(Robust) | 受异常值影响小 | 数据含异常值 | |
| L2 归一化 | 文本分类、高维稀疏数据 |
量化金融中的缩放注意点: 在量化交易中,使用未来信息进行缩放会导致未来数据泄漏(Look-ahead Bias)。务必使用"滚动窗口"或"扩展窗口"方式进行缩放:
# 正确:滚动标准化
def rolling_zscore(series, window=60):
rolling_mean = series.rolling(window=window, min_periods=1).mean()
rolling_std = series.rolling(window=window, min_periods=1).std()
return (series - rolling_mean) / rolling_std.replace(0, 1)
类别型特征(Categorical Features)是机器学习中最常见的特征类型之一,需要转换为数值形式供模型使用。
常用编码方法对比:
| 编码方法 | 维度 | 优点 | 缺点 | 适用模型 |
|---|---|---|---|---|
| Label Encoding | 1 列 | 内存占用小 | 暗示序关系 | 树模型(LightGBM, XGBoost) |
| One-Hot Encoding | K 列 | 无偏序假设 | 高基数时维度爆炸 | 线性模型、神经网络 |
| Target Encoding | 1 列 | 捕获类别与目标的关系 | 容易过拟合 | 树模型 |
| Frequency Encoding | 1 列 | 简单有效 | 无法区分同频类别 | 通用 |
| Ordinal Encoding | 1 列 | 保留序信息 | 仅适用于有序类别 | 通用 |
Target Encoding 的正确实现:
Target Encoding 用目标变量的均值替换类别,但直接应用会导致严重的过拟合。正确做法是使用交叉验证方式:
from sklearn.model_selection import KFold
import numpy as np
def target_encoding(X, y, col, n_folds=5):
"""带交叉验证的目标编码"""
encoded = np.zeros(len(X))
kf = KFold(n_splits=n_folds, shuffle=True, random_state=42)
for train_idx, val_idx in kf.split(X):
# 在训练集上计算每个类别的均值
means = X.iloc[train_idx].groupby(col)[y.name].mean()
# 对验证集应用
encoded[val_idx] = X.iloc[val_idx][col].map(means).fillna(y.mean())
return encoded
高基数类别(High-Cardinality Features)的处理:
当类别数超过数百甚至数千时(如用户 ID、IP 地址、邮政编码),One-Hot 编码不再可行。推荐方案:
分箱(Binning)将连续数值离散化,可以捕获非线性关系,提高模型鲁棒性:
# 等宽分箱
df['age_bin'] = pd.cut(df['age'], bins=5, labels=['Very Young', 'Young', 'Middle', 'Senior', 'Elderly'])
# 等频分箱(每个箱有相同样本数)
df['income_quantile'] = pd.qcut(df['income'], q=5, labels=['Q1', 'Q2', 'Q3', 'Q4', 'Q5'])
# 自定义分箱(基于领域知识)
bins = [0, 18, 30, 45, 60, 100]
df['age_group'] = pd.cut(df['age'], bins=bins)
分箱效果的数值示例:假设预测信用卡违约,原始特征"收入"线性模型权重的 ,分箱后每个区间的权重可能为 ,能更好地捕捉"收入越高违约率越低"的非线性趋势。
时间特征有丰富的结构信息可以被提取:
基础分解:
循环编码(Cyclic Encoding):
对于小时、月份等循环特征,直接编码为 0-23 或 1-12 会丢失"23 点"和"0 点"的邻近关系。正弦-余弦编码可以保留这种循环关系:
滑动窗口统计:
很多机器学习模型假设特征与目标之间存在线性关系,或特征服从某种分布。当数据偏离这些假设时,数学变换可以改善模型效果。
| 变换 | 公式 | 效果 | 应用场景 |
|---|---|---|---|
| 对数变换 | 压缩右偏分布 | 收入、价格、交易量 | |
| 平方根 | 中等压缩 | 计数数据 | |
| Box-Cox | 参数化,自动优化 | 通用 | |
| Yeo-Johnson | 类似 Box-Cox,支持负数 | 处理负值数据 | 通用 |
| 倒数变换 | 强压缩 | 速度转化为时间 |
Box-Cox 变换详解:
Box-Cox 变换是一个参数化变换族,通过最大似然估计自动选择最优的 值:
实际应用示例:假设某金融因子原始分布偏度 (严重右偏),应用 Box-Cox 变换后偏度降至 ,使其更接近正态分布,有利于线性模型和 PCA 的使用。
多项式特征通过引入特征的幂次和交叉项来捕获非线性关系:
from sklearn.preprocessing import PolynomialFeatures
poly = PolynomialFeatures(degree=2, interaction_only=False, include_bias=False)
X_poly = poly.fit_transform(X)
# 新增特征: x1^2, x2^2, x1*x2
print(poly.get_feature_names_out())
注意: 多项式特征会爆炸式增加维度。对于 个原始特征, 次多项式的特征数为 。当 时已有 286 个特征。建议使用 interaction_only=True 限制为交叉项。
特征构造(Feature Construction)是特征工程中最具创造性的环节,需要领域知识和数据洞察。
聚合特征(Aggregation Features):
对于分组数据(如每个用户的多次交易记录),可以构造:
| 聚合函数 | 含义 | 应用场景 |
|---|---|---|
count |
计数 | 用户活跃度 |
mean, median |
中心趋势 | 平均消费额 |
std, var |
波动性 | 收入稳定性 |
min, max |
极值 | 最大交易额 |
skew, kurt |
分布形状 | 偏态特征 |
nunique |
多样性 | 浏览类目数 |
first, last |
时间序列端点 | 最新行为 |
分组聚合的量化金融示例(股票因子):
对于每只股票,过去 20 个交易日的价格数据可以构造:
import numpy as np
def stock_features(prices_series):
return {
'returns_20d': prices_series.pct_change(20).iloc[-1],
'volatility_20d': prices_series.pct_change().std() * np.sqrt(252), # 年化波动率
'max_drawdown_20d': (prices_series / prices_series.cummax() - 1).min(),
'skew_20d': prices_series.pct_change().dropna().skew(),
'kurt_20d': prices_series.pct_change().dropna().kurt(),
'ma_ratio_5_20': prices_series.rolling(5).mean().iloc[-1] / prices_series.rolling(20).mean().iloc[-1],
'volume_ratio': df['volume'].pct_change(5).iloc[-1],
}
金融量化特征示例:
| 特征名称 | 构造公式 | 含义 |
|---|---|---|
| 动量 | 过去 k 日的累计收益 | |
| 波动率 | 价格波动程度 | |
| 夏普比 | 风险调整后收益 | |
| 相对强弱 | 最近趋势强度 | |
| 乖离率 | 价格偏离均线的程度 | |
| 换手率 | 交易活跃度 |
文本特征示例:
| 特征名称 | 构造方法 |
|---|---|
| TF-IDF | 词频-逆文档频率 |
| N-gram | 连续的 N 个词作为特征 |
| 词嵌入均值 | 词向量的平均池化 |
| 情感得分 | 基于词典或模型的情感分析 |
| 文本复杂度 | 平均句长、词长、词汇多样性 |
交叉特征(Interaction Features)捕获两个或多个特征之间的交互效应。
离散-离散交叉: 如"性别 × 年龄段"构造新的类别组合。
离散-连续交叉: 如按"城市"分组的"收入均值"。
连续-连续交叉: 如"面积 × 楼层数"。
在量化交易中,交叉特征的典型例子是"市值 × 动量",即在大市值和小市值股票中分别考察动量因子的效果——这其实就是 Fama-French 因子模型的思路:
其中 SMB(Small Minus Big)就是"市值"和"收益"的交叉特征。
特征选择(Feature Selection)从高维特征集中找出最相关的子集,降低过拟合风险、减少训练时间、提高模型可解释性。
| 类别 | 代表方法 | 优点 | 缺点 |
|---|---|---|---|
| Filter(过滤法) | 方差阈值、卡方检验、互信息、相关系数 | 计算快,独立于模型 | 忽略特征交互 |
| Wrapper(包裹法) | 递归特征消除(RFE)、前向/后向搜索 | 考虑特征交互 | 计算开销大 |
| Embedded(嵌入法) | Lasso(L1 正则化)、树模型特征重要性 | 效率与效果平衡 | 依赖特定模型 |
互信息(Mutual Information)衡量一个特征 对目标 的"信息贡献",能捕获非线性关系:
对比相关系数和互信息的数值示例:
| 特征 | Pearson 相关系数 | 互信息 | 说明 |
|---|---|---|---|
| 0.85 | 0.72 | 强线性相关 | |
| 0.02 | 0.68 | 非线性相关(如 ),相关系数无法检测 | |
| 0.45 | 0.12 | 较弱的相关性 | |
| 0.01 | 0.01 | 噪声特征 |
Lasso 回归在损失函数中添加 L1 正则化项,自动将不重要的特征系数压缩为 0:
越大,被惩罚为 0 的特征越多。这种特性使 Lasso 成为天然的特征选择器。
from sklearn.linear_model import Lasso
from sklearn.preprocessing import StandardScaler
# 先标准化
scaler = StandardScaler()
X_scaled = scaler.fit_transform(X)
# Lasso 特征选择
lasso = Lasso(alpha=0.01, random_state=42)
lasso.fit(X_scaled, y)
selected_features = X.columns[lasso.coef_ != 0]
print(f'从 {X.shape[1]} 个特征中选择了 {len(selected_features)} 个')
降维(Dimensionality Reduction)将高维特征映射到低维空间,常用于可视化、去噪、和加速训练。
PCA 通过线性变换将原始特征投影到方差最大的方向上,是一种无监督降维方法。
数学本质: PCA 找到一个正交变换矩阵 ,使得投影后的数据方差最大化:
这等价于对协方差矩阵 做特征值分解。
量化金融中的 PCA 应用:
在量化中,PCA 常用于因子模型构造。以美股为例,对全市场股票收益率协方差矩阵做 PCA,前几个主成分通常对应:
| 主成分 | 解释方差比例 | 实际含义 |
|---|---|---|
| PC1 | ~35% | 市场整体走势(市场因子) |
| PC2 | ~12% | 行业轮动 |
| PC3 | ~8% | 大小盘风格 |
| PC4 | ~6% | 价值/成长风格 |
| PC5 | ~5% | 波动率因子 |
前 20 个主成分通常能解释 80% 以上的收益率方差,这为特征降维提供了理论依据。
from sklearn.decomposition import PCA
# 保留 95% 方差的 PCA
pca = PCA(n_components=0.95)
X_pca = pca.fit_transform(X_scaled)
print(f'原始维度: {X_scaled.shape[1]}, PCA后维度: {X_pca.shape[1]}')
print(f'解释方差比例累计: {pca.explained_variance_ratio_.cumsum()[-1]:.2%}')
这些是非线性降维方法,特别适合高维数据的可视化:
| 方法 | 核心思想 | 适合 | 缺点 |
|---|---|---|---|
| t-SNE | 保持高维邻域分布 | 可视化(2D/3D) | 计算慢,随机性大 |
| UMAP | 保持拓扑结构 | 大规模数据可视化 | 超参敏感 |
近年来,自动化特征工程工具减轻了手工构造特征的负担:
| 工具 | 核心方法 | 适用场景 |
|---|---|---|
| Featuretools | 深度特征合成(DFS) | 关系型数据、时间序列 |
| AutoFeat | 数学变换 + 特征选择 | 表格数据 |
| tsfresh | 时间序列特征自动提取 | 时间序列 |
| Sklearn Pipeline | 组合转换器、列变换器 | 通用流程 |
import featuretools as ft
# 定义实体
es = ft.EntitySet(id='transactions')
es = es.add_dataframe(
dataframe_name='transactions',
dataframe=transactions_df,
index='transaction_id',
time_index='timestamp'
)
es = es.add_dataframe(
dataframe_name='customers',
dataframe=customers_df,
index='customer_id'
)
es = es.add_relationship('customers', 'customer_id', 'transactions', 'customer_id')
# 自动构造特征
feature_matrix, feature_defs = ft.dfs(
entityset=es,
target_dataframe_name='customers',
max_depth=2,
agg_primitives=['sum', 'mean', 'std', 'count', 'max', 'min'],
trans_primitives=['day', 'month', 'weekday']
)
print(f'自动生成了 {len(feature_defs)} 个特征')
在量化交易中,特征工程直接关系到策略的收益和稳定性。
价量特征:
基本面特征:
另类数据特征:
构造完特征后,需要通过严格的回测验证其有效性:
Rank IC(秩相关系数)检验:
其中 和 分别是特征值和未来收益的秩。
典型的因子有效性判断标准:
| Rank IC 均值 | 标准差 | ICIR | 评价 |
|---|---|---|---|
| > 0.05 | < 0.1 | > 0.5 | 优秀因子 |
| 0.02 ~ 0.05 | < 0.15 | > 0.2 | 可用因子 |
| < 0.02 | — | < 0.1 | 弱因子,考虑淘汰 |
1. 未来数据泄漏(Look-ahead Bias)
这是量化中最致命的错误。例如,用当天的收盘价计算因子来预测当天的涨跌——收盘价本身就包含了当天的涨跌信息。
✅ 正确做法:使用截断数据的滚动窗口计算
2. 幸存者偏差(Survivorship Bias)
只使用当前仍在交易的股票构建特征,忽略了已经退市的股票。这会导致特征高估收益。
✅ 正确做法:使用全样本历史数据,包括已退市股票
3. 多重比较偏差(Multiple Comparison Bias)
测试 1000 个随机特征,期望能找到 50 个"显著"的特征(在 5% 显著性水平下)。
✅ 正确做法:使用 Bonferroni 校正、FDR 控制,或在独立数据集上验证
4. 特征与目标的相关性过强
如果特征本身就是目标变量的某种变换(如用"今天收盘价"预测"明天收盘价"),模型会过拟合噪声。
✅ 正确做法:使用滞后特征,确保特征在目标之前可获得
| 工具 | 主要功能 | 入门难度 |
|---|---|---|
| Scikit-learn | 标准化的特征工程 Pipeline | ⭐ |
| Pandas | 数据清洗、聚合、变换 | ⭐ |
| Feature-engine | 专门的特征工程库 | ⭐⭐ |
| Featuretools | 自动化特征合成 | ⭐⭐ |
| tsfresh | 时间序列特征提取 | ⭐⭐ |
| Category Encoders | 多种类别编码 | ⭐ |
一个典型的特征工程项目流程:
原始数据 → 数据清洗 → EDA → 基础特征构造 → 特征变换
↓
特征选择 ← 特征重要性排序 ← 特征评估 ← 交叉验证
↓
模型训练 → 模型评估 → 特征重要性回溯 → 迭代优化
关键原则: