图片

第一部分 模型的建立与参数调优

阶段 0: 导入库与环境设置

这个阶段是所有工作的基础,我们加载所有需要的Python库,并进行一些全局的美化设置,以确保代码能顺利运行且生成的图表美观、统一。

阶段 0 代码及注释

python

# 0. 导入库与环境设置
# ===================================================================
import numpy as np # 导入NumPy库,用于高效的数值计算,特别是数组操作。通常简写为np。
import pandas as pd # 导入Pandas库,用于数据处理和分析,核心数据结构是DataFrame。通常简写为pd。
import matplotlib.pyplot as plt # 导入Matplotlib的pyplot模块,用于数据可视化和绘图。通常简写为plt。
import seaborn as sns # 导入Seaborn库,它基于Matplotlib,提供了更高级、更美观的统计图形。通常简写为sns。
import shap # 导入SHAP库,用于模型解释。
import lime # 导入LIME库,是另一种用于模型解释的库。
import lime.lime_tabular # 从LIME库中导入专门用于处理表格数据的模块。
import warnings # 导入warnings模块,用于控制警告信息的输出。
import lightgbm as lgb # 导入LightGBM库,一个高性能的梯度提升框架。通常简写为lgb。

from sklearn.model_selection import train_test_split, GridSearchCV, KFold # 从scikit-learn导入模型选择相关的工具:数据分割、网格搜索和K折交叉验证。
from sklearn.linear_model import LinearRegression # 从scikit-learn导入线性回归模型。
from sklearn.tree import DecisionTreeRegressor # 从scikit-learn导入决策树回归模型。
from sklearn.ensemble import RandomForestRegressor, BaggingRegressor # 从scikit-learn导入集成学习模型:随机森林和Bagging。
from sklearn.metrics import mean_absolute_error, mean_squared_error, r2_score # 从scikit-learn导入用于评估回归模型性能的指标。
from sklearn.inspection import PartialDependenceDisplay # 从scikit-learn导入用于绘制部分依赖图的工具。
from sklearn.preprocessing import StandardScaler # 从scikit-learn导入用于数据标准化的工具。

# --- Matplotlib 和 Seaborn 美化设置 ---
plt.rcParams['font.family'] = 'sans-serif'# 设置Matplotlib的默认字体族为无衬线字体,以避免某些环境下中文或特殊符号显示为方框。
plt.style.use('seaborn-v0_8-whitegrid') # 应用Seaborn的'whitegrid'样式,使图表背景为白色网格,更加美观。
warnings.filterwarnings("ignore") # 忽略所有警告信息。在最终报告中可以这样做,但在开发阶段建议查看警告。
阶段 0 代码作用详解
  • 库导入:

  • numpy 和 pandas 是数据科学的基石,分别负责数值计算和数据框操作。

  • matplotlib 和 seaborn 是数据可视化的核心。

  • shap 和 lime 是用于模型可解释性的高级工具。

  • lightgbm 是我们将要重点使用和优化的核心模型。

  • sklearn (scikit-learn) 是一个综合性的机器学习库,这里我们从中导入了:

  • 数据划分工具 (train_test_split)。

  • 模型调优工具 (GridSearchCV)。

  • 多种基础模型(线性回归、决策树、随机森林等)作为比较基准。

  • 模型评估指标(MAE, MSE, R²)。

  • 模型解释工具(部分依赖图)。

  • 数据预处理工具(标准化)。

  • 环境美化:

  • plt.rcParams['font.family'] = 'sans-serif': 这是一个兼容性设置,确保图表中的文字能正常显示。

  • plt.style.use(...): 统一所有图表的视觉风格,让报告看起来更专业。

  • warnings.filterwarnings("ignore"): 在运行代码时,可能会因为库版本更新等原因产生一些不影响结果的警告信息。这行代码可以隐藏它们,使输出更整洁。

如何应用到其他数据集?
  • 这部分代码是通用的。在任何新项目开始时,你都可以复制这部分代码。

  • 你需要确保你的Python环境中已经安装了所有这些库。如果没有,可以通过 pip 来安装,例如:pip install numpy pandas matplotlib seaborn shap lime lightgbm scikit-learn


阶段 1: 数据加载与初步探查

在这一阶段,我们加载数据集,并使用Pandas的内置函数快速了解数据的基本情况,如数据维度、列名、数据类型、是否有缺失值以及数据的统计摘要。

阶段 1 代码及注释

python

# ===================================================================
# 1. 数据加载与初步探查
# ===================================================================
print("--- 1. 数据加载与初步探查 ---") # 打印阶段标题。
data = pd.read_csv('公众号Python机器学习ml-2025-7-26数据.csv') # 使用pandas的read_csv函数加载名为'数据15.3.csv'的CSV文件到DataFrame `data` 中。
pd.set_option('display.max_columns', None) # 设置pandas的显示选项,确保在打印DataFrame时能显示所有列,而不是用省略号代替。

print("数据信息 (data.info()):") # 打印提示信息。
data.info() # 调用.info()方法,打印DataFrame的简要信息,包括索引类型、列名、非空值数量和数据类型。
print("\n描述性统计 (data.describe()):") # 打印提示信息。
print(data.describe()) # 调用.describe()方法,为数值类型的列生成描述性统计数据,如计数、均值、标准差、最小值、四分位数和最大值。
print("\n数据前5行 (data.head()):") # 打印提示信息。
print(data.head()) # 调用.head()方法,显示DataFrame的前5行,以便直观地查看数据内容和格式。
阶段 1 代码作用详解
  • pd.read_csv('公众号Python机器学习ml-2025-7-26数据.csv'): 这是数据分析的起点。它读取磁盘上的 CSV 文件,并将其转换为 Pandas DataFrame,这是一种功能强大的二维表格数据结构。

  • pd.set_option(...): 当数据集特征(列)很多时,默认情况下 Pandas 会折叠显示。这行代码取消了这个限制,让我们能看到所有列。

  • data.info(): 这是一个非常重要的初步检查步骤。通过输出,我们可以快速知道:

  • 数据有多少行(样本数)和多少列(特征数+目标变量)。

  • 每一列的名称是什么。

  • 每一列有多少非空值(如果Non-Null Count小于总行数,说明有缺失值)。

  • 每一列的数据类型(Dtype),例如 float64(浮点数)或 object(通常是字符串)。

  • data.describe(): 这为我们提供了对数值型特征分布的快速概览。我们可以从中看到:

  • 均值 (mean) 和 中位数 (50%) 的对比,可以初步判断数据是否倾斜。

  • 标准差 (std),了解数据的离散程度。

  • 最小值 (min) 和 最大值 (max),可以发现可能的异常值或数据范围。

  • data.head(): 让我们直观地看到数据的具体值,确认数据是否已正确加载。

如何应用到其他数据集?
  • 将 '公众号Python机器学习ml-2025-7-26数据.csv' 替换为你自己的数据文件名。如果文件不在当前目录,需要提供完整的文件路径(例如 'C:/Users/YourUser/Documents/my_data.csv')。

  • 如果你的数据不是 CSV 格式,Pandas 也提供了其他读取函数,如 pd.read_excel()pd.read_sql() 等。


阶段 2: 探索性数据分析 (EDA)

EDA 是通过可视化的方式深入理解数据的过程。我们通过绘制图表来探索目标变量的分布、特征之间的关系,以及特征与目标变量的关系。

阶段 2 代码及注释

python

# ===================================================================
# 2. 探索性数据分析 (EDA)
# ===================================================================
print("\n--- 2. 探索性数据分析 (EDA) ---") # 打印阶段标题。

# --- 2.1 目标变量分布 ---
plt.figure(figsize=(12, 5)) # 创建一个尺寸为12x5英寸的图形窗口。
plt.subplot(1, 2, 1) # 在1行2列的网格中,选择第1个位置创建子图。
sns.histplot(data.iloc[:, 0], kde=True, bins=30) # 使用Seaborn绘制目标变量(第一列)的直方图和核密度估计曲线。
plt.title(f'Distribution of Target Variable "{data.columns[0]}"') # 设置子图的标题,动态显示目标变量的名称。
plt.xlabel("Value") # 设置X轴标签。
plt.ylabel("Frequency") # 设置Y轴标签。

plt.subplot(1, 2, 2) # 在1行2列的网格中,选择第2个位置创建子图。
sns.boxplot(y=data.iloc[:, 0]) # 使用Seaborn绘制目标变量(第一列)的箱线图。
plt.title(f'Boxplot of Target Variable "{data.columns[0]}"') # 设置子图的标题。
plt.tight_layout() # 自动调整子图参数,使其填充整个图像区域,避免重叠。
plt.savefig('EDA_目标变量分布.png') # 将生成的图像保存为文件。
plt.show() # 显示图像。

# --- 2.2 特征与目标变量的相关性热力图 ---
plt.figure(figsize=(12, 10)) # 创建一个尺寸为12x10英寸的图形窗口。
correlation_matrix = data.corr() # 计算DataFrame中所有数值列之间的皮尔逊相关系数矩阵。
sns.heatmap(correlation_matrix, annot=True, cmap='coolwarm', fmt='.2f', linewidths=.5) # 使用Seaborn绘制热力图来可视化相关系数矩阵。
# annot=True: 在格子上显示数值。cmap='coolwarm': 设置颜色映射。fmt='.2f': 数值格式化为两位小数。
plt.title('Correlation Heatmap of Features and Target') # 设置图像标题。
plt.savefig('EDA_相关性热力图.png') # 保存图像。
plt.show() # 显示图像。

