查看全课大纲(第 18 / 35 节)

特征衍生

约 9 分钟

小象实战讲义 · 特征工程精讲与实践

特征衍生是特征构建的高级阶段,旨在从已有特征中挖掘更深层次的模式与关联,构建出更具表征能力的新特征。本节我们将学习两种核心的特征衍生方法:多项式特征和基于树模型的组合特征构建,它们能有效提升线性模型的非线性拟合能力,同时保持模型的可解释性与在线推理效率。

💡 核心导读

  • 多项式特征:通过数学运算(如平方、交叉相乘)将连续数值特征扩展为高阶组合,揭示特征间的交互作用。
  • 树模型构建组合特征:利用GBDT、随机森林等树模型的决策路径,自动学习并生成一系列条件组合特征,再输入给线性模型(如逻辑回归)进行训练,实现“树模型做特征工程,线性模型做预测”的经典范式。
  • 工业实践优势:该方法结合了树模型强大的非线性拟合能力与线性模型的高效、可解释性,是工业界广泛应用的成熟方案。

一、多项式特征(Polynomial Features)

多项式特征是一种基于已有数值型特征,通过构造多项式项(如平方、立方、交叉项)来生成新特征的方法。它能够帮助线性模型捕捉特征之间的交互效应和非线性关系。

其核心是生成最高次幂不超过指定 degree 的所有多项式组合。例如,对于两个特征 x1 和 x2,degree=2 时会生成:x1, x2, x1^2, x1*x2, x2^2。

下面我们使用泰坦尼克数据集中的 Pclass(船舱等级)和 SibSp(堂兄弟姐妹/配偶数量)两个数值特征进行演示。

import pandas as pd
import numpy as np
from sklearn.preprocessing import PolynomialFeatures

# 构造一个微型可复现的泰坦尼克数据集样本
data = pd.DataFrame({
    'Pclass': [3, 1, 3, 2, 1],
    'SibSp': [1, 1, 0, 1, 0],
    'Age': [22.0, 38.0, 26.0, 35.0, 35.0],
    'Fare': [7.2500, 71.2833, 7.9250, 53.1000, 51.8625],
    'Survived': [0, 1, 1, 1, 0]
})

# 选取用于构建多项式特征的两列
X_sample = data[['Pclass', 'SibSp']]
print("原始特征数据:")
print(X_sample)
print()

# 实例化多项式特征转换器,最高次为2,不包含常数项
poly = PolynomialFeatures(degree=2, include_bias=False)
# 拟合并转换数据
X_poly = poly.fit_transform(X_sample)
# 获取生成的特征名称
feature_names = poly.get_feature_names_out(['Pclass', 'SibSp'])

# 将结果转换为DataFrame以便查看
X_poly_df = pd.DataFrame(X_poly, columns=feature_names)
print("生成的多项式特征:")
print(X_poly_df)
原始特征数据:
   Pclass  SibSp
0       3      1
1       1      1
2       3      0
3       2      1
4       1      0

生成的多项式特征:
   Pclass  SibSp  Pclass^2  Pclass SibSp  SibSp^2
0     3.0    1.0       9.0           3.0      1.0
1     1.0    1.0       1.0           1.0      1.0
2     3.0    0.0       9.0           0.0      0.0
3     2.0    1.0       4.0           2.0      1.0
4     1.0    0.0       1.0           0.0      0.0

可以看到,除了原始特征 Pclass 和 SibSp 外,我们还得到了 Pclass^2、Pclass SibSp(交叉项)和 SibSp^2 三个新特征。

二、基于树模型构建组合特征

树模型(如决策树、随机森林、GBDT)通过一系列“if-else”规则对数据进行划分。每一条从根节点到叶子节点的路径,都是一组特征的特定条件组合。我们可以将这些路径(即叶子节点的索引)提取出来,进行独热编码(One-Hot Encoding),从而得到一系列新的二值组合特征。这些特征再输入给逻辑回归等线性模型,就构成了著名的 GBDT+LR 或 RF+LR 架构——该范式出自 Facebook 2014 年发表的论文(用树模型做特征变换,再用逻辑回归做最终的点击率预估),此后在工业界被长期沿用。

