【机器学习】评分卡的制作

背景介绍

在银行借贷场景中,评分卡是一种以分数形式来衡量一个客户的信用风险大小的手段,它衡量向别人借钱的人(受
信人,需要融资的公司)不能如期履行合同中的还本付息责任,并让借钱给别人的人(授信人,银行等金融机构)
造成经济损失的可能性。一般来说,评分卡打出的分数越高,客户的信用越好,风险越小。
这些”借钱的人“,可能是个人,有可能是有需求的公司和企业。对于企业来说,我们按照融资主体的融资用途,分
别使用企业融资模型,现金流融资模型,项目融资模型等模型。而对于个人来说,我们有”四张卡“来评判个人的信
用程度:A卡,B卡,C卡和F卡。而众人常说的“评分卡”其实是指A卡,又称为申请者评级模型,主要应用于相关融
资类业务中新用户的主体评级,即判断金融机构是否应该借钱给一个新用户,如果这个人的风险太高,我们可以拒
绝贷款。本文是菜菜老师的评分卡案例的复现,以个人消费类贷款数据来进行建模。

数据清洗

—首先导入要用的库

import pandas as pd
import numpy as np
import matplotlib.pyplot as plt 
%matplotlib inline
from sklearn.linear_model import LogisticRegression as LR
from sklearn.ensemble import RandomForestRegressor as RFR
import imblearn
from imblearn.over_sampling import SMOTE
from sklearn.model_selection import train_test_split

— 读取个人消费类贷款数据

data = pd.read_csv('rankingcard.csv',index_col=0)
data.head()

原始数据
以下是数据每列代表的含义
特征/标签 含义
SeriousDlqin2yrs 出现 90 天或更长时间的逾期行为(即定义好坏客户)
RevolvingUtilizationOfUnsecuredLines 贷款以及信用卡可用额度与总额度比例
age 借款人借款年龄
NumberOfTime30-59DaysPastDueNotWorse 过去两年内出现35-59天逾期但是没有发展得更坏的次数
DebtRatio 每月偿还债务,赡养费,生活费用除以月总收入
MonthlyIncome 月收入
NumberOfOpenCreditLinesAndLoans 开放式贷款和信贷数量
NumberOfTimes90DaysLate 过去两年内出现90天逾期或更坏的次数
NumberRealEstateLoansOrLines 抵押贷款和房地产贷款数量,包括房屋净值信贷额度
NumberOfTime60-89DaysPastDueNotWorse 过去两年内出现60-89天逾期但是没有发展得更坏的次数
NumberOfDependents 家庭中不包括自身的家属人数(配偶,子女等)

去除原始数据重复值,看结果

data.drop_duplicates(inplace=True)
data.index = range(data.shape[0])
data,info()

在这里插入图片描述
可以发现,总共149391条数据,其中NumberOfDependents 和MonthlyIncome有缺失值,对于NumberOfDependents 的缺少,采用平均值进行填充,MonthlyIncome缺失了两万多条数据,我们将采用随机森林算法进行填充。怎么填充呢?随机森林利“既然可以使用A,B,C去预测Z,那我也可以使用A,C,Z去预测B”的思想来填补缺失值。对于一个有n个特征的数据来说,其中特征T有缺失值,我们就把特征T当作标签,其他的n-1个特征和原本的标签组成新的特征矩阵。那对于T来说,它没有缺失的部分,就是我们的Y_train,这部分数据既有标签也有特征,而它缺失的部分,只有特征没有标签,就是我们需要预测的部分。特征T不缺失的值对应的其他n-1个特征 + 本来的标签:X_train 特征T不缺失的值:Y_train 特征T缺失的值对应的其他n-1个特征 + 本来的标签:X_test 特征T缺失的值:未知,我们需要预测的Y_test。来我们看下面的缺失值填充代码:

data['NumberOfDependents'].fillna(int(data['NumberOfDependents'].mean()),inplace=True)#均值填充