# --- 2.3 查看与目标变量最相关的几个特征的散点图 ---
target_corr = correlation_matrix.iloc[1:, 0].abs().sort_values(ascending=False) # 提取除目标变量自身外的所有特征与目标变量的相关系数,取绝对值并降序排列。
top_features = target_corr.head(3).index # 获取相关性最高的3个特征的名称。

print(f"\n与目标变量最相关的3个特征是: {list(top_features)}") # 打印这3个特征的名称。
plt.figure(figsize=(15, 5)) # 创建一个尺寸为15x5英寸的图形窗口。
for i, feature inenumerate(top_features): # 遍历这3个特征。
plt.subplot(1, 3, i + 1) # 在1行3列的网格中,为每个特征创建一个子图。
sns.scatterplot(x=data[feature], y=data.iloc[:, 0]) # 绘制当前特征与目标变量的散点图。
plt.title(f'"{feature}" vs "{data.columns[0]}"') # 设置子图标题。
plt.xlabel(feature) # 设置X轴标签为当前特征名。
plt.ylabel(data.columns[0]) # 设置Y轴标签为目标变量名。
plt.tight_layout() # 自动调整布局。
plt.savefig('EDA_高相关特征散点图.png') # 保存图像。
plt.show() # 显示图像。
阶段 2 代码作用详解
  • 2.1 目标变量分布:

  • 目的: 了解我们要预测的变量是什么样的。

  • 直方图 (histplot): 显示了目标变量值的频率分布。我们可以看到数据主要集中在哪个区间,分布是否对称(类似正态分布),还是有偏斜。

  • 箱线图 (boxplot): 从另一个角度展示了数据的分布,清晰地标出了中位数、四分位数和潜在的异常值。

  • 2.2 相关性热力图:

  • 目的: 快速发现变量之间的线性关系。

  • data.corr(): 计算出每对变量之间的相关系数(-1到1之间)。

  • sns.heatmap: 将数值矩阵用颜色可视化。暖色(如红色)通常表示正相关,冷色(如蓝色)表示负相关。颜色越深,相关性越强。这有助于我们初步判断哪些特征可能对目标变量有较强的预测能力。

  • 2.3 高相关特征散点图:

  • 目的: 对热力图中发现的最强相关性进行视觉验证。

  • 代码首先自动找出与目标变量相关性最高的3个特征,然后为每个特征绘制散点图。

  • 散点图 (scatterplot): 揭示了两个变量之间的关系模式。我们可以看到关系是线性的、曲线的,还是没有明显关系。

如何应用到其他数据集?
  • 这部分代码是高度自动化的,它假设目标变量在第一列 (data.iloc[:, 0])

  • 如果你的目标变量在其他位置,例如名为 'target' 的列,你需要将 data.iloc[:, 0] 替换为 data['target']

  • 你可以调整 top_features = target_corr.head(3).index 中的数字 3,来查看更多或更少的高相关特征。


图片

图片

图片

阶段 3: 数据准备

在训练模型之前,必须将数据划分为两部分:一部分用于训练模型(训练集),另一部分用于评估模型的性能(测试集)。这可以防止模型过拟合,并得到对模型泛化能力的无偏估计。

阶段 3 代码及注释

python

# ===================================================================
# 3. 数据准备
# ===================================================================
print("\n--- 3. 数据准备 ---") # 打印阶段标题。
X = data.iloc[:, 1:]  # 将除第一列外的所有列作为特征变量X。
y = data.iloc[:, 0]  # 将第一列作为目标变量y。
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.3, random_state=42) # 使用train_test_split函数将数据划分为训练集和测试集。
# test_size=0.3: 将30%的数据划为测试集,70%为训练集。
# random_state=42: 设置随机种子,确保每次划分结果都一样,便于复现实验。
print(f"训练集大小: {X_train.shape}, 测试集大小: {X_test.shape}") # 打印训练集和测试集的形状,确认划分成功。
阶段 3 代码作用详解
  • X = data.iloc[:, 1:] 和 y = data.iloc[:, 0]:

  • 这里再次基于“目标变量在第一列”的假设,将数据分离为特征 X 和目标 yX 是一个DataFrame,包含所有自变量;y 是一个Series,包含因变量。

  • train_test_split(...):

  • 这是机器学习中的一个核心步骤。

  • test_size=0.3 指定了测试集占总数据的比例。30% 是一个常用的比例。

  • random_state=42 是一个“随机种子”。设置它能保证你每次运行代码时,数据划分的结果都是完全一样的。这对于调试和复现结果至关重要。如果不设置,每次划分都会是随机的。

如何应用到其他数据集?
  • 你需要根据你的数据结构修改 X 和 y 的定义。如果你的目标列叫 'price',特征列是 'area' 和 'rooms',你应该这样写:

python

X = data[['area', 'rooms']]
y = data['price']
  • 你可以根据需要调整 test_size,例如 0.2 或 0.25

  • random_state 可以是任何整数。


阶段 4: 基线模型比较

在投入大量时间去优化一个复杂模型之前,先用一些简单的、开箱即用的模型进行快速测试。这为我们提供了一个性能基准(Baseline),任何更复杂的模型都应该要超越这个基准才有价值。

阶段 4 代码及注释

python

# ===================================================================
# 4. 基线模型比较(包含LightGBM)
# ===================================================================
print("\n--- 4. 基线模型比较 ---") # 打印阶段标题。
models = { # 创建一个字典,键是模型名称,值是模型对象实例。
"Linear Regression": LinearRegression(), # 线性回归模型。
"Decision Tree": DecisionTreeRegressor(random_state=42), # 决策树回归模型。
"Random Forest": RandomForestRegressor(random_state=42, n_estimators=100), # 随机森林回归模型。
"LightGBM (Baseline)": lgb.LGBMRegressor(random_state=42, verbose=-1) # 使用默认参数的LightGBM回归模型。
}

results = {} # 创建一个空字典,用于存储每个模型的评估结果。
for name, model in models.items(): # 遍历models字典中的每一个模型。
model.fit(X_train, y_train) # 使用训练数据(X_train, y_train)来训练当前模型。
y_pred = model.predict(X_test) # 使用训练好的模型对测试集X_test进行预测。
r2 = r2_score(y_test, y_pred) # 计算真实值y_test和预测值y_pred之间的R²分数。
mse = mean_squared_error(y_test, y_pred) # 计算均方误差(MSE)。
results[name] = {'R²': r2, 'MSE': mse} # 将模型的R²和MSE结果存入results字典。
print(f"{name} -> R²: {r2:.4f}, MSE: {mse:.4f}") # 打印当前模型的性能指标。

results_df = pd.DataFrame(results).T # 将results字典转换为DataFrame,并转置(.T)使模型名称成为行索引。
fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(15, 6)) # 创建一个1行2列的子图网格。
results_df['R²'].plot(kind='bar', ax=ax1, title='R² Score Comparison of Different Models') # 在第一个子图上绘制R²分数的条形图。
ax1.set_ylabel('R² Score') # 设置y轴标签。
ax1.tick_params(axis='x', rotation=45) # 将x轴的标签旋转45度,防止重叠。

results_df['MSE'].plot(kind='bar', ax=ax2, title='MSE Comparison of Different Models', color='orange') # 在第二个子图上绘制MSE的条形图。
ax2.set_ylabel('MSE') # 设置y轴标签。
ax2.tick_params(axis='x', rotation=45) # 旋转x轴标签。
plt.tight_layout() # 自动调整布局。
plt.savefig('模型比较_性能对比.png') # 保存图像。
plt.show() # 显示图像。

图片

阶段 4 代码作用详解
  • models = {...}: 定义一个包含多个模型的字典,这样做的好处是可以通过一个简单的循环来训练和评估所有模型,代码非常整洁。

  • for 循环: 这是本阶段的核心。它自动化了以下流程:

  1. 取出模型。

  2. 用 .fit() 在训练集上训练。

  3. 用 .predict() 在测试集上预测。

  4. 计算 R² 和 MSE 指标。

  5. 存储并打印结果。

  • 结果可视化:

  • 将结果字典转换为 Pandas DataFrame 是一个非常方便的操作,便于后续处理和绘图。

  • 通过条形图,我们可以非常直观地比较不同模型的性能。R² 越高越好,MSE 越低越好。从图中可以一目了然地看出哪个模型最有潜力。

如何应用到其他数据集?
  • 你可以自由地在 models 字典中添加或删除模型。例如,你可以加入 from sklearn.svm import SVR 并添加 "SVR": SVR() 到字典中。

  • 整个流程是自动的,只要你的 X_trainy_train 等数据准备好了,这部分代码无需修改即可运行。


阶段 5: LightGBM超参数调优

在基线比较中,我们通常会发现像 LightGBM 或 RandomForest 这样的集成模型表现更好。然而,它们的默认参数不一定最适合我们的特定数据集。因此,这一阶段的目标是通过网格搜索交叉验证(Grid Search CV) 来找到 LightGBM 的最佳超参数组合。

阶段 5 代码及注释

python

# ===================================================================
# 5. LightGBM超参数调优
# ===================================================================
print("\n--- 5. LightGBM超参数调优 ---") # 打印阶段标题。

