WOE信用评分卡(python+Excel实现法)

WOE信用评分卡(Python+Excel实现)

一、项目实施背景

信用评分卡是近年来兴起的一种为保障银行和其他金融部门的金融安全而设立的一种关于人生金融权限的划定模型。该模型指根据用户的信用历史资料,利用一定的信用评分模型,得到不同等级的信用分数。根据用户的信用分数,来决定是否放贷,以及相应的授信额度。随着目前小额消费贷的蓬勃发展,银行方面面临着单笔额度较小,申请额度分散,缺乏抵押以及客户质量不确定等诸多挑战,信用评分卡将会有广泛的应用前景。

二、项目具体目标及使用数据

数据来自于Kaggle,cs-train.csv 中是关于客户申请贷款的15万条征信数据。其中SeriousDlqin2yrs,即是否存在2年内超过90天违约甚至更严重的情况。下表显示出了各个用户特征变量的描述。

|变量名 |类型 |描述|
|--------|--------|
|SeriousDlqin2yrs |布尔型 |Target变量,超过90天或更糟的逾期拖欠|
|RevolvingUtilizationOfUnsecuredLines |百分比 |无担保放款的循环利用:除了不动产和像车贷那样除以信用额度总和的无分期付款债务的信用卡和个人信用额度总额|
|Age |整型 |借款人当时的年龄|
|NumberOfTime30-59DaysPastDueNotWorse |整型 |35-59天逾期但不糟糕次数|
|DebtRatio |百分比 |负债比率|
|MonthlyIncome |浮点 |月收入
|NumberOfOpenCreditLinesAndLoans |整型 |开放式信贷和贷款数量,开放式贷款(分期付款如汽车贷款或抵押贷款)和信贷(如信用卡)的数量|
|NumberOfTimes90DaysLate |整型 |90天逾期次数:借款者有90天或更高逾期的次数
|NumberRealEstateLoansOrLines |整型| 不动产贷款或额度数量:抵押贷款和不动产放款包括房屋净值信贷额度
|NumberOfTime60-89DaysPastDueNotWorse| 整型 |60-89天逾期但不糟糕次数:借款人在在过去两年内有60-89天逾期还款但不糟糕的次数|
|NumberOfDependents |整型 |家属数量:不包括本人在内的家属数量|

三、项目的具体流程

评分卡模型的建立具体有四个过程:
过程一:数据预处理,对所有数据进行去极值以及对数据的缺失值进行处理。
过程二:数据的拆分,将原始数据拆成验证集和训练集
过程三:对数据进行WOE离散化处理
过程四:检验离散化后的数据是否相关,从而决定是否进行降维处理
过程五:调整训练集的平衡性
过程六:训练模型,并使用验证集调整模型参数
过程七:将通过验证后的模型的权重提取出来,计算测试样本中每个客户的违约率过程八:用验证集检验模型的有效性以及稳定性
过程九:通过客户违约率给客户进行打分

四、数据预处理

步骤1:加载数据以及去极值

第一步先加载numpy,pandas,matplotlib 然后将csv中数据读入dataframe中

import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
def load_data():
    df_train=pd.read_csv('./Credit_Score_Data/cs-training.csv',sep=',',header='infer')
    df_train.drop(['Unnamed: 0'],inplace=True,axis=1)
    return df_train
df_train=load_data()

然后我们再通过Boxplot观察每个变量的极值问题

def outlier_check(df,c_name):
    p=df_train[[c_name]].boxplot(return_type='dict')
    x_outliers=p['fliers'][0].get_xdata()
    y_outliers = p['fliers'][0].get_ydata()
    for j in range(1):
        plt.annotate(y_outliers[j], xy=(x_outliers[j], y_outliers[j]), xytext=(x_outliers[j] + 0.02, y_outliers[j]))
    plt.show()

首先是变量 RevolvingUtilizationOfUnsecuredLines

outlier_check(df_train,'RevolvingUtilizationOfUnsecuredLines')

这里写图片描述
这里我们删除10000以上的记df_train=df_train[df_train['RevolvingUtilizationOfUnsecuredLines']<10000]

