消费金融信用评分卡(A卡) Python建模实践项目

项目目的:本文通过对kaggle上的Give Me Some Credit数据的挖掘分析,结合信用评分卡的建立原理,从数据的预处理、变量选择、建模分析到创建信用评分,创建了一个简单的信用评分系统进行在贷前风险控制;以最大收益化放款给用户

流程:

(一):导入数据

(二):数据预处理

(三):探索分析

(四):特征选择

(五):模型训练

(六):模型评估

(七):模型结果转评分

计算用户总分

(一):导入数据

import pandas as pd

import numpy as np

df=pd.read_csv("D:\\打包7.2\\练习项目汇总\\评分卡C卡\\cs-training.csv")

df.head()#查看各字段名,有12列数据,将ID列设置为索引列

df.info()

df=df.iloc[:,1:]   

df.head()


#查看各字段结构

df.info()

df=df.iloc[:,1:]  

df.head()

#上图可知道样本量为15万行,12列变量

#将各英文字段转为中文字段名方便理解

states={'SeriousDlqin2yrs':'好坏客户',

        'RevolvingUtilizationOfUnsecuredLines':'可用额度比值',

        'age':'年龄',

        'NumberOfTime30-59DaysPastDueNotWorse':'逾期30-59天笔数',

        'DebtRatio':'负债率',

        'MonthlyIncome':'月收入',

        'NumberOfOpenCreditLinesAndLoans':'信贷数量',

        'NumberOfTimes90DaysLate':'逾期90天笔数',

        'NumberRealEstateLoansOrLines':'固定资产贷款量',

        'NumberOfTime60-89DaysPastDueNotWorse':'逾期60-89天笔数',

        'NumberOfDependents':'家属数量'}

df.rename(columns=states,inplace=True)

df.head()    #修改英文字段名为中文字段名

#如上图可以知晓--坏客户是1,好客户对应0,

二、数据预处理

缺失值处理

异常值处理

1、缺失值处理

#查看各字段数据缺失情况

df.info()

#由上图可以知晓月收入和家属数量存在缺失

print("月收入缺失比:{:.2%}".format(df['月收入'].isnull().sum()/df.shape[0]))

#月收入缺失比:19.82%

print("家属数量缺失比:{:.2%}".format(df['家属数量'].isnull().sum()/df.shape[0]))

#家属数量缺失比:2.62%

#月收入缺失较大,使用平均值进行填充,家属数量缺失较少,将缺失的删掉,另外,如果字段缺失过大,将失去分析意义,可以将整个字段删除

df=df.fillna({'月收入':df['月收入'].mean()})

df1=df.dropna()

df1.shape

#缺失值处理总数据量为(146076, 11)


2、异常值处理

#可以通过箱线图观察异常值

import matplotlib.pyplot as plt

from pylab import mpl

mpl.rcParams['font.sans-serif'] = ['SimHei']

x1=df['可用额度比值']

x2=df['负债率']

x3=df1["年龄"]

x4=df1["逾期30-59天笔数"]

x5=df1["逾期60-89天笔数"]

x6=df1["逾期90天笔数"]

x7=df1["信贷数量"]

x8=df1["固定资产贷款量"]

fig=plt.figure(figsize=(20,15))

ax1=fig.add_subplot(221)

ax2=fig.add_subplot(222)

ax3=fig.add_subplot(223)

ax4=fig.add_subplot(224)

ax1.boxplot([x1,x2])

ax1.set_xticklabels(["可用额度比值","负债率"], fontsize=20)

ax2.boxplot(x3)

ax2.set_xticklabels("年龄", fontsize=20)

ax3.boxplot([x4,x5,x6])

ax3.set_xticklabels(["逾期30-59天笔数","逾期60-89天笔数","逾期90天笔数"], fontsize=20)

ax4.boxplot([x7,x8])

ax4.set_xticklabels(["信贷数量","固定资产贷款量"], fontsize=20)