# LightGBM参数网格
param_grid = { # 定义一个字典,作为超参数的搜索空间。
'n_estimators': [100, 200], # 树的数量(迭代次数)。
'max_depth': [-1, 10], # 每棵树的最大深度,-1表示不限制。
'num_leaves': [31, 50], # 每棵树的叶子节点数。
'learning_rate': [0.05, 0.1], # 学习率。
'feature_fraction': [0.8, 0.9], # 每次迭代时随机选择特征的比例。
'bagging_fraction': [0.8, 0.9] # 每次迭代时随机选择数据的比例(不进行重采样)。
}

# 初始化LightGBM模型
lgb_model = lgb.LGBMRegressor(random_state=42, verbose=-1) # 创建一个LightGBM回归器实例。

# 网格搜索
grid_search = GridSearchCV( # 创建一个GridSearchCV对象,用于执行网格搜索和交叉验证。
estimator=lgb_model, # 要调优的模型。
param_grid=param_grid, # 要搜索的参数网格。
cv=5, # 交叉验证的折数,这里是5折。
n_jobs=-1, # 使用所有可用的CPU核心进行并行计算,-1表示使用所有核心。
verbose=1, # 打印详细的运行日志,1表示打印简要日志。
scoring='r2'# 用于评估模型性能的指标,这里选择R²。
)

print("开始LightGBM超参数调优...") # 打印提示信息。
grid_search.fit(X_train, y_train) # 在训练集上执行网格搜索。这会自动进行交叉验证。

# 获取最佳模型
final_model = grid_search.best_estimator_ # 从grid_search结果中获取性能最好的模型实例。
print(f"\n最佳参数组合: {grid_search.best_params_}") # 打印找到的最佳参数组合。
print(f"交叉验证最佳R²得分: {grid_search.best_score_:.4f}") # 打印在交叉验证中得到的最高R²分数。
阶段 5 代码作用详解
  • param_grid: 定义了我们要搜索的参数范围。例如,'n_estimators': [100, 200] 意味着程序会分别测试 n_estimators 为100和200时的模型性能。

  • GridSearchCV:

  • 工作原理: 它会尝试 param_grid 中所有可能的参数组合。对于每一种组合,它会执行 K-折交叉验证(这里 cv=5)。

  • K-折交叉验证: 将训练集分成5份,轮流用其中4份训练模型,1份验证,重复5次。最后取5次验证得分的平均值作为该参数组合的最终得分。这个过程可以防止在调优过程中发生过拟合。

  • scoring='r2': 指定了我们优化的目标是最大化 R² 分数。

  • n_jobs=-1: 极大地加快了搜索速度,特别是当参数网格很大时。

  • grid_search.fit(X_train, y_train): 这是最耗时的一步。它会执行上面描述的整个搜索和验证过程。

  • grid_search.best_estimator_: 训练完成后,GridSearchCV 会自动用找到的最佳参数在整个训练集上重新训练一个模型,我们通过这个属性可以获取到这个最终的、优化好的模型。

如何应用到其他数据集?
  • param_grid 是最需要根据你的具体问题和计算资源来调整的部分。

  • 如果计算时间充裕,可以扩大搜索范围,例如 n_estimators 可以尝试 [100, 200, 300, 500]

  • 如果想快速得到结果,可以缩小范围。

  • cv 的值也可以调整,通常在 3 到 10 之间。

  • scoring 可以换成其他指标,例如 'neg_mean_squared_error' (注意是负数,因为GridSearch默认最大化指标)。


阶段 6: 最终模型评估

在找到最佳模型后,我们需要在从未参与过训练和调优的测试集上进行最后一次评估。这能反映出模型在真实世界新数据上的表现(泛化能力)。

阶段 6 代码及注释

python

# ===================================================================
# 6. 最终模型评估
# ===================================================================
print("\n--- 6. 最终模型评估 ---") # 打印阶段标题。
y_pred = final_model.predict(X_test) # 使用调优后的最终模型对测试集进行预测。

# --- 6.1 性能指标 ---
mae = mean_absolute_error(y_test, y_pred) # 计算平均绝对误差(MAE)。
mse = mean_squared_error(y_test, y_pred) # 计算均方误差(MSE)。
r2 = r2_score(y_test, y_pred) # 计算R²分数。

print(f"测试集 MAE: {mae:.4f}") # 打印MAE。
print(f"测试集 MSE: {mse:.4f}") # 打印MSE。
print(f"测试集 R²: {r2:.4f}") # 打印R²。

# --- 6.2 预测值 vs 真实值散点图 ---
plt.figure(figsize=(10, 8)) # 创建一个10x8英寸的图形窗口。
plt.scatter(y_test, y_pred, alpha=0.6, s=50) # 绘制真实值(y_test)与预测值(y_pred)的散点图。
plt.plot([y_test.min(), y_test.max()], [y_test.min(), y_test.max()], 'r--', lw=2) # 绘制一条y=x的红色虚线作为参考。
plt.xlabel('Actual Values') # 设置X轴标签。
plt.ylabel('Predicted Values') # 设置Y轴标签。
plt.title(f'LightGBM Final Model: Actual vs Predicted (R² = {r2:.4f})') # 设置标题,并动态显示R²分数。
plt.axis('equal') # 设置x,y轴的刻度单位长度相等。
plt.axis('square') # 设置绘图区域为正方形。
# 添加统计信息文本框
plt.text(0.05, 0.95, f'MAE: {mae:.4f}\nMSE: {mse:.4f}\nR²: {r2:.4f}', # 在图的左上角添加性能指标文本。
transform=plt.gca().transAxes, verticalalignment='top', # 设置文本框的位置和对齐方式。
bbox=dict(boxstyle='round', facecolor='wheat', alpha=0.8)) # 设置文本框的样式。
plt.grid(True, alpha=0.3) # 添加半透明的网格。
plt.savefig('LightGBM_真实值vs预测值.png', dpi=300) # 保存图像。
plt.show() # 显示图像。

# --- 6.3 残差分析 ---
residuals = y_test - y_pred # 计算残差(真实值 - 预测值)。
plt.figure(figsize=(15, 5)) # 创建一个15x5英寸的图形窗口。

plt.subplot(1, 3, 1) # 在1行3列的网格中选择第1个位置。
sns.scatterplot(x=y_pred, y=residuals) # 绘制预测值与残差的散点图。
plt.axhline(y=0, color='r', linestyle='--') # 绘制一条y=0的水平参考线。
plt.xlabel('Predicted Values') # 设置X轴标签。
plt.ylabel('Residuals') # 设置Y轴标签。
plt.title('Residual Scatter Plot') # 设置标题。

plt.subplot(1, 3, 2) # 在1行3列的网格中选择第2个位置。
sns.histplot(residuals, kde=True, bins=30) # 绘制残差的直方图。
plt.title('Residual Distribution') # 设置标题。
plt.xlabel('Residual') # 设置X轴标签。

plt.subplot(1, 3, 3) # 在1行3列的网格中选择第3个位置。
from scipy import stats # 导入scipy.stats模块,用于统计分析。
stats.probplot(residuals, dist="norm", plot=plt) # 绘制残差的正态概率图(Q-Q图)。
plt.title('Residual Q-Q Plot') # 设置标题。
plt.tight_layout() # 自动调整布局。
plt.savefig('LightGBM_残差分析.png', dpi=300) # 保存图像。
plt.show() # 显示图像。

图片

图片

阶段 6 代码作用详解
  • 6.1 性能指标: 计算并打印最终模型在测试集上的关键性能指标。这是对模型性能最直接的量化评估。

  • 6.2 预测值 vs 真实值散点图:

  • 这是一个非常直观的评估图。图中的点越是紧密地聚集在红色的 y=x 对角线周围,说明模型的预测就越准确。

  • 将性能指标直接写在图上,使得这张图本身就是一份小型的评估报告。

  • 6.3 残差分析:

  • 残差 (residuals) 是模型预测的误差。分析残差的模式可以帮助我们判断模型是否良好。

  • 残差散点图: 一个好的模型,其残差应该随机分布在 0 线上下,不应有任何明显的模式(如喇叭形、曲线形)。如果出现模式,说明模型可能没有捕捉到数据中的某些信息。

  • 残差直方图 和 Q-Q图: 这两个图用于检验残差是否服从正态分布。对于许多统计模型,这是一个重要的假设。一个钟形的直方图和一条直线状的Q-Q图表明残差是正态的,这是模型表现良好的一个迹象。

如何应用到其他数据集?
  • 这部分代码是完全自动化的。只要前面的步骤正确执行,生成了 final_modelX_test, 和 y_test,这部分代码就可以直接运行,无需任何修改。它会自动计算指标并生成评估图表。

第二部分SHAP可视化解释分析

阶段一:计算 SHAP 值 (核心计算步骤)

这是所有 SHAP 可视化分析的基础。在此阶段,我们为测试集中的每一个样本、每一个特征计算其对应的 SHAP 值。

阶段 7.1 代码及注释

python

# --- 7.1 计算SHAP值 ---
print("正在计算SHAP值...") # 打印提示信息,告知用户正在进行SHAP值的计算。
explainer = shap.TreeExplainer(final_model) # 创建一个针对树模型的SHAP解释器。`final_model`是我们已经训练好的模型(如XGBoost, LightGBM等)。
shap_values = explainer.shap_values(X_test) # 使用解释器计算测试集`X_test`中每个样本的SHAP值。