def rfr_tianchong(data,toFill):
    df = data.copy()
    X = df.drop([toFill],axis=1)
    y = df.loc[:,toFill]
    
    Ytrain = y[y.notnull()]
    Ytest = y[y.isnull()]
    Xtrain = X.loc[Ytrain.index,:]
    Xtest = X.loc[Ytest.index,:]
    
    rfr = RFR(random_state=0).fit(Xtrain,Ytrain)
    ypredict = rfr.predict(Xtest)
    return ypredict

ypredict = rfr_tianchong(data,"MonthlyIncome")
data.loc[data['MonthlyIncome'].isnull(),"MonthlyIncome"] = ypredict
data.info()

result
到这里,我们对于缺失值进行了填充,共计149391条数据。在处理数据之前,我们先对数据进行预览,见下图,发现,年龄最小值为0,并且发现,NumberOfTimes90DaysLate(过去两年内出现90天逾期或更坏的次数)最大值为98,显然不合理,我们可以去咨询业务人员,请教他们这个逾期次数是如何计算的。如果这个指标是正常的,那这些两年内逾期了98次的客户,应该都是坏客户。在我们无法询问他们情况下,我们发现这种客户只有225个,于是我们删除这样的样本。

data.describe([0.01,0.25,0.5,0.75,0.99]).T

在这里插入图片描述

data = data[data['age']!=0]
data = data[data['NumberOfTimes90DaysLate']<90]
data.index = range(data.shape[0])
X = data.iloc[:,1:]
Y = data.iloc[:,0]
Y.value_counts()

在这里插入图片描述
发现样本有严重的不均衡现象,我们将使用SMOTE上采样方法进行均衡样本。

X,Y = SMOTE(random_state=1).fit_resample(X,Y)
print('0占的比例为{},1占的比例为{}'.format(Y.value_counts()[0]/Y.shape[0],Y.value_counts()[1]/Y.shape[0]))

在这里插入图片描述
训练集和验证集的划分

Xtrain,Xvali,Ytrain,Yvali = train_test_split(X,Y,test_size=0.3,random_state=420)
model_data = pd.concat([Ytrain,Xtrain],axis=1)
model_data.index = range(model_data.shape[0])
vali_data = pd.concat([Yvali, Xvali], axis=1)
vali_data.index = range(vali_data.shape[0])
vali_data.columns = data.columns

至此,我们对数据进行了去除重复值,缺失值的填充,样本不均衡的处理以及不合理值的处理。接下来,我们对样本进行分箱处理。

