本文为 Python 60 Days Challenge 课程学习整理,原作者与原 Notebook 链接见「课程资料」。代码按章节顺序运行;语法示意和故意报错的反例已单独标注。

数据准备: 将课程仓库中的 data.csv/data.xlsx 放在 Notebook 的当前工作目录,路径已移除原作者电脑的盘符。读取 Excel 需要 openpyxl。各节额外依赖按文中安装说明准备。

文中保留的截图、性能表和数值属于原讲义的历史示例,不是本次修订后的重新训练结果。实际输出受数据、版本、随机种子和训练预算影响。

本次校订: 特征筛选只拟合训练数据。若需要用交叉验证选择筛选参数,也必须把填补、编码和筛选一起放到每折的 Pipeline 中;不能先用整个训练集筛好特征再声称 CV 完全独立。SHAP 数组已兼容新旧返回形状。

打卡任务

  • 阅读本节讲义或 Notebook
  • 跑通示例代码
  • 整理 3 个关键点
  • 完成当日打卡输出

课程资料

学习内容

先运行之前的代码

import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
import seaborn as sns
from sklearn.base import clone
from sklearn.compose import ColumnTransformer, make_column_selector
from sklearn.impute import SimpleImputer
from sklearn.pipeline import Pipeline
from sklearn.preprocessing import OneHotEncoder
from sklearn.model_selection import train_test_split, cross_val_score, cross_validate
from sklearn.ensemble import RandomForestClassifier
from sklearn.metrics import classification_report, confusion_matrix, precision_score, recall_score

data = pd.read_csv('data.csv').drop(columns=['Id'], errors='ignore')
# 标签不能作为特征或用填补器补造;这里只排除缺失标签的记录。
data = data.dropna(subset=['Credit Default'])
preprocessor = ColumnTransformer([
    ('numeric', SimpleImputer(strategy='median', keep_empty_features=True),
     make_column_selector(dtype_include=np.number)),
    ('categorical', Pipeline([
        ('imputer', SimpleImputer(strategy='most_frequent', keep_empty_features=True)),
        ('onehot', OneHotEncoder(handle_unknown='ignore', sparse_output=False))]),
     make_column_selector(dtype_exclude=np.number))
])

def make_classifier(model):
    # 把填补和编码放进模型流水线,每个交叉验证折只在本折训练数据上拟合。
    return Pipeline([('preprocess', clone(preprocessor)), ('model', model)])
X = data.drop(columns=['Credit Default'])
y = data['Credit Default']
X_train, X_test, y_train, y_test = train_test_split(
    X, y, test_size=0.2, random_state=42, stratify=y)
# 本节不做交叉验证;预处理器仅在训练集拟合。
X_train_raw, X_test_raw = X_train.copy(), X_test.copy()
train_array = preprocessor.fit_transform(X_train_raw)
names = preprocessor.get_feature_names_out()
X_train = pd.DataFrame(train_array, columns=names, index=X_train_raw.index)
X_test = pd.DataFrame(preprocessor.transform(X_test_raw), columns=names, index=X_test_raw.index)
from sklearn.ensemble import RandomForestClassifier #随机森林分类器

from sklearn.metrics import accuracy_score, precision_score, recall_score, f1_score # 用于评估分类器性能的指标
from sklearn.metrics import classification_report, confusion_matrix #用于生成分类报告和混淆矩阵
import warnings #用于忽略警告信息
# 保留警告,便于发现弃用接口和数值问题。 # 忽略所有警告信息
# --- 1. 默认参数的随机森林 ---
# 评估基准模型,这里确实不需要验证集
print("--- 1. 默认参数随机森林 (训练集 -> 测试集) ---")
import time # 这里介绍一个新的库,time库,主要用于时间相关的操作,因为调参需要很长时间,记录下会帮助后人知道大概的时长
start_time = time.time() # 记录开始时间
rf_model = RandomForestClassifier(random_state=42)
rf_model.fit(X_train, y_train) # 在训练集上训练
rf_pred = rf_model.predict(X_test) # 在测试集上预测
end_time = time.time() # 记录结束时间

print(f"训练与预测耗时: {end_time - start_time:.4f} 秒")
print("\n默认随机森林 在测试集上的分类报告:")
print(classification_report(y_test, rf_pred))
print("默认随机森林 在测试集上的混淆矩阵:")
print(confusion_matrix(y_test, rf_pred))

过去电脑性能比较差,特征数目太多计算起来很慢。同时特征中可能存在很多冗余特征干扰解释性、存在噪声特征干扰精度。