这种方法的核心思想是:让树模型负责自动、高效地发现有效的特征组合,让线性模型负责在这些组合特征上进行快速、可解释的预测。

2.1 GBDT + LR 范式

我们首先演示使用梯度提升树(GBDT)来构建组合特征,并与逻辑回归(LR)结合。

import pandas as pd
import numpy as np
from sklearn.ensemble import GradientBoostingClassifier
from sklearn.preprocessing import OneHotEncoder
from sklearn.linear_model import LogisticRegression
from sklearn.metrics import roc_auc_score

# 1. 准备数据(硬编码微型样本,包含缺失值填充逻辑)
data = pd.DataFrame({
    'Pclass': [3, 1, 3, 2, 1, 3, 2, 3],
    'Age': [22.0, 38.0, 26.0, 35.0, 35.0, np.nan, 54.0, 2.0],
    'Fare': [7.25, 71.28, 7.92, 53.10, 51.86, 8.05, 51.86, 21.07],
    'SibSp': [1, 1, 0, 1, 0, 0, 0, 3],
    'Survived': [0, 1, 1, 1, 0, 0, 0, 1]
})
# 简单填充缺失值
data_filled = data.fillna(data['Age'].mean())
# 划分特征和标签
X = data_filled[['Pclass', 'Age', 'Fare', 'SibSp']]
y = data_filled['Survived']
# 简单按顺序划分训练集和测试集
X_train, X_test = X.iloc[:5], X.iloc[5:]
y_train, y_test = y.iloc[:5], y.iloc[5:]

print("训练集特征:")
print(X_train)
print("\n训练集标签:", y_train.values)
print("\n测试集特征:")
print(X_test)
print("测试集标签:", y_test.values)
print()

# 2. 使用GBDT拟合数据,并获取叶子节点索引
gbdt = GradientBoostingClassifier(n_estimators=5, random_state=42, max_depth=3)
gbdt.fit(X_train, y_train)
# apply方法返回每个样本在每棵树上的叶子节点索引
train_leaf_index_gbdt = gbdt.apply(X_train)[:, :, 0]  # shape: (n_samples, n_estimators)
test_leaf_index_gbdt = gbdt.apply(X_test)[:, :, 0]
print("GBDT叶子节点索引(训练集前2个样本):")
print(train_leaf_index_gbdt[:2])
print()

# 3. 对叶子节点索引进行独热编码
one_hot_gbdt = OneHotEncoder(sparse_output=True)
# 在训练集索引上拟合编码器
X_train_onehot_gbdt = one_hot_gbdt.fit_transform(train_leaf_index_gbdt)
X_test_onehot_gbdt = one_hot_gbdt.transform(test_leaf_index_gbdt)
print(f"独热编码后特征维度(训练集): {X_train_onehot_gbdt.shape}")
print()

# 4. 将GBDT生成的特征输入逻辑回归
lr_on_gbdt_feat = LogisticRegression(max_iter=1000, random_state=42)
lr_on_gbdt_feat.fit(X_train_onehot_gbdt, y_train)
y_pred_proba_gbdt = lr_on_gbdt_feat.predict_proba(X_test_onehot_gbdt)[:, 1]
auc_gbdt_lr = roc_auc_score(y_test, y_pred_proba_gbdt)
print(f"AUC (GBDT特征 + LR): {auc_gbdt_lr:.4f}")
print()

# 5. 可选:将GBDT特征与原始特征拼接(转为密集数组后用 numpy 水平拼接)
X_train_ext = np.hstack([X_train_onehot_gbdt.toarray(), X_train])
X_test_ext = np.hstack([X_test_onehot_gbdt.toarray(), X_test])
lr_on_ext = LogisticRegression(max_iter=1000, random_state=42)
lr_on_ext.fit(X_train_ext, y_train)
y_pred_proba_ext = lr_on_ext.predict_proba(X_test_ext)[:, 1]
auc_ext = roc_auc_score(y_test, y_pred_proba_ext)
print(f"AUC (GBDT特征 + 原始特征 + LR): {auc_ext:.4f}")
训练集特征:
   Pclass   Age   Fare  SibSp