#异常值处理消除不合逻辑的数据和超级离群的数据,可用额度比值应该小于1,年龄为0的是异常值,逾期天数笔数大于80的是超级离群数据,固定资产贷款量大于50的是超级离群数据,将这些离群值过滤掉,筛选出剩余部分数据。

df1=df1[df1['可用额度比值']<1]

df1=df1[df1['年龄']>0]

df1=df1[df1['逾期30-59天笔数']<80]

df1=df1[df1['逾期60-89天笔数']<80]

df1=df1[df1['逾期90天笔数']<80]

df1=df1[df1['固定资产贷款量']<50]

df1.shape

##缺失值处理总数据量为(146076, 11)

三、探索分析

探索分析在整个流程当中对数据的变动不大,主要起到了一种催化的作用,作为一种“启动”开展后续工作,可以更好的了解到数据之间的一些联系和变化规律。同时在多变量分析中通过相关性也可以过滤掉一部分变量。

单变量分析

多变量分析

1、单变量分析

是分析一个自变量和因变量之间的联系,此处以年龄和好坏客户为例进行分析。

将年龄均分成5组,求出每组的总的用户数

#求各组的坏客户数

age_cut_grouped1=df1['好坏客户'].groupby(age_cut).sum()

age_cut_grouped1

#merge联结

df2=pd.merge(pd.DataFrame(age_cut_group),pd.DataFrame(age_cut_grouped1),left_index=True,right_index=True)

df2.rename(columns={'好坏客户_x':'总客户数','好坏客户_y':'坏客户数'},inplace=True)

df2

#加一列好客户数

df2.insert(2,"好客户数",df2["总客户数"]-df2["坏客户数"])

df2

#再加一列坏客户占比

df2.insert(2,"坏客户占比",df2["坏客户数"]/df2["总客户数"])

df2

#作图展示

ax1=df2[["好客户数","坏客户数"]].plot.bar(figsize=(10,5))

ax1.set_xticklabels(df2.index,rotation=15)

ax1.set_ylabel("客户数")

ax1.set_title("年龄与好坏客户数分布图")

#趋势

ax11=df2["坏客户占比"].plot(figsize=(10,5))

ax11.set_xticklabels([0,20,29,38,47,55,64,72,81,89,98,107])

ax11.set_ylabel("坏客户率")

ax11.set_title("坏客户率随年龄的变化趋势图")

#可以看出随着年龄的增长,坏客户率在降低,其中38~55之间变化幅度最大

2、多变量分析

多变量分析就是对各个变量之间的相关性进行探索,线性回归模型中的特征之间由于存在精确相关关系或高度相关关系而使模型估计失真或难以估计准确,相关系数为1或者-1变量之间的相关性最大,对于相关性大的两组变量可以择一处理

import seaborn as sns

corr = df1.corr()#计算各变量的相关性系数

xticks = list(corr.index)#x轴标签

yticks = list(corr.index)#y轴标签

fig = plt.figure(figsize=(15,10))

ax1 = fig.add_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=15)

ax1.set_yticklabels(yticks, rotation=0, fontsize=15)

plt.show()

#可以看到各变量之间的相关性比较小,所以不需要操作,一般相关系数大于0.6可以进行变量剔除。

四、特征选择

这里使用IV值进行特征选择

WOE分箱

WOE值计算

IV值计算

WOE值替换

1、WOE分箱

将连续变量离散化,特征离散化后,模型会更稳定,降低了模型过拟合的风险。同时由于逻辑回归模型的每个变量的每种情况都会有对应的特征权值,使用分箱之后可以降低数据量,使模型泛化能力增强。

在学习笔记(一)中提到woe分箱可以先进行等频分箱,然后按照woe的单调性进行调整,保持单调性可以使连续数据转化为离散时数据之间能有一定的联系和趋势而不是孤立的几个数据(另外单调从系数的正负上也反映的单变量对结果的影响趋势),当然woe不一定保证完全单调递增或递减,保持一定趋势即可,这种趋势一般不会发生完全逆转(通过调整分箱值使得单调递增转为递减),所以按照客观事实进行调整即可,我们将按照此方法求取woe 值。