所以在面对高维特征的时候常常需要引入特征降维,我们之前课程中的项目的特征也就小几十个,不太需要做降维,对于某些特征较多的数据,如基因数据、微生物数据、传感器数据等,特征较多,所以会考虑特征降维。

特征降维一般有2种策略:

  1. 特征筛选:从n个特征中筛选出m个特征,比如方差筛选,剔除方差过小的特征;利用皮尔逊相关系数筛选;lasso筛选(lasso自带的系数可以理解为重要性)、利用树模型自带的重要性、shap重要性等筛选;特征递归方法
  2. 特征组合:从n个特征中组合出m个特征,如pca等

今天这节先说一下特征筛选

特征筛选

方差筛选

方差筛选是一种简单而有效的特征筛选方法。它的核心逻辑是:特征的方差反映了数据的变化程度,方差很小的特征几乎没有变化,对模型的预测帮助不大。比如,一个特征的值在所有样本中几乎都一样(方差接近0),那么它对区分不同类别或预测结果几乎没有贡献。因此,方差筛选会设定一个方差阈值,剔除方差低于这个阈值的特征,保留那些变化较大的特征,从而减少特征数量,提高模型效率。

这种方法特别适合处理高维数据,能快速去掉不重要的特征,但它不考虑特征与目标变量之间的关系,可能会误删一些低方差但有意义的特征。

# 打印标题,表明这是方差筛选的部分
print("--- 方差筛选 (Variance Threshold) ---")

# 导入需要的工具库
from sklearn.feature_selection import VarianceThreshold  # 方差筛选工具,用于剔除方差小的特征
import time  # 用于记录代码运行时间,方便比较效率

# 记录开始时间,后面会计算整个过程耗时
start_time = time.time()

# 创建方差筛选器,设置方差阈值为0.01
# 阈值是指方差的最小值,低于这个值的特征会被删除(可以根据数据情况调整阈值)
selector = VarianceThreshold(threshold=0.01)

# 对训练数据进行方差筛选,fit_transform会计算每个特征的方差并剔除不满足阈值的特征
# X_train是原始训练数据,X_train_var是筛选后的训练数据
X_train_var = selector.fit_transform(X_train)

# 对测试数据应用同样的筛选规则,transform会直接用训练数据的筛选结果处理测试数据
# X_test是原始测试数据,X_test_var是筛选后的测试数据
X_test_var = selector.transform(X_test)

# 获取被保留下来的特征名称
# selector.get_support()返回一个布尔值列表,表示哪些特征被保留,这个是selector这个实例化的类的一个方法
# X_train.columns是特征的名称,结合布尔值列表可以提取保留特征的名字
selected_features_var = X_train.columns[selector.get_support()].tolist()

# 打印筛选后保留的特征数量和具体特征名称,方便查看结果
print(f"方差筛选后保留的特征数量: {len(selected_features_var)}")
print(f"保留的特征: {selected_features_var}")

# 创建一个随机森林分类模型,用于在筛选后的数据上进行训练和预测
# random_state=42是为了保证每次运行结果一致,方便教学和对比
rf_model_var = RandomForestClassifier(random_state=42)

# 在筛选后的训练数据上训练模型
# X_train_var是筛选后的特征数据,y_train是对应的目标标签
rf_model_var.fit(X_train_var, y_train)

# 使用训练好的模型对筛选后的测试数据进行预测
# X_test_var是筛选后的测试特征数据,rf_pred_var是预测结果
rf_pred_var = rf_model_var.predict(X_test_var)

# 记录结束时间,计算整个训练和预测过程的耗时
end_time = time.time()
print(f"训练与预测耗时: {end_time - start_time:.4f} 秒")

# 打印模型在测试集上的分类报告,展示模型的性能
# 分类报告包括精确率、召回率、F1分数等指标,帮助评估模型好坏
print("\n方差筛选后随机森林在测试集上的分类报告:")
print(classification_report(y_test, rf_pred_var))

# 打印混淆矩阵,展示模型预测的详细结果
# 混淆矩阵显示了真实标签和预测标签的对应情况,比如多少样本被正确分类,多少被错分
print("方差筛选后随机森林在测试集上的混淆矩阵:")
print(confusion_matrix(y_test, rf_pred_var))

皮尔逊相关系数筛选

皮尔逊相关系数筛选是一种基于特征与目标变量之间相关性的特征选择方法。它的核心逻辑是:计算每个特征与目标变量之间的相关系数(范围在-1到1之间,值越大表示正相关越强,值越小表示负相关越强,接近0表示几乎无关),然后根据相关系数的绝对值大小,选择与目标变量相关性较高的特征,剔除相关性较低的特征。这种方法适用于目标变量是连续型的情况(如果是分类问题,可以先对目标变量编码)。通过皮尔逊相关系数筛选,我们可以保留那些对预测目标最有帮助的特征,减少无关或冗余特征的干扰。