前面提到过,我们要制作评分卡,是要给各个特征进行分档,以便业务人员能够根据新客户填写的信息为客户打分。因此在评分卡制作过程中,一个重要的步骤就是分箱。可以说,分箱是评分卡最难,也是最核心的思路,分箱的本质,其实就是离散化连续变量,好让拥有不同属性的人被分成不同的类别(打上不同的分数),其实本质比较类似于聚类。那么,要分多少个箱子才合适?最开始我们并不知道,但是既然是将连续型变量离散化,想也知道箱子个数必然不能太多,最好控制在十个以下。而用来制作评分卡,最好能在4~5个为最佳。我们知道,离散化连续变量必然伴随着信息的损失,并且箱子越少,信息损失越大。为了衡量特征上的信息量以及特征对预测函数的贡献,银行业定义了概念Information value(IV):
在这里插入图片描述
其中N是这个特征上箱子的个数,i代表每个箱子, good%是这个箱内的优质客户(标签为0的客户)占整个特征中所有优质客户的比例, bad%是这个箱子里的坏客户(就是那些会违约,标签为1的那些客户)占整个特征中所有坏
客户的比例,而WOEi则写作:
在这里插入图片描述
这是我们在银行业中用来衡量违约概率的指标,中文叫做证据权重(weight of Evidence),本质其实就是优质客户
比上坏客户的比例的对数。WOE是对一个箱子来说的,WOE越大,代表了这个箱子里的优质客户越多。而IV是对
整个特征来说的,IV代表的意义,由下表来控制:
在这里插入图片描述
我们希望不同属性的人有不同的分数,因此我们希望在同一个箱子内的人的属性是尽量相似的,而不同箱子的人的属性是尽量不同的,即业界常说的”组间差异大,组内差异小“。对于评分卡来说,就是说我们希望一个箱子内的人违约概率是类似的,而不同箱子的人的违约概率差距很大,即WOE差距要大,并且每个箱子中坏客户所占的比重(Bad%)也要不同。那我们,可以使用卡方检验来对比两个箱子之间的相似性,如果两个箱子之间卡方检验的P值很大,则说明他们非常相似,那我们就可以将这两个箱子合并为一个箱子。
基于这样的思想,我们总结出我们对一个特征进行分箱的步骤:
1)我们首先把连续型变量分成一组数量较多的分类型变量,比如,将几万个样本分成100组,或50组
2)确保每一组中都要包含两种类别的样本,否则IV值会无法计算
3)我们对相邻的组进行卡方检验,卡方检验的P值很大的组进行合并,直到数据中的组数小于设定的N箱为止
4)我们让一个特征分别分成[2,3,4…20]箱,观察每个分箱个数下的IV值如何变化,找出最适合的分箱个数
5)分箱完毕后,我们计算每个箱的WOE值,观察分箱效果
这些步骤都完成后,我们可以对各个特征都进行分箱,然后观察每个特征的IV值,以此来挑选特征。
接下来,我们就以"age"为例子,来看看分箱如何完成。

# 等频分箱
model_data['qcut'],updown = pd.qcut(model_data['age'],retbins=True,q=20)

在这里插入图片描述

count_y0 = model_data[model_data["SeriousDlqin2yrs"]==0].groupby('qcut').count()["SeriousDlqin2yrs"]
count_y1 = model_data[model_data["SeriousDlqin2yrs"]==1].groupby('qcut').count()["SeriousDlqin2yrs"]
num_bins = [*zip(updown,updown[1:],count_y0,count_y1)]
columns = ['min','max','count_0','count_1']
df = pd.DataFrame(num_bins,columns=columns)
df

在这里插入图片描述

df['total'] = df["count_0"]+df["count_1"]
df['percentage'] = df['total']/df['total'].sum()
df['good%'] = df['count_0']/df['count_0'].sum()
df['bad%'] = df['count_1']/df['count_1'].sum()
df['woe'] = np.log(df['good%']/df['bad%'])
df.head()

在这里插入图片描述

IV = np.sum((df["good%"]-df['bad%'])*df['woe'])
IV

20个箱子得到 IV = 0.35182516103443695

def get_woe(num_bins):
    # 通过 num_bins 数据计算 woe
    columns = ["min","max","count_0","count_1"]
    df = pd.DataFrame(num_bins,columns=columns)
    df["total"] = df.count_0 + df.count_1
    df["percentage"] = df.total / df.total.sum()
    df["bad_rate"] = df.count_1 / df.total
    df["good%"] = df.count_0/df.count_0.sum()
    df["bad%"] = df.count_1/df.count_1.sum()
    df["woe"] = np.log(df["good%"] / df["bad%"])
    return df
def get_IV(df):
    IV = np.sum((df["good%"]-df['bad%'])*df['woe'])
    return IV    
# 画出IV曲线
num_bins_  = num_bins.copy()
import scipy
axisx = []
IV = []