此处取分箱值可以先把数据导出使用excel进行调整

也可以使用python自动分箱woe转化

cut1=pd.qcut(df1["可用额度比值"],4,labels=False)

cut2=pd.qcut(df1["年龄"],8,labels=False)

bins3=[-1,0,1,3,5,13]

cut3=pd.cut(df1["逾期30-59天笔数"],bins3,labels=False)

cut4=pd.qcut(df1["负债率"],3,labels=False)

cut5=pd.qcut(df1["月收入"],4,labels=False)

cut6=pd.qcut(df1["信贷数量"],4,labels=False)

bins7=[-1, 0, 1, 3,5, 20]

cut7=pd.cut(df1["逾期90天笔数"],bins7,labels=False)

bins8=[-1, 0,1,2, 3, 33]

cut8=pd.cut(df1["固定资产贷款量"],bins8,labels=False)

bins9=[-1, 0, 1, 3, 12]

cut9=pd.cut(df1["逾期60-89天笔数"],bins9,labels=False)

bins10=[-1, 0, 1, 2, 3, 5, 21]

cut10=pd.cut(df1["家属数量"],bins10,labels=False)

2、WOE值计算

rate=df1["好坏客户"].sum()/(df1["好坏客户"].count()-df1["好坏客户"].sum())

def get_woe_data(cut):

    grouped=df1["好坏客户"].groupby(cut,as_index = True).value_counts()

    woe=np.log(grouped.unstack().iloc[:,1]/grouped.unstack().iloc[:,0]/rate)

    return woe

cut1_woe=get_woe_data(cut1)

cut2_woe=get_woe_data(cut2)

cut3_woe=get_woe_data(cut3)

cut4_woe=get_woe_data(cut4)

cut5_woe=get_woe_data(cut5)

cut6_woe=get_woe_data(cut6)

cut7_woe=get_woe_data(cut7)

cut8_woe=get_woe_data(cut8)

cut9_woe=get_woe_data(cut9)

cut10_woe=get_woe_data(cut10)

#随便挑几个变量看下woe

cut1_woe.plot.bar(color='b',alpha=0.3,rot=0)

cut2_woe.plot.bar(color='b',alpha=0.3,rot=0)

cut3_woe.plot.bar(color='b',alpha=0.3,rot=0)

#可以看出woe已调整到具有单调性

3、IV值计算

def get_IV_data(cut,cut_woe):

    grouped=df1["好坏客户"].groupby(cut,as_index = True).value_counts()

    cut_IV=((grouped.unstack().iloc[:,1]/df1["好坏客户"].sum()-grouped.unstack().iloc[:,0]/(df1["好坏客户"].count()-df1["好坏客户"].sum()))*cut_woe).sum()   

    return cut_IV

#计算各分组的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=pd.DataFrame([cut1_IV,cut2_IV,cut3_IV,cut4_IV,cut5_IV,cut6_IV,cut7_IV,cut8_IV,cut9_IV,cut10_IV],index=['可用额度比值','年龄','逾期30-59天笔数','负债率','月收入','信贷数量','逾期90天笔数','固定资产贷款量','逾期60-89天笔数','家属数量'],columns=['IV'])

iv=IV.plot.bar(color='b',alpha=0.3,rot=30,figsize=(10,5),fontsize=(10))

iv.set_title('特征变量与IV值分布图',fontsize=(15))

iv.set_xlabel('特征变量',fontsize=(15))

iv.set_ylabel('IV',fontsize=(15))


IV#各个变量的IV值如下 ,一般选取IV大于0.02的特征变量进行后续训练,从以上可以看出所有变量均满足,所以选取全部的

4、WOE值替换

df_new=pd.DataFrame() #新建df_new存放woe转换后的数据