print(f"SHAP值形状: {shap_values.shape}") # 打印输出SHAP值数组的形状,方便我们理解其结构。
print(f"基准值(expected_value): {explainer.expected_value:.4f}") # 打印解释器的基准值,即模型在整个数据集上的平均预测输出。
阶段 7.1 代码作用详解
  1. print("正在计算SHAP值..."):

    • 作用: 在控制台输出一行文字,提示用户计算过程已经开始。因为 SHAP 值的计算可能需要一些时间,这是一个很好的用户体验实践。

  2. explainer = shap.TreeExplainer(final_model):

    • 作用: 这是 SHAP 分析的第一步:创建解释器(Explainer)

    • shap.TreeExplainer: SHAP 库为不同类型的模型提供了专门优化的解释器。TreeExplainer 是专门为树模型(如 XGBoost, LightGBM, CatBoost, Scikit-learn的 RandomForestClassifier 和 DecisionTreeClassifier)设计的,它的计算速度非常快。

    • final_model: 这是我们传入的、已经训练好的模型对象。解释器会分析这个模型的内部结构(如树的分割点和叶子节点值)来高效计算 SHAP 值。

  3. shap_values = explainer.shap_values(X_test):

    • 正的 SHAP 值: 表示该特征的取值使得模型的预测结果增加

    • 负的 SHAP 值: 表示该特征的取值使得模型的预测结果减小

    • 作用: 这是核心计算步骤。它调用解释器的 shap_values 方法,传入我们想要解释的数据集 X_test

    • 返回值 shap_values: 这是一个 NumPy 数组,其形状通常是 (样本数, 特征数)shap_values[i, j] 代表的是对于第 i 个样本,第 j 个特征的 SHAP 值。这个值表示该特征将此样本的预测值从**基准值(expected_value)**推高或拉低了多少。

  4. print(f"SHAP值形状: {shap_values.shape}"):

    • 作用: 打印 shap_values 数组的维度。例如,如果 X_test 有 1000 个样本和 15 个特征,输出将是 SHAP值形状: (1000, 15)。这有助于我们确认计算是否正确覆盖了所有样本和特征。

  5. print(f"基准值(expected_value): {explainer.expected_value:.4f}"):

    • 作用: 打印模型的基准值。这个值也称为 base value。它代表了模型在没有输入任何特征信息时(或者说,在整个训练数据集上)的平均预测输出。在解释单个样本时,所有特征的 SHAP 值之和加上这个基准值,就精确等于该样本的最终模型预测输出。即:f(x) = expected_value + sum(shap_values_for_x)

如何应用到其他数据集?
  • 将 final_model 替换为你自己训练好的树模型对象。如果你的模型不是树模型(例如,是线性模型或神经网络),你需要使用不同的解释器,如 shap.KernelExplainer 或 shap.DeepExplainer

  • 将 X_test 替换为你想要进行解释的任何数据集(通常是测试集或验证集),它应该是一个 Pandas DataFrame 或 NumPy 数组,且列的顺序和特征与训练时一致。


阶段二:SHAP 摘要图系列 (全局特征分析)

这一阶段通过一系列摘要图,从宏观角度展示特征的重要性以及它们对模型输出的影响模式。

阶段 7.2 代码及注释

python

# --- 7.2 SHAP摘要图系列 ---
fig, axes = plt.subplots(2, 2, figsize=(20, 16)) # 创建一个2x2的子图网格,用于容纳四种不同的SHAP摘要图。

# Beeswarm plot (蜂群图)
plt.subplot(2, 2, 1) # 选择第一个子图位置(左上角)。
shap.summary_plot(shap_values, X_test, plot_type="dot", show=False) # 绘制蜂群图,这是最常用的摘要图类型。
plt.title("SHAP Summary Plot (Beeswarm)", fontsize=14) # 为子图设置标题。

# Bar plot (条形图)
plt.subplot(2, 2, 2) # 选择第二个子图位置(右上角)。
shap.summary_plot(shap_values, X_test, plot_type="bar", show=False) # 绘制条形图,展示全局特征重要性。
plt.title("SHAP Global Feature Importance", fontsize=14) # 为子图设置标题。

# Violin plot (小提琴图)
plt.subplot(2, 2, 3) # 选择第三个子图位置(左下角)。
shap.summary_plot(shap_values, X_test, plot_type="violin", show=False) # 绘制小提琴图,展示SHAP值的分布。
plt.title("SHAP Violin Plot", fontsize=14) # 为子图设置标题。

# Heatmap for top samples (热力图)
plt.subplot(2, 2, 4) # 选择第四个子图位置(右下角)。
shap.plots.heatmap(shap.Explanation(values=shap_values[:20], # 使用前20个样本的SHAP值。
                                    base_values=explainer.expected_value, # 传入基准值。
                                    data=X_test.values[:20], # 传入前20个样本的原始特征值。
                                    feature_names=X_test.columns.tolist()), # 传入特征名称列表。
                   show=False) # 绘制热力图,show=False表示暂时不显示图像。
plt.title("SHAP Heatmap (Top 20 Samples)", fontsize=14) # 为子图设置标题。

plt.tight_layout() # 自动调整子图参数,使之填充整个图像区域。
plt.savefig('SHAP_综合分析.png', dpi=300, bbox_inches='tight') # 将整个图像保存为文件。
plt.show() # 显示绘制好的图像。

图片

阶段 7.2 代码作用详解
  1. fig, axes = plt.subplots(...): 创建一个大的画布 (fig) 和一个包含四个子图 (axes) 的 2x2 网格,方便我们将多个相关的图并列展示。

  2. shap.summary_plot(..., plot_type="dot", ...) (蜂群图):

    • Y轴: 按重要性从上到下排列的特征。

    • X轴: SHAP 值。

    • 每个点: 代表测试集中的一个样本。

    • 颜色: 代表该点对应样本的特征值大小(红色代表高特征值,蓝色代表低特征值)。

    • 作用: 这是信息量最丰富的摘要图。

    • 如何解读:

    • 洞察: 我们可以看到特征值的高低如何影响 SHAP 值。例如,如果某个特征的红点(高值)集中在X轴正半轴,蓝点(低值)集中在负半轴,说明该特征值越高,对模型输出的贡献越正向。

  3. shap.summary_plot(..., plot_type="bar", ...) (条形图):

    • 作用: 展示全局特征重要性

    • 如何解读: 它计算每个特征的 SHAP 值的平均绝对值 (mean(|SHAP value|)), 然后按大小排序。条形越长,代表该特征对模型预测的平均影响越大。这与传统的特征重要性图类似,但它基于 SHAP 值,更加可靠。

  4. shap.summary_plot(..., plot_type="violin", ...) (小提琴图):

    • 作用: 结合了箱线图和密度图的优点,展示每个特征的 SHAP 值的分布情况。

    • 如何解读: 小提琴的宽度表示在该 SHAP 值处的样本密度。这比条形图提供了更多信息,例如,某个特征的影响是对称的,还是主要体现在正向或负向。

  5. shap.plots.heatmap(...) (热力图):

    • X轴: 样本(这里是前20个)。

    • Y轴: 特征。

    • 颜色: 代表 SHAP 值(红色=正向推动,蓝色=负向推动)。

    • 文字: (需要配置)可以显示原始特征值。

    • 作用: 这种图同时可视化了特征值和 SHAP 值,特别适合观察样本间的相似性和模型的决策模式。

    • 如何解读:

    • shap.Explanation(...): 这是 SHAP 库中一个现代化的数据结构,它将 SHAP 值、基准值、原始数据和特征名打包在一起,使得传递给绘图函数更加清晰和不容易出错。

  6. plt.tight_layout()plt.savefig(...)plt.show(): 标准的 Matplotlib 操作,分别用于自动调整布局、保存图片和显示图片。

如何应用到其他数据集?
  • 这部分代码直接使用上一阶段计算出的 shap_values 和 X_test。只要上一阶段配置正确,这里无需任何修改即可运行。

  • 你可以调整热力图中的样本数量,例如将 shap_values[:20] 改为 shap_values[:50] 来观察更多样本。


阶段三:SHAP 依赖图 (特征交互分析)

依赖图展示了单个特征的取值如何影响其自身的 SHAP 值,并且能够通过颜色揭示它与其他特征的交互效应

阶段 7.3 代码及注释

python

# --- 7.3 SHAP依赖图(交互效应分析)---
print("正在生成SHAP依赖图...") # 打印提示信息。
# 选择最重要的特征进行依赖图分析
importances = np.abs(shap_values).mean(0) # 计算每个特征的平均绝对SHAP值,作为其全局重要性。
top_features_indices = np.argsort(importances)[-4:] # 获取重要性排名最高的4个特征的索引。
top_features = X_test.columns[top_features_indices] # 根据索引获取这4个特征的名称。

for feature in top_features: # 遍历这4个最重要的特征。
    plt.figure(figsize=(10, 6)) # 为每个特征创建一个新的图形窗口。
    shap.dependence_plot(feature, shap_values, X_test, interaction_index="auto", show=False) # 绘制依赖图。
    plt.title(f'SHAP Dependence Plot: {feature}') # 设置图形标题。
    plt.tight_layout() # 自动调整布局。
    plt.savefig(f'SHAP_依赖图_{feature}.png', dpi=300, bbox_inches='tight') # 保存当前特征的依赖图。
    plt.show() # 显示当前图形。