while (len(num_bins_)>2):
    pvs = []
    for i in range(len(num_bins_)-1):
        x1 = num_bins_[i][2:]
        x2 = num_bins_[i+1][2:]
        pv = scipy.stats.chi2_contingency([x1,x2])[1]
        pvs.append(pv)
    i = pvs.index(max(pvs))
    num_bins_[i:i+2] = [(
        num_bins_[i][0],
        num_bins_[i+1][1],
        num_bins_[i][2]+num_bins_[i+1][2],
        num_bins_[i][3]+num_bins_[i+1][3]
    )]
    
    bins_df = get_woe(num_bins_)
    axisx.append(len(num_bins_))
    IV.append(get_IV(bins_df))
    
plt.figure()
plt.plot(axisx,IV)
plt.xticks(axisx)
plt.xlabel("number of box")
plt.ylabel("IV")
plt.show()

在这里插入图片描述
我们采用同样的道理对其他特征进行分箱处理。

def graphforbestbin(DF, X, Y, n=5,q=20,graph=True):
    """
    自动最优分箱函数,基于卡方检验的分箱
    参数:
    DF: 需要输入的数据
    X: 需要分箱的列名
    Y: 分箱数据对应的标签 Y 列名
    n: 保留分箱个数
    q: 初始分箱的个数
    graph: 是否要画出IV图像
    区间为前开后闭 (]
    """
    DF = DF[[X,Y]].copy()
    
    DF["qcut"],bins = pd.qcut(DF[X], retbins=True, q=q,duplicates="drop")
    coount_y0 = DF.loc[DF[Y]==0].groupby(by="qcut").count()[Y]
    coount_y1 = DF.loc[DF[Y]==1].groupby(by="qcut").count()[Y]
    num_bins = [*zip(bins,bins[1:],coount_y0,coount_y1)]
    
    for i in range(q):
        if 0 in num_bins[0][2:]:
            num_bins[0:2] = [(
                num_bins[0][0],
                num_bins[1][1],
                num_bins[0][2]+num_bins[1][2],
                num_bins[0][3]+num_bins[1][3])]
            continue
            
        for i in range(len(num_bins)):
            if 0 in num_bins[i][2:]:
                num_bins[i-1:i+1] = [(
                    num_bins[i-1][0],
                    num_bins[i][1],
                    num_bins[i-1][2]+num_bins[i][2],
                    num_bins[i-1][3]+num_bins[i][3])]
                break
        else:
            break
            
    def get_woe(num_bins):
        columns = ["min","max","count_0","count_1"]
        df = pd.DataFrame(num_bins,columns=columns)
        df["total"] = df.count_0 + df.count_1
        df["percentage"] = df.total / df.total.sum()
        df["bad_rate"] = df.count_1 / df.total
        df["good%"] = df.count_0/df.count_0.sum()
        df["bad%"] = df.count_1/df.count_1.sum()
        df["woe"] = np.log(df["good%"] / df["bad%"])
        return df

    def get_iv(df):
        iv = np.sum((df["good%"]-df['bad%'])*df['woe'])
        return iv
    IV = []
    axisx = []
    while len(num_bins) >= n:
        pvs = []
        for i in range(len(num_bins)-1):
            x1 = num_bins[i][2:]
            x2 = num_bins[i+1][2:]
            pv = scipy.stats.chi2_contingency([x1,x2])[1]
            pvs.append(pv)
        i = pvs.index(max(pvs))
        num_bins[i:i+2] = [(
            num_bins[i][0],
            num_bins[i+1][1],
            num_bins[i][2]+num_bins[i+1][2],
            num_bins[i][3]+num_bins[i+1][3])]
        
        bins_df = pd.DataFrame(get_woe(num_bins))
        axisx.append(len(num_bins))
        IV.append(get_iv(bins_df))
    if graph:
        plt.figure()
        plt.plot(axisx,IV)
        plt.xticks(axisx)
        plt.show()
    return bins_df

对所有特征进行分箱选择

model_data.columns

for i in model_data.columns[1:-1]:
    print(i)
    graphforbestbin(model_data,i,"SeriousDlqin2yrs",n=2,q=20)

在这里插入图片描述

