逻辑回归制作金融申请评分卡

本文以个人消费类贷款数据,来简单介绍金融申请评分卡的建模和制作流程。

1.1导库,获取数据

#导库
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
%matplotlib inline
from sklearn.linear_model import LogisticRegression as LR

#获取数据
data=pd.read_csv(r'E:\code\pingfenka\rankingcard.csv',index_col=0)

1.2探索数据与数据预处理

#观察数据类型
data.head()

#观察数据结构
data.shape
data.info()

在这里插入图片描述

1.2.1去除重复值

#去除重复值
data.drop_duplicates(inplace=True)
data.info()

#恢复索引
data.index=range(data.shape[0])
data.info()

1.2.2填补缺失值

#探索缺失值
data.info()
data.isnull().sum()/data.shape[0]  #缺失值占比
#data.isnull().mean()

#家属人数缺失值比较少,直接用均值填充
data['NumberOfDependents'].fillna(int(data['NumberOfDependents'].mean()),inplace=True)

随机森林填补缺失值(收入):

#随机森林填补缺失值包装成函数
def fill_missing_rf(X,y,to_fill):
    #X是要填补的特征矩阵,y是没有缺失值的标签,to_fill是要填补列的名称
    
    #构建新特征矩阵和新标签
    df=X.copy()
    fill=df.loc[:,to_fill]
    df=pd.concat([df.loc[:,df.columns!=to_fill],pd.DataFrame(y)],axis=1)
    
    #找出训练集和测试集
    Ytrain=fill[fill.notnull()]
    Ytest=fill[fill.isnull()]
    Xtrain=df.iloc[Ytrain.index,:]
    Xtest=df.iloc[Ytest.index,:]
    
    #用随机森林回归来填补缺失值
    from sklearn.ensemble import RandomForestRegressor as rfr
    rfr=rfr(n_estimators=100)
    rfr=rfr.fit(Xtrain,Ytrain)
    Ypredict=rfr.predict(Xtest)
    
    return Ypredict

用上述随机森林填补收入列缺失值:

X=data.iloc[:,1:]
y=data['SeriousDlqin2yrs']
X.shape

y_pred=fill_missing_rf(X,y,'MonthlyIncome')
#确认结果合理后,就可以将缺失数据覆盖
data.loc[data.loc[:,'MonthlyIncome'].isnull(),'MonthlyIncome']=y_pred

1.2.3描述性统计处理异常值

#描述性统计
data.describe([0.01,0.1,0.25,0.5,.75,.9,.99]).T

#年龄的最小值为0,不符合业务需求。年龄为0的只有一条数据,直接删除
(data['age']==0).sum()
data=data[data['age']!=0]

#有三个过去两年内预期次数的指标,最大值为98,不太合理。
#有255个样本是这种情况,而且标签并不都是1,异常样本,直接删除。
data[data.loc[:,'NumberOfTimes90DaysLate'] > 90].count()
data[data.loc[:,'NumberOfTimes90DaysLate'] > 90]['SeriousDlqin2yrs'].value_counts()

data=data[data.loc[:,'NumberOfTimes90DaysLate'] < 90]
#充值索引
data.index=range(data.shape[0])
data.info()

1.2.4样本不均衡问题

#探索标签的分布
X=data.iloc[:,1:]
y=data.iloc[:,0]

y.value_counts()

n_sample=X.shape[0]

n_1_sample=y.value_counts()[1]
n_0_sample=y.value_counts()[0]

print('样本个数:{}; 1占{:.2%}; 0占{:.2%}'.format(n_sample,n_1_sample/n_sample,n_0_sample/n_sample))
#样本严重不均衡,金融行业违约率本身就比较低。

#上采样法平衡样本
import imblearn
from imblearn.over_sampling import SMOTE

sm=SMOTE(random_state=42)  #实例化
X,y=sm.fit_sample(X,y)

n_sample_=X.shape[0]

pd.Series(y).value_counts()

n_1_sample=pd.Series(y).value_counts()[1]
n_0_sample=pd.Series(y).value_counts()[0]

print('样本个数:{}; 1占{:.2%}; 0占{:.2%}'.format(n_sample_,n_1_sample/n_sample_,n_0_sample/n_sample_))

1.2.5分训练集和测试集

#分训练集和测试集
from sklearn.model_selection import train_test_split
X=pd.DataFrame(X)
y=pd.DataFrame(y)

X_train,X_vali,Y_train,Y_vali=train_test_split(X,y,test_size=0.3,random_state=420)
model_data=pd.concat([Y_train,X_train],axis=1)  #合并,利于后续分箱

model_data.index=range(model_data.shape[0])
model_data.columns=data.columns

vali_data=pd.concat([Y_vali,X_vali],axis=1)
vali_data.index=range(vali_data.shape[0])
vali_data.columns=data.columns

model_data.to_csv(r'E:\code\pingfenka\model_data.csv')
vali_data.to_csv(r'E:\code\pingfenka\vali_data.csv')

1.3分箱

1.3.1等频分箱

#以age为例,按照等频对需要分箱的列进行分箱
model_data['qcut'],updown=pd.qcut(model_data['age'],retbins=True,q=20)

#添加新列qcut,即每个样本对应的箱子
model_data['qcut']

#所有箱子的上限和下限
updown

#统计每个分箱中0和1的数量
coount_y0=model_data[model_data['SeriousDlqin2yrs']==0].groupby(by='qcut').count()['SeriousDlqin2yrs']
coount_y1=model_data[model_data['SeriousDlqin2yrs']==1].groupby(by='qcut').count()['SeriousDlqin2yrs']