def replace_data(cut,cut_woe):

    a=[]

    for i in cut.unique():

        a.append(i)

        a.sort()

    for m in range(len(a)):

        cut.replace(a[m],cut_woe.values[m],inplace=True)

    return cut

df_new["好坏客户"]=df1["好坏客户"]

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["负债率"]=replace_data(cut4,cut4_woe)

df_new["月收入"]=replace_data(cut5,cut5_woe)

df_new["信贷数量"]=replace_data(cut6,cut6_woe)

df_new["逾期90天笔数"]=replace_data(cut7,cut7_woe)

df_new["固定资产贷款量"]=replace_data(cut8,cut8_woe)

df_new["逾期60-89天笔数"]=replace_data(cut9,cut9_woe)

df_new["家属数量"]=replace_data(cut10,cut10_woe)

df_new.head()

五、模型训练

信用评分卡主要使用的算法模型是逻辑回归。logistic模型客群变化的敏感度不如其他高复杂度模型,因此稳健更好,鲁棒性更强。另外,模型直观,系数含义好阐述、易理解,使用逻辑回归优点是可以得到一个变量之间的线性关系式和对应的特征权值,方便后面将其转成一一对应的分数形式。


#使用sklearn库

from sklearn.linear_model import LogisticRegression

from sklearn.model_selection import train_test_split

x=df_new.iloc[:,1:]

y=df_new.iloc[:,:1]

x_train,x_test,y_train,y_test=train_test_split(x,y,test_size=0.6,random_state=0)

model=LogisticRegression()

clf=model.fit(x_train,y_train)

print('测试成绩:{}'.format(clf.score(x_test,y_test)))


#求特征权值系数coe,后面训练结果转分值时会用到:

coe=clf.coef_        #特征权值系数,后面转换为打分规则时会用到

coe

y_pred=clf.predict(x_test)

六、模型评估

模型评估主要看AUC和K-S值

from sklearn.metrics import roc_curve, auc

fpr, tpr, threshold = roc_curve(y_test, y_pred)

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")

plt.show()

roc_auc

roc_auc

fig, ax = plt.subplots()

ax.plot(1 - threshold, tpr, label='tpr') # ks曲线要按照预测概率降序排列,所以需要1-threshold镜像

ax.plot(1 - threshold, fpr, label='fpr')

ax.plot(1 - threshold, tpr-fpr,label='KS')

plt.xlabel('score')

plt.title('KS Curve')

plt.ylim([0.0, 1.0])

plt.figure(figsize=(20,20))

legend = ax.legend(loc='upper left')

plt.show()

max(tpr-fpr)

#又上图可知  ROC0.57, K-S值0.15左右,建模效果一般

七、模型结果转评分

由此可知每个变量不同分段对应的分数是B、β、ω这三个值的乘积,其中β(特征权值系数coe)和ω(WOE值)在前面已知,所以只要知道了AB的值就可以给用户打分了,这里要求AB的值要预先设定几个阈值,

偏移量A=offset

比例因子B=factor

b=offset+factor*log(o)

b+p=offset+factor *log(2o)

#假设好坏比为20的时候分数为600分,每高20分好坏比翻一倍

#现在我们求每个变量不同woe值对应的分数刻度可得:

factor = 20 / np.log(2)

offset = 600 - 20 * np.log(20) / np.log(2)

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)

x4 = get_score(coe[0][3], cut4_woe, factor)

x5 = get_score(coe[0][4], cut5_woe, factor)

x6 = get_score(coe[0][5], cut6_woe, factor)

x7 = get_score(coe[0][6], cut7_woe, factor)

x8 = get_score(coe[0][7], cut8_woe, factor)

x9 = get_score(coe[0][8], cut9_woe, factor)

x10 = get_score(coe[0][9], cut10_woe, factor)

print("可用额度比值对应的分数:{}".format(x1))

print("年龄对应的分数:{}".format(x2))

print("逾期30-59天笔数对应的分数:{}".format(x3))

