Kaggle竞赛——心脏病患者预测与可视化

目录

  • 准备工作
  • [1. 特征解释](#1. 特征解释)
  • [2. 生成探索性数据分析报告](#2. 生成探索性数据分析报告)
    • [2.1 数据集导入](#2.1 数据集导入)
    • [2.2 生成数据分析报告](#2.2 生成数据分析报告)
  • [3. 可视化分析](#3. 可视化分析)
    • [3.1 特征相关性分析](#3.1 特征相关性分析)
    • [3.2 患病人数统计](#3.2 患病人数统计)
    • [3.3 特征与是否患病之间的关系](#3.3 特征与是否患病之间的关系)
  • [4. 数据处理](#4. 数据处理)
    • [4.1 定类数据处理](#4.1 定类数据处理)
    • [4.2 独热编码](#4.2 独热编码)
  • [5. 模型搭建](#5. 模型搭建)
    • [5.1 随机森林模型](#5.1 随机森林模型)
    • [5.2 可视化决策树](#5.2 可视化决策树)
    • [5.3 特征重要性分析](#5.3 特征重要性分析)
    • [5.4 模型测试](#5.4 模型测试)
    • [5.5 混淆矩阵与ROC曲线](#5.5 混淆矩阵与ROC曲线)
  • [6. pdpbox数据可视化分析](#6. pdpbox数据可视化分析)
    • [6.1 先验分析](#6.1 先验分析)
    • [6.2 部分依赖图](#6.2 部分依赖图)
      • [6.2.1 单变量PDP图](#6.2.1 单变量PDP图)
      • [6.2.2 二维PDP图](#6.2.2 二维PDP图)
    • [6.3 ICE曲线](#6.3 ICE曲线)
  • [7. shap模型可解释性分析](#7. shap模型可解释性分析)
    • [7.1 计算shap值](#7.1 计算shap值)
    • [7.2 特征重要性](#7.2 特征重要性)
    • [7.3 可视化](#7.3 可视化)
      • [7.3.1 shap值图](#7.3.1 shap值图)
      • [7.3.2 特征交互图](#7.3.2 特征交互图)
    • [7.4 单样本预测的解释可视化](#7.4 单样本预测的解释可视化)
    • [7.5 多样本预测的解释可视化](#7.5 多样本预测的解释可视化)
    • [7.6 shap依赖图](#7.6 shap依赖图)
    • [7.7 部分依赖图](#7.7 部分依赖图)
    • [7.8 决策图](#7.8 决策图)
    • [7.9 分析被错误分类的样本](#7.9 分析被错误分类的样本)
    • [7.10 样本的特征交互关系](#7.10 样本的特征交互关系)
    • [7.11 单个样本分析](#7.11 单个样本分析)
    • [7.12 单个特征分析](#7.12 单个特征分析)

Kaggle中已经没有对应的比赛,所以只能从数据集中划分出测试集以验证模型的准确率。模型仅使用随机森林,本文侧重点在于数据分析的可视化和模型可解释性分析。所使用的python虚拟环境、数据集和代码已打包上传到Gitee,『点击直达』

参考资料:【子豪兄Kaggle】玩转UCI心脏病二分类数据集

准备工作

打包环境,生成依赖文件

bash 复制代码
# 不用先激活环境,通过 --name-- 后的参数指定即可
conda env export --name kaggle --file D:\env\environment.yml
bash 复制代码
# 需要先激活环境
conda env export > D:\env\environment.yml

从依赖文件中创建环境

python 复制代码
conda env create --file environment.yml

在 Jupyter Notebook 中添加虚拟环境

  1. 激活环境:conda activate kaggle
  2. 安装 ipykernel:conda install ipykernel
  3. 将环境添加为 Jupyter 内核: python -m ipykernel install --user --name kaggle --display-name "kaggle"
  4. 选择内核:在 Jupyter Notebook 顶部菜单栏 "Kernel" 的 "Change kernel" 中选择已添加的环境。

1. 特征解释

数据集特征解释:

特征 描述 数值表示
age 年龄
sex 性别 0=女;1=男
cp 心绞痛病史 0=典型心绞痛 ;1=非典型心绞痛;2=无心绞痛;3=无症状
trestbps 静息血压
chol 胆固醇含量
fbs 空腹时是否血糖高 0=否;1=是
restecg 静息时的心电图特征 0=正常;1=ST-T波有异常;2=根据Estes准则,有潜在的左心室肥厚
thalach 最大心率
exang 运动是否会导致心绞痛 0=否;1=是
oldpeak 相对于休息,运动引起的 ST 段抑制
slope 心电图种ST波峰值的坡度 0=上升;1=平坦;2=下降
ca 心脏周边大血管的个数
thal 是否患地中海贫血症 0=未知;1=正常;2=固定缺陷;3=可逆缺陷
target 标签列,是否患心脏病 0=否;1=是

官网特征解释见『Heart Disease』

2. 生成探索性数据分析报告

安装pandas_profiling模块,安装之后使用import ydata_profiling进行导入(模块名 pandas_profiling 在 2023.4.1 被弃用)。生成的探索性数据分析(EDA)报告包含特征分析、缺失值分析、数据分布、相关性分析、重复行分析

python 复制代码
pip install pandas_profiling

2.1 数据集导入

python 复制代码
import pandas as pd
import numpy as np
import ydata_profiling
import matplotlib.pyplot as plt
import seaborn as sns
import warnings
warnings.filterwarnings('ignore')

data = pd.read_csv("D:/Desktop/kaggle数据集/heart-disease/heart-disease.csv")
data.head()

2.2 生成数据分析报告

python 复制代码
profile = ydata_profiling.ProfileReport(data)
profile
# 将报告以html网页形式保存
profile.to_file("D:/Desktop/profile.html")

数据集分析概览:

特征分析:

3. 可视化分析

3.1 特征相关性分析

python 复制代码
#-----------------------------------------------------------------------------------------------------#
# 特征相关性分析
# corr(): 各列之间的相关性矩阵
# annot=True: 显示数值
# square=True: 单元格设置为正方形
# fmt=".1f": 数值保留小数点后一位
#-----------------------------------------------------------------------------------------------------#
plt.figure(figsize=(20,10))
sns.heatmap(data.corr(), annot=True, square=True, fmt=".1f");

3.2 患病人数统计

python 复制代码
# 统计患病与不患病的人数
sns.countplot(x='target', data=data);

3.3 特征与是否患病之间的关系

年龄与是否患病之间的关系:

python 复制代码
#-----------------------------------------------------------------------------------------------------#
# crosstab(data.age, data.target): 创建交叉表,横轴为age,纵轴为target
#-----------------------------------------------------------------------------------------------------#
pd.crosstab(data.age, data.target).plot(kind='bar', figsize=(20, 6))
plt.xlabel("Age")
plt.ylabel("Count");

可以发现,患病的年龄集中在 29-54 岁之间。


不同性别,患心脏病与不患心脏病的分布情况:

python 复制代码
pd.crosstab(data.sex, data.target).plot(kind='bar', figsize=(15,6))
# 标签水平显示
plt.xticks(rotation=0)
plt.legend(['Not Disease','Disease'])
plt.xlabel("Sex(0 = Female, 1=Male)")
plt.ylabel("Count");

由图可知, 女性患心脏病的比例更高。


不同年龄段,不同最大心率,患心脏病与不患心脏病患者的散点分布:

python 复制代码
plt.scatter(x=data.age[data.target==1], y=data.thalach[data.target==1], c='red', label="Disease")
plt.scatter(x=data.age[data.target==0], y=data.thalach[data.target==0], c='blue', label="Not Disease")
plt.legend()
plt.xlabel("Age")
plt.ylabel("Maxium Heart Rate");

由图可知, 最大心率越高,患病的概率越大,患病的年龄集中在 29-54 岁之间。

4. 数据处理

4.1 定类数据处理

数据集中的特征可分为定类、定序、定距、定比四种,区别如下:

特征类型 描述 举例 运算
定类(Norminal) 离散值(不可排序) 颜色、性别(可以是字符串或者数值) 仅可判断是否相等
定序(Ordinal) 离散值(可排序) 满意度评分、学历 定类运算+排序
定距(Interval) 连续值(无绝对零点) 温度(零度并不代表无温度)、年份("0"并不表示无年份) 定序运算+加减
定比(Ratio) 连续纸(有绝对零点) 身高、体重,血压 定距运算+乘除

在pandas种,离散的定类特征列应该是object类型;连续的定距和定比特征应该是int64或者float64类型


需要将数据集中的定类数据由整数编码转为实际的字符串:

python 复制代码
data['sex'][data['sex'] == 0] = 'female'
data['sex'][data['sex'] == 1] = 'male'

data['cp'][data['cp'] == 0] = 'typical angina'
data['cp'][data['cp'] == 1] = 'atypical angina'
data['cp'][data['cp'] == 2] = 'non-anginal pain'
data['cp'][data['cp'] == 3] = 'asymptomatic'

data['fbs'][data['fbs'] == 0] = 'lower than 120mg/ml'
data['fbs'][data['fbs'] == 1] = 'greater than 120mg/ml'

data['restecg'][data['restecg'] == 0] = 'normal'
data['restecg'][data['restecg'] == 1] = 'having ST-T wave abnormality'
data['restecg'][data['restecg'] == 2] = 'left ventricular hypertrophy'

data['exang'][data['exang'] == 0] = 'no'
data['exang'][data['exang'] == 1] = 'yes'

data['slope'][data['slope'] == 0] = 'upsloping'
data['slope'][data['slope'] == 1] = 'flat'
data['slope'][data['slope'] == 2] = 'downsloping'

data['thal'][data['thal'] == 0] = 'unknown'
data['thal'][data['thal'] == 1] = 'normal'
data['thal'][data['thal'] == 2] = 'fixed defect'
data['thal'][data['thal'] == 3] = 'reversable defect'

4.2 独热编码

python 复制代码
# 独热编码
dum_data = pd.get_dummies(data)

将定类的数据先还原之后再用独热编码的好处:很多机器学习模型(如线性回归、逻辑回归等)会对数值型定类数据进行线性假设。如果直接使用序数编码,模型可能会认为 3 比 2 和 1 更大,从而在拟合过程中引入错误的线性关系。这种情况下,需要先将整数值还原回原始的分类标签,再进行独热编码。独热编码之后每个类别都被转换为独立的特征(类型为bool),模型不会再对bool值进行错误的线性假设。

5. 模型搭建

5.1 随机森林模型

python 复制代码
# 划分数据集和标签
y = dum_data['target']
X = dum_data.drop('target', axis=1)

# 划分训练集和测试集
from sklearn.model_selection import train_test_split
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2, random_state=1)
print(f"训练集大小{X_train.shape}")
print(f"训练集大小{X_test.shape}")
python 复制代码
训练集大小(242, 26)
训练集大小(61, 26)
python 复制代码
# 模型搭建
from sklearn.ensemble import RandomForestClassifier

# random_state=100: 在相同的数据和参数下,模型的训练结果(例如生成的决策树、特征重要性、模型性能等)每次运行都相同
model = RandomForestClassifier(max_depth=5, n_estimators=100, random_state=100)
model.fit(X_train, y_train)

5.2 可视化决策树

可视化随机森林中下标为7的决策树。需要在『官网』安装Graphviz(安装时勾选添加路径到环境变量)。

python 复制代码
estimator = model.estimators_[7]
estimator
python 复制代码
DecisionTreeClassifier(max_depth=5, max_features='auto', random_state=638252938)
python 复制代码
feature_names = X_train.columns
y_train_str = y_train.astype("str")
y_train_str[y_train_str == '0'] = 'no disease'
y_train_str[y_train_str == '1'] = 'disease'
python 复制代码
#-----------------------------------------------------------------------------------------------------#
# 可视化决策树
# out_file='tree.dot': 指定输出文件名为 tree.dot,该文件包含决策树的结构
# rounded=True: 使节点边角为圆角
# proportion=True: 显示每个节点的样本比例
# label='root': 为根节点添加标签(不使用该属性的话图会太小,看不清)
# precision=2: 数值保留两位小数
# filled=True: 根据类别使用不同的颜色填充节点
#-----------------------------------------------------------------------------------------------------#
from sklearn.tree import export_graphviz
export_graphviz(estimator, out_file='tree.dot', 
                           feature_names=feature_names,  
                           class_names=y_train_str.values,  
                           rounded=True, proportion=True,
                           label='root',
                           precision=2, filled=True)  

# 使用 Graphviz 的 dot 命令将 tree.dot 文件转换为 PNG 格式
# -Tpng: 指定输出格式为 PNG
# -o tree.png: 指定输出文件名为 tree.png
# -Gdpi=600: 设置输出图像的分辨率为 600 DPI
from subprocess import call
call(['dot', '-Tpng', 'tree.dot', '-o', 'tree.png', '-Gdpi=600'])

from IPython.display import Image
Image(filename='tree.png')

5.3 特征重要性分析

python 复制代码
feature_names = X_test.columns
feature_importances = model.feature_importances_
# np.argsort: 返回数组中元素从小到大的索引
# [::-1]: 反转数组,即从大到小
indices = np.argsort(feature_importances)[::-1]

for index in indices:
    print("feature %s (%f)"%(feature_names[index], feature_importances[index]))
python 复制代码
feature cp_typical angina (0.142222)
feature thalach (0.122956)
feature oldpeak (0.108774)
...

5.4 模型测试

定性结果

python 复制代码
y_pred = model.predict(X_test)
y_pred
python 复制代码
array([0, 1, 0, 0, 0, 0, 0, 1, 0, 1, 1, 0, 1, 0, 0, 1, 1, 0, 1, 1, 1, 0,
       1, 1, 1, 1, 0, 1, 1, 1, 1, 1, 1, 1, 0, 1, 0, 0, 1, 0, 1, 0, 0, 0,
       0, 1, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 1, 0, 0], dtype=int64)

定量结果

python 复制代码
#定量分类(不患心脏病与患心脏病的置信度)
y_proba = model.predict_proba(X_test)
y_proba
python 复制代码
array([[0.81317679, 0.18682321],
       [0.2447506 , 0.7552494 ],
       ...])

5.5 混淆矩阵与ROC曲线

相关概念:

TP(真阳性):实际为正例且被正确预测为正例的样本数

FP(假阳性):实际为反例但被错误预测为正例的样本数

FN(假阴性):实际为正例但被错误预测为反例的样本数

TN(真阴性):实际为反例且被正确预测为反例的样本数


真阳性率:
T P R = T P T P + F N TPR=\frac{TP}{TP+FN} TPR=TP+FNTP

假阳性率:
F P R = F P F P + T N FPR=\frac{FP}{FP+TN} FPR=FP+TNFP

精确率(查准率):
P r e c i s i o n = T P T P + F P Precision=\frac{TP}{TP+FP} Precision=TP+FPTP

召回率(查全率):
R e c a l l = T P T P + F N Recall=\frac{TP}{TP+FN} Recall=TP+FNTP

精确率和召回率的调和平均值:
F 1 − s c o r e = 2 ∗ P r e c i s i o n ∗ R e c a l l P r e c i s i o n + R e c a l l F1-score=\frac{2*Precision*Recall}{Precision+Recall} F1−score=Precision+Recall2∗Precision∗Recall
真阳性率、查准率、召回率、F1-score和ROC面积与分类效果正相关,假阳性率与其负相关。

ROC曲线:横轴是假阳性率(FPR),纵轴是真阳性率(TPR)。ROC曲线关注的是真阳性率和假阳性率之间的平衡,即模型在不同阈值下正确分类正例和负例的能力。曲线所围面积越大,模型的分类性能越好。

AUC(Area Under the Curve):ROC 曲线下的面积,用于衡量模型的整体性能。AUC 值越接近 1,模型性能越好。


通过混淆矩阵分析定性结果:

python 复制代码
#-----------------------------------------------------------------------------------------------------#
# 行为真实标签,列为预测标签,标签顺序为[0,1]
# 例如第二行第一列的4,表示实际为1(患病),预测为0(不患病)
#-----------------------------------------------------------------------------------------------------#
from sklearn.metrics import confusion_matrix
confusion = confusion_matrix(y_test, y_pred,labels=[0,1])
confusion
python 复制代码
array([[21,  9],
       [ 4, 27]], dtype=int64)

通过ROC曲线(受试者工作特征曲线)分析定量结果:

python 复制代码
#-----------------------------------------------------------------------------------------------------#
# y_test: 真实标签
# y_pred_quant: 每个样本属于正类的预测概率(置信度)
# thresholds: 阈值
#-----------------------------------------------------------------------------------------------------#
from sklearn.metrics import roc_curve, auc
y_pred_quant = y_proba[:,1]
fpr, tpr, thresholds = roc_curve(y_test, y_pred_quant)
print("fpr:\n", fpr)
print("tpr:\n", tpr)
print("thresholds:\n", thresholds)
pythonfpr: 复制代码
 [0.         0.         0.         0.06666667 0.06666667 0.1
 0.1        0.16666667 0.16666667 0.2        0.2        0.23333333
 0.23333333 0.26666667 0.26666667 0.3        0.3        0.46666667
 0.46666667 0.6        0.6        1.        ]
tpr:
 [0.         0.03225806 0.35483871 0.35483871 0.4516129  0.4516129
 0.61290323 0.61290323 0.64516129 0.64516129 0.70967742 0.70967742
 0.74193548 0.74193548 0.80645161 0.80645161 0.90322581 0.90322581
 0.96774194 0.96774194 1.         1.        ]
thresholds:
 [1.96089603 0.96089603 0.88384259 0.87940765 0.82902295 0.82220243
 0.71001001 0.69203148 0.65818888 0.6507687  0.63753046 0.63498126
 0.63098892 0.62282083 0.59854867 0.57898037 0.45385261 0.39956627
 0.22940531 0.14913866 0.11592048 0.00841077]

这里的阈值并不是取自预测的概率值。

绘制ROC曲线:

python 复制代码
# 绘制roc曲线,对角线是基线,即模型随机猜测的性能(随机猜测时,样本被分为正例和反例的概率相同,即FPR和TPR相同)
plt.plot(fpr, tpr)
plt.plot([0,1], [0, 1], ls="--", c=".3")
plt.title("ROC curve")
plt.xlabel("FPR(1 - Specificity)")
plt.ylabel("TPR")
plt.grid(True)

6. pdpbox数据可视化分析

使用pdpbox工具包分析数据,需要安装 0.2.0 版本才行(默认会安装 0.3.0 版本,该版本更新了一些函数用法,容易报错!)。『pdpbox官方文档』

python 复制代码
pip install pdpbox==0.2.0 -i https://pypi.tuna.tsinghua.edu.cn/simple 

6.1 先验分析

根据数据的原始分布对特征中不同类别的患病概率进行先验分析,并分析特征之间的相互影响关系。

不同性别下患病的比例:

python 复制代码
from pdpbox import pdp, get_dataset, info_plots

# summary_df: 数据统计信息
fig,axes, summary_df = info_plots.target_plot(
    df = dum_data, feature = 'sex_male', feature_name='sex', target='target'
)
_ = axes['bar_ax'].set_xticklabels(['Female', 'Male'])

不同最大心率下患病的比例:

python 复制代码
fig, axes, summary_df = info_plots.target_plot(
    df = dum_data, feature = 'thalach', feature_name='maximum heart rate', target='target'
)

由图可知,最大心率越大,患心脏病的比例越高。


年龄与最大心率之间的影响关系:

python 复制代码
feat_name1 = 'age'
feat_name2 = 'thalach'
nick_name1 = 'age'
nick_name2 = 'maximum heart rate'

fig, axes, summary_df = info_plots.target_plot_interact(
    df = dum_data, features = [feat_name1, feat_name2], feature_names=[nick_name1, nick_name2], target='target'
)

由图可知,在低年龄段(29-51),最大心率越大,患心脏病的概率越大,在高年龄段则没有联系。

6.2 部分依赖图

部分依赖图(Partial Dependence Plot,PDP)显示了一个或两个特征对模型预测结果的边际影响,部分依赖图可以显示目标和特征之间的关系是线性的、单调的还是更复杂的。PDP反映了某一特征在不同值变化时对模型预测结果的影响,PDP图是ICE曲线的平均。

6.2.1 单变量PDP图

对性别特征的分析:

python 复制代码
# 单变量PDP图(反映了某一特征在不同值变化时对模型预测结果的影响)
fig, axes, summary_df = info_plots.actual_plot(
    model = model, X=X_train, feature='sex_male', feature_name='gender', predict_kwds={}
)

由图可知,当样本的性别由女性变为男性时,患心脏病的概率会降低。这与先验分析基本一致,但是不能以先验分布作为结论,训练得到的模型绘制的数据分布才能作为结论。

对"thalach"(最大心率)特征的分析:

python 复制代码
fig, axes, summary_df = info_plots.actual_plot(
    model = model, X=X_train, feature='thalach', feature_name='maxium_heart_rate', predict_kwds={}
)

由图可知,与先验分析基本吻合。

PDP图的另一种画法:

python 复制代码
pdp_dist = pdp.pdp_isolate(
    model=model, dataset=X_test, model_features=base_features, feature="thalach"
)
fig, axes = pdp.pdp_plot(pdp_dist, "maxium_heart_rate")

6.2.2 二维PDP图

最大心率与大血管数的二维PDP图:

python 复制代码
# 出现ypeError: clabel() got an unexpected keyword argument 'contour_label_fontsize'报错,
# 由图可知,血管越多,最大心率越小,患病的概率越小;血管越少,最大心率越大,患病的概率越大
inter1 = pdp.pdp_interact(
    model = model, dataset=X_test, model_features=base_features, features=['thalach', 'ca']
)
fig, axes = pdp.pdp_interact_plot(
    pdp_interact_out=inter1, feature_names=['maxium_heart_rate','num_vessels'], plot_type='contour'
)

运行代码时会出现报错:

python 复制代码
ypeError: clabel() got an unexpected keyword argument 'contour_label_fontsize'

需将D:\Program Files (x86)\anaconda3\envs\kaggle\Lib\site-packages\pdpbox\pdp_plot_utils.py中的

python 复制代码
inter_ax.clabel(c2, contour_label_fontsize=fontsize, inline=1)

改为

python 复制代码
inter_ax.clabel(c2, fontsize=fontsize, inline=1)

运行结果:

slope_flat 和 oldpeak 的二维PDP图:

python 复制代码
inter1 = pdp.pdp_interact(
    model = model, dataset=X_test, model_features=base_features, features=['slope_flat', 'oldpeak']
)
fig, axes = pdp.pdp_interact_plot(
    pdp_interact_out=inter1, feature_names=['slope_flat','oldpeak'], plot_type='contour'
)

6.3 ICE曲线

ICE(Individual Conditional Expectation,个体条件期望)曲线通过展示单个实例中指定特征变化时预测目标如何变化,‌来提供模型预测的局部解释。‌与PDP不同,‌PDP显示的是特征的平均影响,‌而ICE则关注于单个实例,‌提供更细致的预测变化分析。‌ICE曲线的数量与样本数量相同,‌每条曲线代表一个样本的预测变化,‌从而揭示了模型预测的异质关系。‌

性别特征的ICE曲线:

python 复制代码
#-----------------------------------------------------------------------------------------------------#
# 绘制ICE曲线(将测试集中每个样本的pdp图单独绘制出来)
#-----------------------------------------------------------------------------------------------------#
base_features = dum_data.columns.values.tolist()
base_features.remove("target")
pdp_dist = pdp.pdp_isolate(
    model=model, dataset=X_test, model_features=base_features, feature="sex_male"
)
# plot_pts_dist为True时会报错,绘制不了数据点的分布情况
fig, axes = pdp.pdp_plot(pdp_dist, "gender", plot_lines=True, frac_to_plot=0.2, plot_pts_dist=False)

年龄特征的ICE曲线:

python 复制代码
pdp_dist = pdp.pdp_isolate(
    model=model, dataset=X_test, model_features=base_features, feature="age"
)
fig, axes = pdp.pdp_plot(pdp_dist, "Age", plot_lines=True)

7. shap模型可解释性分析

Shapley值 :是一种衡量每个特征对模型预测的边际贡献的方法,计算复杂度高,适合用于小规模数据和理论分析。
SHAP值 :是基于Shapley值的实际实现,通过高效的近似算法能够在大规模数据和复杂模型中应用,提供统一的解释框架和丰富的可视化工具。
SHAP越大,代表该特征对模型预测的贡献越大,特征越重要。


shap工具包的直观作用如下图,机器学习模型通常都是黑盒,即对于给定的输入特征和模型预测输出结果,我们并不知道哪些特征对预测结果的影响最大或者特征对预测结果有哪些贡献(正向贡献或者负向贡献)。而shap工具包赋予机器学习模型可解释性,打破机器学习模型的黑盒,让我们清楚地知道特征是如何影响模型的预测结果。『shap官方文档』

7.1 计算shap值

计算测试集每个样本的每个特征对两类预测结果的shap值:

python 复制代码
# 导入shap机器学习可解释性分析工具包
import shap
# 初始化绘图环境
shap.initjs()

explainer = shap.TreeExplainer(model)
shap_values = explainer.shap_values(X_test)

61个样本,26个特征,对"患病"预测结果的shap值:

python 复制代码
shap_values[1].shape
python 复制代码
(61, 26)
python 复制代码
shap_values[1]
python 复制代码
array([[-0.01991538, -0.01593832,  0.00940888, ...,  0.00085783,
        -0.04006538,  0.00045913],
       [ 0.00745719,  0.00438513,  0.01242003, ...,  0.00164105,
        -0.05582202,  0.00023585],
       [ 0.00459926,  0.00017691, -0.01073604, ...,  0.00109371,
        -0.04040455,  0.00014795],
       ...,
       [ 0.03977282,  0.00520192,  0.00404147, ...,  0.00234018,
        -0.02645105,  0.00021561],
       [-0.0194963 , -0.00737674, -0.02220931, ..., -0.01112436,
         0.01327783,  0.00010092],
       [-0.02583896, -0.05348468, -0.02133602, ...,  0.00105833,
         0.02569354,  0.00020135]])
python 复制代码
# 测试集中的所有样本,预测为"不患病"和"患病"各自的平均概率
expected_value = explainer.expected_value
expected_value
python 复制代码
array([0.45190083, 0.54809917])

对某个样本,模型预测为"患病"的概率等于测试集"患病"的平均概率与该样本各特征对"患病"预测结果的shap值之和。

python 复制代码
y_pred_proba_shap = shap_values[1].sum(axis=1)+explainer.expected_value[1]

该值与定量预测结果的第二列(患病概率)的值一致。

7.2 特征重要性

对于某个特征,计算测试集中所有样本的该特征的shap值之和(shap值越高特征越重要),即为该特征的重要性。

python 复制代码
shap.summary_plot(shap_values[1], X_test, plot_type='bar')

7.3 可视化

7.3.1 shap值图

各特征的数值大小与shap值的关系图:

python 复制代码
shap.summary_plot(shap_values[1], X_test)

图中每一行表示一个特征,红色表示该特征的值较高的数据点,蓝色值表示该特征的值较低的数据点。越靠右的点表示这个特征对预测为"患病"的正向影响越高(shap值越大),越靠左shap值越小(红蓝点能区分的情况下)。

由图可知,典型心绞痛(cp_typical angina)的值越大,shap值越小,对模型预测患病有负向贡献。最大心率(thalach)的值越大,shap值越大,对模型预测患病有正向贡献。


小提琴图展示数据分布情况:

python 复制代码
shap.summary_plot(shap_values[1], X_test, plot_type='violin')

7.3.2 特征交互图

特征交互图(观察哪些特征组合对模型预测影响最大):

python 复制代码
#-----------------------------------------------------------------------------------------------------#
# 对角线与上述关系图一致,其他两两特征之间没有明显的影响
# shap_interaction_values: 是一个矩阵,形状为 (样本数, 特征数, 特征数),其中每个元素表示两个特征的交互值
#-----------------------------------------------------------------------------------------------------#
shap_interaction_values = explainer.shap_interaction_values(X_test)
shap.summary_plot(shap_interaction_values[1], X_test)

7.4 单样本预测的解释可视化

取测试集中的第四个样本(在原始数据集上的索引为174),绘制"患病"预测结果的各特征的shap值:

python 复制代码
idx = 174
patient = X.iloc[idx]
shap.summary_plot(shap_interaction_values[1][3], X_test, plot_type="bar")

df.loc[0:2, 'A']: 基于标签(行和列的名称),表示选择索引(并不一定是下标,例如Series中的索引可以无序)为 0 到 2 的行和标签为 'A'的列,返回一个Series 。
df.iloc[0:2, 0]:基于下标位置,选择第 0 和 1 行,第一列,返回一个Series。
df.loc[0:1]: 选择第 0 和 1 行,返回一个DataFrame。
df.iloc[0:1]: 选择第 0 行, 返回一个DataFrame。
注:df.iloc[0:2, :]等价于df.iloc[0:2];df.iloc[2, :]等价于df.iloc[2]。

绘制力导向图(各特征之间的贡献情况):

python 复制代码
shap_values_patient = explainer.shap_values(patient)
shap.force_plot(explainer.expected_value[1], shap_values_patient[1], patient)

红色表示正向贡献,蓝色表示负向贡献,越长表示shap值越高,即特征影响越大。红蓝条之间的长度差距即为base value(平均结果)到最终预测结果(f(x))之间的差距。

python 复制代码
print('{}号病人是否患病 {} ,模型预测患病概率为 {:.2f}'.format(idx, bool(y_test[idx]), y_proba[3][1]))
python 复制代码
174号病人是否患病 False ,模型预测患病概率为 0.02

绘制瀑布图(展示了某个病人从测试集平均结果到最终预测结果的决策过程,以及各特征对预测结果的贡献影响):

python 复制代码
# 创建 Explanation 对象
# 注:data参数的值为DataFrame时会报错,为Series时不会报错。该参数可省略。
shap_values_exp = shap.Explanation(values=shap_values_patient[1], base_values=explainer.expected_value[1], data=patient)

# 绘制瀑布图
shap.waterfall_plot(shap_values_exp, max_display=15)

7.5 多样本预测的解释可视化

python 复制代码
#-----------------------------------------------------------------------------------------------------#
# 多样本预测的解释可视化(将测试集所有样本的力导向图旋转九十度并拼接在一起)
# 可以在下拉菜单选择按照相似性聚类展示、按照预测结果概率从大到小展示、按照测试集原本样本顺序、按照某个特征分别展示
# 相似性排序:查看相似的病人都具有那些特征
#-----------------------------------------------------------------------------------------------------#
number_show = 60
shap_values_summary = explainer.shap_values(X_test.iloc[:number_show])
shap.force_plot(explainer.expected_value[1], shap_values_summary[1], X_test.iloc[:number_show])

7.6 shap依赖图

用于展示模型对于给定特征的依赖情况。

展示thalach特征从小变大时对预测结果为"患病"的shap值的变化情况:

python 复制代码
shap.dependence_plot("thalach", shap_values[1], X_test, interaction_index=None)

7.7 部分依赖图

展示thalach特征从小变大时模型预测结果,纵轴表示实际预测结果(概率),两条虚线是均值。

python 复制代码
shap.partial_dependence_plot("thalach", model.predict, X_test, model_expected_value=True, feature_expected_value=True)

由图可知,心率越大,预测的结果越大(患心脏病概率越大)。

7.8 决策图

瀑布图只能展示单个数据的决策过程,决策图可以展示测试集所有数据的决策过程。所有预测过程都从底部均值出发。

python 复制代码
shap.decision_plot(expected_value[1], shap_values[1], X_test)

使用层次聚类(hierarchical clustering)来排列特征,以便更好地展示特征之间的关系:

python 复制代码
# 查看典型决策路径,方便分析异常点
shap.decision_plot(expected_value[1], shap_values[1], X_test, feature_order='hclust')

7.9 分析被错误分类的样本

挑选出测试集中模型预测错误的样本,一共13个样本分类错误:

python 复制代码
misclassified = y_pred != y_test
misclassified_df = misclassified[misclassified==True]
misclassified_df
python 复制代码
278    True
302    True
228    True
150    True
189    True
95     True
138    True
182    True
283    True
188    True
299    True
259    True
110    True
Name: target, dtype: bool

选出278号病人详细分析:

python 复制代码
mis_patient = X.iloc[278:279]
patient_proba = model.predict_proba(mis_patient)
print('{}号病人是否患病 {} ,模型预测患病概率为 {:.2f}'.format(278, bool(y_test[278]), patient_proba[0][1]))

shap_values_patient = explainer.shap_values(mis_patient)
shap.force_plot(explainer.expected_value[1],shap_values_patient[1], mis_patient)
python 复制代码
278号病人是否患病 False ,模型预测患病概率为 0.88

在决策图中标记(点画线)出测试集中模型预测错误的样本:

python 复制代码
# highlight=misclassified: 如果 misclassified 是布尔数组,数组中为 True 的位置对应的样本将被突出显示(点画线形式)
shap.decision_plot(expected_value[1], shap_values[1], highlight=misclassified)

只查看分类错误的样本:

7.10 样本的特征交互关系

绘制测试集中下标为5的样本的特征之间的交互关系:

python 复制代码
plt.figure(figsize=(10, 10))
sns.heatmap(shap_interaction_values[1][5], annot=True, fmt=".1f", square=True);

7.11 单个样本分析

绘制25号病人某一特征变化时对模型分类结果的影响:

python 复制代码
idx = 25
X_test.iloc[idx]
python 复制代码
age                                      38.0
trestbps                                138.0
chol                                    175.0
thalach                                 173.0
oldpeak                                   0.0
ca                                        4.0
sex_female                                0.0
sex_male                                  1.0
cp_asymptomatic                           0.0
cp_atypical angina                        0.0
cp_non-anginal pain                       1.0
cp_typical angina                         0.0
fbs_greater than 120mg/ml                 0.0
fbs_lower than 120mg/ml                   1.0
restecg_having ST-T wave abnormality      1.0
restecg_left ventricular hypertrophy      0.0
restecg_normal                            0.0
exang_no                                  1.0
exang_yes                                 0.0
slope_downsloping                         1.0
slope_flat                                0.0
slope_upsloping                           0.0
thal_fixed defect                         1.0
thal_normal                               0.0
thal_reversable defect                    0.0
thal_unknown                              0.0
Name: 164, dtype: float64
python 复制代码
# 选取最大心率特征
feature_selected ="thalach"

# 将数据集中最小心率和最大心率之间的长度分为200份
sep = 200
feature_selected_min = dum_data[feature_selected].min()
feature_selected_max = dum_data[feature_selected].max()

step = (feature_selected_max-feature_selected_min)/sep
rg = np.arange(feature_selected_min, feature_selected_max, step)

# 生成一个数组,其中 idx 被重复 200 次
# drop=True 表示不保留旧的索引列,而是创建一个新的默认整数索引
R = dum_data.iloc[np.repeat(idx, len(rg))].reset_index(drop=True)
R = R.drop("target", axis=1)
# 只改变选中列特征的值,其他特征不变
R[feature_selected] = rg

hypothetical_shap_values = explainer.shap_values(R)[1]
shap.dependence_plot(feature_selected, hypothetical_shap_values, R, interaction_index=None)

绘制25号病人thalach特征的决策图,选取3个样本(thalach特征值不同):

python 复制代码
shap.decision_plot(expected_value[1], hypothetical_shap_values[[10, 100, 199]], X_test.iloc[idx])

7.12 单个特征分析

python 复制代码
# 找出各样本在"ca特征下的shap值
shap_ca = shap_values[1][:, X_test.columns.get_loc("ca")]
# 找出最大值和最小值,由结果可知最小值的绝对值更大
shap_max = np.max(shap_ca)
shap_min = np.min(shap_ca)
print(f"最大值:{shap_max}, 最小值:{shap_min}")
python 复制代码
最大值:0.08015789240556535, 最小值:-0.1500817362994171

找出全部测试集中ca特征对哪个样本的影响最大(shap绝对值最大),可以分析受影响较大的这一类人具有什么相同的特征:

python 复制代码
max_imp = np.where(shap_old == shap_min)
print('{}号病人是否患病: {},模型预测患病概率为 {:.2f}'.format(max_imp[0][0], bool(y_test.iloc[58]), y_proba[58][1]))
python 复制代码
33号病人是否患病: True,模型预测患病概率为 0.65

绘制该病人的瀑布图:

python 复制代码
max_imp_patient = X_test.iloc[max_imp[0][0]]
shap_values_imp = explainer.shap_values(max_imp_patient)
# 创建 Explanation 对象
# 注:data参数的值为DataFrame时会报错,为Series时不会报错。该参数可省略。
exp_imp = shap.Explanation(values=shap_values_imp[1], base_values=explainer.expected_value[1], data=max_imp_patient)
# 绘制瀑布图
shap.waterfall_plot(exp_imp)

绘制决策图:

python 复制代码
shap.decision_plot(expected_value[1], shap_values_imp[1], max_imp_patient)
相关推荐
SpongeG42 分钟前
数据结构第三周做题总结_链表
数据结构·算法·链表
everyStudy44 分钟前
前端五种排序
前端·算法·排序算法
little redcap3 小时前
第十九次CCF计算机软件能力认证-乔乔和牛牛逛超市
数据结构·c++·算法
muyierfly3 小时前
34.贪心算法1
算法·贪心算法
D11_4 小时前
Pandas缺失值处理
python·机器学习·数据分析·numpy·pandas
luthane6 小时前
python 实现average mean平均数算法
开发语言·python·算法
静心问道6 小时前
WGAN算法
深度学习·算法·机器学习
杰九6 小时前
【算法题】46. 全排列-力扣(LeetCode)
算法·leetcode·深度优先·剪枝
manba_6 小时前
leetcode-560. 和为 K 的子数组
数据结构·算法·leetcode
liuyang-neu6 小时前
力扣 11.盛最多水的容器
算法·leetcode·职场和发展