0       3  22.0   7.25      1
1       1  38.0  71.28      1
2       3  26.0   7.92      0
3       2  35.0  53.10      1
4       1  35.0  51.86      0

训练集标签: [0 1 1 1 0]

测试集特征:
   Pclass        Age   Fare  SibSp
5       3  30.285714   8.05      0
6       2  54.000000  51.86      0
7       3   2.000000  21.07      3
测试集标签: [0 0 1]

GBDT叶子节点索引(训练集前2个样本):
[[5. 2. 2. 5. 4.]
 [6. 6. 6. 6. 6.]]

独热编码后特征维度(训练集): (5, 20)

AUC (GBDT特征 + LR): 0.5000

AUC (GBDT特征 + 原始特征 + LR): 0.0000

2.2 随机森林 + LR 范式

同样的流程也适用于随机森林(Random Forest)。

import pandas as pd
import numpy as np
from sklearn.ensemble import RandomForestClassifier
from sklearn.preprocessing import OneHotEncoder
from sklearn.linear_model import LogisticRegression
from sklearn.metrics import roc_auc_score

# 1. 准备数据(与上一块完全相同的硬编码数据,保证可比性)
data = pd.DataFrame({
    'Pclass': [3, 1, 3, 2, 1, 3, 2, 3],
    'Age': [22.0, 38.0, 26.0, 35.0, 35.0, np.nan, 54.0, 2.0],
    'Fare': [7.25, 71.28, 7.92, 53.10, 51.86, 8.05, 51.86, 21.07],
    'SibSp': [1, 1, 0, 1, 0, 0, 0, 3],
    'Survived': [0, 1, 1, 1, 0, 0, 0, 1]
})
data_filled = data.fillna(data['Age'].mean())
X = data_filled[['Pclass', 'Age', 'Fare', 'SibSp']]
y = data_filled['Survived']
X_train, X_test = X.iloc[:5], X.iloc[5:]
y_train, y_test = y.iloc[:5], y.iloc[5:]

# 2. 使用随机森林拟合数据,并获取叶子节点索引
rf = RandomForestClassifier(n_estimators=5, random_state=42, max_depth=3)
rf.fit(X_train, y_train)
train_leaf_index_rf = rf.apply(X_train)
test_leaf_index_rf = rf.apply(X_test)
print("RF叶子节点索引(训练集前2个样本):")
print(train_leaf_index_rf[:2])
print()

# 3. 对叶子节点索引进行独热编码
one_hot_rf = OneHotEncoder(sparse_output=True)
X_train_onehot_rf = one_hot_rf.fit_transform(train_leaf_index_rf)
X_test_onehot_rf = one_hot_rf.transform(test_leaf_index_rf)
print(f"独热编码后特征维度(训练集): {X_train_onehot_rf.shape}")
print()

# 4. 将RF生成的特征输入逻辑回归
lr_on_rf_feat = LogisticRegression(max_iter=1000, random_state=42)
lr_on_rf_feat.fit(X_train_onehot_rf, y_train)
y_pred_proba_rf = lr_on_rf_feat.predict_proba(X_test_onehot_rf)[:, 1]
auc_rf_lr = roc_auc_score(y_test, y_pred_proba_rf)
print(f"AUC (RF特征 + LR): {auc_rf_lr:.4f}")
print()

# 5. 对比:仅使用原始特征+逻辑回归
lr_raw = LogisticRegression(max_iter=1000, random_state=42)
lr_raw.fit(X_train, y_train)
y_pred_proba_raw = lr_raw.predict_proba(X_test)[:, 1]
auc_raw = roc_auc_score(y_test, y_pred_proba_raw)
print(f"AUC (仅原始特征 + LR): {auc_raw:.4f}")
RF叶子节点索引(训练集前2个样本):
[[1 0 1 4 1]
 [4 0 6 6 2]]

独热编码后特征维度(训练集): (5, 14)

AUC (RF特征 + LR): 0.5000

AUC (仅原始特征 + LR): 0.0000

说明:由于我们使用的示例数据量极小(仅8行,测试集只有3个样本、其中1个正例),AUC 的可能取值本身只有 0、0.5、1 三种,因此无法反映方法优劣。在实际工业场景中,数据规模庞大,特征复杂,GBDT+LR 或 RF+LR 架构通常能显著超越仅使用原始特征的线性模型,因为它通过树模型自动发现了有效的非线性组合特征。

