本文通过对kaggle上的Give Me Some Credit数据的挖掘分析,结合信用评分卡的建立原理,从数据的预处理、建模分析、建立评分卡创建了一个简单的信用评分系统。
客户申请评分卡是一种统计模型,它可基于对当前申请人的各项资料进行评估并给出一个分数,该评分能定量对申请人做出等级划分。
申请评分卡由一系列特征项分割组成,每个特征项相当于申请表上的一个问题(例如,年龄、银行流水、收入等)。每一个特征项都有一系列可能的水平,相当于每一个问题的一系列可能答案(例如,对于年龄这个问题,答案可能就有30岁以下、30到45等)。在开发评分卡系统模型中,先确定特征水平与申请人未来信用表现之间的逻辑关系,然后给特征水平分配适当的分数权重,分配的分数权重要反映这种相互关系。分数权重越大,说明该特征水平对预测的作用越大。一个申请的得分是其属性分值的简单求和。如果申请人的信用评分大于等于金融放款机构所设定的界限分数,此申请处于可接受的风险水平并将被批准;低于界限分数的申请人将被拒绝或给予标示以便进一步审查。
2、评分卡模型的构建步骤
1、数据抽取和整理
2、模型设计
3、数据加工清洗
4、单变量WOE分析和转换
5、 逻辑回归模型训练
6、 评分转换
7、模型实施和监控
2.1 数据导入
import pandas as pd import numpy as np import matplotlib.pyplot as plt import seaborn as sns %matplotlib inline df = pd.read_csv(r"D:\cs-training.csv") df.shape (150000, 12) print(df.columns)
2.2 数据预处理
2.2.1 列名转换
# 转换列名 new_name = {'Unnamed: 0':"用户ID", "SeriousDlqin2yrs":'好坏客户', 'RevolvingUtilizationOfUnsecuredLines':"可用额度比值", "age":"年龄", "NumberOfTime30-59DaysPastDueNotWorse":"逾期30-59天笔数", "DebtRatio":"负债率", "MonthlyIncome":"月收入", "NumberOfOpenCreditLinesAndLoans":"信贷数量", "NumberOfTimes90DaysLate":"逾期90天笔数", "NumberRealEstateLoansOrLines":"固定资产贷款数量", "NumberOfTime60-89DaysPastDueNotWorse":"逾期60-89天笔数", "NumberOfDependents":"家属数量"} df.rename(columns=new_name,inplace = True) df.head()
2.2.2 缺失值处理
df.info() <class 'pandas.core.frame.DataFrame'> RangeIndex: 150000 entries, 0 to 149999 Data columns (total 12 columns): 用户ID 150000 non-null int64 好坏客户 150000 non-null int64 可用额度比值 150000 non-null float64 年龄 150000 non-null int64 逾期30-59天笔数 150000 non-null int64 负债率 150000 non-null float64 月收入 120269 non-null float64 信贷数量 150000 non-null int64 逾期90天笔数 150000 non-null int64 固定资产贷款数量 150000 non-null int64 逾期60-89天笔数 150000 non-null int64 家属数量 146076 non-null float64 dtypes: float64(4), int64(8) memory usage: 13.7 MB ### 缺失比率 print("月收入缺失比:{:.2%}".format((150000-120269)/150000)) print("家属数量缺失比:{:.2%}".format((150000-146076)/150000))
月收入缺失比:19.82%
家属数量缺失比:2.62%
家属数量对应的缺失比例低于5%,可直接删除;月收入缺失比比较高,不能直接删除,利用填充平均值的方法进行补充;
2.2.3 异常值处理
异常值处理之前,我们首先需要对异常值进行检测,这里是用箱形图判断异常值。
### 异常值处理 # 可以解决中文乱码 plt.rcParams['font.sans-serif']=['SimHei'] plt.rcParams['font.family']='sans-serif' x1 = df['可用额度比值'] x2 = df['负债率'] labels = ['可用额度比值','负债率'] plt.boxplot([x1,x2],labels = labels) plt.title("可用额度及负债率箱线图",loc = 'center') plt.grid(b = 'True',axis = 'y',linestyle='dashed',linewidth=1) plt.show()
可用额度的比值是该客户的可用额度比总额度的比值,所以值不应该大于1,大于1的部分进行删除。
x3 = df['年龄'] labels = ['年龄'] plt.boxplot(x3,labels = labels) plt.title("年龄箱线图",loc = 'center') plt.grid(b = 'True',axis = 'y',linestyle='dashed',linewidth=1) plt.show()
年龄不应该有0岁,显然是异常值,因此需要删除等于0岁的记录。
x4 = df['逾期30-59天笔数'] x5 = df['逾期60-89天笔数'] x6 = df['逾期90天笔数'] labels = ['逾期30-59天笔数','逾期60-89天笔数','逾期90天笔数'] plt.boxplot([x4,x5,x6],labels = labels) plt.title("逾期天数箱线图",loc = 'center') plt.grid(b = 'True',axis = 'y',linestyle='dashed',linewidth=1) plt.show()
### 三个变量都有异常值,查看各个特征异常值数量,异常值较少,直接删除 df[df["逾期30-59天笔数"]>60].shape df[df["逾期60-89天笔数"]>60].shape df[df["逾期90天笔数"]>60].shape (0, 12) x7=df["信贷数量"] x8=df["固定资产贷款数量"] labels = ['信贷数量','固定资产贷款量'] plt.boxplot([x7,x8],labels = labels) plt.title("信贷数量及固定资产贷款量箱线图",loc = 'center') plt.grid(b = 'True',axis = 'y',linestyle='dashed',linewidth=1) plt.show()
固定资产贷款数量大于50的算作异常值,予以删除。
# 月收入 plt.boxplot(df['月收入'],labels = ['月收入']) plt.title("月收入箱线图",loc = 'center') plt.grid(b = 'True',axis = 'y',linestyle='dashed',linewidth=1) plt.show()
## 异常值过滤 df = df[df["固定资产贷款数量"]<50] df = df[df["逾期30-59天笔数"]<80] df = df[df["年龄"]>0] df = df[df['可用额度比值']<=1]
3、数据探索性分析
这里针对用户的年龄、月收入、负债率等指标进行分析,看坏账用户与哪些指标有关。
3.1 单变量分析
# 整体好坏客户占比 group = df.groupby('好坏客户')['用户ID'].count() group 好坏客户 0 134078 1 8498 Name: 用户ID, dtype: int64 print("坏客户占比:{:.2%}".format(group[1]/df.shape[0])) 坏客户占比:5.96% group.plot(kind='bar')
# 年龄分箱,判断与目标变量的关系 age_cut = pd.cut(df['年龄'],5) age_cut_grouped = df.groupby(age_cut)['好坏客户'].count() age_cut_grouped1 = df.groupby(age_cut)['好坏客户'].sum() # 使用索引作为连接键 df1 = pd.merge(pd.DataFrame(age_cut_grouped),pd.DataFrame(age_cut_grouped1),left_index=True,right_index = True) df1.rename(columns = {'好坏客户_x':'用户量','好坏客户_y':'坏客户'},inplace = True) df1['好客户'] = df1['用户量'] - df1['坏客户'] df1.insert(3,'坏客户率',df1['坏客户']/df1['用户量']) df1
df1[['好客户','坏客户']].plot.bar() plt.title('各年龄段下好坏用户数') plt. Text(0.5,1,'各年龄段下好坏用户数')
df1['坏客户率'].plot() <matplotlib.axes._subplots.AxesSubplot at 0x24e99b1b048>
通过上面年龄-好坏客户分布图和坏客户率与年龄变化图可以看出,38-55岁这部分人群的好客户和坏客户的绝对数量均是top1,随着年龄的增长、坏客户率在降低,且38-72岁之间降低最快。
# 月收入与好坏客户的关系 cut_bins=[0,5000,10000,15000,20000,100000] month_cut=pd.cut(df["月收入"],cut_bins) month_cut_grouped=df["好坏客户"].groupby(month_cut).count() month_cut_grouped1=df["好坏客户"].groupby(month_cut).sum() df2=pd.merge(pd.DataFrame(month_cut_grouped), pd.DataFrame(month_cut_grouped1),right_index=True,left_index=True) df2.rename(columns={"好坏客户_x":"用户数","好坏客户_y":"坏客户"},inplace=True) df2['好客户'] = df2['用户数'] - df2['坏客户'] df2.insert(3,'坏客户率',df2['坏客户']/df2['用户数']) ax2 = df2[['好客户','坏客户']].plot.bar() ax2.set_xticklabels(df2.index, rotation = 15) ax2.set_ylabel('客户数')
ax21 = df2['坏客户率'].plot() ax21.set_ylabel('坏客户率') ax21.set_title('月收入与坏客户率关系')
好坏客户主要集中在月收入10000以下的群体中,月收入在0-15000之间,坏客户率随着月收入的增加而降低,之后进入平稳,当月收入超过20000时,坏客户率又开始上升。这里为什么月收入越高超过20000时,坏客户率开始上升?结合这些人群的职业,发现这部分人不是普通工作,收入不太稳固。
4. 多变量分析
多变量分析主要是分析变量之间的关系。
### 多变量相关性分析 corr = df.corr() xticks = list(corr.index) yticks = list(corr.index) ax1=plt.subplot(1,1,1) sns.heatmap(corr, annot=True, cmap="rainbow",ax=ax1,linewidths=.5, annot_kws={'size': 9, 'weight': 'bold', 'color': 'blue'}) ax1.set_xticklabels(xticks, rotation=35, fontsize=10) ax1.set_yticklabels(yticks, rotation=0, fontsize=10) plt.show()
热力图表示不同变量之间的关系,单元格颜色越深,表示交叉的两个变量相关性越强。整体变量之间相关系数小于0.5,偏向弱相关,可不处理。
5. 特征选择
1、共线性检验
共线性是线性回归模型中的特征之间由于存在相关关系而使模型估计失真或者难以估计准确。而评分卡中用到的是LR模型是广义线性模型,所以需要进行共线性检验。
由上图热力图,变量之间的相关性比较小,所以不存在共线性问题。
2、特征选择
特征选择是指剔除跟目标变量不太相关的特征,过滤掉一些对于目标变量影响权重较小的特征变量,本篇我们使用IV值进行特征筛选。
WOE称为迹象权数(weight of evidence),计算公式为:ln(正常件占比/违约件占比) = ln((组内正常/组内违约)/(整体正常/整体违约)),违约件占率高于正常件时,迹象权数为负数。绝对值越高,表示该组别好坏客户的区隔程度越高,各组之间WOE值差距尽可能拉开并呈现由低至高的合理趋势
。
迹象权数相近,可见好/坏概念特征相仿,所以合并组减少N(N代表组数),从而突出好坏客户的资产分界线特征;
WOE其实描述了变量当前这个分组,对判断个体是否会响应(或者说属于哪个类)所起到影响方向和大小,当WOE为正时,变量当前取值对判断个体是否会响应起到的正向的影响,当WOE为负时,起到了负向影响。而WOE值的大小,则是这个影响的大小的体现。
IV称为信息值(Information value,IV),计算公式为SUM((正常件占比-违约件占比)* 迹象权数),IV值表示变量预测能力的强度
;
IV值于WOE值的区别:IV值是WOE值乘上(正常件-违约件占比)两类总体占比差值,体现了变量当前分组中个体的数量占总体个体数量的比例。
2.1 WOE分箱
所谓的WOE分箱就是将连续变量离散化, 即切分成不同的区间段,离散化后的变量具有很好的稳定性,比如年龄这个连续变量,如果是连续值的时候21和29就是两个不同的值,对模型的效果可能就不一样,如果离散化成20-30的时候,这两个年龄对模型的效果就是一样,更加稳定。
分组的原则:
数据切分的进行分组,分组的原则为组间差异大,组内差异小。分组占率不宜低于5%,且各组中必须同时拥有好坏客户。分裂出的分箱数<=预设的最大分箱数;
分组中,响应的比例越大(违约件组内占比越大),WOE值越大,WOE值差距要尽可能拉开,并呈现由低至高的单调趋势。
# 整体好坏比 total_rate = df['好坏客户'].sum()/(df['好坏客户'].count()-df['好坏客户'].sum()) # 分箱 cut1=pd.qcut(df["可用额度比值"],4,labels=False) cut2=pd.qcut(df["年龄"],8,labels=False) bins3=[-1,0,1,3,5,13] cut3=pd.cut(df["逾期30-59天笔数"],bins3,labels=False) cut4=pd.qcut(df["负债率"],3,labels=False) cut5=pd.qcut(df["月收入"],4,labels=False) cut6=pd.qcut(df["信贷数量"],4,labels=False) bins7=[-1, 0, 1, 3,5, 20] cut7=pd.cut(df["逾期90天笔数"],bins7,labels=False) bins8=[-1, 0,1,2, 3, 33] cut8=pd.cut(df["固定资产贷款数量"],bins8,labels=False) bins9=[-1, 0, 1, 3, 12] cut9=pd.cut(df["逾期60-89天笔数"],bins9,labels=False) bins10=[-1, 0, 1, 2, 3, 5, 21] cut10=pd.cut(df["家属数量"],bins10,labels=False) type(cut1)
pandas.core.series.Series
# 定义WOE函数 def get_woe(cut): grouped = df.groupby(cut)['好坏客户'].value_counts() # 组内好坏比 woe = np.log(pd.DataFrame(grouped).unstack().iloc[:,1]/pd.DataFrame(grouped).unstack().iloc[:,0]/total_rate) return woe # 计算WOE值,如果计算出来的WOE不是单调的就需要重新分箱 cut1_woe = get_woe(cut1) cut2_woe = get_woe(cut2) cut3_woe = get_woe(cut3) cut4_woe = get_woe(cut4) cut5_woe = get_woe(cut5) cut6_woe = get_woe(cut6) cut7_woe = get_woe(cut7) cut8_woe = get_woe(cut8) cut9_woe = get_woe(cut9) cut10_woe = get_woe(cut10) print(cut1_woe) 可用额度比值 0 -1.204125 1 -1.135924 2 -0.247292 3 1.044612 dtype: float64 print(cut2_woe) 年龄 0 0.518093 1 0.316312 2 0.247161 3 0.166882 4 -0.025377 5 -0.395231 6 -0.855562 7 -1.020918 dtype: float64 print(cut1_woe) 可用额度比值 0 -1.204125 1 -1.135924 2 -0.247292 3 1.044612 dtype: float64 plt.bar(range(len(cut1_woe)),cut1_woe) <BarContainer object of 4 artists>
plt.bar(range(len(cut2_woe)),cut2_woe)
plt.bar(range(len(cut3_woe)),cut3_woe) <BarContainer object of 5 artists>
plt.bar(range(len(cut4_woe)),cut4_woe) <BarContainer object of 3 artists>
plt.bar(range(len(cut5_woe)),cut5_woe) <BarContainer object of 4 artists>
2.2 计算IV值
### 计算每个变量的IV值 def get_iv_data(cut,cut_woe): grouped = df.groupby(cut)['好坏客户'].value_counts() cut_iv = ((grouped.unstack().iloc[:,1]/df['好坏客户'].sum()- grouped.unstack().iloc[:,0]/(df['好坏客户'].count()-df['好坏客户'].sum()))*cut_woe).sum() return cut_iv cut1_iv = get_iv_data(cut1,cut1_woe) cut2_iv = get_iv_data(cut2,cut2_woe) cut3_iv = get_iv_data(cut3,cut3_woe) cut4_iv = get_iv_data(cut4,cut4_woe) cut5_iv = get_iv_data(cut5,cut5_woe) cut6_iv = get_iv_data(cut6,cut6_woe) cut7_iv = get_iv_data(cut7,cut7_woe) cut8_iv = get_iv_data(cut8,cut8_woe) cut9_iv = get_iv_data(cut9,cut9_woe) cut10_iv = get_iv_data(cut10,cut10_woe) # 各组IV值可视化 df_iv = pd.DataFrame([cut1_iv,cut2_iv,cut3_iv,cut4_iv,cut5_iv,cut6_iv,cut7_iv,cut8_iv,cut9_iv,cut10_iv]) df_iv.plot(kind = 'bar') for a,b in enumerate(df_iv.values): plt.text(a,b,'%.2f' % b,ha='center', va= 'bottom',fontsize=9)
可以看到[“负债率”,“月收入”,“信贷数量”,“固定资产贷款量”,“家属数量”]这几个特征的IV值过低,对目标变量的影响较小,将其过滤掉。
2.3 用woe值代替原始数据
# 将原始数据用转换后的WOE值代替 # 原始分组后的数据集,根据各组标签替代为对应的WOE值 def replace_data(cut,cut_woe): # 保存各箱标签 a = [] for i in cut.unique(): a.append(i) a.sort() # 每箱从小到大排序,并计算对应的WOE值 for m in range(len(a)): cut.replace(a[m],cut_woe.values[m],inplace = True) return cut # 进行替换 df_new = pd.DataFrame() df_new['可用额度比值'] = replace_data(cut1,cut1_woe) df_new['年龄']=replace_data(cut2,cut2_woe) df_new['逾期30-59天笔数'] = replace_data(cut3,cut3_woe) df_new['逾期90天笔数'] = replace_data(cut7,cut7_woe) df_new['逾期60-89天笔数'] = replace_data(cut9,cut9_woe)
6、模型训练
# 模型训练及测试 from sklearn.linear_model import LogisticRegression from sklearn.model_selection import train_test_split from sklearn.metrics import roc_curve, auc df_new['好坏客户'] = df['好坏客户'] df_new.info() ------------------------------------ <class 'pandas.core.frame.DataFrame'> Int64Index: 142576 entries, 0 to 149999 Data columns (total 6 columns): 可用额度比值 142576 non-null float64 年龄 142576 non-null float64 逾期30-59天笔数 142576 non-null float64 逾期90天笔数 142576 non-null float64 逾期60-89天笔数 142576 non-null float64 好坏客户 142576 non-null int64 dtypes: float64(5), int64(1) memory usage: 7.6 MB x = df_new.iloc[:,0:-1] y = df_new.iloc[:,-1] x_train,x_test,y_train,y_test = train_test_split(x,y,test_size = 0.4,random_state = 0) model = LogisticRegression() clf = model.fit(x_train,y_train) # 预测标签 y_pred = clf.predict(x_test) print("测试集的平均正确率:{}".format(clf.score(x_test,y_test))) ------------------------------------- 测试集准确率:0.9423471445354281 # 计算回归值,取值小于0,表示p(y=1) <0.5,则该处标签属于1,否则属于0 y_pred1 = clf.decision_function(x_test)
ROC曲线
## 绘制ROC曲线并计算AUC的值 fpr, tpr, threshold = roc_curve(y_test,y_pred1) roc_auc = auc(fpr,tpr) roc_auc = auc(fpr, tpr) plt.plot(fpr, tpr, color='darkorange', label='ROC curve (area = %0.2f)' % roc_auc) plt.plot([0, 1], [0, 1], color='navy', linestyle='--') plt.xlim([0.0, 1.0]) plt.ylim([0.0, 1.0]) plt.xlabel('False Positive Rate') plt.ylabel('True Positive Rate') plt.title('ROC_curve') plt.legend(loc="lower right") lt.show()
## 由于样本不平衡导致正确率与AUC差距大
7 计算得分
# 系数值 coe = clf.coef_ ### 计算得分 # score = offset+factor*log(odds) # 约定:当odds增加一倍,分数增加20分;当odds = 1,分数为600分 factor = 20 /np.log(2) offset = 600 # 分数facor*woe_i*beta # 定义变量分数计算函数 def get_score(coe,woe,factor): scores = [] for w in woe: score=round(coe*w*factor,0) scores.append(score) return scores # 计算每个变量得分 x1 = get_score(coe[0][0],cut1_woe,factor) x2 = get_score(coe[0][1],cut2_woe,factor) x3 = get_score(coe[0][2],cut3_woe,factor) x7 = get_score(coe[0][3],cut7_woe,factor) x9 = get_score(coe[0][4],cut9_woe,factor) # 每个特征对应的分数 print('可用额度比值对应分箱:{}'.format(cut1)) print('可用额度比值对应的分数:{}'.format(x1)) print('年龄对应的分数:{}'.format(x2)) print('逾期30-59天笔数对应的分数:{}'.format(x3)) print('逾期90天笔数对应的分数:{}'.format(x7)) print('逾期60-89天笔数对应的分数:{}'.format(x9))
可用额度比值对应的分数:[-22.0, -21.0, -5.0, 19.0]
年龄对应的分数:[8.0, 5.0, 4.0, 3.0, -0.0, -6.0, -13.0, -16.0]
逾期30-59天笔数对应的分数:[-8.0, 14.0, 27.0, 37.0, 42.0]
逾期90天笔数对应的分数:[-6.0, 34.0, 48.0, 57.0, 57.0]
逾期60-89天笔数对应的分数:[-3.0, 24.0, 35.0, 39.0]
上面得到的是不同特征值对应的分数,评分越高表明该用户越有可能相应目标变量,成为坏用户;特征划分区间是依次递增的,特征区间值与得分是相对应的,年龄越大,坏账的可能性越低;逾期笔数越多,坏账可能性越高,得分越高;最后将所有的变量对应的得分相加,就是每个用户的得分。
转载https://zhuanlan.zhihu.com/p/131011501
参考《python金融风控评分卡模型和数据分析(加强版)》
扣扣学习群:1026993837 领学习资料
在公众号「python风控模型」里回复关键字:学习资料