print("负债率对应的分数:{}".format(x4))

print("月收入对应的分数:{}".format(x5))

print("信贷数量对应的分数:{}".format(x6))

print("逾期90天笔数对应的分数:{}".format(x7))

print("固定资产贷款量对应的分数:{}".format(x8))

print("逾期60-89天笔数对应的分数:{}".format(x9))

print("家属数量对应的分数:{}".format(x10))

#如上图所示:得出每个变量字段不同分箱阶段对应的分数值,方便后面计算每个观测值所对应的变量字段不同的分箱阶段的分数

#可以看出分数越高,成为坏客户的可能性越大。像年龄越大坏客率越低,可用额度比值、逾期笔#数这几个变量的分数跨度较大对最后的总分有更大的影响,这些都印证了前面探索分析的结果。

八、计算用户总分

1.取自动分箱的边界分割点

cu1=pd.qcut(df1["可用额度比值"],4,labels=False,retbins=True)

bins1=cu1[1]

cu2=pd.qcut(df1["年龄"],8,labels=False,retbins=True)

bins2=cu2[1]

# bins3=[-1,0,1,3,5,13]

# cut3=pd.cut(df1["逾期30-59天笔数"],bins3,labels=False)

cu4=pd.qcut(df1["负债率"],3,labels=False,retbins=True)

bins4=cu4[1]

cu5=pd.qcut(df1["月收入"],4,labels=False,retbins=True)

bins5=cu5[1]

cu6=pd.qcut(df1["信贷数量"],4,labels=False,retbins=True)

bins6=cu6[1]

2.各变量对应的分数求和,算出每个用户的总分

def compute_score(series,bins,score):

    list = []

    i = 0

    while i < len(series):

        value = series[i]

        j = len(bins) - 2

        m = len(bins) - 2

        while j >= 0:

            if value >= bins[j]:

                j = -1

            else:

                j -= 1

                m -= 1

        list.append(score[m])

        i += 1

    return list

#test1 = pd.read_csv('D:\\打包7.2\\练习项目汇总\\评分卡C卡\\cs-test.csv')

test1['x1'] = pd.Series(compute_score(test1['RevolvingUtilizationOfUnsecuredLines'], bins1, x1))

test1['x2'] = pd.Series(compute_score(test1['age'], bins2, x2))

test1['x3'] = pd.Series(compute_score(test1['NumberOfTime30-59DaysPastDueNotWorse'], bins3, x3))

test1['x4'] = pd.Series(compute_score(test1['DebtRatio'], bins4, x4))

test1['x5'] = pd.Series(compute_score(test1['MonthlyIncome'], bins5, x5))

test1['x6'] = pd.Series(compute_score(test1['NumberOfOpenCreditLinesAndLoans'], bins6, x6))

test1['x7'] = pd.Series(compute_score(test1['NumberOfTimes90DaysLate'], bins7, x7))

test1['x8'] = pd.Series(compute_score(test1['NumberRealEstateLoansOrLines'], bins8, x8))

test1['x9'] = pd.Series(compute_score(test1['NumberOfTime60-89DaysPastDueNotWorse'], bins9, x9))

test1['x10'] = pd.Series(compute_score(test1['NumberOfDependents'], bins10, x10))

test1['Score'] = test1['x1']+test1['x2']+test1['x3']+test1['x4']+test1['x5']+test1['x6']+test1['x7']+test1['x8']+test1['x9']+test1['x10']+600

test1.to_csv('D:\\打包7.2\\练习项目汇总\\评分卡C卡\\ScoreData1.csv', index=False)

#如上图excel截图就是每个观测值所有指标的

参考资料:https://www.cnblogs.com/daliner/p/10268350.html


最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,332评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,508评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,812评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,607评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,728评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,919评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,071评论 3 410
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,802评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,256评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,576评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,712评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,389评论 4 332
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,032评论 3 316
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,798评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,026评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,473评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,606评论 2 350