查看全课大纲(第 25 / 35 节)
- 1.特征工程简介
- 2.特征工程的处理流程
- 3.数据与特征概述
- 4.数据统计、相似度与数据探索
- 5.数据探索工具 Pandas_profiling
- 6.工具应用:《泰坦尼克》分析报告
- 7.构造一个微型链家租房数据集用于演示
- 8.数据预处理综述
- 9.缺失值处理实操
- 10.异常值处理实操
- 11.连续值特征构建
- 12.类别型数据特征构建
- 13.特征幅度缩放
- 14.特征离散化
- 15.特征编码:让类别数据参与建模
- 16.特征变换
- 17.时间特征
- 18.特征衍生
- 19.自动化特征工具
- 20.特征选择综述
- 21.特征选择方法
- 22.过滤式特征选择方法实操
- 23.包裹式特征选择方法
- 24.嵌入式特征选择
- 25.混序法与混合法特征选择
- 26.特征降维综述
- 27.案例实操:特征降维
- 28.特征监控概述与指标
- 29.Iris 数据集特征漂移检测案例
- 30.特征监控实战:用分布对比检测数据漂移
- 31.实时特征监控
- 32.电商销量预测项目背景介绍
- 33.数据预处理和特征工程(一)
- 34.数据预处理和特征工程(二)
- 35.数据建模
混序法与混合法特征选择
约 4 分钟
小象实战讲义 · 特征工程精讲与实践
在特征选择的工具箱中,除了基于统计的过滤法、基于模型迭代的包裹法和基于正则化的嵌入法,还有两种非常直观且实用的实验性方法:混序法和混合法。本节我们将学习如何通过打乱特征值来评估其重要性,以及如何将多种选择策略组合使用,从而更稳健地筛选出对模型最有价值的特征。
💡 核心导读
- 混序法思想:通过打乱某个特征的值,观察模型性能(如AUC)的下降幅度,来量化该特征的重要性。
- 混序法实操:使用自研工具
feature_shuffle_rf对乳腺癌数据集进行特征重要性评估。 - 混合法思想:综合运用多种特征选择方法(如模型重要度、递归特征消除/增加),形成更可靠的筛选策略。
- 混合法实操:使用
RFE和自研的递归特征消除/增加工具,演示混合选择流程。
5.1 混序法:通过特征打乱评估重要性
混序法(Feature Shuffling)是一种基于实验的特征重要性评估方法。其核心思想非常直观:如果一个特征对模型预测至关重要,那么将其值随机打乱(Shuffle)后,模型性能应该会显著下降。
原理拆解:
- 正常建模:使用原始特征训练模型,得到一个基准性能(如AUC)。
- 特征打乱:针对待评估的某个特征,将其在训练集和测试集中的值分别进行随机重排。这相当于切断了该特征与样本标签之间的真实关联。
- 性能对比:使用打乱后的特征重新训练模型(或仅对测试集进行预测),计算新的性能指标。
- 重要性量化:计算性能指标的下降幅度(如
AUC_原始 - AUC_打乱后)。下降幅度越大,说明该特征越重要。
这种方法不依赖于特定模型的内部结构(如决策树的分裂增益或线性模型的系数),是一种与模型相对独立的评估方式,结果易于解释。
5.1.1 混序法代码实践
我们使用课程提供的工具 feature_shuffle_rf(位于 utils 目录)在经典的乳腺癌数据集上进行演示。该工具内部使用随机森林作为评估模型。
import pandas as pd
import numpy as np
from sklearn.datasets import load_breast_cancer
from sklearn.model_selection import train_test_split
# 注意:实际工具函数 feature_shuffle 需要从 utils 导入,此处为演示逻辑,我们模拟其核心过程。
# 加载数据并构造DataFrame
data = load_breast_cancer()
df = pd.DataFrame(data.data, columns=data.feature_names)
df['target'] = data.target
# 划分训练集和测试集
X_train, X_test, y_train, y_test = train_test_split(df.drop('target', axis=1),
df['target'],
test_size=0.2,
random_state=42)
print(f"训练集形状: {X_train.shape}, 测试集形状: {X_test.shape}")训练集形状: (455, 30), 测试集形状: (114, 30)接下来,我们模拟混序法的核心计算逻辑。为了演示清晰,我们使用一个极简的示例数据。
import pandas as pd
import numpy as np
from sklearn.ensemble import RandomForestClassifier
from sklearn.metrics import roc_auc_score
# 构造一个极简的演示数据(8个样本,3个特征)
X_demo = pd.DataFrame({
'feature_A': [5, 1, 8, 2, 9, 3, 4, 7],
'feature_B': [0.3, 0.1, 0.5, 0.2, 0.6, 0.15, 0.25, 0.45],
'feature_C': [10, 20, 30, 70, 80, 90, 40, 85] # 这个特征与标签强相关
})
# 构造一个简单的二分类标签,与 feature_C 强相关
y_demo = pd.Series([0, 0, 0, 1, 1, 1, 0, 1])
# 划分训练测试(前6个作为训练,最后2个作为测试,两部分都包含两个类别)
X_train_demo = X_demo.iloc[:6].copy()
y_train_demo = y_demo.iloc[:6].copy()
X_test_demo = X_demo.iloc[6:].copy()
y_test_demo = y_demo.iloc[6:].copy()
print("演示数据 - 训练特征:")
print(X_train_demo)
print("\n演示数据 - 训练标签:")
print(y_train_demo.values)
print("\n演示数据 - 测试特征:")
print(X_test_demo)
print("\n演示数据 - 测试标签:")
print(y_test_demo.values)演示数据 - 训练特征:
feature_A feature_B feature_C
0 5 0.30 10
1 1 0.10 20
2 8 0.50 30
3 2 0.20 70
4 9 0.60 80
5 3 0.15 90
演示数据 - 训练标签:
[0 0 0 1 1 1]
演示数据 - 测试特征:
feature_A feature_B feature_C
6 4 0.25 40
7 7 0.45 85
演示数据 - 测试标签:
[0 1]import pandas as pd
import numpy as np
from sklearn.ensemble import RandomForestClassifier
from sklearn.metrics import roc_auc_score
# 构造与上节相同的极简演示数据(8 个样本,3 个特征,feature_C 与标签强相关)
X_demo = pd.DataFrame({
'feature_A': [5, 1, 8, 2, 9, 3, 4, 7],
'feature_B': [0.3, 0.1, 0.5, 0.2, 0.6, 0.15, 0.25, 0.45],
'feature_C': [10, 20, 30, 70, 80, 90, 40, 85]
})
y_demo = pd.Series([0, 0, 0, 1, 1, 1, 0, 1])
X_train_demo = X_demo.iloc[:6].copy()
y_train_demo = y_demo.iloc[:6].copy()
X_test_demo = X_demo.iloc[6:].copy()
y_test_demo = y_demo.iloc[6:].copy()
# 初始化随机森林模型
rf_model = RandomForestClassifier(n_estimators=10, random_state=42)
# 1. 计算基准性能(使用原始特征)
rf_model.fit(X_train_demo, y_train_demo)
y_pred_proba_original = rf_model.predict_proba(X_test_demo)[:, 1]
auc_original = roc_auc_score(y_test_demo, y_pred_proba_original)
print(f"基准模型 AUC: {auc_original:.4f}")
# 2. 对每个特征进行乱序并评估
feature_importance_shuffle = {}
for col in X_train_demo.columns:
# 复制数据,避免污染原始数据
X_train_shuffled = X_train_demo.copy()
X_test_shuffled = X_test_demo.copy()
# 打乱当前特征在训练集和测试集中的值
# 注意:原课工具 feature_shuffle_rf 的口径是「只训练一次基准模型,
# 逐个打乱特征列后直接预测(不再重新训练)」,对比 AUC 下降幅度。
# 本演示为展示完整流程,采用「打乱后重新拟合再预测」的变体,
# 两种做法的核心判据相同:重要特征被打乱后 AUC 显著下降。
X_train_shuffled[col] = np.random.RandomState(seed=42).permutation(X_train_shuffled[col].values)
X_test_shuffled[col] = np.random.RandomState(seed=42).permutation(X_test_shuffled[col].values)
# 使用打乱后的特征重新训练和预测
rf_model.fit(X_train_shuffled, y_train_demo)
y_pred_proba_shuffled = rf_model.predict_proba(X_test_shuffled)[:, 1]
auc_shuffled = roc_auc_score(y_test_demo, y_pred_proba_shuffled)
# 计算AUC下降幅度
auc_drop = auc_original - auc_shuffled
feature_importance_shuffle[col] = auc_drop
print(f"特征 '{col}' 打乱后 AUC: {auc_shuffled:.4f}, AUC下降: {auc_drop:.4f}")
# 3. 按重要性排序
print("\n特征重要性排序(AUC下降幅度越大越重要):")
for feat, imp in sorted(feature_importance_shuffle.items(), key=lambda x: x[1], reverse=True):
print(f" {feat}: {imp:.4f}")基准模型 AUC: 1.0000
特征 'feature_A' 打乱后 AUC: 1.0000, AUC下降: 0.0000
特征 'feature_B' 打乱后 AUC: 1.0000, AUC下降: 0.0000
特征 'feature_C' 打乱后 AUC: 0.0000, AUC下降: 1.0000
特征重要性排序(AUC下降幅度越大越重要):
feature_C: 1.0000
feature_A: 0.0000
feature_B: 0.0000结果解读:
- 在我们的演示数据中,
feature_C与标签强相关,打乱其值后模型完全失效(AUC从1.0降至0.0),下降幅度为1.0,表明它非常重要。 feature_A和feature_B与标签无关,打乱它们对模型性能没有影响(AUC下降为0),表明它们在当前任务中不重要。
在实际项目中,课程提供的 feature_shuffle_rf 工具会对所有特征执行此过程,并返回每个特征对应的AUC下降值。我们可以设定一个阈值(如 auc_drop > 0),选择那些对模型性能有实质影响的特征。
5.2 混合法:综合策略的威力
混合法(Hybrid Method)不是一种特定的算法,而是一种工程思想:不局限于单一的特征选择方法,而是根据实际情况,组合使用过滤、包裹、嵌入或混序等方法,以期获得更稳定、更可靠的特征子集。
常见的混合策略包括:
- 先过滤,后包裹:先用方差过滤或相关系数法快速去掉大量明显无关或冗余的特征,再用递归特征消除(RFE)等包裹法在剩余特征中精细筛选。
- 模型重要度 + 实验验证:利用随机森林、XGBoost等模型输出特征重要度排序,再通过混序法验证排名靠前的特征是否真的对性能有显著影响。
- 多模型共识:使用不同类型的模型(如线性模型、树模型)分别进行特征选择,取这些模型共同认可的重要特征。
5.2.1 混合法代码实践:递归特征消除与增加
我们结合课程提供的混合法工具,演示两种常见的包裹式策略:递归特征消除(RFE)和递归特征增加(RFA)。这些工具已封装在 utils.hybrid 模块中。
首先,我们使用 sklearn 内置的 RFE 配合随机森林进行特征选择。
import pandas as pd
import numpy as np
from sklearn.datasets import load_breast_cancer
from sklearn.model_selection import train_test_split
from sklearn.ensemble import RandomForestClassifier
from sklearn.feature_selection import RFE
# 加载数据
data = load_breast_cancer()
df = pd.DataFrame(data.data, columns=data.feature_names)
df['target'] = data.target
# 划分数据集
X_train, X_test, y_train, y_test = train_test_split(df.drop('target', axis=1),
df['target'],
test_size=0.2,
random_state=42)
print("数据准备完毕。")
# 使用RFE选择10个特征
# n_features_to_select 决定停止条件,这里我们指定保留10个特征
selector = RFE(estimator=RandomForestClassifier(n_estimators=20, random_state=42),
n_features_to_select=10)
selector.fit(X_train, y_train)
# 获取被选中的特征
selected_feat_rfe = X_train.columns[selector.support_]
print(f"RFE选出的 {len(selected_feat_rfe)} 个特征:")
print(list(selected_feat_rfe))数据准备完毕。
RFE选出的 10 个特征:
['mean texture', 'mean concave points', 'area error', 'worst radius', 'worst texture', 'worst perimeter', 'worst area', 'worst smoothness', 'worst concavity', 'worst concave points']接下来,我们演示课程自研工具 recursive_feature_elimination_rf 的核心逻辑。该工具与标准RFE略有不同,它根据移除特征后模型性能(AUC)的下降是否超过一个容忍度(tol)来决定是否删除该特征。
import pandas as pd
import numpy as np
from sklearn.ensemble import RandomForestClassifier
from sklearn.metrics import roc_auc_score
# 为了清晰演示,我们使用一个更小的自制数据集
np.random.seed(42)
X_small = pd.DataFrame({
'f1': np.random.randn(8),
'f2': np.random.randn(8),
'f3': np.random.randn(8) * 0.1, # 噪声特征,方差小
'f4': np.array([1,1,1,2,2,2,1,2]), # 与标签有一定关联
})
# 构造标签,与 f4 有较强关系(前 6 行做训练集、后 2 行做测试集,两部分都含两个类别)
y_small = pd.Series([0,0,0,1,1,1,0,1])
X_train_small = X_small.iloc[:6].copy()
y_train_small = y_small.iloc[:6].copy()
X_test_small = X_small.iloc[6:].copy()
y_test_small = y_small.iloc[6:].copy()
print("小型演示数据(训练集):")
print(X_train_small)
print("标签:", y_train_small.values)
print("\n小型演示数据(测试集):")
print(X_test_small)
print("标签:", y_test_small.values)
# 演示递归特征消除的核心逻辑(简化版,非完整工具函数)
def demo_recursive_elimination(X_train, y_train, X_test, y_test, tol=0.001, n_estimators=5):
"""
简化的递归特征消除演示
tol: 容忍度,如果移除某个特征导致的AUC下降小于tol,则移除它。
"""
from sklearn.ensemble import RandomForestClassifier
from sklearn.metrics import roc_auc_score
features_to_keep = list(X_train.columns)
print(f"初始特征数: {len(features_to_keep)}")
# 初始模型(使用所有特征)
model_all = RandomForestClassifier(n_estimators=n_estimators, random_state=42)
model_all.fit(X_train[features_to_keep], y_train)
y_pred_all = model_all.predict_proba(X_test[features_to_keep])[:, 1]
auc_all = roc_auc_score(y_test, y_pred_all)
print(f"初始模型 AUC: {auc_all:.4f}")
features_to_remove = []
for feature in list(features_to_keep): # 遍历当前保留的特征列表副本
# 尝试移除该特征
candidate_features = [f for f in features_to_keep if f not in features_to_remove and f != feature]
if not candidate_features: # 避免特征被删光
continue
model_reduced = RandomForestClassifier(n_estimators=n_estimators, random_state=42)
model_reduced.fit(X_train[candidate_features], y_train)
y_pred_reduced = model_reduced.predict_proba(X_test[candidate_features])[:, 1]
auc_reduced = roc_auc_score(y_test, y_pred_reduced)
auc_drop = auc_all - auc_reduced
print(f" 尝试移除 '{feature}',AUC变为 {auc_reduced:.4f},下降 {auc_drop:.4f}")
if auc_drop < tol: # 下降很小,可以移除
print(f" -> 决定移除 '{feature}'")
features_to_remove.append(feature)
# 更新基准AUC为移除该特征后的AUC
auc_all = auc_reduced
else:
print(f" -> 保留 '{feature}'")
final_features = [f for f in features_to_keep if f not in features_to_remove]
print(f"\n最终保留特征数: {len(final_features)}")
return final_features
# 运行演示
print("\n=== 容忍度 tol = 0.01 ===")
final_kept_features_001 = demo_recursive_elimination(X_train_small, y_train_small, X_test_small, y_test_small, tol=0.01)
print(f"\n最终保留的特征: {final_kept_features_001}")小型演示数据(训练集):
f1 f2 f3 f4
0 0.496714 -0.469474 -0.101283 1
1 -0.138264 0.542560 0.031425 1
2 0.647689 -0.463418 -0.090802 1
3 1.523030 -0.465730 -0.141230 2
4 -0.234153 0.241962 0.146565 2
5 -0.234137 -1.913280 -0.022578 2
标签: [0 0 0 1 1 1]
小型演示数据(测试集):
f1 f2 f3 f4
6 1.579213 -1.724918 0.006753 1
7 0.767435 -0.562288 -0.142475 2
标签: [0 1]
=== 容忍度 tol = 0.01 ===
初始特征数: 4
初始模型 AUC: 1.0000
尝试移除 'f1',AUC变为 1.0000,下降 0.0000
-> 决定移除 'f1'
尝试移除 'f2',AUC变为 1.0000,下降 0.0000
-> 决定移除 'f2'
尝试移除 'f3',AUC变为 1.0000,下降 0.0000
-> 决定移除 'f3'
最终保留特征数: 1
最终保留的特征: ['f4']演示总结:
- 逐一尝试移除特征并观察 AUC 变化:移除噪声特征
f1、f2、f3后 AUC 均保持 1.0000(下降 0.0000,小于容忍度),它们被逐个删除;最终只保留了与标签强关联的f4——它承载了这份数据的全部判别信息(若把它也移除,模型将失去所有依据,这正是工具用空集保护避免的操作)。 - 容忍度
tol是一个关键超参数,它控制了特征选择的严格程度。tol越小,越不容易删除特征;tol越大,特征筛选越激进。
课程提供的 hybrid.recursive_feature_addition_rf 工具则采用相反的思路:从一个特征开始,逐步添加最能提升模型性能的特征,直到添加新特征带来的提升小于容忍度。
📝 动手练一练
练习 1(概念辨析):混序法(shuffle-based)与前向特征选择(逐个添加特征)在”评估一个特征的价值”时,思路有何本质不同?各自的计算代价受什么因素主导?
👉 点击查看参考答案
混序法是”破坏性验证”:训练完成后,把某一列特征值随机打乱再评估模型,性能掉得越多说明该特征贡献越大——它只需要在已训练模型上做预测,不用反复重训。前向选择是”建设性验证”:从零开始逐个尝试加入特征,每加一个都要重新训练模型并评估——训练次数随特征数呈倍数增长。混序法的代价主要在”每个特征一次预测 + 一次基线训练”,前向选择的代价主要在”多轮全量重训”,特征多、模型慢时前向选择明显更贵。
练习 2(动手实算):本节示例中,乱序 feature_C 后 AUC 从基准值明显下降,而乱序 feature_A 影响很小。如果把演示数据中 feature_C 的标签相关性改弱(比如 y_demo 改为 [0, 1, 0, 1]),你预期三个特征的 AUC 降幅排序会怎么变化?为什么?
👉 点击查看参考答案
y_demo 改为 [0, 1, 0, 1] 后,feature_C 的取值 10/20/30/40 对应标签 0/1/0/1,不再单调相关,乱序它带来的信息损失会大幅减小;此时三个特征都几乎没有稳定预测力,各特征的 AUC 降幅都会趋近于零(在 2 个测试样本的极简数据上波动会很大)。这说明混序法度量的是”特征对模型的可预测贡献”,标签与特征的关联强度决定了乱序的代价——也提醒我们:极小样本上的特征重要性结论不可靠,工业实践中要用足够大的数据。
本章小结
本节我们深入学习了两种高级的特征选择方法:
- 混序法:一种直观的实验性方法,通过打乱特征值并观察模型性能下降来评估特征重要性。它不依赖于模型内部结构,解释性强。
- 混合法:一种工程实践思想,强调灵活组合多种特征选择策略(如过滤、包裹、嵌入、混序),以得到更稳健的特征子集。我们重点演示了递归特征消除(RFE)和递归特征增加(RFA)两种包裹式策略的混合应用。
至此,我们已经完成了特征选择模块所有方法的学习。从基于统计的过滤法,到基于模型迭代的包裹法,再到基于正则化的嵌入法,以及本节学习的混序法与混合法,你已经拥有了一个完整的特征选择工具箱。
📋 行动清单
- 运行混序法示例:尝试在课程提供的乳腺癌数据集上完整运行
feature_shuffle_rf,观察所有特征的AUC下降排名。 - 调整混合策略参数:修改递归特征消除/增加演示代码中的容忍度
tol,观察不同严格度下最终保留的特征有何不同。 - 设计自己的混合流程:针对一个你熟悉的数据集,设计一个两步走的混合选择策略(例如:先用相关系数过滤掉相关性极低的特征,再用混序法对剩余特征进行重要性排序)。
—— 小象教研组