图片

图片

图片

图片

阶段 7.3 代码作用详解
  1. importances = np.abs(shap_values).mean(0):

    • 作用: 这行代码计算了每个特征的全局重要性,方法是取其 SHAP 值绝对值的平均值。这与条形摘要图的计算方式一致。

  2. top_features_indices = np.argsort(importances)[-4:]:

    • 作用np.argsort 对重要性值进行排序并返回索引。[-4:] 表示我们取排序后(从小到大)的最后 4 个索引,即最重要性最高的 4 个特征的索引。

  3. top_features = X_test.columns[top_features_indices]:

    • 作用: 使用上一步得到的索引,从 X_test 的列名中提取出最重要的特征名称。

  4. for feature in top_features::

    • 作用: 这是一个循环,对每个选出的重要特征,都执行一次绘图操作。

  5. shap.dependence_plot(feature, shap_values, X_test, interaction_index="auto", ...):

    • 趋势: 观察点的分布趋势,可以看到主特征值变化时,它对模型预测的贡献(SHAP值)是如何变化的(线性、非线性、U型等)。

    • 交互: 观察颜色分布。如果图中存在明显的垂直颜色分离(例如,上半部分主要是红色点,下半部分主要是蓝色点),这表明主特征的效果受到了交互特征的强烈影响。例如,“当交互特征 B 的值很高(红色)时,特征 A 的增加会产生更强的正向影响”。

    • X轴: 主特征的原始值。

    • Y轴: 主特征对应的 SHAP 值。

    • 颜色: 由 interaction_index 决定的交互特征的值。

    • 作用: 绘制核心的依赖图。

    • feature: 要绘制的主特征的名称。

    • shap_valuesX_test: 计算好的 SHAP 值和原始数据。

    • interaction_index="auto": 这是此图的关键!它告诉 SHAP 自动找出与当前 feature 交互作用最强的另一个特征,并用该交互特征的值来为图中的点着色。

    • 如何解读:

    • 洞察:

如何应用到其他数据集?
  • 这部分代码是自动化的。它会自动找出最重要的特征进行分析。你只需要确保 shap_values 和 X_test 是正确的。

  • 如果你想分析特定的特征,可以手动创建一个列表替换 top_features,例如:my_features_to_plot = ['age', 'income', 'education'],然后 for feature in my_features_to_plot:

  • 可以修改 [-4:] 来改变要分析的特征数量,例如 [-6:] 表示分析最重要的 6 个特征。


阶段四:SHAP 瀑布图 (单个样本预测分解)

瀑布图是解释单个预测的绝佳工具。它清晰地展示了从模型的基准值开始,每个特征是如何一步步将预测推向最终结果的。

阶段 7.4 代码及注释

python

# --- 7.4 SHAP瀑布图(单个预测详细分解)---
print("正在生成SHAP瀑布图...") # 打印提示信息。
fig, axes = plt.subplots(1, 2, figsize=(24, 8)) # 创建一个1x2的子图网格,用于并排展示两个样本的瀑布图。