auto_col_bins = {"RevolvingUtilizationOfUnsecuredLines":6,
                "age":5,
                "DebtRatio":4,
                "MonthlyIncome":3,
                "NumberOfOpenCreditLinesAndLoans":5}
#不能使用自动分箱的变量
hand_bins = {"NumberOfTime30-59DaysPastDueNotWorse":[0,1,2,13]
            ,"NumberOfTimes90DaysLate":[0,1,2,17]
            ,"NumberRealEstateLoansOrLines":[0,1,2,4,54]
            ,"NumberOfTime60-89DaysPastDueNotWorse":[0,1,2,8]
            ,"NumberOfDependents":[0,1,2,3]}
#保证区间覆盖使用 np.inf替换最大值,用-np.inf替换最小值
hand_bins = {k:[-np.inf,*v[:-1],np.inf] for k,v in hand_bins.items()}
bins_of_col = {}

# 生成自动分箱的分箱区间和分箱后的 IV 值
for col in auto_col_bins:
    bins_df = graphforbestbin(model_data,col
                                ,"SeriousDlqin2yrs"
                                ,n=auto_col_bins[col]
                                #使用字典的性质来取出每个特征所对应的箱的数量
                                ,q=20
                                ,graph=False)
    bins_list = sorted(set(bins_df["min"]).union(bins_df["max"]))
    #保证区间覆盖使用 np.inf 替换最大值 -np.inf 替换最小值
    bins_list[0],bins_list[-1] = -np.inf,np.inf
    bins_of_col[col] = bins_list
#合并手动分箱数据
bins_of_col.update(hand_bins)
bins_of_col

在这里插入图片描述
计算各箱的WOE并映射到数据中

data = model_data.copy()

def get_woe(df,col,y,bins):
    df = df[[col,y]].copy()
    df["cut"] = pd.cut(df[col],bins)
    bins_df = df.groupby("cut")[y].value_counts().unstack()
    woe = bins_df["woe"] =np.log((bins_df[0]/bins_df[0].sum())/(bins_df[1]/bins_df[1].sum()))
    return woe
woeall = {}
for col in bins_of_col:
    woeall[col] = get_woe(model_data,col,"SeriousDlqin2yrs",bins_of_col[col])
woeall

接下来,映射到各个表中。

model_woe = pd.DataFrame(index=model_data.index)
#将原数据分箱后,按箱的结果把WOE结构用map函数映射到数据中
model_woe["age"] = pd.cut(model_data["age"],bins_of_col["age"]).map(woeall["age"])
for col in bins_of_col:
    model_woe[col] = pd.cut(model_data[col],bins_of_col[col]).map(woeall[col])
model_woe["SeriousDlqin2yrs"] = model_data["SeriousDlqin2yrs"]
model_woe.head()

在这里插入图片描述

vali_woe = pd.DataFrame(index=vali_data.index)
for col in bins_of_col:
    vali_woe[col] = pd.cut(vali_data[col],bins_of_col[col]).map(woeall[col])
vali_woe["SeriousDlqin2yrs"] = vali_data["SeriousDlqin2yrs"]
vali_woe.head()

在这里插入图片描述

逻辑回归建模与验证

在这里分箱完成,获取了建模数据,只需要将对应的数据带入算法即可,在此,不多述。

vali_X = vali_woe.iloc[:,:-1]
vali_y = vali_woe.iloc[:,-1]
X = model_woe.iloc[:,:-1]
y = model_woe.iloc[:,-1]
from sklearn.linear_model import LogisticRegression as LR
c_1 = np.linspace(0.01,0.2,20)

score = []
for i in c_1:
    lr = LR(solver='liblinear',C=i).fit(X,y)
    score.append(lr.score(vali_X,vali_y))
plt.figure()
plt.plot(c_2,score)
plt.show()

在这里插入图片描述

  • 3
    点赞
  • 14
    收藏
    觉得还不错? 一键收藏
  • 3
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值