皮尔逊相关系数筛选法是一种基于变量相关性的经典特征选择技术,常用于处理目标变量为连续型的场景。若面对分类问题,通常需要先对目标变量进行编码处理,将其转化为数值型数据后再开展分析。

print("--- 皮尔逊相关系数筛选 ---")
from sklearn.feature_selection import SelectKBest, f_classif
import time

start_time = time.time()

# 计算特征与目标变量的相关性,选择前k个特征(这里设为10个,可调整)
# 注意:皮尔逊相关系数通常用于回归问题(连续型目标变量),但如果目标是分类问题,可以用f_classif
k = 10
selector = SelectKBest(score_func=f_classif, k=k)
X_train_corr = selector.fit_transform(X_train, y_train)
X_test_corr = selector.transform(X_test)

# 获取筛选后的特征名
selected_features_corr = X_train.columns[selector.get_support()].tolist()
print(f"皮尔逊相关系数筛选后保留的特征数量: {len(selected_features_corr)}")
print(f"保留的特征: {selected_features_corr}")

# 训练随机森林模型
rf_model_corr = RandomForestClassifier(random_state=42)
rf_model_corr.fit(X_train_corr, y_train)
rf_pred_corr = rf_model_corr.predict(X_test_corr)

end_time = time.time()
print(f"训练与预测耗时: {end_time - start_time:.4f} 秒")
print("\n皮尔逊相关系数筛选后随机森林在测试集上的分类报告:")
print(classification_report(y_test, rf_pred_corr))
print("皮尔逊相关系数筛选后随机森林在测试集上的混淆矩阵:")
print(confusion_matrix(y_test, rf_pred_corr))

lasso筛选(基于L1正则化)

Lasso回归(Least Absolute Shrinkage and Selection Operator)是一种结合特征选择和模型训练的方法。它的核心逻辑是:在进行线性回归的同时,通过引入L1正则化项(即惩罚项),强制将一些不重要特征的回归系数压缩到0,从而实现特征筛选。换句话说,Lasso会自动“挑选”对预测目标有贡献的特征(系数不为0),而剔除无关或冗余的特征(系数为0)。这种方法特别适合处理高维数据,可以减少特征数量,提高模型的解释性和计算效率。

print("--- Lasso筛选 (L1正则化) ---")
from sklearn.linear_model import Lasso
from sklearn.feature_selection import SelectFromModel
import time

start_time = time.time()

# 使用Lasso回归进行特征筛选
lasso = Lasso(alpha=0.01, random_state=42)  # alpha值可调整
selector = SelectFromModel(lasso)
selector.fit(X_train, y_train)
X_train_lasso = selector.transform(X_train)
X_test_lasso = selector.transform(X_test)

# 获取筛选后的特征名
selected_features_lasso = X_train.columns[selector.get_support()].tolist()
print(f"Lasso筛选后保留的特征数量: {len(selected_features_lasso)}")
print(f"保留的特征: {selected_features_lasso}")

# 训练随机森林模型
rf_model_lasso = RandomForestClassifier(random_state=42)
rf_model_lasso.fit(X_train_lasso, y_train)
rf_pred_lasso = rf_model_lasso.predict(X_test_lasso)

end_time = time.time()
print(f"训练与预测耗时: {end_time - start_time:.4f} 秒")
print("\nLasso筛选后随机森林在测试集上的分类报告:")
print(classification_report(y_test, rf_pred_lasso))
print("Lasso筛选后随机森林在测试集上的混淆矩阵:")
print(confusion_matrix(y_test, rf_pred_lasso))

这个时候要注意,lasso本质上是回归模型,实际上用这个方法来筛选也是用回归模型对分类问题建模结束了,然后打印特征重要度,她是把0和1目标变量视为连续值来进行回归的。效果会差一点,不符合逻辑,但是确实可以计算

树模型重要性

print("--- 树模型自带的重要性筛选 ---")
from sklearn.feature_selection import SelectFromModel
import time

start_time = time.time()

# 使用随机森林的特征重要性进行筛选
rf_selector = RandomForestClassifier(random_state=42)
rf_selector.fit(X_train, y_train)
selector = SelectFromModel(rf_selector, threshold="mean")  # 阈值设为平均重要性,可调整
X_train_rf = selector.transform(X_train)
X_test_rf = selector.transform(X_test)