变量Age
这里写图片描述
这里我们删除0以及90以上的记录df_train=df_train[np.logical_and(df_train['age']>0,df_train['age']<90)]

变量NumberOfTime30-59DaysPastDueNotWorse
这里写图片描述
这里我们删除20以上的记录df_train = df_train[df_train['NumberOfTime30-59DaysPastDueNotWorse'] <20]

变量DebtRatio
这里写图片描述
这里我们删除50000以上的记录df_train = df_train[df_train['DebtRatio']<50000]

变量MonthlyIncome
这里写图片描述
这里我们删除500000以上的记录df_train = df_train[df_train['MonthlyIncome']<500000]

变量NumberOfOpenCreditLinesAndLoans
这里写图片描述
考虑到业务需要,我们认为这些借款机构特别多的人有欺诈的可能,因此我们这里不删除极值

变量NumberOfTimes90DaysLate
这里写图片描述
删除20以上的记录df_train = df_train[df_train['NumberOfTimes90DaysLate']<20]

变量NumberRealEstateLoansOrLines
这里写图片描述
删除6以上的记录df_train = df_train[df_train['NumberRealEstateLoansOrLines']<6]

变量NumberOfTime60-89DaysPastDueNotWorse
这里写图片描述
删除20以上的记录df_train = df_train[df_train['NumberOfTime60-89DaysPastDueNotWorse']<20]

变量NumberOfDependents
这里写图片描述
删除3以上的记录df_train = df_train[df_train['NumberOfDependents']<3]

步骤2:处理缺失值

接下来我们要明白数据的缺失值的分布情况, 使用count轻松得出。

def check_na(df):
    print(df.count(axis=0))

显示结果为:
SeriousDlqin2yrs 106829
RevolvingUtilizationOfUnsecuredLines 106829
age 106829
NumberOfTime30-59DaysPastDueNotWorse 106829
DebtRatio 106829
MonthlyIncome 106829
NumberOfOpenCreditLinesAndLoans 106829
NumberOfTimes90DaysLate 106829
NumberRealEstateLoansOrLines 106829
NumberOfTime60-89DaysPastDueNotWorse 106829
NumberOfDependents 106829

通过len(df_train)可以得出总共有106829个样本,因此在去除极值以后数据没有缺失值

五、数据拆分

在对数据进行进一步的拆分之前,我们要考虑到对我们训练后的机器学习算法进行有效的检验。因此我们有必要将所有的原始数据样本进行拆分,分为验证集以及训练集集。训练集用于训练模型从而使我们能够预测输入样本的违约率,而测试集则是用于验证我们训练后模型的有效性。

在进行数据拆分的过程中,要非常注意样本值的不平横性。在总共10万多个样本中SeriousDlqin2yrs为1的仅占1/10左右,如果完全采用随机抽样,那么违约样本的比例可能出现失真。因此,我们采取的方法是在违约与非违约的样本中各按一定的比例进行抽样。比如说如果验证集比例为0.2,我们在9万个非违约样本中抽出1.8万个,再从1万个违约样本中抽出2千个。抽取的代码如下所示:

def data_sample(inputX,index,test_Ratio=0.2):
    from random import sample
    data_array=np.atleast_1d(inputX)
    class_array=np.unique(data_array)
    test_list=[]
    train_list=[]
    for c in class_array:
        temp=[]
        for i,value in enumerate(data_array):
            if value==c:
                temp.append(index[i])
        test_list.extend(sample(temp,int(len(temp)*test_Ratio)))
    return list(set(index) - set(test_list)), test_list

def split_sample(df_train):
    train_list,test_list=data_sample(df_train['SeriousDlqin2yrs'].tolist(),df_train.index.tolist())
    df_train_section=df_train.ix[train_list,:]
    df_test_section=df_train.ix[test_list,:]
    return df_train_section,df_test_section
 
df_train,df_test=split_sample(df_train)

六、WOE数据离散化