三、方法对比与工业实践要点

通过上面的演示,我们可以总结出以下要点:

  1. 效果对比:在原课程的泰坦尼克全量数据集实验中(以原课 notebook 输出为准确口径),GBDT特征+LR 的 AUC 达到 0.7746;再拼接原始特征后为 0.7743(略有损失,与数据规模有关);RF+LR 为 0.7479;而仅使用原始特征的逻辑回归仅为 0.6989。这验证了树模型构建组合特征的有效性。(注:课程视频中对后三值的口述与 notebook 输出略有出入,此处以确定性工件为准。)
  2. 工业界偏好:GBDT+LR(或其变种,如使用XGBoost、LightGBM)是工业界,尤其是在推荐系统、广告点击率预估等场景中,经过长期验证的成熟方案。其优势在于:
    • 强大的表征能力:树模型自动学习复杂的特征交叉,省去了人工设计组合特征的繁琐。
    • 高效的在线推理:最终的预测模型是逻辑回归,其计算复杂度低,响应速度快,非常适合高并发在线服务。
    • 一定的可解释性:相比于深度神经网络,逻辑回归的权重可以一定程度上反映组合特征的重要性。
  3. 适用场景:当直接使用非线性模型(如GBDT、RF本身)效果已经很好时,为何还要用LR?答案在于效率与可控性。在需要极低延迟、模型需要频繁更新、或对模型可解释性有要求的场景中,“树模型做特征工程 + 线性模型做预测”的范式提供了优秀的平衡。

📝 动手练一练

练习 1(概念辨析):多项式特征扩展为什么对线性模型帮助最大,而对树模型收益有限?两者的决策机制差异在哪里?

👉 点击查看参考答案

线性模型只能对特征做加权求和,特征间的交互与非线性关系必须靠人工”造”出来——多项式特征恰好把 x1·x2、x² 这类交互项显式展开成新列,等于把非线性”翻译”回线性可用的形式,所以提升显著。树模型本身就是按阈值递归切分,天然能逼近任意非线性交互,把 x1·x2 显式喂进去通常只是让树更容易找到分裂点、边际收益小,且会成倍增加特征数、消耗更多内存与训练时间。这也是为什么树模型路线更常用”叶子节点编码”这类组合特征,而不是盲目做多项式展开。

练习 2(动手实算):两个特征 x1、x2,PolynomialFeatures(degree=2, include_bias=False) 会生成几列?如果原始特征有 10 个,degree=3 时会膨胀到多少列?这提示我们在什么情况下要警惕该方法的维度爆炸?

👉 点击查看参考答案

两特征 degree=2:x1、x2、x1²、x1·x2、x2²,共 5 列。十特征 degree=3(include_bias=False)会膨胀到 285 列——总项数正是组合数 C(13,3)-1,包含所有一次项、二次项(含自幂与交叉)与三次项。特征数继续增大或 degree 继续提高时,列数会按组合数爆炸式增长(30 特征 degree=3 已上千列)。警示:原始特征较多时,先做特征筛选/降维,或限制 interaction_only=True 只保留交叉项、只对少数强相关特征做多阶展开,避免”维度灾难”吃掉模型的泛化能力。

本章小结

本节我们深入学习了两种高级的特征衍生技术:多项式特征和基于树模型的组合特征构建。它们是特征工程从“手工制作”迈向“自动化学习”的关键一步,能够显著提升模型对数据中复杂模式的捕捉能力。

📋 行动清单

  • 动手尝试多项式特征:在自己的数据集上,对数值型特征尝试 degree=2 或 3 的多项式扩展,观察模型效果变化。
  • 实现GBDT+LR流程:使用sklearn完整走通“GBDT拟合 -> 提取叶子索引 -> 独热编码 -> 逻辑回归训练”的流程,理解数据在各个环节的形态变化。
  • 思考业务应用:结合你正在处理的实际业务问题,思考哪些特征之间存在潜在的交互效应,是否可以通过本节介绍的方法来自动挖掘这些组合。

—— 小象教研组