# 获取筛选后的特征名
selected_features_rf = X_train.columns[selector.get_support()].tolist()
print(f"树模型重要性筛选后保留的特征数量: {len(selected_features_rf)}")
print(f"保留的特征: {selected_features_rf}")

# 训练随机森林模型
rf_model_rf = RandomForestClassifier(random_state=42)
rf_model_rf.fit(X_train_rf, y_train)
rf_pred_rf = rf_model_rf.predict(X_test_rf)

end_time = time.time()
print(f"训练与预测耗时: {end_time - start_time:.4f} 秒")
print("\n树模型重要性筛选后随机森林在测试集上的分类报告:")
print(classification_report(y_test, rf_pred_rf))
print("树模型重要性筛选后随机森林在测试集上的混淆矩阵:")
print(confusion_matrix(y_test, rf_pred_rf))

SHAP重要性筛选

print("--- SHAP重要性筛选 ---")
import shap
from sklearn.feature_selection import SelectKBest
import time

start_time = time.time()

# 使用随机森林模型计算SHAP值
rf_shap = RandomForestClassifier(random_state=42)
rf_shap.fit(X_train, y_train)
explainer = shap.TreeExplainer(rf_shap)
shap_values = explainer.shap_values(X_train)

# 计算每个特征的平均SHAP值(取绝对值的平均)
# SHAP 新版本返回 (样本, 特征, 类别),旧版本返回每类一个数组的列表。
if isinstance(shap_values, list):
    positive_shap = shap_values[1]
else:
    values = np.asarray(shap_values)
    positive_shap = values[:, :, 1] if values.ndim == 3 else values
mean_shap = np.abs(positive_shap).mean(axis=0)
k = 10  # 选择前10个特征,可调整
top_k_indices = np.argsort(mean_shap)[-k:]
X_train_shap = X_train.iloc[:, top_k_indices]
X_test_shap = X_test.iloc[:, top_k_indices]

# 获取筛选后的特征名
selected_features_shap = X_train.columns[top_k_indices].tolist()
print(f"SHAP重要性筛选后保留的特征数量: {len(selected_features_shap)}")
print(f"保留的特征: {selected_features_shap}")

# 训练随机森林模型
rf_model_shap = RandomForestClassifier(random_state=42)
rf_model_shap.fit(X_train_shap, y_train)
rf_pred_shap = rf_model_shap.predict(X_test_shap)

end_time = time.time()
print(f"训练与预测耗时: {end_time - start_time:.4f} 秒")
print("\nSHAP重要性筛选后随机森林在测试集上的分类报告:")
print(classification_report(y_test, rf_pred_shap))
print("SHAP重要性筛选后随机森林在测试集上的混淆矩阵:")
print(confusion_matrix(y_test, rf_pred_shap))

递归特征消除RFE

递归特征消除(Recursive Feature Elimination, 简称RFE)是一种特征选择方法,广泛用于机器学习中,特别是在分类和回归问题中,用于从一组特征中筛选出对模型性能贡献最大的子集。RFE的核心思想是通过递归地移除最不重要的特征,逐步缩小特征集,直到达到预设的特征数量或满足其他停止条件。

print("--- 递归特征消除 (RFE) ---")
from sklearn.feature_selection import RFE
import time

start_time = time.time()

# 使用随机森林作为基础模型进行RFE
base_model = RandomForestClassifier(random_state=42)
rfe = RFE(base_model, n_features_to_select=10)  # 选择10个特征,可调整
rfe.fit(X_train, y_train)
X_train_rfe = rfe.transform(X_train)
X_test_rfe = rfe.transform(X_test)

# 获取筛选后的特征名
selected_features_rfe = X_train.columns[rfe.support_].tolist()
print(f"RFE筛选后保留的特征数量: {len(selected_features_rfe)}")
print(f"保留的特征: {selected_features_rfe}")

# 训练随机森林模型
rf_model_rfe = RandomForestClassifier(random_state=42)
rf_model_rfe.fit(X_train_rfe, y_train)
rf_pred_rfe = rf_model_rfe.predict(X_test_rfe)

end_time = time.time()
print(f"训练与预测耗时: {end_time - start_time:.4f} 秒")
print("\nRFE筛选后随机森林在测试集上的分类报告:")
print(classification_report(y_test, rf_pred_rfe))
print("RFE筛选后随机森林在测试集上的混淆矩阵:")
print(confusion_matrix(y_test, rf_pred_rfe))

上面这些方法的计算耗时没意义,目的是筛选出最后用的特征,可以看到只保留这几个特征效果仍然很好。说明可以在未来的建模中减少计算资源