WOE法是信用卡评分模型常用的数据离散化的方法。具体关于WOE法的介绍可以参考[http://blog.sina.com.cn/s/blog_8813a3ae0102uyo3.html]。我们这里将数据导出,然后使用Excel手工来决定数据离散的划分点。

writer=pd.ExcelWriter('C:/Users/Simonchen/Split_Result.xlsx')
df_train.to_excel(writer,'Train',index=False)
writer.save()

对于第一个变量RevolvingUtilizationOfUnsecuredLines ,其分析结果如下:
这里写图片描述
划分的时候注意每个人分段中good的数值尽可能平均,不要出现good太小的情况。WOE要保持一定的梯度。在分段以后按照WOE排序给每一段打一个分。这个分就是后面逻辑回归需要的参数。对于其他参数也采取该方法进行划分。具体方法由下图所示。

|变量名 |划分 |分数|
|--------|--------|
|RevolvingUtilizationOfUnsecuredLines |[0,0.03,0.12,0.4,0.8] |[1,0,2,3,4]|
|Age |[0,40,50,60,70] |[4,3,2,1,0]|
|NumberOfTime30-59DaysPastDueNotWorse |[0,1,2] |[0,1,2]|
|DebtRatio |[0,1,2] |[0,1,2]|
|MonthlyIncome |[0,1000,5000,10000] |[2,0,1,3]|
|NumberOfOpenCreditLinesAndLoans|[0,5,8,13]|[3,1,0,2]|
|NumberOfTimes90DaysLate|[0,1]|[0,1]|
|NumberRealEstateLoansOrLines|[0,1,2]|[2,0,1]|
|NumberOfTime60-89DaysPastDueNotWorse|[0,1]|[0,1]|
|NumberOfDependents|[0,1,2]|[0,1,2]|

我们使用python代码对原始数据进行划分
划分函数

def WOE_Convert(input,sp,rank):
    result=[]
    for v in input:
        for i in range(len(sp)):
            if i<len(sp)-1:
                if v>=sp[i] and v<sp[i+1]:
                    result.append(rank[i])
                    break
            else:
                if v>=sp[i]:
                    result.append(rank[i])
                else:
                    result.append(np.NaN)
    return result

我们使用该划分函数对原先的训练集合进行划分:

df_train['RevolvingUtilizationOfUnsecuredLines']=WOE_Convert(df_train['RevolvingUtilizationOfUnsecuredLines'],[0,0.03,0.12,0.4,0.8],[-1.2614,-1.2615,-0.5676,0.3631,1.2785])
df_train['age'] = WOE_Convert(df_train['age'],[0,40,50,60,70] ,[0.042,0.2259,-0.012,-0.5732,-0.9604])
df_train['NumberOfTime30-59DaysPastDueNotWorse'] = WOE_Convert(df_train['NumberOfTime30-59DaysPastDueNotWorse'],[0,1,2] ,[-0.51757,0.8626,1.8545])
df_train['DebtRatio'] = WOE_Convert(df_train['DebtRatio'],[0,1,2] ,[-0.02811,0.71398,-0.2327])
df_train['MonthlyIncome'] = WOE_Convert(df_train['MonthlyIncome'],[0,1000,5000,10000] ,[-0.41436,0.2844,-0.15859,0.54148])
df_train['NumberOfOpenCreditLinesAndLoans'] = WOE_Convert(df_train['NumberOfOpenCreditLinesAndLoans'],[0,5,8,13],[0.3140,-0.0993,-0.14208,-0.01537])
df_train['NumberOfTimes90DaysLate'] = WOE_Convert(df_train['NumberOfTimes90DaysLate'], [0,1],[-0.35838,2.26282])
df_train['NumberRealEstateLoansOrLines'] = WOE_Convert(df_train['NumberRealEstateLoansOrLines'], [0,1,2],[0.2032,-0.2005,-0.0691])
df_train['NumberOfTime60-89DaysPastDueNotWorse'] = WOE_Convert(df_train['NumberOfTime60-89DaysPastDueNotWorse'],[0,1],[-0.26171,2.006374])
df_train['NumberOfDependents'] = WOE_Convert(df_train['NumberOfDependents'],[0,1,2],[-0.1252,0.1254,0.2252])

df_test['RevolvingUtilizationOfUnsecuredLines']=WOE_Convert(df_test['RevolvingUtilizationOfUnsecuredLines'],[0,0.03,0.12,0.4,0.8],[-1.2614,-1.2615,-0.5676,0.3631,1.2785])
df_test['age'] = WOE_Convert(df_test['age'],[0,40,50,60,70] ,[0.042,0.2259,-0.012,-0.5732,-0.9604])
df_test['NumberOfTime30-59DaysPastDueNotWorse'] = WOE_Convert(df_test['NumberOfTime30-59DaysPastDueNotWorse'],[0,1,2] ,[-0.51757,0.8626,1.8545])
df_test['DebtRatio'] = WOE_Convert(df_test['DebtRatio'],[0,1,2] ,[-0.02811,0.71398,-0.2327])
df_test['MonthlyIncome'] = WOE_Convert(df_test['MonthlyIncome'],[0,1000,5000,10000] ,[-0.41436,0.2844,-0.15859,0.54148])
df_test['NumberOfOpenCreditLinesAndLoans'] = WOE_Convert(df_test['NumberOfOpenCreditLinesAndLoans'],[0,5,8,13],[0.3140,-0.0993,-0.14208,-0.01537])
df_test['NumberOfTimes90DaysLate'] = WOE_Convert(df_test['NumberOfTimes90DaysLate'], [0,1],[-0.35838,2.26282])
df_test['NumberRealEstateLoansOrLines'] = WOE_Convert(df_test['NumberRealEstateLoansOrLines'], [0,1,2],[0.2032,-0.2005,-0.0691])
df_test['NumberOfTime60-89DaysPastDueNotWorse'] = WOE_Convert(df_test['NumberOfTime60-89DaysPastDueNotWorse'],[0,1],[-0.26171,2.006374])
df_test['NumberOfDependents'] = WOE_Convert(df_test['NumberOfDependents'],[0,1,2],[-0.1252,0.1254,0.2252])

将数据标准正太化

#计算df_train各个特征的方差和平均数,并对df_train中的X变量标准正太化
Y_train=df_train['SeriousDlqin2yrs'].values.tolist()
df_train_X=df_train.drop(['SeriousDlqin2yrs'],axis=1)
std_series=df_train_X.std()
mean_series=df_train_X.mean()
df_train_X=df_train_X.add(mean_series).div(std_series)
#计算df_test做同样的操作
Y_test=df_test['SeriousDlqin2yrs'].values.tolist()
df_test_X=df_test.drop(['SeriousDlqin2yrs'],axis=1)
std_series=df_test_X.std()
mean_series=df_test_X.mean()
df_test_X=df_test_X.add(mean_series).div(std_series)

相关性检验
使用python自带的相关性检验工具进行检验
首先我们将df_train中的X,Y变量提取出来,然后对X变量求协方差矩阵

Mcov=np.corrcoef(np.array(X_train).T)
df_exl=pd.DataFrame(data=Mcov)
writer=pd.ExcelWriter('./cov_matrix.xlsx')
df_exl.to_excel(writer,'cov',index=False)
writer.save()

这里写图片描述
这里我们可以看出不存在0.5以上的相关性,因此我们可以认为数据的相关性不强没有必要进行降维操作。

八、使用SMOTE法调整训练样本的平衡性

在最初的训练样本中,不违约的用户为79860人,违约的用户约为5604人。二者比例约为15:1由于数据的不平衡,逻辑回归模型倾向于自动删除部分高风险的用户,因此我们采用SMOTE法过采样违约的用户使得训练样本尽可能的平衡,关于SMOTE法的介绍可以参考https://www.zhihu.com/question/285824343
其python中已经有了包可以使用
#由于数据不平衡,采用smote法对训练集中少数类别采用SMOTE过过采样

from collections import Counter
#print(Counter(Y_train)) #计算初在训练集中,0有79860,1为5604,两者比例约为15:1
from imblearn.over_sampling import SMOTE #使用pip install imblearn安装该包
smo = SMOTE(ratio={1: 40000},random_state=42,n_jobs=-1)#将比例调整为2:1左右
X_train, Y_train = smo.fit_sample(X_train, Y_train)
#print(Counter(Y_train))

九、使用逻辑回归对模型进行训练

为了对用户进行评分,我们必须根据已经拥有的用户信息预测用户可能的违约率。对于这一类预测问题,我们常用的方法就是逻辑回归模型。当然,首先我们必须确保数据进入模型以前已经被进行过标准正太化,方法如下

from sklearn.preprocessing import StandardScaler
StdScaler=StandardScaler()
X_train=StdScaler.fit_transform(X_Train)
X_test=StdScaler.transform(X_Test)

随后我们引入逻辑回归模型,并使用L2正则化防止过拟合,具体原理可以参考http://chenrudan.github.io/blog/2016/01/09/logisticregression.html
其中正则化强度C由验证曲线,validation_curve来确定,具体代码如下:

from sklearn.model_selection import validation_curve
from sklearn.linear_model import LogisticRegression
param_range = [0.001,0.01,0.1,1,10]
train_scores, test_scores = validation_curve(estimator=LogisticRegression(dual=True), X=X_train, y=Y_train, param_name='C', param_range=param_range, cv=3) #使用逻辑回归的验证器,验证的参数为C,cross validation的折数为3
train_mean = np.mean(train_scores, axis=1)
train_std = np.std(train_scores, axis=1)
test_mean = np.mean(test_scores, axis=1)
test_std = np.std(test_scores, axis=1)
plt.plot(param_range, train_mean, color='b', marker='o', markersize=5, label='param_train')
plt.fill_between(param_range, train_mean+train_std, train_mean-train_std, alpha=0.3, color='b')
plt.plot(param_range, test_mean, ls='--', color='g', marker='s', markersize=5, label='param_test')
plt.fill_between(param_range, test_mean+test_std, test_mean-test_std, alpha=0.3, color='g')
plt.grid()
plt.xscale('log')
plt.xlabel('param: penalty level')
plt.ylabel('precision')
plt.legend(loc='lower right')
plt.show()

显示出的验证曲线如下:
在这里插入图片描述
可以看出当C>=0.01时,差别并不大,因此我们选C=0.1作为正则化强度

这里我们通过训练集得出权重W0~W10

from sklearn.linear_model import LogisticRegression
LR=LogisticRegression(dual=True,C=0.1)
LR.fit(X_train,Y_train)
weight=LR.coef_[0]
intercept=LR.intercept_[0]
output_df=pd.DataFrame(data=np.array(weight).T,columns=['WEIGHT'])
output_df['INTERCEPT']=intercept
output_df['FEATURE']=df_train_X.columns
output_df.set_index('FEATURE',inplace=True)
writer=pd.ExcelWriter('./weight.xlsx')
output_df.to_excel(writer,"weight")
writer.save()

这里写图片描述

让后我们再将标准正态化之后的验证集也导入excel中:

output_df=df_test_X
output_df['TargetTag1']=Y_test
writer=pd.ExcelWriter('./test_data.xlsx')
output_df.to_excel(writer,'Test_Data')
writer.save()

十、模型验证

接下来的分析工作由Excel+VBA完成,在我们获得了权重以后我们就可以知道每一个用户的预测违约率,即 P p r e d i c t ( Y i = 1 ∣ X = X i ) P_{predict}(Y_i=1|X=X_i) Ppredict(Yi=1X=Xi)
P p r e d i c t ( Y i = 1 ∣ X = X i ) = 1 1 + e − Θ T X i P_{predict}(Y_i=1|X=X_i)=\dfrac{1}{1+e^{-\Theta_TX_i}} Ppredict(Yi=1X=Xi)=1+eΘTXi1
其中 Y i Y_i Yi是第i个样本违约的情况, X i X_i Xi为第i个样本的特征向量,其中第一个元素为常数项1。 Θ \Theta Θ为每个特征元素的权重。这里我使用VBA计算违约概率,代码为

Function CalculateProbability(id As Variant, Optional isPCA As Boolean = False) As Double
    Dim currSheet As Worksheet
    Dim weightSheet As Worksheet
    Dim rowNum As Long
    Dim colNum As Long
    Dim i As Long
    Dim theta As Double
    If isPCA = True Then
        Set currSheet = ThisWorkbook.Worksheets("PCA_Test_Model")
        Set weightSheet = ThisWorkbook.Worksheets("PCA_Weight")
    Else
        Set currSheet = ThisWorkbook.Worksheets("Model_Test")
        Set weightSheet = ThisWorkbook.Worksheets("Weight")
    End If
    i = 2
    Do While weightSheet.Cells(i, 1).Value <> ""
        rowNum = WorksheetFunction.Match(id, currSheet.Range("A:A"), 0)
        colNum = WorksheetFunction.Match(weightSheet.Cells(i, 1).Value, currSheet.Range("1:1"), 0)
        theta = theta + weightSheet.Cells(i, 2).Value * currSheet.Cells(rowNum, colNum)
        i = i + 1
    Loop
    theta = theta + weightSheet.Cells(2, 3).Value
    CalculateProbability = 1 / (1 + Exp(-theta))
End Function

对于每个概率的阀值,我们分别计算Recall和FPrate
这里写图片描述

R e c a l l = P ( Y p r e d i c t = 1 ∣ Y a c t u a l = 1 ) Recall=P(Y_{predict}=1|Y_{actual}=1) Recall=P(Ypredict=1Yactual=1)
F P = P ( Y p r e d i c t = 1 ∣ Y a c t u a l = 0 ) FP=P(Y_{predict}=1|Y_{actual}=0) FP=P(Ypredict=1Yactual=0)

ROC曲线验证模型

根据Recall 和 FP的可以做出ROC曲线
这里写图片描述

可以得到AUC值为0.86,说明预测效果极好。

PSI模型稳定信

在这里插入图片描述
由于PSI小于10%,说明模型非常稳定

十一、评分卡输出

我们取Recall-FP最大时的阀值0.07. 当预测值大于0.07时我们认为是违约,当预测值小于0.07时我们认为是不违约。

这里我们将P=0.07时定义为60分,P=0时定义为100分,则可以得到
S c o r e = − 571 ∗ P p r e d i c t + 100 Score=-571*P_{predict}+100 Score=571Ppredict+100
当样本分数超过60分时,我们认为可能是违约用户。如果当样本分数低于60分,我们认为是好用户。

  • 10
    点赞
  • 106
    收藏
    觉得还不错? 一键收藏
  • 7
    评论
WOE信用评分是一种基于WOE(Weight of Evidence)转换的信用评分模型,常用于风控领域。在Python中,我们可以使用pandas库和sklearn库中的一些模块来实现WOE评分模型的构建。 首先,我们需要对数据进行分箱处理,并计算每个分箱中好坏样本的数量,从而计算出每个分箱中好坏样本的比例和WOE值。然后,我们可以使用LogisticRegression模型进行拟合,得到每个特征的系数,进而计算出每个样本的分数。 下面是一个简单的示例代码: ```python import pandas as pd from sklearn.linear_model import LogisticRegression from sklearn.metrics import roc_auc_score # 分箱函数 def binning(col, target, max_bins=10): bins = pd.qcut(col, max_bins, duplicates='drop') grouped = df.groupby(bins)[target].agg(['count', 'sum']) grouped['bad_rate'] = grouped['sum'] / grouped['count'] return grouped # 计算WOE值 def calc_woe(grouped): total_good = grouped['sum'].sum() total_bad = grouped['count'].sum() - total_good woe = pd.Series() for idx, row in grouped.iterrows(): good = row['sum'] bad = row['count'] - good woe[idx] = np.log((good / total_good) / (bad / total_bad)) return woe # 数据导入 df = pd.read_csv('credit.csv') # 分箱处理 binning_result = binning(df['age'], df['target']) woe_age = calc_woe(binning_result) # LogisticRegression模型拟合 X = pd.cut(df['age'], bins=binning_result.index, labels=woe_age) y = df['target'] lr = LogisticRegression() lr.fit(X.to_frame(), y) # 计算AUC值 y_prob = lr.predict_proba(X.to_frame())[:, 1] auc = roc_auc_score(y, y_prob) print('AUC score:', auc) ```

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值