案例背景
在信贷风控场景下,其实模型的可解释性就变得很重要。在平时做一些普通的机器学习的案例的时候,我们根本不关心这些变量是怎么究竟影响到模型最后的决策的,随便直接把数据丢进去,再把要预测的数据丢进去就能出结果。但是在信贷模型下,每一个决策都得很谨慎,不然的话会造成极大的损失。因此需要把握好模型的对每一个特征的可解释性。
模型的决策结果肯定是依赖于数据的,而数据就是不同的一组组的特征,因此很多解释性就是去寻找这些特征对模型最后的预测结果的影响。
而sklearn自带的模型它由于机器学习大多数都是黑箱过程,所以很难有其可解释性,这个时候就得借助一些其他的第三方库,例如shap ,scorecardpy。这些包可以比较好的去观察特征对模型决策的影响。
这些可解释性不仅是在业务层面具有较好的应用,让决策者更放心的使用这些特征去做决策,更重要的是它可以以此来衍生特征,构建特征工程优化模型的性能。或者是作为风控策略的一些较好的策略筛选模板,因此它的应用性和其功能都是必要的。
有的同学肯定会说基于树的模型不是最后都能输输出一个模型重要决策分吗?这个分数就是根据损失函数的下降程度来评估的,但是这个分数只能说明这个特征在模型里面很重要,并不能说明这个特征具体的取值到底在模型的决策中起到了较大还是较小,正向还是负向的影响。
也没有办法解释单个样本或者某些样本为什么被预测错误?例如有的样本是正类样本,但是模型给他一个非常低的概率。很明显模型错的有些离谱,但是我们没有办法去找到为什么模型预测错了,到底是哪些特征的数值让他产出了这么一个错误的结果?而且有的模型在一个数据集上表现的效果比较好,换一个数据集效果就非常差了,这是为什么呢?都是需要探索的,这些都是可解释性所需要去做的。
数据集介绍
真实场景的数据肯定都拿不到,都是每个企业的机密,也是个人隐私信息的机密,泄露了就要律师函警告了。所以本次就使用kaggle上的一个信贷是否违约的数据进行一个演示,我们目标是寻找一定的特征可解释性。
数据长这个样子:
数据的每个变量的解释如下:
变量名称 | 中文含义 | 变量类型 |
---|---|---|
Home Ownership | 房屋所有权 | 分类变量 |
Annual Income | 年收入 | 数值变量 |
Years in current job | 目前工作的年限 | 分类变量 |
Tax Liens | 税收留置权 | 数值变量 |
Number of Open Accounts | 开设账户数量 | 数值变量 |
Years of Credit History | 信用历史年限 | 数值变量 |
Maximum Open Credit | 最大可用信用额度 | 数值变量 |
Number of Credit Problems | 信用问题数量 | 数值变量 |
Months since last delinquent | 上次逾期至今的月数 | 数值变量 |
Bankruptcies | 破产次数 | 数值变量 |
Purpose | 贷款目的 | 分类变量 |
Term | 贷款期限 | 分类变量 |
Current Loan Amount | 当前贷款金额 | 数值变量 |
Current Credit Balance | 当前信用余额 | 数值变量 |
Monthly Debt | 月负债 | 数值变量 |
Credit Score | 信用评分 | 数值变量 |
Credit Default | 信用违约 | 二元变量 |
当然,需要本次案例数据和全部代码文件的同学可以参考:信贷违约
代码实现
shap和scorecardpy这两个库都是依赖这些常见的数据分析包所建立起来的,他们安装都很简单,直接pip install就可以了。
导入所有的包
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
import seaborn as sns
import scorecardpy as sc
import shap
plt.rcParams ['font.sans-serif'] ='SimHei' #显示中文
plt.rcParams ['axes.unicode_minus']=False #显示负号
读取数据,分别读取测试集跟训练集,并且打印它们的形状,展示前五行。
train=pd.read_csv('train.csv').set_index('Id')
test=pd.read_csv('test.csv').set_index('Id')
print(train.shape,test.shape)
train.head()
可以看到训练集是17列,测试集是16列,测试集是少了响应变量y这一列,所以我们要对它进行预测。
查看一下数据的信息
train.info()
可以看到数据有缺失值,也有浮点数整形数,还有一些类别变量是文本型的。
查看一下类别平衡的概率。
train['Credit Default'].value_counts()
还好,虽然它是一个不平衡的样本,但是远没有达到极端的不平衡的情况,白样本5000多个,黑样本2000多个,其比例不均衡,但是5:2也也还好,是可以直接放入模型训练的,不需要做一些什么过采样采样的操作。
我们查看一下非数值型的数据的描述性统计。
train.select_dtypes(exclude=['number']).describe()
可以看到这4个文本型的变量,他们基本都是类别变量。唯一取值都不超过15个。所以是可以直接用于作为类别变量的。
其实除了独立热编码和因子化的顺序编码外,还有很多种编码的方法。例如信贷场景里面常用的就是woe编码,我也没有测试过哪种编码的方式比较好,但是在通用的角度来说的话,还是就原始的信息直接放入训练是最好的,因此我不会对这些文本变量去做一些数值型的映射。也不会对缺失值进行填充。为了尽可能的保留数据原始的信息,所以我会直接将这些文本变量给它变成类别变量。Pandas里面是有这种内置的 类和方法。
def ensure_category_consistency(train, test, columns):
for col in columns:
# 找到所有可能的类别
categories = pd.Categorical(train[col]).categories #.append(test[col])
# 定义一致的类别顺序
train[col] = pd.Categorical(train[col], categories=categories)
test[col] = pd.Categorical(test[col], categories=categories)
# 转换为category数据类型
train[col] = train[col].astype('category')
test[col] = test[col].astype('category')
return train, test
### 类别变量转化
columns_to_convert = train.select_dtypes(exclude=['number']).columns.to_list()
train, test = ensure_category_consistency(train, test, columns_to_convert)
上面就是自定义一个函数,然后将测试集跟训练集都进行了同样的类别编码。
我们可以再度查看这个数据的信息。
train.info()
可以看到这4个字符串变量变成了类别变量,缺失值也并未进行填充。
数据到这里就算预处理好了,但有的同学很不理解,这确实是没填充类别变量也没数值化,怎么才能训练?其实大多数的传统的逻辑回归或者是向量机模型他们都不支持这种数据带缺失或者是文本型进行训练和预测。
但是树模型尤其是现在最新的lightgbm跟XGboost是完全可以直接进行训练的,如果说做机器学习还不知道这两个方法,那只能说是新手中的新手了。。。基本上工业界只会用这些方法,他们对比梯度提升,adaboost等传统的bagging模型优势在于更快,更强,优化了更多特征选择分享训练损失的处理。而且可以保留数据的原始信息,不用进行一些乱七八糟的填充和编码,会让解释性变得更好,噪音更少。
其实我感觉最大的优势是快吧,你像工业级的数据都是海量的,上百万的,不用lightgbm,用随机森林怎么跑得动。。。
机器学习
还是一样,我们放入4个评价指标,自定义函数,准确率,精准度,召回率,f1值作为分类问题的评价指标。
from sklearn.metrics import confusion_matrix
from sklearn.metrics import classification_report
from sklearn.metrics import cohen_kappa_score
def evaluation(y_test, y_predict):
accuracy=classification_report(y_test, y_predict,output_dict=True)['accuracy']
s=classification_report(y_test, y_predict,output_dict=True)['weighted avg']
precision=s['precision']
recall=s['recall']
f1_score=s['f1-score']
#kappa=cohen_kappa_score(y_test, y_predict)
return accuracy,precision,recall,f1_score #, kappa
然后取出x跟y划分训练集跟测试集,这里的测试集应该叫验证集。因为最后的测试题是不带y的,我们要对它进行预测。
#划分训练集和测试集
X=train.iloc[:,:-1] ; y=train.iloc[:,-1]
from sklearn.model_selection import train_test_split
X_train,X_test,y_train,y_test=train_test_split(X,y,stratify=y,test_size=0.2,random_state=0)
导入模型并且实例化。
from xgboost.sklearn import XGBClassifier
from lightgbm import LGBMClassifier
#极端梯度提升
model7 = XGBClassifier(objective='binary:logistic',random_state=0,enable_categorical=True)
#轻量梯度提升
model8 = LGBMClassifier(objective='binary',random_state=1,verbose=-1)
model_list=[model7,model8]
model_name=['极端梯度提升','轻量梯度提升']
因为这一次数据没有给他标准的处理的问题嘛,就保留的原始信息,所以说其他的模型也就不做对比了,因为放入训练会报错。只使用这两个模型。而且基本上其他的模型应该也效果不可能超过这两个模型,大家如果自己有兴趣可以试一试。
循环遍历两个模型,然后计算每一个模型上预测出来的数据的评价指标。并用数据框进行储存。
df_eval=pd.DataFrame(columns=['Accuracy','Precision','Recall','F1_score'])
for i in range(len(model_list)):
model_C=model_list[i]
name=model_name[i]
print(f'{name}在训练中.....')
model_C.fit(X_train, y_train)
pred=model_C.predict(X_test)
#s=classification_report(y_test, pred)
s=evaluation(y_test,pred)
df_eval.loc[name,:]=list(s)
查看评价指标
df_eval
效果也是lgbm更好一点。这也是为什么lgbm出来之后xgboost也有不再热门的原因。lgbm大多数的时候效果是比较好的,并且训练速度要快很多。
超参数搜索
我们简单网格化定义几个超参数,搜索一下。对模型的效果进行一定的优化。
# 网格化搜索最优超参数
from sklearn.model_selection import KFold, train_test_split, GridSearchCV
lgbm_model = LGBMClassifier(objective='binary',random_state=1,verbose=-1)
param_dict = {'max_depth': [2,4,6],'n_estimators': [50,100,200],'eta':[0.05,0.1,0.2]}
clf = GridSearchCV(lgbm_model, param_dict, verbose=1)
clf.fit(X_train, y_train)
print(clf.best_score_)
print(clf.best_params_)
将搜索出来的这个超参数代入模型去训练。
model=LGBMClassifier(objective='binary',random_state=1,verbose=-1,max_depth=6,n_estimators=100,eta=0.05)
model.fit(X_train, y_train)
y_pred=model.predict(X_test)
evaluation(y_test,y_pred)
可以看到整体的准确率精准度召回率f1值,都稍微提高了一些。
寻找最优阈值(最高F1)
模型直接预测的时候是用0.5作为阈值的划分点的,但其实作为2分类问题我们可以去寻找。最优的阈值划分点所谓的最优就是指f1值最高,可以让精准率和召回率达到一定的平衡,并且都非常高。
有的同学说你不是做信贷吗?那你肯定是想把坏人都给过滤掉,拒绝掉。可以让模型的召回率更高不就行了吗?但其实也不对的,因为召回率太高,你能够准确的识别每一个黑客户的同时,也会过滤掉非常多的好客户,而信贷公司其实是希望你能望你能多放一些好客户进来的。因为他们每多放一个贷款就能够多一笔利息收入,但是他又不能放的太多 来者不拒,这样坏账就会很多,所以他是要在尽可能的拒绝掉黑客户的同时减少误杀好客户的概率,也就是要平衡精准度和召回率两个指标。而 因为精准度跟召回率他们是跷跷板的关系,所以能够达到更好的平衡的还是看f1值。
模型预测的概率是在0~1之间。这之间的任何一个取值都有可能作为阈值大于这个阈值就是坏样本。小于这个阈值就是好样本,所以我们自定义一个函数在0.1~0.9之间搜索最优的阈值
# 找到0-1分类最佳的概率切分值
from sklearn.metrics import f1_score
def find_best_threshold(predict_proba, y_true, start_threshold=0.1, end_threshold=0.9, step=0.001,plot_show=True):
"""
自动寻找最优阈值以最大化 F1 分数
参数:
predict_proba: 预测概率数组,shape 为 (n_samples, n_classes)
y_true: 真实标签数组
start_threshold: 阈值搜索的起始值
end_threshold: 阈值搜索的结束值
step: 阈值步长
返回:
best_threshold: 最优阈值
best_f1_score: 对应的最佳 F1 分数
# 示例用法
# best_threshold, best_f1 = find_best_threshold(predict_proba, y_test)
# preds = (predict_proba[:, 1] > best_threshold).astype('int')
"""
thresholds = np.arange(start_threshold, end_threshold, step)
scores = [f1_score(y_true, (predict_proba[:, 1] > t).astype('int'), average='macro') for t in thresholds]
best_idx = np.argmax(scores)
best_threshold = thresholds[best_idx]
best_score = scores[best_idx]
print(f'Optimal threshold: {best_threshold:.8f}, Best F1 score: {best_score:.4f}')
if plot_show:
plt.figure(figsize=(5, 3),dpi=128)
plt.plot(thresholds, scores, label='F1 Score',color='lime')
plt.axvline(best_threshold, color='k', linestyle='--', label=f'Optimal Threshold: {best_threshold:.8f}')
plt.xlabel('Threshold') ; plt.ylabel('F1 Score')
plt.title('F1 Score vs. Threshold')
plt.legend()
#plt.grid(True)
plt.show()
return best_threshold, best_score
我们输入模型的预测出的概率结果和真实的标签。得到最优的阈值和其可视化的图形
best_threshold, best_f1=find_best_threshold(model.predict_proba(X_test), y_test)
然后我们再用这个阈值划分的结果去评估一下
preds = (model.predict_proba(X_test)[:, 1] > best_threshold).astype('int')
evaluation(y_test,preds)
可以看到和之前的模型直接预测的评估指标对比,f1值确实高了一点点。
AUC,ROC,KS
信贷场景是一个经典的2分类问题,就是判断它是不是会违约,从而做出要不要给他贷款的决策。2分类问题就逃不开要计算roc曲线,计算auc值和ks。
我们用如下的代码画出roc曲线跟pr曲线
from sklearn.metrics import roc_curve, auc, precision_recall_curve
y_pred_proba = model.predict_proba(X_test)[:, 1]
# 计算ROC曲线和AUC值
fpr, tpr, _ = roc_curve(y_test, y_pred_proba)
roc_auc = auc(fpr, tpr)
# 计算PR曲线
precision, recall, _ = precision_recall_curve(y_test, y_pred_proba)
# 创建1*2的子图
plt.figure(figsize=(10, 4),dpi=128)
# 绘制ROC曲线
plt.subplot(1, 2, 1)
plt.plot(fpr, tpr, color='tomato', lw=2, label='AUC = %0.2f' % roc_auc)
plt.plot([0, 1], [0, 1], color='k', lw=1, linestyle='--')
plt.xlim([0.0, 1.0]) ; plt.ylim([0.0, 1.05])
plt.xlabel('False Positive Rate') ; plt.ylabel('True Positive Rate')
plt.title('Receiver Operating Characteristic (ROC) Curve')
plt.legend(loc="lower right")
# 绘制PR曲线
plt.subplot(1, 2, 2)
plt.plot(recall, precision, color='skyblue', lw=2)
plt.xlim([0.0, 1.0]) ; plt.ylim([0.0, 1.05])
plt.xlabel('Recall') ; plt.ylabel('Precision')
plt.title('Precision-Recall (PR) Curve')
# 显示图像
plt.tight_layout()
plt.show()
可以看到这个ROC是一个半圆形状还是挺正常的。然后PR曲线也就是前面说的召回率跟精准度的曲线,他们类似于一个y=-x的函数,这种负向的线性关系也是很正常的。
进一步画出ks的图。如果还有同学不知道KS是啥的话(上班前我也不知道),那我就让大模型来解释一下吧:
KS(Kolmogorov-Smirnov)统计量是用于评估模型在二分类任务中表现的一种指标,特别是在信贷违约预测的场景中,KS值常用于衡量模型区分违约客户和非违约客户的能力。
在信贷违约预测中,我们通常会建立一个模型来预测借款人是否会违约。KS统计量的计算步骤如下:
-
排序与分组:将所有借款人根据模型的预测概率进行排序,然后将这些借款人分为违约(positive class)和非违约(negative class)两组。
-
计算累积分布函数(CDF):计算每组的累积分布函数。具体而言,违约客户的CDF表示在不同的预测概率阈值下,违约客户的累积比例;非违约客户的CDF则类似。
-
求取KS值:KS值是两个CDF之间的最大差值,即: [ KS = \max |CDF_{\text{违约}}(p) - CDF_{\text{非违约}}(p)| ] 其中,(p)是预测的概率值。
-
解释:
- KS值的范围在0到1之间,值越大,表示模型的区分能力越强。
- 一般来说,KS值超过40%被认为是良好的模型性能,超过70%则非常优秀。
Ks的计算其实并不复杂,手算也可以,但是有包为啥不用的?我们直接用这个包进行可视化。
import scikitplot as skplt
skplt.metrics.plot_ks_statistic(y_test,model.predict_proba(X_test))
plt.show()
可以看到这个模型的KS有0.386,AUC有0.78,其性能是还可以的。
下面我们自定义两个函数,对于我们模型预测的概率进行一个分箱的观察,然后我们还需要对模型的特征的iv值进行一定的计算,分相观察每个特征所能带来的信息增益。看不懂没关系,下面使用都很简单。
def calculate_pred_proba_bin(true_labels,predictions, bins=10):
# 创建分箱区间
bin_edges = np.linspace(0, 1, bins + 1)
# 分箱
bin_labels = [f"{bin_edges[i]:.2f}-{bin_edges[i+1]:.2f}" for i in range(len(bin_edges)-1)]
bin_indices = np.digitize(predictions, bin_edges, right=False) - 1
# 创建数据框
df = pd.DataFrame({ 'bin': [bin_labels[i] for i in bin_indices], 'label': true_labels })
# 统计各个分箱的总数、类别为0和1的样本数
result = df.groupby('bin')['label'].agg(total='count',
count_0=lambda x: (x == 0).sum(),
count_1=lambda x: (x == 1).sum()).reset_index()
# 计算坏样本率和坏样本在所有坏样本中的比例
total_bad_samples = result['count_1'].sum()
result['bad_rate'] = result['count_1'] / result['total']
result['bad_percent'] = result['count_1'] / total_bad_samples
#result=result.sort_values('bin',ascending=False)
result['lift']=result['bad_rate']/(total_bad_samples/result['total'].sum())
result['cumulative_bad_percent'] = result['bad_percent'][::-1].cumsum()[::-1]
return result.style.bar(color='skyblue').format(subset=['bad_rate','bad_percent','lift','cumulative_bad_percent'], precision=4)
def scorecardpy_display_bin(bins_info):
df_list = []
for col, bin_data in bins_info.items():
df = pd.DataFrame(bin_data)
df_list.append(df)
result_df = pd.concat(df_list, ignore_index=True)
# 增加 lift 列
total_bad = result_df['bad'].sum() ; total_count = result_df['count'].sum()
overall_bad_rate = total_bad / total_count
result_df['lift'] = result_df['badprob'] / overall_bad_rate
result_df=result_df.sort_values(['total_iv','variable'],ascending=False).set_index(['variable','total_iv','bin'])[['count_distr',
'count','good','bad','badprob','lift','bin_iv','woe']]
return result_df.style.format(subset=['count','good','bad'], precision=0).format(subset=['count_distr', 'bad','lift',
'badprob','woe','bin_iv'], precision=4).bar(subset=['badprob','bin_iv','lift'], color=['#d65f5f', '#5fba7d'])
对模型的预测结果,我们进一步的去观察他每个预测的概率区间里面有多少个样本,多少个好样本,坏样本,坏样本的比例有多少,它的提升度有多少,累积的这个坏样本的百分比有多少。
calculate_pred_proba_bin(y_test,y_pred_proba, bins=20)
我们可以清楚的看到模型大概在0.25附近,它的样本特别多,然后在0.05以下基本是没有任何的预测错误。模型有非常大的把握他们都是白样本,并且也预测对了。在0.8以上基本也达到了100%的预测正确率。
基本上随着模型的概率越来越高,它的把握越来越大,其提升度也就是每一箱的这个黑样本的比例也是越来越高的。说明模型其性能没有什么太多的问题。
如果要进行使用的话建议是可以放在0.5区间以上的全部拒绝掉,0.15到0.5之间的样本再进行一次审查。
这个其实就对模型的预测结果和它的性能,它的效益进行了一定的可解释性,从而能够更好的去使用它。
还可以看不同阈值的F1值的变化
from sklearn.metrics import precision_score, recall_score, f1_score
def calculate_prob_thresholds_F1(y_test, y_pred_proba, threshold_range=(0, 1, 0.05)):
"""
计算不同阈值下的精度、召回率、F1值,并统计触发的样本数和真实黑样本数。
参数:
y_test: 真实标签数组
y_pred_proba: 预测概率数组
threshold_range: 阈值范围,以 (start, stop, step) 的形式定义
返回:
results_df: 包含阈值及各类指标的 DataFrame,带有条形图样式
"""
thresholds = [round(n,2) for n in np.arange(*threshold_range)]
results = []
for threshold in thresholds:
y_pred = np.where(y_pred_proba >= threshold, 1, 0)
precision_val = precision_score(y_test, y_pred)
recall_val = recall_score(y_test, y_pred)
f1_val = f1_score(y_test, y_pred)
triggered_samples = np.sum(y_pred == 1)
triggered_black_samples = np.sum((y_pred == 1) & (y_test == 1))
black_rate=triggered_black_samples/triggered_samples
results.append((str(threshold), triggered_samples, triggered_black_samples, precision_val, recall_val, f1_val, ))
results_df = pd.DataFrame(results, columns=['Threshold', 'Samples', 'Black_Samples','Precision', 'Recall', 'F1']).set_index('Threshold')
return results_df.style.bar(color='pink').format(subset=['Precision', 'Recall', 'F1'], precision=4)
calculate_prob_thresholds_F1(y_test,y_pred_proba)
精准度随着阈值变高而变高,召回率随着阈值变高而变低,F1值最高在0.3附近,一目了然。
IV特征表示
IV也是信贷模型里面常用的一个指标。如果不知道的话可以去搜搜这个网上一堆。
我们使用scorecardpy计算每个特征的iv进行分组。从而观察不同的特征在不同的区间里面的黑样本的比例。从而我们可以推测每个特征大概率是怎么影响标签y响应变量的。
train_miss=train.copy()
train_miss['Years in current job']=train_miss['Years in current job'].astype('str').fillna('missing').astype('category')
bins_adj = sc.woebin(train_miss, y="Credit Default")
scorecardpy_display_bin(bins_adj)
特征太多就不截图完了。这里我们可以清楚的看到,第一个特征信用评分的IV是0.55是非常高的,说明他对我们的黑白样本的识别能力,能起到一个非常好的作用,具体我们可以看到它的评分在750以前是越低风险会越高,所谓风险就是黑样本的比例也是我们的这个提升度。而在750以后,我们可以看到这个特征,在750之后的这一箱它的提升度超级高,黑样本的比例超级高,它所贡献的iv也是非常多的。
所以我们可以简单的推测,某个样本的这个特征,如果处于678区间以下或者750区间以上,会让模型更加倾向于把它识别为一个黑样本,尤其是750以上的区间。而这个特征如果处于698~750这个区间,模型可能就会倾向于将它认为是一个白样本。这种分段的非线性的结构正是树模型所擅长的,我们待会儿可以从shap包所展现出来的样本的预测决策结果来验证我们的这个推断是否具有正确性。
同理,我们可以看到别的特征也是可以进行一样的推断。第二个特征当前贷款金额我们可以看到他的最后一箱也就是处于78w值之后,模型所展现的黑样本浓度变得极低。这一箱所贡献的iv也特别高。我们可以推测这个特征,如果某个样本在这个特征上出现了一个大于78万的值,那么模型可能会倾向于将它认为是一个白样本,会给他一个较低的概率分。
上面的这两个特征都是非线性的,没有一个标准的单调递增或者单调递减的一个效果,但是我们可以看到年收入这个特征它明显是越高。黑样本浓度就越低,所以这个特征具有非常好的单调性。即这个人的年收入和他贷款是否会违约形成了一个非常强的线性的关系,及年收入越高,贷款违约的概率越低。
剩下的特征分析就不一一列举了,一般来说iv在0.1以上,说明这个特征就具有了一定的区分能力,0.02以下的特征可以不用太多的关注。
其实scorecardpy这个包的功能还是很多的,他可以直接woe编码,输出模型的roc和ks图等。后面可以继续探索他它的其他功能和用法,目前只是用来做iv分箱可视化。
变量重要性
上面的iv它的重要性,以一定程度的反映了这个特征对于响应变量y的一定的线性的可解释性,但是对于模型中非线性的部分以及相互交互作用的部分。是没有办法用IV解释的,因此用模型自带的特征重要评分输出的特征排序会跟iv的这个重要变量的特征排序不太一样,我们下面输出这个模型里面比较重要的特征排序,进行可视化
sorted_index = model.feature_importances_.argsort()[::-1]
plt.figure(figsize=(8, 6),dpi=128)
# 使用 seaborn 来绘制条形图
sns.barplot(x=model.feature_importances_[sorted_index], y=X.columns[sorted_index], orient='h')
plt.xlabel('Feature Importance') # x轴标签
plt.ylabel('Feature') # y轴标签
plt.show()
我们使用shap包,来对模型进行一定的可解释性,首先计算它的夏普值,输出它的变量重要性
explainer = shap.TreeExplainer(model)
shap_values = explainer(X_test)
shap.summary_plot(shap_values, X_test, plot_type="bar")
可以看到它的变量重要性我觉得和iv比较类似,这也和shap值的计算有关。
反正不同的方法评价变量的重要性,结果都略有不同,但是毫无疑问的是,重要的变量肯定都是那几个,只是他们在不同的方法里面重要的程度不一样而已,而不重要的方法它特征肯定都排在末尾的,例如这个案例里面,最后这两个特征无论什么方法都是排在最下面的,也就是说这两个特征对于预测并不太重要。
然后我们需要这个里面的数值的话,可以这样获取:
mean_shap_values = np.abs(shap_values.values).mean(axis=0)
# 将结果放入DataFrame中便于比较
df_mean_shap_values = pd.DataFrame({ 'feature': X_test.columns, 'mean_shap_value': mean_shap_values})
# 按平均绝对SHAP值排序
df_mean_shap_values = df_mean_shap_values.sort_values(by='mean_shap_value', ascending=False)
df_mean_shap_values.head(20).style.bar(color='skyblue')
这个好像在shap包里面叫什么蜂鸣图:
shap.summary_plot(shap_values, X_test)
他也是输出了模型的特征重要性排序,但不一样的是他给出了一个方向性的关系及这个特征的取值大小是对模型的决策的影响的程度。
我们前面从iv的分箱中可以得知 第一个特征: 当前贷款 这个变量在大于78万的时候。它的黑样本浓度是非常低的。我们可以在图中看到第一个特征,它左边偏红色那一块儿取值就应该是这个78万以上的较大的值,并且它的shap值对应的是非常低的,也就是说模型会给出一个较低的概率分数,也就是说模型可能会让它趋向于预测出零的结果,及识别为白样本。
同理,第二个特征:信用评分,我们当时在iv里面看到大于780之后的黑样本的浓度变得超级高,因此我们可以看到第二个特征在右边。比较红的那一排的点,说明它的取值很大,并且它的夏普值较高,也就是说它可能会让模型给出一个倾向于更高的概率分数,也就是预测把它识别为一个黑样本的概率更大。
第三个特征跟第四个特征类似。例如第三个特征,年收入我们前面看到具有很好的单调性,也就是说它这个取值较大的时候,黑样本的概率是比较低的,反应在这个图上也就是它红色的点都会有一个较低的夏普值,会让模型给出一个更低的预测概率,从而识别为一个好样本,即收入越高 ,贷款违约的概率越低。
并且这个图还给出了类似于小提琴图一样的分布集中会变得很宽,也就是它在对每个取值的数量上还画出了一个概率密度分布,这个主要是看样本特征在每个数值区间上的取值的一个分布。例如年收入大家都集中在中下区间,也就是说年收入的靠近0的这一块儿,它的概率密度比较高,反应在图上就是有一个比较鼓的区间。
单个样本的可解释性
前面说了,有时候我们很关心模型,它到底对单个样本是通过哪些特征决策作为一个依据来倾向于给出预测结果的。所以我们需要挑一些样本来进行一定的可视化,我们拿出一个数据框,把测试集的真实标签以及模型给出的预测概率值放进去
y_sample=y_test.to_frame().reset_index(drop=True)
y_sample['probability']=y_pred_proba
y_sample.head()
可以看到索引为3的这个样本它是零,并且模型也给出了一个0.004的超低的概率,我们来看一看模型做出如此正确的决断到底是依据了哪个数据哪个特征:
shap.initjs()
shap.force_plot(explainer.expected_value, shap_values.values[3, :], X_test.iloc[3, :], link="logit")
可以看到模型蓝色这一块儿,就是让其预测概率倾向于零的影响的区间,基本上都是被当前贷款这一个特征给覆盖满了,也就是说,这个样本当前贷款等于一个亿,这一个特征的取值让模型非常自信的给出了超低的预测概率。就是这个特征影响了模型,让它倾向于输出了一个很低的概率。因为我们前面在iv里面看到的也是当这个取值非常大的时候黑样本浓度很低,因此这是可以对得上的,成功的解释了这个样本,就是因为受到这个特征的影响,从而给出了一个较低的概率,并且这个影响的趋势我们是能够从iv里面观察到的,结论也是一致的。
所以接下来我们就要去对比,真实样本为0,且模型预测出很低的概率以及模型预测出很高的概率的两种情况。他们的这个特征的分布,shap值,以及影响模型预测概率的方向。
因为真实样本为0模型预测出很低的概率,说明模型预测的是正确的,而如果真实样本为零模型预测出很高的概率,说明他预测错了,我们就得去找为什么他预测错了究竟是哪些特征在导致影响了模型的预测的概率产生了错误偏差。所以我们要把预测对和预测错误的情况都捞出来看一下,才能知道他到底是在哪些特征上产生了差异,所以导致模型预测了错误。
同理,我们还要去对比真实样本为1时,模型预测出很低的概率以及模型预测出很高的概率两种情况。模型预测出很高,说明他预测的正确,模型预测的概率很低,说明他预测的错误,我们就得去对比一下到底是哪些特征出现了差异,从而导致他预测出现了错误。
类别为0,概率很低的样本
### 类比为0,概率很低的样本
id_list=y_sample[(y_sample['Credit Default']==0)&(y_sample['probability']<0.002)].index
print(id_list)
shap.force_plot(explainer.expected_value, shap_values.values[id_list, :], X_test.iloc[id_list, :], link="logit")
这个图是动态可交互的,鼠标放在不同的区域会有不同的特征显示。呃,静态图可能不太好展示,但是这里我们可以看到:这个预测正确的样本很大概率是受到 当前贷款 这个变量的影响才能对白样本做出一个正确的,低概率的预测。
类别为0,概率高的样本
### 类比为0,概率高的样本
id_list=y_sample[(y_sample['Credit Default']==0)&(y_sample['probability']>0.7)].index
print(id_list)
shap.force_plot(explainer.expected_value, shap_values.values[id_list, :], X_test.iloc[id_list, :], link="logit")
这里的样本都是模型预测错误的样本,模型给出了一个较高的概率。而模型之所以给出较高的概率,主要还是受到了当前贷款这个变量的影响,如果没猜错的话,这些样本应该都是有一个较低的当前贷款,所以说模型会倾向于给出一个较高的预测概率,认为他们是黑样本,从而产生了这种错误的预测判断。
类别为1,概率很低的样本
id_list=y_sample[(y_sample['Credit Default']==1)&(y_sample['probability']<0.1)].index
print(id_list)
shap.force_plot(explainer.expected_value, shap_values.values[id_list, :], X_test.iloc[id_list, :], link="logit")
同样这里类别唯一应该是黑样本,但是模型给出了一个较低的概率,也说明模型预测错误了,而导致这一预测结果的罪魁祸首变量(在图中看不到哈,我自己和图形交互之后才能够看得到的)是信用评分这个变量。
因为我们前面从iv里面能够看到信用评分在较中间的区间是有一个低的黑样本浓度,可能会被预测成为一个白样本,而虽然这些样本它是黑样本,但是由于他们的这个信用评分处于698-750中间的这个区间,所以他们被识别成了一个白样本,模型给出了较低的预测概率。
### 类别为1,概率很高的样本
id_list=y_sample[(y_sample['Credit Default']==1)&(y_sample['probability']>0.995)].index
print(id_list)
shap.force_plot(explainer.expected_value, shap_values.values[id_list, :], X_test.iloc[id_list, :], link="logit")
我们从这个图就能很清楚的看到,模型他把这些黑样本识别的非常好的原因就是因为他们的这个信用评分这个特征的取值非常大。所以让模型有超级大的把握,输出一个很高的概率认为他们是黑样本。
决策图
事实上模型如何做出决策的关于shap包里面有一个更好的展示图表,我们这里就将样本为1,即黑样本,但是模型分别预测出很高的概率和很低的概率的样本都放进去画一个图,即对模型预测正确和预测错误的两组样本进行一个决策对比。
#正类样本
id_list1=y_sample[(y_sample['Credit Default']==1)&(y_sample['probability']>0.995)].index #预测概率很高的(正确)
id_list2=y_sample[(y_sample['Credit Default']==1)&(y_sample['probability']<0.1)].index #预测概率很低的(错误)
id_list=id_list1.append(id_list2)
shap.decision_plot(explainer.expected_value, shap_values.values[id_list, :], X_test.iloc[id_list, :], link="logit")
从图中我们可以清楚的看到他预测错误,也就是蓝色这边,他把黑样本给了一个非常低的概率是为什么呢?,主要就是因为信用评分这个特征,这些被预测出的样本,他们的信用评分大多数都处于中间的698~750的这个区间,而这个区间在样本里面,它的黑样本浓度是很低的,因此模型就错误的把这些样本给了一个较低的概率,从而模型错认为他们是白样本,识别错误。
同理我们将样本为0,即白样本,但是模型分别预测出很高的概率和很低的概率的样本都放进去画一个图,即对模型预测正确和预测错误的两组样本进行一个决策对比。
id_list1=y_sample[(y_sample['Credit Default']==0)&(y_sample['probability']<0.002)].index #预测概率很高的(错误)
id_list2=y_sample[(y_sample['Credit Default']==0)&(y_sample['probability']>0.7)].index #预测概率很低的(正确)
id_list=id_list1.append(id_list2)
shap.decision_plot(explainer.expected_value, shap_values.values[id_list, :], X_test.iloc[id_list, :], link="logit")
由于这一批是白样本,所以模型预测为1的时候它是错误的,也就是右边红色这些样本被错误的给出了一个较高的概率,主要就是因为当前贷款这一变量。我们从前面可以知道当前贷款这个变量是在78万以上才会有非常低的黑样本浓度,而78万以下会被认为有较高的风险。也就是说这些被错误认为是黑的样本他们的当前贷款的取值肯定都是78万以下,所以造成了他们会被给出了一个较高的概率被识别成了黑样本,从而被预测错误。
热力图
当然shap包里面还有一些别的图:
shap.plots.heatmap(shap_values, max_display=12)
交互影像图
shap.plots.scatter(shap_values[:, "Credit Score"], color=shap_values[:, "Current Loan Amount"])
具体我也不太知道咋看,目前还没研究,知道的同学可以相互交流。
总结
我这一个专题主要是研究这些特征的重要性和可解释性,以及是如何对样本进行决策的,以及决策错误究竟是为什么而去寻找数据特征层面的影响了模型的原因。目前看来我们可以对于特征的方向性以及被预测错误的样本他们的特征的一个特点,都能够找到更好的解释和可视化。
shap包和scorecardpy这两个包主要都是用于信贷场景模型及其评估和解释性的,其主要功能肯定不止我所展示的这一些,具体以后还有更多的应用场景等待挖掘。
创作不易,看官觉得写得还不错的话点个关注和赞吧,
需要这篇文章的数据和全部代码文件的同学可以参考:信贷违约
本人会持续更新python数据分析领域的代码文章~(需要定制类似的代码可私信)