for i, idx inenumerate([0, len(X_test) // 2]):  # 选择第一个样本(索引0)和中间位置的样本进行分析。
    plt.subplot(1, 2, i + 1) # 选择当前的子图位置。
    shap.plots.waterfall(shap.Explanation(values=shap_values[idx], # 提取该样本的所有特征的SHAP值。
                                          base_values=explainer.expected_value, # 传入模型的基准值。
                                          data=X_test.iloc[idx].values, # 提取该样本的原始特征值。
                                          feature_names=X_test.columns.tolist()), # 传入特征名称列表。
                         show=False) # 绘制瀑布图。
    plt.title(f'SHAP Waterfall Plot - Sample {idx if idx >= 0elselen(X_test) + idx}') # 设置子图标题,显示样本索引。
plt.tight_layout() # 自动调整布局。
plt.savefig('SHAP_瀑布图.png', dpi=500, bbox_inches='tight') # 保存图像。
plt.show() # 显示图像。

图片

阶段 7.4 代码作用详解
  1. for i, idx in enumerate([0, len(X_test) // 2]):

    • 作用: 循环遍历一个索引列表。这里我们选择了两个有代表性的样本:第一个样本(索引 0)和位于测试集正中间的样本(索引 len(X_test) // 2)。

  2. shap.plots.waterfall(shap.Explanation(...)):

    • 图的底部是 E[f(x)],即模型的基准值

    • 向上的红色箭头/条块代表具有正 SHAP 值的特征,它们将预测值推高

    • 向下的蓝色箭头/条块代表具有负 SHAP 值的特征,它们将预测值拉低

    • 每个条块的长度代表该特征贡献的绝对大小

    • 所有条块累加后,最终到达图顶部的 f(x),这就是该样本的最终模型预测值

    • values=shap_values[idx]: 传入我们选定样本(索引为idx)的那一行 SHAP 值。

    • base_values=explainer.expected_value: 传入全局基准值。

    • data=X_test.iloc[idx].values: 传入该样本的原始数据值。

    • feature_names=X_test.columns.tolist(): 传入特征名。

    • 作用: 为指定的单个样本绘制瀑布图。

    • shap.Explanation(...): 再次使用 Explanation 对象来封装解释一个样本所需的所有信息。

    • 如何解读:

如何应用到其他数据集?
  • 这部分代码的核心是选择要分析的样本。你可以修改列表 [0, len(X_test) // 2] 来检查任何你感兴趣的样本。例如,你可以放入模型预测错误或预测正确的典型样本的索引,来深入分析原因。

  • 例如,要分析第 100 号样本,可以写成 for idx in [100]:


阶段五:SHAP 力图 (单个样本预测的另一种视角)

力图(Force Plot)提供了另一种紧凑的方式来可视化单个样本的预测分解,它形象地展示了各个特征是“推力”还是“拉力”。

阶段 7.5 代码及注释

python

# --- 7.5 SHAP力图 ---
print("正在生成SHAP力图...") # 打印提示信息。
shap.initjs() # 初始化JavaScript环境,用于在Jupyter等环境中渲染交互式力图(在此处非必需,但为好习惯)。
# 选择几个有代表性的样本
sample_indices = [0, len(X_test) // 4, len(X_test) // 2, 3 * len(X_test) // 4, -1] # 选择分布在测试集中的5个样本。
for i, idx inenumerate(sample_indices): # 遍历这5个样本。
    force_plot = shap.force_plot(explainer.expected_value, # 传入基准值。
                                 shap_values[idx], # 传入当前样本的SHAP值。
                                 X_test.iloc[idx], # 传入当前样本的原始特征值。
                                 matplotlib=True, show=False) # 指定使用Matplotlib后端绘图,并暂时不显示。
    plt.title(f"SHAP Force Plot - Sample {idx if idx >= 0elselen(X_test) + idx}") # 为力图添加标题。
    plt.savefig(f'SHAP_力图_样本_{i}.png', bbox_inches='tight', dpi=300) # 将每个力图保存为单独的文件。
    plt.show() # 显示当前力图。

图片

图片

图片

图片

阶段 7.5 代码作用详解
  1. shap.initjs():

    • 作用: 当你在 Jupyter Notebook 或 JupyterLab 中使用 SHAP 时,这个函数会加载必要的 JavaScript 库,使得力图可以交互式地显示。虽然我们在这里设置了 matplotlib=True 来生成静态图,但保留这行代码是一个好习惯。

  2. sample_indices = [...]:

    • 作用: 定义一个列表,包含我们想要分析的几个样本的索引。这里选择了 0%,25%,50%,75% 和最后一个样本,以获得对整个数据集的不同部分的概览。

  3. shap.force_plot(..., matplotlib=True, ...):

    • 基准值 (base value): 是图中央的分界线。

    • 输出值 (output value): 是最终的预测结果,以粗体显示。

    • 红色部分: 代表具有正 SHAP 值的特征,它们像一股“推力”,将预测结果从基准值向**右(更高)**推。

    • 蓝色部分: 代表具有负 SHAP 值的特征,它们像一股“拉力”,将预测结果从基准值向**左(更低)**推。

    • 每个色块的宽度与其 SHAP 值的绝对大小成正比。

    • 第一个参数 explainer.expected_value 是基准值。

    • 第二个参数 shap_values[idx] 是单个样本的 SHAP 值向量。

    • 第三个参数 X_test.iloc[idx] 是单个样本的原始特征值,用于在图中显示。

    • matplotlib=True: 强制 SHAP 使用 Matplotlib 引擎生成一个静态的、可保存的图片,而不是默认的交互式 JavaScript 图。

    • 作用: 绘制力图。

    • 参数:

    • 如何解读:

如何应用到其他数据集?
  • 与瀑布图类似,你可以自定义 sample_indices 列表,放入任何你感兴趣的样本索引进行分析。


阶段六:SHAP 部分依赖图 (PDP/ICE)

这部分代码使用了 Scikit-learn 的功能来绘制部分依赖图(PDP)和个体条件期望图(ICE)。虽然它们不直接来自 shap 库,但它们与 SHAP 提供的模型解释功能相辅相成,经常一起使用。

  • PDP (Partial Dependence Plot): 显示一个特征对模型平均预测结果的影响。

  • ICE (Individual Conditional Expectation): 显示一个特征对每个单独样本预测结果的影响。

阶段 7.6 代码及注释

python

# --- 7.6 SHAP部分依赖图 ---
print("正在生成SHAP部分依赖图...") # 打印提示信息。
# 选择最重要的特征进行PDP分析
# 注意:这部分我们复用之前计算出的 top_features
from sklearn.inspection import PartialDependenceDisplay # 从sklearn.inspection导入部分依赖图的绘图工具

top_pdp_features = top_features[:2]  # 从最重要的特征中选择前两个进行PDP分析。
for feature in top_pdp_features: # 遍历这两个特征。
    plt.figure(figsize=(10, 6)) # 创建新的图形窗口。
    PartialDependenceDisplay.from_estimator( # 使用from_estimator方法从模型直接创建PDP图。
        final_model, # 我们训练好的模型。
        X_train, # PDP/ICE通常在训练集上计算,以反映模型的学习行为。
        features=[feature], # 指定要分析的特征。
        kind='average', # 'average'表示只绘制平均的部分依赖图(PDP)。
        grid_resolution=50, # 在特征值范围内取50个点进行计算。
        ax=plt.gca() # 在当前Matplotlib坐标轴上绘图。
    )
    plt.title(f'Partial Dependence Plot: {feature}') # 设置标题。
    plt.xlabel(feature) # 设置X轴标签。
    plt.ylabel('Partial Dependence') # 设置Y轴标签。
    plt.grid(True, alpha=0.3) # 添加网格线。
    plt.tight_layout() # 自动调整布局。
    plt.savefig(f'SHAP_PDP_{feature}.png', dpi=300, bbox_inches='tight') # 保存PDP图。
    plt.show() # 显示PDP图。

#同时绘制部分依赖图和个体条件期望图
print("正在绘制部分依赖图和个体条件期望图...") # 打印提示信息。
for feature in top_pdp_features: # 再次遍历这两个特征。
    fig, ax = plt.subplots(figsize=(10, 6)) # 创建新的图形和坐标轴对象。
    PartialDependenceDisplay.from_estimator( # 再次调用绘图函数。
        final_model, # 我们训练好的模型。
        X_train, # 同样使用训练集。
        features=[feature], # 指定要分析的特征。
        kind='both', # 'both'表示同时绘制PDP(平均线)和ICE(个体线)。
        grid_resolution=50, # 同样设置网格分辨率。
        ax=ax # 在指定的坐标轴上绘图。
    )
    ax.set_title(f'Partial Dependence and ICE Plot: {feature}') # 设置标题。
    ax.set_xlabel(feature) # 设置X轴标签。
    ax.set_ylabel('Partial Dependence / ICE') # 设置Y轴标签。
    ax.grid(True, alpha=0.3) # 添加网格线。
    plt.tight_layout() # 自动调整布局。
    plt.savefig(f'SHAP_PDP_ICE_{feature}.png', dpi=300, bbox_inches='tight') # 保存PDP/ICE图。
    plt.show() # 显示PDP/ICE图。

图片

图片

图片

  1. 图片

    from sklearn.inspection import PartialDependenceDisplay:

    • 作用: 导入 Scikit-learn 库中用于绘制 PDP 和 ICE 图的专用类。

  2. PartialDependenceDisplay.from_estimator(...):

    • 作用: 这是一个便捷的工厂方法,它接收一个训练好的模型和数据,自动完成 PDP/ICE 的计算和绘图。

    • X_train: 注意这里使用的是训练集 X_train。PDP/ICE 的理论是基于在数据集的分布上进行积分或求期望,因此使用训练集来代表这个分布是标准做法。

    • kind='average': 只绘制一条粗线,代表 PDP。这条线显示了当目标特征变化时,模型预测值的平均变化情况。

    • kind='both': 绘制多条线。其中粗线是 PDP,而每一条细线代表一个样本的 ICE 曲线。这可以让我们看到特征对不同样本的影响是否存在很大差异(即是否存在异质性效应)。如果所有细线都大致平行,说明特征的影响比较一致;如果细线交叉混乱,说明存在强烈的交互效应。

如何应用到其他数据集?
  • 将 final_model 替换为你自己的模型。这个函数兼容所有 Scikit-learn 接口的模型(有 .predict 或 .predict_proba 方法的)。

  • 将 X_train 替换为你自己的训练数据。

  • 修改 top_pdp_features 列表,放入任何你希望进行 PDP/ICE 分析的特征名称。


完整的 SHAP 分析流程从计算核心的 SHAP 值开始,然后通过摘要图提供了全局的、宏观的特征洞察;接着通过依赖图揭示了特征间的复杂交互;再利用瀑布图和力图对单个样本的预测进行了微观层面的精确分解;最后结合 PDP/ICE 图验证了特征的平均效应和个体效应。

第三部分  LIME 是什么,它与 SHAP 有何不同?

在进入代码之前,我们先理解 LIME 的核心思想以及它和 SHAP 的根本区别。

  • LIME (局部可解释模型无关解释):

    • 核心思想: LIME 认为,即使一个全局模型(如我们的 final_model)非常复杂,但在某个特定样本的周围一个很小的邻域内,它的行为可以用一个简单的、可解释的模型(如线性回归)来近似。

    • 工作方式: LIME 通过在待解释样本周围生成一些“扰动”的新样本,用我们的黑箱模型去预测这些新样本,然后用这些新数据训练一个简单的线性模型。这个线性模型的系数就成了对原始样本的“解释”。

    • 一句话总结: LIME 是用一个简单的局部模型模仿一个复杂的全局模型在某个点上的行为。

  • LIME vs. SHAP 的关键区别:

    • 理论基础: SHAP 基于严谨的博弈论(沙普利值),追求“公平”的贡献分配;LIME 基于局部线性近似,追求“直观”的局部解释。

    • 稳定性: SHAP 的结果通常更稳定,因为它有坚实的数学公理保证。LIME 的结果可能因为随机扰动而产生一些波动。

    • 范围: SHAP 天然地同时提供局部和全局解释。LIME 主要用于局部解释,其全局解释是通过聚合大量局部解释得到的,计算成本高且不如 SHAP 原生。

现在,让我们深入代码,看看 LIME 是如何工作的。


阶段 8: LIME 解释分析

这个阶段我们将使用 LIME 库来解释我们训练好的 final_model。我们将首先初始化解释器,然后对单个样本进行详细分析,最后尝试从局部解释中推断出全局重要性。

阶段 8.1: 初始化 LIME 解释器

这是使用 LIME 的第一步,我们需要创建一个解释器对象,并为其提供我们数据的背景信息。

阶段 8.1 代码及注释

python

# ===================================================================
# 8. LIME解释分析
# ===================================================================
print("\n--- 8. LIME解释分析 ---") # 打印阶段标题。

# --- 8.1 初始化LIME解释器 ---
print("正在初始化LIME解释器...") # 打印提示信息。
lime_explainer = lime.lime_tabular.LimeTabularExplainer( # 创建一个用于表格数据的LIME解释器实例。
    X_train.values, # 传入训练集的特征数据。LIME需要它来了解每个特征的分布,以便生成“逼真”的扰动样本。
    feature_names=X_train.columns, # 传入特征的名称列表。
    class_names=['target'], # 对于回归问题,可以简单地命名目标变量。
    mode='regression', # 必须指定模式为'regression',告诉LIME我们正在处理一个回归问题。
    discretize_continuous=True, # 是否将连续特征离散化。True会把连续值分箱,使解释更易读(例如“当age在30-40之间时”)。
    random_state=42# 设置随机种子,确保LIME在生成扰动样本时的随机过程是可复现的。
)
阶段 8.1 代码作用详解
  • lime.lime_tabular.LimeTabularExplainer(...): 创建一个专门用于处理表格数据(像Excel表格或Pandas DataFrame)的解释器。

  • X_train.values: 这是非常关键的一步。LIME 需要知道你原始数据的分布情况(均值、方差、分位数等),这样它在为一个样本生成局部扰动时,才能创造出“合理”的、与真实数据分布相似的新样本。因此,我们传入训练集数据作为参考。

  • feature_names=X_train.columns: 提供特征列的名称,这样在最终的解释报告中,显示的将是易于理解的特征名,而不是列索引(如 feature_0)。

  • mode='regression': 明确告诉 LIME 我们的任务是回归。如果是分类任务,应设置为 'classification'

  • discretize_continuous=True: 这是一个让解释更人性化的选项。它会将连续的特征(如年龄、收入)自动分箱(例如,分成四分位数)。这样,解释结果可能会显示为 "30 < age <= 40" 对预测的贡献,而不是 "age=35.2" 的贡献,前者通常更易于理解。

  • random_state=42: LIME 的扰动过程包含随机性。设置此参数可以保证每次运行代码时,对于同一个样本,LIME 生成的解释都是一致的。

如何应用到其他数据集?
  • 将 X_train 替换为你自己的训练集 DataFrame。确保它不包含目标变量。

  • 如果你的任务是分类,请将 mode 改为 'classification'

  • 其他参数通常可以保持默认。


阶段 8.2: LIME 单个样本分析

这是 LIME 的核心应用场景:解释模型为什么对某一个特定的样本做出如此的预测。我们定义一个函数来封装这个过程,使其可以方便地对多个样本进行分析。

阶段 8.2 代码及注释

python

# --- 8.2 LIME单个样本分析 ---
defanalyze_sample_with_lime(sample_idx, title_suffix=""): # 定义一个函数,用于分析指定索引的样本。
"""使用LIME分析单个样本"""
    exp = lime_explainer.explain_instance( # 调用解释器的explain_instance方法来生成解释。
        X_test.iloc[sample_idx].values, # 传入我们想要解释的单个样本的数据(必须是numpy数组)。
        final_model.predict, # 传入我们训练好的黑箱模型的预测函数。LIME会反复调用它。
        num_features=len(X_test.columns) # 指定在解释中希望看到的特征数量,这里是所有特征。
    )

# 获取解释结果
    explanation = exp.as_list() # 将解释结果转换为一个列表,每个元素是(特征描述, 贡献值)的元组。
    features = [item[0] for item in explanation] # 提取所有特征描述。
    contributions = [item[1] for item in explanation] # 提取所有特征的贡献值。

# 可视化
    fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(16, 6)) # 创建一个1x2的子图网格。

# 条形图
    colors = ['red'if x < 0else'green'for x in contributions] # 根据贡献值的正负决定条形的颜色(绿色正向,红色负向)。
    bars = ax1.barh(features, contributions, color=colors, alpha=0.7) # 绘制水平条形图展示每个特征的贡献。
    ax1.set_xlabel('LIME Feature Contributions') # 设置x轴标签。
    ax1.set_title(f'LIME Explanation - Sample {sample_idx}{title_suffix}') # 设置图表标题。
    ax1.grid(True, alpha=0.3) # 添加网格线。

# 添加数值标签
for bar, contrib inzip(bars, contributions): # 遍历每个条形及其贡献值。
        width = bar.get_width() # 获取条形的宽度。
        ax1.text(width + (0.01if width >= 0else -0.01), bar.get_y() + bar.get_height() / 2, # 在条形末端添加文本。
f'{contrib:.3f}', ha='left'if width >= 0else'right', va='center') # 设置文本内容、对齐方式。

# HTML版本(在Jupyter中更好看)
    exp.save_to_file(f'LIME_解释_样本_{sample_idx}.html') # 将解释结果保存为一个交互式的HTML文件。

# 展示预测信息
    actual_val = y_test.iloc[sample_idx] # 获取该样本的真实值。
    pred_val = final_model.predict([X_test.iloc[sample_idx].values])[0] # 获取该样本的预测值。

    info_text = f"""Sample {sample_idx} Details: # 准备一个信息字符串。
    Actual value: {actual_val:.4f}
    Predicted value: {pred_val:.4f}
    Error: {abs(actual_val - pred_val):.4f}

    LIME Explanation (Top 5 Features):"""

for i, (feature, contrib) inenumerate(explanation[:5]): # 遍历贡献最大的前5个特征。
        info_text += f"\n{i + 1}. {feature}: {contrib:+.4f}"# 将它们添加到信息字符串中。

    ax2.text(0.05, 0.95, info_text, transform=ax2.transAxes, # 在第二个子图上显示这个信息文本。
             verticalalignment='top', fontsize=10,
             bbox=dict(boxstyle='round', facecolor='lightblue', alpha=0.8)) # 设置文本框样式。
    ax2.axis('off') # 关闭第二个子图的坐标轴。

    plt.tight_layout() # 自动调整布局。
    plt.savefig(f'LIME_解释_样本_{sample_idx}.png', dpi=300, bbox_inches='tight') # 保存生成的图像。
    plt.show() # 显示图像。

return explanation # 返回解释结果列表。


# 分析多个样本
print("正在分析不同类型的样本...") # 打印提示信息。
sample_indices = [0, len(X_test) // 4, len(X_test) // 2, 3 * len(X_test) // 4, -1] # 选择几个有代表性的样本索引。
sample_names = ["首个", "1/4处", "中间", "3/4处", "最后"] # 为这些样本命名。

lime_results = {} # 创建一个空字典来存储所有样本的LIME结果。
for idx, name inzip(sample_indices, sample_names): # 遍历选定的样本。
    real_idx = idx if idx >= 0elselen(X_test) + idx # 处理负数索引(如-1代表最后一个)。
print(f"\n正在分析{name}样本 (索引: {real_idx})") # 打印当前分析的样本信息。
    explanation = analyze_sample_with_lime(real_idx, f"({name} sample)") # 调用我们定义的函数进行分析。
    lime_results[f"样本_{real_idx}"] = explanation # 将结果存入字典。

图片

图片

阶段 8.2 代码作用详解
  • lime_explainer.explain_instance(...): 这是执行LIME解释的核心函数。

    • X_test.iloc[sample_idx].values: 传入要解释的那个样本的特征值。

    • final_model.predict: 传入模型本身的预测函数。LIME会像一个用户一样,不断地用它生成的扰动数据去“询问”这个函数,观察其输出变化。

    • num_features: 控制在解释中显示多少个特征。

  • exp.as_list(): 将 LIME 的解释对象转换成一个易于处理的列表,每个元素是一个元组,包含特征描述(例如 "feature_name <= 10.5")和它对预测的贡献值(即局部线性模型的系数)。

  • 可视化: 代码生成了一个包含两部分的图:

    1. 左侧(条形图): 直观地展示了每个特征对当前样本预测的贡献。绿色条块代表正向贡献(推高预测值),红色代表负向贡献(拉低预测值)。条块越长,贡献越大。

    2. 右侧(文本信息): 汇总了该样本的真实值、预测值、误差,以及贡献最大的几个特征,提供了一个快速的文字摘要。

  • exp.save_to_file(...): LIME 可以生成一个漂亮的 HTML 文件,在浏览器中打开时,可以更详细地看到解释细节。

  • 循环分析: 代码最后选择了一系列分布在测试集不同位置的样本,并循环调用分析函数,从而让我们能看到模型在不同类型样本上的决策行为。

如何应用到其他数据集?
  • 这个函数是通用的。只要你已经初始化了 lime_explainer 并拥有 X_test 和 final_model,就可以直接使用。

  • 你可以修改 sample_indices 列表,放入任何你感兴趣的样本索引进行分析,例如模型预测最准的样本,或预测最不准的样本。


阶段 8.3: LIME 全局特征重要性分析

LIME 本质是局部的,但我们可以通过一个变通的方法来模拟全局重要性:对许多样本进行局部解释,然后取每个特征贡献度的平均绝对值

阶段 8.3 代码及注释

python

# --- 8.3 LIME全局特征重要性分析 ---
print("\n正在进行LIME全局特征重要性分析...") # 打印阶段标题。

# 对测试集的一个子集进行LIME分析(计算开销大,选择前50个样本)
n_samples = min(50, len(X_test)) # 选择分析的样本数,最多50个或测试集总数(取较小者)。
lime_global_importance = {feature: [] for feature in X_test.columns} # 创建一个字典,为每个特征准备一个空列表来存储其贡献值。

print(f"正在对前{n_samples}个样本进行LIME分析...") # 打印提示信息。
for i inrange(n_samples): # 遍历这50个样本。
if i % 10 == 0: # 每处理10个样本,打印一次进度。
print(f"处理进度: {i}/{n_samples}")

    exp = lime_explainer.explain_instance( # 为当前样本生成LIME解释。
        X_test.iloc[i].values,
        final_model.predict,
        num_features=len(X_test.columns)
    )

    explanation = exp.as_list() # 获取解释列表。
    explanation_dict = dict(explanation) # 将解释列表转换为字典,便于查找。

for feature in X_test.columns: # 遍历所有原始特征名。
# 找到匹配的特征名(LIME可能会修改特征名,如'age' -> '30 < age <= 40')
        matching_key = None# 初始化匹配键。
for key in explanation_dict.keys(): # 遍历LIME返回的特征描述。
if feature in key or key in feature: # 如果原始特征名包含在LIME描述中,或反之。
                matching_key = key # 找到了匹配项。
break# 停止搜索。

if matching_key: # 如果找到了匹配。
            lime_global_importance[feature].append(explanation_dict[matching_key]) # 将该贡献值添加到对应特征的列表中。
else: # 如果没找到。
            lime_global_importance[feature].append(0) # 记为贡献值为0。

# 计算平均重要性
lime_avg_importance = {feature: np.mean(np.abs(values)) # 对每个特征,计算其所有贡献值绝对值的平均值。
for feature, values in lime_global_importance.items()}

# 可视化全局重要性对比
fig, (ax1, ax2, ax3) = plt.subplots(1, 3, figsize=(24, 8)) # 创建一个1x3的子图网格。

# SHAP全局重要性 (假设shap_values已在第7部分计算好)
shap_importance = np.abs(shap_values).mean(0) # 计算SHAP的全局重要性。
shap_importance_dict = dict(zip(X_test.columns, shap_importance)) # 将其转换为字典。

# 条形图比较
features_sorted = sorted(shap_importance_dict.keys(), # 根据SHAP重要性对特征进行排序。
                         key=lambda x: shap_importance_dict[x], reverse=True)

shap_vals = [shap_importance_dict[f] for f in features_sorted] # 获取排序后的SHAP重要性值。
lime_vals = [lime_avg_importance[f] for f in features_sorted] # 获取按同样顺序排列的LIME重要性值。

ax1.barh(features_sorted, shap_vals, alpha=0.7, label='SHAP', color='blue') # 绘制SHAP重要性条形图。
ax1.set_xlabel('SHAP Importance') # 设置x轴标签。
ax1.set_title('SHAP Global Feature Importance') # 设置标题。
ax1.grid(True, alpha=0.3) # 添加网格。

ax2.barh(features_sorted, lime_vals, alpha=0.7, label='LIME', color='orange') # 绘制LIME重要性条形图。
ax2.set_xlabel('LIME Importance') # 设置x轴标签。
ax2.set_title('LIME Global Feature Importance') # 设置标题。
ax2.grid(True, alpha=0.3) # 添加网格。

# 相关性散点图
ax3.scatter(shap_vals, lime_vals, alpha=0.7, s=60) # 绘制SHAP重要性与LIME重要性的散点图。
for i, feature inenumerate(features_sorted): # 遍历所有特征。
    ax3.annotate(feature, (shap_vals[i], lime_vals[i]), # 为每个点添加特征名标签。
                 xytext=(5, 5), textcoords='offset points', fontsize=8)

ax3.set_xlabel('SHAP Importance') # 设置x轴标签。
ax3.set_ylabel('LIME Importance') # 设置y轴标签。
ax3.set_title('SHAP vs LIME Importance Correlation') # 设置标题。
ax3.grid(True, alpha=0.3) # 添加网格。

# 计算相关系数
correlation = np.corrcoef(shap_vals, lime_vals)[0, 1] # 计算两种重要性度量的皮尔逊相关系数。
ax3.text(0.05, 0.95, f'Correlation: {correlation:.3f}', # 在图上显示相关系数值。
         transform=ax3.transAxes, verticalalignment='top',
         bbox=dict(boxstyle='round', facecolor='yellow', alpha=0.8))

plt.tight_layout() # 自动调整布局。
plt.savefig('SHAP_vs_LIME_全局重要性对比.png', dpi=300, bbox_inches='tight') # 保存图像。
plt.show() # 显示图像。

图片

阶段 8.3 代码作用详解
  • 计算逻辑:

    1. 选择一个样本子集(因为对所有样本计算LIME开销太大)。

    2. 循环遍历这个子集,为每个样本生成一个LIME解释。

    3. 将每次解释中每个特征的贡献值存储起来。

    4. 最后,对每个特征,计算它在所有解释中贡献值绝对值的平均值,作为其“全局”重要性。

  • 特征名匹配: 由于LIME的离散化会改变特征名(例如 age 变为 35.20 < age <= 48.90),代码中有一段逻辑 if feature in key or key in feature: 来尝试将LIME返回的描述性名称匹配回原始的特征名。

  • 对比可视化:

    • 条形图: 并排展示了由SHAP和LIME计算出的全局特征重要性。我们可以直观地看到两者在排序和量级上是否存在差异。

    • 散点图: 横轴是SHAP重要性,纵轴是LIME重要性。如果两种方法高度一致,那么所有的点应该大致分布在一条过原点的直线上。

    • 相关系数: 精确地量化了两种方法得到的重要性排序的一致性。越接近1,说明一致性越好。

如何应用到其他数据集?
  • 这部分代码是自动化的,但它依赖于第7部分计算出的 shap_values

  • 你可以调整 n_samples 的大小。值越大,LIME的全局重要性结果越稳定,但计算时间也越长。

  • 如果你的特征名非常特殊,特征名匹配逻辑可能需要微调。


阶段 9: SHAP vs LIME 深度对比分析

这个阶段不再是执行复杂的计算,而是对前面得到的结果进行总结、对比和升华,并提供决策建议。它将两种方法的对比从纯代码层面提升到了方法论和实践策略层面。

阶段 9 代码及注释

# 9. SHAP vs LIME 深度对比分析

# 生成最终报告图
fig = plt.figure(figsize=(20, 16)) # 创建一个大的图形窗口。

# 重要性对比 - 使用极坐标创建雷达图
from math import pi # 导入pi用于角度计算。

features_radar = list(features_sorted[:8])  # 选择SHAP认为最重要的前8个特征用于雷达图。
shap_vals_radar = [shap_importance_dict[f] for f in features_radar] # 获取这些特征的SHAP值。
lime_vals_radar = [lime_avg_importance[f] for f in features_radar] # 获取这些特征的LIME值。

# 归一化,使不同量纲的重要性值可以在同一张雷达图上比较。
shap_vals_radar = np.array(shap_vals_radar) / max(shap_vals_radar) # SHAP值除以其最大值。
lime_vals_radar = np.array(lime_vals_radar) / max(lime_vals_radar) # LIME值除以其最大值。

angles = [n / len(features_radar) * 2 * pi for n inrange(len(features_radar))] # 计算每个特征在雷达图上的角度。
angles += angles[:1] # 闭合雷达图,需要将第一个角度追加到末尾。

shap_vals_radar = np.concatenate([shap_vals_radar, [shap_vals_radar[0]]]) # 闭合SHAP值数组。
lime_vals_radar = np.concatenate([lime_vals_radar, [lime_vals_radar[0]]]) # 闭合LIME值数组。

# 创建极坐标子图
ax1 = plt.subplot(2, 2, 1, projection='polar') # 创建一个极坐标(雷达图)子图。
ax1.set_theta_offset(pi / 2) # 设置起始角度。
ax1.set_theta_direction(-1) # 设置角度方向为顺时针。
ax1.plot(angles, shap_vals_radar, 'o-', linewidth=2, label='SHAP', color='blue') # 绘制SHAP的雷达图线。
ax1.fill(angles, shap_vals_radar, alpha=0.25, color='blue') # 填充SHAP区域。
ax1.plot(angles, lime_vals_radar, 'o-', linewidth=2, label='LIME', color='orange') # 绘制LIME的雷达图线。
ax1.fill(angles, lime_vals_radar, alpha=0.25, color='orange') # 填充LIME区域。
ax1.set_xticks(angles[:-1]) # 设置刻度位置。
ax1.set_xticklabels(features_radar) # 设置刻度标签为特征名。
ax1.set_ylim(0, 1) # 设置y轴范围。
ax1.set_title('Feature Importance Radar Chart Comparison', y=1.08) # 设置标题。
ax1.legend(loc='upper right', bbox_to_anchor=(0.1, 0.1)) # 显示图例。
ax1.grid(True) # 显示网格。

# 一致性分析
ax2 = plt.subplot(2, 2, 2) # 创建第二个子图。
ax2.scatter(shap_vals, lime_vals, alpha=0.7, s=80) # 绘制SHAP vs LIME重要性的散点图。
z = np.polyfit(shap_vals, lime_vals, 1) # 计算线性拟合的系数。
p = np.poly1d(z) # 创建一个多项式函数。
ax2.plot(shap_vals, p(shap_vals), "r--", alpha=0.8) # 绘制拟合的趋势线。
ax2.set_xlabel('SHAP Importance') # 设置x轴标签。
ax2.set_ylabel('LIME Importance') # 设置y轴标签。
ax2.set_title(f'Consistency Analysis (R = {correlation:.3f})') # 设置标题,并显示相关系数。
ax2.grid(True, alpha=0.3) # 显示网格。

# 方法比较矩阵
ax4 = plt.subplot(2, 2, 3) # 创建第三个子图。
methods_comparison = np.array([ # 创建一个主观评分矩阵。
    [5, 3, 5, 5, 5, 4, 5],  # SHAP评分: 理论、效率、范围、稳定、可视化、交互、适用性
    [3, 4, 3, 3, 3, 2, 5]   # LIME评分
])

im = ax4.imshow(methods_comparison, cmap='RdYlBu_r', aspect='auto') # 使用热力图可视化评分矩阵。
ax4.set_xticks(range(7)) # 设置x轴刻度。
ax4.set_xticklabels(['Theory', 'Efficiency', 'Scope', 'Stability', 'Visualization', 'Interaction', 'Compatibility'], # 设置x轴标签。
                    rotation=45, ha='right')
ax4.set_yticks([0, 1]) # 设置y轴刻度。
ax4.set_yticklabels(['SHAP', 'LIME']) # 设置y轴标签。
ax4.set_title('Method Comparison Score (1-5 points)') # 设置标题。

# 添加数值标签
for i inrange(2): # 遍历行。
for j inrange(7): # 遍历列。
        text = ax4.text(j, i, methods_comparison[i, j], ha="center", va="center", color="white", fontweight='bold') # 在热力图格子上添加评分数值。

plt.colorbar(im, ax=ax4) # 添加颜色条。
plt.tight_layout() # 自动调整布局。
plt.savefig('SHAP_LIME_终极对比分析.png', dpi=300, bbox_inches='tight') # 保存最终的对比图。
plt.show() # 显示图像

    • 图片

      雷达图: 使用了雷达图来对比两种方法对顶级特征的看法。归一化处理是关键,它消除了两种方法重要性度量的量纲差异,使得形状对比成为可能。

    • 一致性分析图: 这是对之前散点图的增强版,加入了趋势线,更清晰地展示了两种重要性度量之间的线性关系。

    • 方法比较矩阵: 这是一个非常直观的主观评分总结。根据对两种方法的理解,在不同维度上给出了1-5分的评分,并通过热力图可视化。这为快速理解两者长短板提供了“一图胜千言”的效果。

数据,请加微信获取

如果你对类似于这样的文章感兴趣。

欢迎关注、点赞、转发

更多推荐