#num_bins值分别为每个区间的上界,下界,0出现的次数,1出现的次数
num_bins=[*zip(updown,updown[1:],coount_y0,coount_y1)]

#zip会按照最短列来进行结合
num_bins

1.3.2确保每个箱中都有0和1

#确保每个箱中都有0和1
for i in range(20):
	#如果第一个组没有包含正样本或负样本,向后合并
    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  #如果对第一组和对后面所有组的判断中,都没有进入if去合并,则提前结束所有的循环   

1.3.3定义WOE和IV函数

#定义WOE和IV函数
#bad rate是一个箱中,坏样本所占的比例
#bad%是一个箱中的坏样本占整个特征中的坏样本的比例
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

#计算IV值
def get_iv(df):
    rate=df['good%']-df['bad%']
    iv=np.sum(rate*df.woe)
    return iv

1.3.4卡方检验,合并箱体,画出IV曲线

num_bins_=num_bins.copy()

import matplotlib.pyplot as plt
import scipy

IV=[]
axisx=[]

while len(num_bins_)>2:
    pvs=[]
    #获取num_bins_两两之间的卡方检验的置信度(或卡方值)
    for i in range(len(num_bins_)-1):
        x1=num_bins_[i][2:]
        x2=num_bins_[i+1][2:]
        #0返回chi2值,1返回p值
        pv=scipy.stats.chi2_contingency([x1,x2])[1]
        pvs.append(pv)
    #通过p值进行处理,合并p值最大的两组   
    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()

1.3.5用最佳分箱个数分箱,并验证分箱结果

def get_bin(num_bins_,n):
    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])]
    return num_bins_

afterbins=get_bin(num_bins,4)
afterbins

bins_df=get_woe(num_bins)
bins_df

1.3.6将选取最佳分箱个数的过程包装为函数

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):
        rate=df['good%']-df['bad%']
        iv=np.sum(rate*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.xlabel('number of box')
        plt.ylabel('IV')
        plt.show()
    return bins_df

1.3.7对所有特征进行分箱选择

model_data.columns

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

不是所有的特征都可以使用这个分箱函数,一些特征无法分出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={}

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

1.4计算各箱的WOE并映射到数据中

data=model_data.copy()

#函数pd.cut,可以根据已知的分箱间隔把数据分箱
data=data[['age','SeriousDlqin2yrs']].copy()
data['cut']=pd.cut(data['age'],[-np.inf, 45.0, 46.98277343010937, 58.65715423057309, 64.0,np.inf])
data

#将数据按分箱结果聚合,并取出其中的标签值
data.groupby('cut')['SeriousDlqin2yrs'].value_counts()

#使用unstack()来将树状结构变成表状结构
data.groupby('cut')['SeriousDlqin2yrs'].value_counts().unstack()

bins_df=data.groupby('cut')['SeriousDlqin2yrs'].value_counts().unstack()
bins_df['woe']=np.log((bins_df[0]/bins_df[0].sum())/(bins_df[1]/bins_df[1].sum()))
bins_df

将上述过程包装成函数:

#将以上过程包装成函数
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

#将所有特征的WOE存储到字典当中
woeall={}
for col in bins_of_col:
    woeall[col]=get_woe(model_data,col,'SeriousDlqin2yrs',bins_of_col[col])

woeall

接下来,将所有WOE映射到原始数据中:

#不希望覆盖掉原本的数据,创建一个新的DataFrame,索引和原始数据model_data一模一样
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'])
model_woe

#对所有特征操作可以写成:
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()

1.5建模与模型验证

#处理测试集
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_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

lr=LR().fit(X,y)
lr.score(vali_X,vali_y)

返回的结果一般,可以试着使用C和max_iter的学习曲线把逻辑回归的效果调上去

#调参
c_1=np.linspace(0.01,1,20)
c_2=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_1,score)
plt.show()

score=[]
for i in c_2:
    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()

lr.n_iter_

score=[]
for i in [1,2,3,4,5,6]:
    lr=LR(solver='liblinear',C=0.025,max_iter=i).fit(X,y)
    score.append(lr.score(vali_X,vali_y))
plt.figure()
plt.plot([1,2,3,4,5,6],score)
plt.show()

尽管从准确率来看,模型效果属于一般。但是从ROC曲线来看,效果还是很不错的。
可以以roc指标来对模型调参。

#从ROC曲线上看看效果
import scikitplot as skplt

vali_proba_df=pd.DataFrame(lr.predict_proba(vali_X))
skplt.metrics.plot_roc(vali_y,vali_proba_df,plot_micro=False,figsize=(6,6),plot_macro=False)

在这里插入图片描述

1.6制作评分卡

假设设定特定违约概率下的预期分值为600,PDO=20

#计算A,B的值
B=20/np.log(2)
A=600+B*np.log(1/60)
B,A

计算基础分

base_score=A-B*lr.intercept_  #截距
base_score

score_age=woeall['age']*(-B*lr.coef_[0][0])  #每个特征对应的参数
score_age

将所有特征的评分卡内容写入本地文件:

file=r'E:\code\pingfenka\ScoreData.csv'

with open(file,'w') as fdata:
    fdata.write('base_score,{}\n'.format(base_score))
for i,col in enumerate(X.columns):
    score=woeall[col]*(-B*lr.coef_[0][i])
    score.name='Score'
    score.index.name=col
    score.to_csv(file,header=True,mode='a')

上面就是申请评分卡的基本流程了,当然里面还有一些细节需要优化。另外针对不同的业务场景,中间各步骤处理的方式及细节会有所不同,掌握基本流程后,再根据实际情况来运用。

评论 10
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值