信用评分系统简介:
信用评分系统有着比经验丰富的人更高的判断能力,能够降低银行贷款的风险。本质这套系统的作用是判断某个个人是否会违约,是不是“坏”客户,最终影响能否贷款给该人。信用评分系统有参数也有非参数模型,这里主要用的是非参数模型、有监督学习模型,也就是通过分析其他贷款人地特征,来判断某个个人是否会违约。本文主要讨论主体评级模型的开发过程。
一、模型开发流程
信用风险评级模型的主要开发流程如下:
(1) 数据获取和整合,导入数据,将杂乱数据处理为模型所需格式。
(2) 数据预处理(EDA)和探索性数据分析,主要工作包括数据清洗、缺失值处理、获取样本总体的大概情况,比如用直方图、箱形图等、异常值处理,主要是为了将获取的原始数据转化为可用作模型开发的格式化数据。
(3) 变量选择,该步骤主要是通过统计学的方法,筛选出对违约状态影响最显著的指标。这里是单变量特征选择方法和基于机器学习模型的方法 。
(4) 模型开发,该步骤主要包括变量分段、变量的WOE(证据权重)变换和逻辑回归估算三部分。
(5) 模型评估,该步骤主要是评估模型的区分能力、预测能力、稳定性,并形成模型评估报告,得出模型是否可以使用的结论。
(6) 信用评分,根据逻辑回归的系数和WOE等确定信用评分的方法。将Logistic模型转换为标准评分的形式。
(7) 建立评分系统,根据信用评分方法,建立自动信用评分系统。
二、数据获取
数据来源地址:https://www.kaggle.com/c/GiveMeSomeCredit/overview/description
数据集名称:Give Me Some Credit
数据集大小:
Data ictionary.csv(12*3),
cs-test.csv(10W*11),
cs-training.csv(15W*11),
sampleEntry.csv(10W*3)
数据描述:
cs-training.csv
序号 | 变量名 | 变量解释 |
1 | SeriousDlqin2yrs | 近两年内是否出现逾期90天及以上的情况 |
2 | RevolvingUtilizationOfUnsecuredLines | 无担保贷款数量占总贷款数比率 |
3 | age | 借款人年龄 |
4 | NumberOfTime30-59DaysPastDueNotWorse | 近两年内逾期30-59天的次数 |
5 | DebtRatio | 负债比 |
6 | MonthlyIncome | 月收入 |
7 | NumberOfOpenCreditLinesAndLoans | 车房贷的数量 |
8 | NumberOfTimes90DaysLate | 过往逾期90天及以上的次数 |
9 | NumberRealEstateLoansOrLines | 不动产抵押贷款的数量 |
10 | NumberOfTime60-89DaysPastDueNotWorse | 近两年内逾期60-89天的次数 |
11 | NumberOfDependents | 家属数量 |
Ps:时间窗口:自变量的观察窗口为过去两年,因变量表现窗口为未来两年。
三、数据预处理和(EDA)探索性数据分析
3.1 缺失值处理
这种情况非常常见,在分析前要先去处理,以免给后续的分析带来阻碍,导致一些分析方法不能运用
缺失值处理的方法,一般包括如下几种。
- 直接使用含有缺失值的特征;
- 删除含有缺失值的特征(该方法在包含缺失值的属性仅仅包含极少量有效值时是有效的);
- 直接删除含有缺失值的样本;
- 缺失值补全:均值插补、建模预测等等
INT:
# Basic packages
import numpy as np
import pandas as pd
# Viz
import matplotlib.pyplot as plt
import seaborn as sns
# settings
import warnings
warnings.filterwarnings("ignore")
from sklearn.ensemble import RandomForestClassifier
from sklearn.ensemble import RandomForestRegressor
#载入数据
data = pd.read_csv(r"C:\Users\86180\Desktop\GiveMeSomeCredit (试验)\cs-training.csv",encoding='gb18030')
#数据集确实和分布情况
data.describe().to_csv(r'C:\Users\86180\Desktop\GiveMeSomeCredit (试验)\DataDescribe.csv')
data.describe()
OUT:
Unnamed: 0 | SeriousDlqin2yrs | RevolvingUtilizationOfUnsecuredLines | age | NumberOfTime30-59DaysPastDueNotWorse | DebtRatio | MonthlyIncome | NumberOfOpenCreditLinesAndLoans | NumberOfTimes90DaysLate | NumberRealEstateLoansOrLines | NumberOfTime60-89DaysPastDueNotWorse | NumberOfDependents | |
---|---|---|---|---|---|---|---|---|---|---|---|---|
count | 150000.000000 | 150000.000000 | 150000.000000 | 150000.000000 | 150000.000000 | 150000.000000 | 1.202690e+05 | 150000.000000 | 150000.000000 | 150000.000000 | 150000.000000 | 146076.000000 |
mean | 75000.500000 | 0.066840 | 6.048438 | 52.295207 | 0.421033 | 353.005076 | 6.670221e+03 | 8.452760 | 0.265973 | 1.018240 | 0.240387 | 0.757222 |
std | 43301.414527 | 0.249746 | 249.755371 | 14.771866 | 4.192781 | 2037.818523 | 1.438467e+04 | 5.145951 | 4.169304 | 1.129771 | 4.155179 | 1.115086 |
min | 1.000000 | 0.000000 | 0.000000 | 0.000000 | 0.000000 | 0.000000 | 0.000000e+00 | 0.000000 | 0.000000 | 0.000000 | 0.000000 | 0.000000 |
25% | 37500.750000 | 0.000000 | 0.029867 | 41.000000 | 0.000000 | 0.175074 | 3.400000e+03 | 5.000000 | 0.000000 | 0.000000 | 0.000000 | 0.000000 |
50% | 75000.500000 | 0.000000 | 0.154181 | 52.000000 | 0.000000 | 0.366508 | 5.400000e+03 | 8.000000 | 0.000000 | 1.000000 | 0.000000 | 0.000000 |
75% | 112500.250000 | 0.000000 | 0.559046 | 63.000000 | 0.000000 | 0.868254 | 8.249000e+03 | 11.000000 | 0.000000 | 2.000000 | 0.000000 | 1.000000 |
max | 150000.000000 | 1.000000 | 50708.000000 | 109.000000 | 98.000000 | 329664.000000 | 3.008750e+06 | 58.000000 | 98.000000 | 54.000000 | 98.000000 | 20.000000 |
INT:
data.isnull().sum()
OUT:
从以上样本的描述来看,变量MonthlyIncome缺失值比较大,不能直接删除样本,也没有大到可以直接删除特征,所以使用补全的方法,这里使用随机森林进行建模预测;而变量NumberOfDependents的缺失值比较少,这里就直接删除含缺失值的样本。
INT:
# 用随机森林对缺失值预测填充函数
def set_missing(df):
# 把已有的数值型特征取出来
process_df = df.iloc[:,[0,1,2,3,4,5,6,7,8,9,10,11]]
# 分成已知该特征和未知该特征两部分
known = process_df[process_df.MonthlyIncome.notnull()].values
unknown = process_df[process_df.MonthlyIncome.isnull()].values
# X为特征属性值
X = known[:, [1,2,3,4,5,7,8,9,10]]
# y为结果标签值
y = known[:,[6]]
# fit到RandomForestRegressor之中
rfr = RandomForestRegressor(random_state=0,
n_estimators=200,max_depth=3,n_jobs=-1)
rfr.fit(X,y)
# 用得到的模型进行未知特征值预测
predicted = rfr.predict(unknown[:,[1,2,3,4,5,7,8,9,10]]).round(0)
print(predicted)
# 用得到的预测结果填补原缺失数据
print(len(predicted))
df.loc[(df.MonthlyIncome.isnull()), 'MonthlyIncome'] = predicted
return df
data=set_missing(data)#用随机森林填补比较多的缺失值
data=data.dropna()#删除比较少的缺失值
data=data.drop_duplicates()#删除重复项
data.to_csv(r'C:\Users\86180\Desktop\GiveMeSomeCredit (试验)\MissingData.csv',index=False)
data.shape
3.2 剔除异常值和探索性数据分析
异常值,即在数据集中存在不合理的值,又称离群点,比如年龄小于0,或者不符合正态分布的数据。
异常值的处理方法常用有四种:
1.删除含有异常值的记录
2.将异常值视为缺失值,交给缺失值处理方法来处理
3.用平均值来修正
4.不处理
这里视情况因为异常样本少,选择了删除异常样本。
INT:
# 年龄等于0的异常值进行剔除
data = data[data['age']> 0]
下面展现总体的直方图,看看全部变量数据的分布情况。
# 来个快速版的直方图
data.hist(bins=50, figsize=(20,15))
plt.show()
OUT:
可以看出DebtRatio、NumberOfTime30-59DaysPastDueNotWorse、NumberOfTime60-89DaysPastDueNotWorse、NumberOfTimes90DaysLate、NumberRealEstateLoansOrLines、RevolvingUtilizationOfUnsecuredLines的数据分布情况比较异常,应该有一些极端的数值影响了分布图像的呈现。下面分别用箱型图查看一下数值的分布,删去极端值。
先查看DebtRatio的箱型图
INT:
datatemp2=data["DebtRatio"]
datatemp2.plot(kind='box',title='DebtRatio Distribution',sym='r+');
OUT:
INT:
print(data[data['DebtRatio'] > 8000].count()) # 看了看需要提出多少异常值
OUT:
可以看出DebtRatio的异常值较少,不影响整体数据,于是选择删去
INT:
#只有几百个样本,对于15万数据来说不影响总体数据结构,所以剔除
data = data[data['DebtRatio'] < 8000]
查看DebtRatio的箱型图
INT:
datatemp3=data["MonthlyIncome"]
datatemp3.plot(kind='box',title='MonthlyIncome Distribution',sym='r+');
OUT:
INT:
print(data[data['MonthlyIncome'] > 50000].count())
OUT:
MonthlyIncome的异常值也较少,删去。
INT:
#剔除异常值
data = data[data['MonthlyIncome'] < 50000]
查看NumberOfDependents的箱型图
INT:
datatemp4=data["NumberOfDependents"]
datatemp4.plot(kind='box',title='NumberOfDependents Distribution',sym='r+');
OUT:
INT:
print(data[data['NumberOfDependents'] > 10].count())
NumberOfDependents的异常值也较少,数值差别大,也可以删去。
查看NumberOfTime30-59DaysPastDueNotWorse的箱型图
INT:
datatemp5=data["NumberOfTime30-59DaysPastDueNotWorse"]
datatemp5.plot(kind='box',title='NumberOfTime30-59DaysPastDueNotWorse Distribution',sym='r+');
OUT:
INT:
print(data[data['NumberOfTime30-59DaysPastDueNotWorse'] > 20].count())
OUT:
NumberOfTime30-59DaysPastDueNotWorse的异常值也较少,数值差别大,也可以删去。
INT:
#剔除异常值
data = data[data['NumberOfTime30-59DaysPastDueNotWorse'] < 20]
查看NumberOfTime60-89DaysPastDueNotWorse的箱型图
INT:
datatemp6=data["NumberOfTime60-89DaysPastDueNotWorse"]
datatemp6.plot(kind='box',title='NumberOfTime60-89DaysPastDueNotWorse Distribution',sym='r+');
OUT:
查看NumberOfTimes90DaysLate的箱型图
INT:
datatemp7=data["NumberOfTimes90DaysLate"]
datatemp7.plot(kind='box',title='NumberOfTimes90DaysLate Distribution',sym='r+');
OUT:
由NumberOfTime60-89DaysPastDueNotWorse、NumberOfTimes90DaysLate的箱型图可以看出,分布很正常,可能是删去NumberOfTime30-59DaysPastDueNotWorse的异常值时也一并删除了
查看NumberRealEstateLoansOrLines的箱型图
INT:
datatemp8=data["NumberRealEstateLoansOrLines"]
datatemp8.plot(kind='box',title='NumberRealEstateLoansOrLines Distribution',sym='r+');
OUT:
INT:
print(data[data['NumberRealEstateLoansOrLines'] > 30].count())
OUT:
NumberRealEstateLoansOrLines的异常值也较少,删去。
INT:
#剔除异常值
data = data[data['NumberRealEstateLoansOrLines'] < 30]
查看RevolvingUtilizationOfUnsecuredLines的箱型图
INT:
datatemp9=data["RevolvingUtilizationOfUnsecuredLines"]
datatemp9.plot(kind='box',title='RevolvingUtilizationOfUnsecuredLines Distribution',sym='r+');
OUT:
INT:
print(data[data['RevolvingUtilizationOfUnsecuredLines'] > 6].count())
OUT:
RevolvingUtilizationOfUnsecuredLines的异常值也较少,删去。
INT:
#剔除异常值
data = data[data['RevolvingUtilizationOfUnsecuredLines'] < 6]
再看看总体以直方图呈现的数据分布情况
INT:
data=data.drop(["Unnamed: 0"],axis=1)
data.hist(bins=50, figsize=(20,15))
plt.show()
OUT:
客户收入和年龄分布如下图所示,可以看到两个变量都大致呈正态分布,符合统计分析的假设。
INT:
plt.figure(figsize=(15,5))
plt.hist(data.MonthlyIncome,bins=70,color="green",alpha=0.8,rwidth=0.9)
plt.title("MonthlyIncome distribution")
plt.ylabel('# of MonthlyIncome', fontsize=12)
plt.xlabel('MonthlyIncome', fontsize=12)
plt.show()
OUT:
INT:
plt.figure(figsize=(15,5))
plt.hist(data.age,bins=50,color="green",alpha=0.8,rwidth=0.9)
plt.title("age distribution")
plt.ylabel('# of age', fontsize=12)
plt.xlabel('age', fontsize=12)
plt.show()
OUT:
由此看出,删去异常样本后,数据整体改善,能够进行下一步的分析。
五、变量选择
特征变量选择(排序)对于数据分析、机器学习从业者来说非常重要。
好的特征选择能够提升模型的性能,更能帮助我们理解数据的特点、底层结构,这对进一步改善模型、算法都有着重要作用。
至于Python的变量选择代码实现可以参考结合Scikit-learn介绍几种常用的特征选择方法。
在本文中,我们采用信用评分模型的变量选择方法,通过WOE分析方法,即是通过比较指标分箱和对应分箱的违约概率来确定指标是否符合经济意义。
首先我们对变量进行离散化(分箱)处理。
5.1 分箱处理
变量分箱(binning)是对连续变量离散化(discretization)的一种称呼。
信用评分卡开发中一般有常用的等距分段、等深分段、最优分段。
其中等距分段(Equval length intervals)是指分段的区间是一致的,比如年龄以十年作为一个分段;
等深分段(Equal frequency intervals)是先确定分段数量,然后令每个分段中数据数量大致相等;
最优分段(Optimal Binning)又叫监督离散化(supervised discretizaion),使用递归划分(Recursive Partitioning)将连续变量分为分段,背后是一种基于条件推断查找较佳分组的算法。
我们首先选择对连续变量进行最优分段,在连续变量的分布不满足最优分段的要求时,再考虑对连续变量进行等距分段。最优分箱的代码如下:
INT:
from sklearn.model_selection import train_test_split
Y = data['SeriousDlqin2yrs']
X = data.iloc[:, 1:]
#测试集占比30%
X_train, X_test, Y_train, Y_test = train_test_split(X, Y, test_size=0.3, random_state=0)
train = pd.concat([Y_train, X_train], axis=1)
test = pd.concat([Y_test, X_test], axis=1)
train.to_csv(r'C:\Users\86180\Desktop\GiveMeSomeCredit (试验)\TrainData.csv',index=False)
test.to_csv(r'C:\Users\86180\Desktop\GiveMeSomeCredit (试验)\TestData.csv',index=False)
INT:
# 定义自动分箱函数
from scipy import stats
def mono_bin(Y, X, n = 20):
r = 0
good=Y.sum()
bad=Y.count()-good
while np.abs(r) < 1:
d1 = pd.DataFrame({"X": X, "Y": Y, "Bucket": pd.qcut(X, n,duplicates="drop")})
# 后面报错You can drop duplicate edges by setting the 'duplicates' kwarg,所以回到这里补充duplicates参数
# pandas中使用qcut(),边界易出现重复值,如果为了删除重复值设置 duplicates=‘drop’,则易出现于分片个数少于指定个数的问题
# 经尝试,设置duplicates参数为“drop”可行,而不能设置为“raise”。
d2 = d1.groupby('Bucket', as_index = True)
r, p = stats.spearmanr(d2.X.mean(), d2.mean().Y)
n = n - 1
d3 = pd.DataFrame(d2.X.min(), columns = ['min'])
d3['min'] = d2.min().X
d3['max'] = d2.max().X
d3['sum'] = d2.sum().Y
d3['total'] = d2.count().Y
d3['rate'] = d2.mean().Y
d3['woe']=np.log((d3['rate']/(1-d3['rate']))/(good/bad))
d4 = (d3.sort_values(by='min')).reset_index(drop=True)
print("=" * 60)
woe=list(d4['woe'].round(3))
print(d4)
return d4
将每个变量都尝试分箱
INT:
mono_bin(data.SeriousDlqin2yrs,data['DebtRatio'])
OUT:
min | max | sum | total | rate | woe | |
0 | 0.000000 | 0.236339 | 45317 | 48158 | 0.941007 | 0.131195 |
---|---|---|---|---|---|---|
1 | 0.236353 | 0.543381 | 45156 | 48157 | 0.937683 | 0.072847 |
2 | 0.543422 | 7977.000000 | 44361 | 48157 | 0.921174 | -0.179918 |
INT:
mono_bin(data.SeriousDlqin2yrs,data.RevolvingUtilizationOfUnsecuredLines)
OUT:
min | max | sum | total | rate | woe | |
---|---|---|---|---|---|---|
0 | 0.000000 | 0.031022 | 35444 | 36118 | 0.981339 | 1.324148 |
1 | 0.031022 | 0.157268 | 35379 | 36118 | 0.979539 | 1.230245 |
2 | 0.157271 | 0.555506 | 34310 | 36118 | 0.949942 | 0.304885 |
3 | 0.555530 | 5.377748 | 29701 | 36118 | 0.822332 | -1.106101 |
INT:
mono_bin(data.SeriousDlqin2yrs,data.age)
OUT:
min | max | sum | total | rate | woe | |
---|---|---|---|---|---|---|
0 | 21 | 31 | 10688 | 12033 | 0.888224 | -0.565603 |
1 | 32 | 36 | 9706 | 10842 | 0.895222 | -0.493100 |
2 | 37 | 40 | 10031 | 11010 | 0.911081 | -0.311427 |
3 | 41 | 44 | 11425 | 12494 | 0.914439 | -0.269250 |
4 | 45 | 47 | 9799 | 10696 | 0.916137 | -0.247351 |
5 | 48 | 50 | 10270 | 11131 | 0.922648 | -0.159443 |
6 | 51 | 53 | 9794 | 10577 | 0.925971 | -0.111938 |
7 | 54 | 56 | 9586 | 10231 | 0.936956 | 0.060478 |
8 | 57 | 60 | 12278 | 12908 | 0.951193 | 0.331514 |
9 | 61 | 63 | 9967 | 10422 | 0.956342 | 0.448407 |
10 | 64 | 67 | 9840 | 10170 | 0.967552 | 0.756788 |
11 | 68 | 74 | 11353 | 11645 | 0.974925 | 1.022153 |
12 | 75 | 107 | 10097 | 10313 | 0.979056 | 1.206384 |
INT:
mono_bin(data.SeriousDlqin2yrs,data.MonthlyIncome)
OUT:
min | max | sum | total | rate | woe | |
---|---|---|---|---|---|---|
0 | 0.0 | 3387.0 | 44406 | 48158 | 0.922090 | -0.167245 |
1 | 3388.0 | 6800.0 | 44623 | 48184 | 0.926096 | -0.110123 |
2 | 6801.0 | 49750.0 | 45805 | 48130 | 0.951693 | 0.342342 |
INT:
mono_bin(data.SeriousDlqin2yrs,data['NumberOfTime30-59DaysPastDueNotWorse'])
OUT:
min | max | sum | total | rate | woe | |
---|---|---|---|---|---|---|
0 | 0 | 1 | 129688 | 136950 | 0.946973 | 0.244146 |
1 | 2 | 2 | 3302 | 4492 | 0.735085 | -1.617756 |
2 | 3 | 13 | 1844 | 3030 | 0.608581 | -2.196980 |
INT:
mono_bin(data.SeriousDlqin2yrs,data['NumberOfTime60-89DaysPastDueNotWorse'])
OUT:
min | max | sum | total | rate | woe | |
---|---|---|---|---|---|---|
0 | 0 | 11 | 134834 | 144472 | 0.933288 | 6.661338e-16 |
INT:
mono_bin(data.SeriousDlqin2yrs,data['NumberOfTimes90DaysLate'])
OUT:
min | max | sum | total | rate | woe | |
---|---|---|---|---|---|---|
0 | 0 | 17 | 134834 | 144472 | 0.933288 | 6.661338e-16 |
INT:
mono_bin(data.SeriousDlqin2yrs,data.NumberRealEstateLoansOrLines)
OUT:
min | max | sum | total | rate | woe | |
---|---|---|---|---|---|---|
0 | 0 | 26 | 134834 | 144472 | 0.933288 | 6.661338e-16 |
INT:
mono_bin(data.SeriousDlqin2yrs,data.NumberOfDependents)
OUT:
min | max | sum | total | rate | woe | |
---|---|---|---|---|---|---|
0 | 0.0 | 1.0 | 105013 | 111869 | 0.938714 | 0.090629 |
1 | 2.0 | 2.0 | 17816 | 19376 | 0.919488 | -0.202920 |
2 | 3.0 | 3.0 | 8590 | 9418 | 0.912083 | -0.298990 |
3 | 4.0 | 9.0 | 3415 | 3809 | 0.896561 | -0.478749 |
INT:
mono_bin(data.SeriousDlqin2yrs,data.NumberOfOpenCreditLinesAndLoans)
OUT:
min | max | sum | total | rate | woe | |
---|---|---|---|---|---|---|
0 | 0 | 57 | 134834 | 144472 | 0.933288 | 6.661338e-16 |
从上面每个变量的分箱情况可以看出,DebtRatio、RevolvingUtilizationOfUnsecuredLines、age、MonthlyIncome是可以得出有效的分箱的,NumberOfTime30-59DaysPastDueNotWorse、NumberOfDependents的分箱非常不均匀,也不是符合最优分箱的,而NumberOfOpenCreditLinesAndLoans、NumberOfTimes90DaysLate、NumberOfTime60-89DaysPastDueNotWorse、NumberRealEstateLoansOrLines最终结果只分得一个组,那么针对不能最优分箱的变量,下面将进行自定义分箱。
INT:
# 连续变量离散化
pinf = float('inf')#正无穷大
ninf = float('-inf')#负无穷大
cutx6 = [ninf, 1, 2, 3, 5, pinf]
cutx7 = [ninf, 0, 1, 3, 5, pinf]
cutx8 = [ninf, 0,1,2, 3, pinf]
cutx9 = [ninf, 0, 1, 3, pinf]
5.2 WOE
WoE分析, 是对指标分箱、计算各个档位的WoE值并观察WoE值随指标变化的趋势。其中WoE的数学定义是:
woe=ln(goodattribute/badattribute)
在进行分析时,我们需要对各指标从小到大排列,并计算出相应分档的WoE值。其中正向指标越大,WoE值越小;反向指标越大,WoE值越大。正向指标的WoE值负斜率越大,反响指标的正斜率越大,则说明指标区分能力好。WoE值趋近于直线,则意味指标判断能力较弱。若正向指标和WoE正相关趋势、反向指标同WoE出现负相关趋势,则说明此指标不符合经济意义,则应当予以去除。
woe函数实现在上面的mono_bin()函数里面已经包含,这里不再重复。
5.3 相关性分析和IV筛选
接下来,我们会用经过清洗后的数据看一下变量间的相关性。注意,这里的相关性分析只是初步的检查,进一步检查模型的IV(证据权重)作为变量筛选的依据。
INT:
# 数据集各变量的相关性。
# 相关性图我们通过Python里面的seaborn包,调用heatmap()绘图函数进行绘制,实现代码如下:
corr = data.corr()#计算各变量的相关性系数
xticks = ['x0','x1','x2','x3','x4','x5','x6','x7','x8','x9','x10']#x轴标签
yticks = list(corr.index)#y轴标签
fig = plt.figure()
ax1 = fig.add_subplot(1, 1, 1)
sns.heatmap(corr, annot=True, cmap='rainbow', ax=ax1, annot_kws={'size': 9, 'weight': 'bold', 'color': 'blue'})#绘制相关性系数热力图
ax1.set_xticklabels(xticks, rotation=0, fontsize=10)
ax1.set_yticklabels(yticks, rotation=0, fontsize=10)
plt.show()
OUT:
从热力图可以看出,各变量之间的相关性很小
接下来,我进一步计算每个变量的Infomation Value(IV)。IV指标是一般用来确定自变量的预测能力。 其公式为:
IV=sum((goodattribute-badattribute)*ln(goodattribute/badattribute))
通过IV值判断变量预测能力的标准是:
< 0.02: unpredictive
0.02 to 0.1: weak
0.1 to 0.3: medium
0.3 to 0.5: strong
> 0.5: suspicious
IV的实现放在mono_bin()函数里面,代码实现如下
INT:
# IV的实现放在mono_bin()函数里面,代码实现如下:
# 定义自动分箱函数
def mono_bin(Y, X, n = 20):
r = 0
good=Y.sum()
bad=Y.count()-good
while np.abs(r) < 1:
d1 = pd.DataFrame({"X": X, "Y": Y, "Bucket": pd.qcut(X, n,duplicates="drop")})
d2 = d1.groupby('Bucket', as_index = True)
r, p = stats.spearmanr(d2.mean().X, d2.mean().Y)
n = n - 1
d3 = pd.DataFrame(d2.X.min(), columns = ['min'])
d3['min']=d2.min().X
d3['max'] = d2.max().X
d3['sum'] = d2.sum().Y
d3['total'] = d2.count().Y
d3['rate'] = d2.mean().Y
d3['woe']=np.log((d3['rate']/(1-d3['rate']))/(good/bad))
d3['goodattribute']=d3['sum']/good
d3['badattribute']=(d3['total']-d3['sum'])/bad
iv=((d3['goodattribute']-d3['badattribute'])*d3['woe']).sum()
d4 = (d3.sort_values(by = 'min')).reset_index(drop=True)
print("=" * 60)
print(d4)
cut=[]
cut.append(float('-inf'))
for i in range(1,n+1):
qua=X.quantile(i/(n+1))
cut.append(round(qua,4))
cut.append(float('inf'))
woe=list(d4['woe'].round(3))
return d4,iv,cut,woe
#自定义分箱函数
# ——该定义函数参考另一篇帖子:https://blog.csdn.net/sunyaowu315/article/details/82981216
def self_bin(Y,X,cat):
good=Y.sum()
bad=Y.count()-good
d1=pd.DataFrame({'X':X,'Y':Y,'Bucket':pd.cut(X,cat)})
d2=d1.groupby('Bucket', as_index = True)
d3 = pd.DataFrame(d2.X.min(), columns=['min'])
d3['min'] = d2.min().X
d3['max'] = d2.max().X
d3['sum'] = d2.sum().Y
d3['total'] = d2.count().Y
d3['rate'] = d2.mean().Y
d3['woe'] = np.log((d3['rate'] / (1 - d3['rate'])) / (good / bad))
d3['goodattribute'] = d3['sum'] / good
d3['badattribute'] = (d3['total'] - d3['sum']) / bad
iv = ((d3['goodattribute'] - d3['badattribute']) * d3['woe']).sum()
d4 = (d3.sort_values(by='min'))
print("=" * 60)
print(d4)
woe = list(d4['woe'].round(3))
return d4, iv,woe
INT:
dfx1, ivx1,cutx1,woex1 = mono_bin(data.SeriousDlqin2yrs, data.RevolvingUtilizationOfUnsecuredLines,n=10)
dfx2, ivx2,cutx2,woex2 = mono_bin(data.SeriousDlqin2yrs, data.age, n=10)
dfx4, ivx4,cutx4,woex4 =mono_bin(data.SeriousDlqin2yrs, data.DebtRatio, n=20)
dfx5, ivx5,cutx5,woex5 =mono_bin(data.SeriousDlqin2yrs, data.MonthlyIncome, n=10)
# 连续变量离散化
cutx3 = [ninf, 0, 1, 3, 5, pinf]
cutx6 = [ninf, 1, 2, 3, 5, pinf]
cutx7 = [ninf, 0, 1, 3, 5, pinf]
cutx8 = [ninf, 0,1,2, 3, pinf]
cutx9 = [ninf, 0, 1, 3, pinf]
cutx10 = [ninf, 0, 1, 2, 3, 5, pinf]
dfx3, ivx3,woex3 = self_bin(data.SeriousDlqin2yrs, data['NumberOfTime30-59DaysPastDueNotWorse'], cutx3)
dfx6, ivx6,woex6= self_bin(data.SeriousDlqin2yrs, data['NumberOfOpenCreditLinesAndLoans'], cutx6)
dfx7, ivx7,woex7 = self_bin(data.SeriousDlqin2yrs, data['NumberOfTimes90DaysLate'], cutx7)
dfx8, ivx8,woex8 = self_bin(data.SeriousDlqin2yrs, data['NumberRealEstateLoansOrLines'], cutx8)
dfx9, ivx9,woex9 = self_bin(data.SeriousDlqin2yrs, data['NumberOfTime60-89DaysPastDueNotWorse'], cutx9)
dfx10, ivx10,woex10 = self_bin(data.SeriousDlqin2yrs, data['NumberOfDependents'], cutx10)
OUT:
INT:
# 生成的IV图代码:
ivlist=[ivx1,ivx2,ivx3,ivx4,ivx5,ivx6,ivx7,ivx8,ivx9,ivx10]#各变量IV
index=['x1','x2','x3','x4','x5','x6','x7','x8','x9','x10']#x轴的标签
fig1 = plt.figure(1)
ax1 = fig1.add_subplot(1, 1, 1)
x = np.arange(len(index))+1
ax1.bar(x, ivlist, width=0.4)#生成柱状图
ax1.set_xticks(x)
ax1.set_xticklabels(index, rotation=0, fontsize=12)
ax1.set_ylabel('IV(Information Value)', fontsize=14)
#在柱状图上添加数字标签
for a, b in zip(x, ivlist):
plt.text(a, b + 0.01, '%.4f' % b, ha='center', va='bottom', fontsize=10)
plt.show()
OUT:
输出的各变量IV值如上图。
可以看出,DebtRatio(x4)、MonthlyIncome(x5)、NumberOfOpenCreditLinesAndLoans(x6)、NumberRealEstateLoansOrLines(x8)和NumberOfDependents(x10)变量的IV值明显较低,所以予以删除。
六、模型分析
证据权重(Weight of Evidence,WOE)转换可以将Logistic回归模型转变为标准评分卡格式。引入WOE转换的目的并不是为了提高模型质量,只是一些变量不应该被纳入模型,这或者是因为它们不能增加模型值,或者是因为与其模型相关系数有关的误差较大,其实建立标准信用评分卡也可以不采用WOE转换。这种情况下,Logistic回归模型需要处理更大数量的自变量。尽管这样会增加建模程序的复杂性,但最终得到的评分卡都是一样的。
在建立模型之前,我们需要将筛选后的变量转换为WoE值,便于信用评分。
6.1 WOE转换
我们已经能获取了每个变量的分箱数据和woe数据,只需要根据各变量数据进行替换,实现代码如下:
INT:
from sklearn.metrics import roc_curve
from sklearn.metrics import auc
from sklearn import metrics
import statsmodels.api as sm
import math
def trans_woe(var,var_name,x_woe,x_cut):
woe_name = var_name + '_woe'
for i in range(len(x_woe)):
if i == 0:
var.loc[(var[var_name]<=x_cut[i+1]),woe_name] = x_woe[i]
elif (i>0) and (i<= len(x_woe)-2):
var.loc[((var[var_name]>x_cut[i])&(var[var_name]<=x_cut[i+1])),woe_name] =
x_woe[i]
else:
var.loc[(var[var_name]>x_cut[len(x_woe)-1]),woe_name] = x_woe[len(x_woe)-1]
return var
x1_name = 'RevolvingUtilizationOfUnsecuredLines'
x2_name = 'age'
x3_name = 'NumberOfTime30-59DaysPastDueNotWorse'
x7_name = 'NumberOfTimes90DaysLate'
x9_name = 'NumberOfTime60-89DaysPastDueNotWorse'
X_train = trans_woe(X_train,x1_name,woex1,cutx1)
X_train = trans_woe(X_train,x2_name,woex2,cutx2)
X_train = trans_woe(X_train,x3_name,woex3,cutx3)
X_train = trans_woe(X_train,x7_name,woex7,cutx7)
X_train = trans_woe(X_train,x9_name,woex9,cutx9)
6.2 Logisic模型建立和模型检验
我们直接调用statsmodels包来实现逻辑回归:
INT:
X_train.to_csv('WoeData.csv', index=False)
#6.2 Logistic模型建立
#导入数据
data = pd.read_csv('WoeData.csv')
#应变量
Y=train['SeriousDlqin2yrs']
# dict_Y = {'month':Y.index,'numbers':Y.values}
# df_month = pd.DataFrame(dict_Y)
#自变量,剔除对因变量影响不明显的变量
X=X_train.drop(['RevolvingUtilizationOfUnsecuredLines','age','NumberOfTime30-59DaysPastDueNotWorse','NumberOfTimes90DaysLate','NumberOfTime60-89DaysPastDueNotWorse','DebtRatio','MonthlyIncome', 'NumberOfOpenCreditLinesAndLoans','NumberRealEstateLoansOrLines','NumberOfDependents'],axis=1)
X1=sm.add_constant(X)
Y1=Y.to_frame(name='SeriousDlqin2yrs')
logit=sm.Logit(Y,X1)
result=logit.fit()
print(result.summary())
X1.columns
如上图所示逻辑回归各变量都已通过显著性检验,满足要求
到这里,我们的建模部分基本结束了。我们需要验证一下模型的预测能力如何。我们使用在建模开始阶段预留的test数据进行检验。通过ROC曲线和AUC来评估模型的拟合能力。
在Python中,可以利用sklearn.metrics,它能方便比较两个分类器,自动计算ROC和AUC。
INT:
#6.3 模型检验
X_test = trans_woe(X_test,x1_name,woex1,cutx1)
X_test = trans_woe(X_test,x2_name,woex2,cutx2)
X_test = trans_woe(X_test,x3_name,woex3,cutx3)
X_test = trans_woe(X_test,x7_name,woex7,cutx7)
X_test = trans_woe(X_test,x9_name,woex9,cutx9)
#应变量
Y_test = test['SeriousDlqin2yrs']
#自变量,剔除对因变量影响不明显的变量,与模型变量对应
X_test = X_test.iloc[:,-5:]
#X_test =X_test.drop(['NumberOfOpenCreditLinesAndLoans','age','NumberOfTime30-59DaysPastDueNotWorse','NumberOfTimes90DaysLate','NumberOfTime60-89DaysPastDueNotWorse','DebtRatio','MonthlyIncome', 'NumberOfOpenCreditLinesAndLoans','NumberRealEstateLoansOrLines','NumberOfDependents'], axis=1)
X3 = sm.add_constant(X_test)
resu = result.predict(X3)#进行预测
#result.score(X3,Y_test)
fpr, tpr, threshold = metrics.roc_curve(Y_test, resu)
rocauc = metrics.auc(fpr, tpr)#计算AUC
plt.plot(fpr, tpr, 'b', label='AUC = %0.2f' % rocauc)#生成ROC曲线
plt.legend(loc='lower right')
plt.plot([0, 1], [0, 1], 'r--')
plt.xlim([0, 1])
plt.ylim([0, 1])
plt.ylabel('真正率')
plt.xlabel('假正率')
plt.show()
OUT:
由上图得知,AUC值为0.85,表明该模型的预测效果不错,正确率较高。
七、信用评分
我们已经基本完成了建模相关的工作,并用ROC曲线验证了模型的预测能力。接下来的步骤,就是将Logistic模型转换为标准评分卡的形式。
评分标准:
a=log(p_good/P_bad)
Score = offset + factor * log(odds)
在建立标准评分卡之前,我们需要选取几个评分卡参数:基础分值、 PDO(比率翻倍的分值)和好坏比。 这里, 我们取600分为基础分值,PDO为20 (每高20分好坏比翻一倍),好坏比取20。
INT:
# 我们取600分为基础分值,PDO为20(每高20分好坏比翻一倍),好坏比取20。
p = 20 / math.log(2)
q = 600 - 20 * math.log(20) / math.log(2)
#7.2 部分评分
#计算分数函数
def get_score(coe,woe,factor):
scores=[]
for w in woe:
score=round(coe*w*factor,0)
scores.append(score)
print(scores)
return scores
coe = [2.6138,0.6228,0.4894,0.5596,0.5747 ,0.4248]#在上面那个logistic的结果中来
baseScore = round(q + p * coe[0], 0)
print(baseScore)
INT:
# 各项部分分数
x1 = get_score(coe[1], woex1, p)
x2 = get_score(coe[2], woex2, p)
x3 = get_score(coe[3], woex3, p)
x7 = get_score(coe[4], woex7, p)
x9 = get_score(coe[5], woex9, p)
可以得出这五个变量的评分卡如下图所示:
八、自动评分系统
根据变量来计算分数,实现如下:
INT:
def compute_score(series,cut,score):
list = []
i = 0
while i < len(series):
value = series[i]
j = len(cut) - 2
m = len(cut) - 2
while j >= 0:
if value >= cut[j]:
j = -1
else:
j -= 1
m -= 1
list.append(score[m])
i += 1
return list
INT:
test1 = pd.read_csv(r'C:\Users\86180\Desktop\GiveMeSomeCredit (试验)\TestData.csv')
test1['BaseScore']=np.zeros(len(test1))+baseScore
test1['x1'] = compute_score(test1['RevolvingUtilizationOfUnsecuredLines'], cutx1, x1)
test1['x2'] = compute_score(test1['age'], cutx2, x2)
test1['x3'] = compute_score(test1['NumberOfTime30-59DaysPastDueNotWorse'], cutx3, x3)
test1['x7'] = compute_score(test1['NumberOfTimes90DaysLate'], cutx7, x7)
test1['x9'] = compute_score(test1['NumberOfTime60-89DaysPastDueNotWorse'], cutx9, x9)
test1['Score'] = test1['x1'] + test1['x2'] + test1['x3'] + test1['x7'] +test1['x9'] + baseScore
test1.to_csv(r'C:\Users\86180\Desktop\GiveMeSomeCredit (试验)\ScoreData.csv', index=False)
得到的部分结果如下:
序号 | SeriousDlqin2yrs | BaseScore | x1 | x2 | x3 | x7 | x9 | Score |
1 | 1 | 595 | 6 | 7 | -21 | -33 | -24 | 530 |
2 | 1 | 595 | 22 | -5 | -11 | -33 | -24 | 544 |
3 | 1 | 595 | 24 | -5 | -11 | -33 | -24 | 546 |
4 | 1 | 595 | 24 | -8 | -11 | -33 | -24 | 543 |
5 | 1 | 595 | 22 | 12 | -21 | -33 | -24 | 551 |
6 | 1 | 595 | 22 | 7 | -11 | -33 | -24 | 556 |
7 | 1 | 595 | 24 | 15 | -11 | -33 | -24 | 566 |
8 | 1 | 595 | 24 | 15 | -11 | -33 | -24 | 566 |
9 | 1 | 595 | -20 | -4 | -21 | -33 | -24 | 493 |
10 | 1 | 595 | 24 | 3 | -11 | -33 | -24 | 554 |
11 | 1 | 595 | 22 | -4 | -11 | -33 | -24 | 545 |
12 | 0 | 595 | -20 | -8 | -11 | -33 | -24 | 499 |
13 | 1 | 595 | 24 | 15 | -11 | -33 | -24 | 566 |
14 | 1 | 595 | 24 | 7 | -11 | -33 | -24 | 558 |
15 | 1 | 595 | 22 | -5 | -11 | -33 | -24 | 544 |
16 | 1 | 595 | 24 | 7 | -11 | -33 | -24 | 558 |
17 | 1 | 595 | 22 | 12 | -11 | -33 | -24 | 561 |
18 | 1 | 595 | 6 | -8 | -11 | -33 | -24 | 525 |
19 | 1 | 595 | 24 | -8 | -11 | -33 | -24 | 543 |
20 | 1 | 595 | -20 | 7 | -21 | -33 | -24 | 504 |
九、总结
本次在kaggle的Give Me Some Credit数据集中,学习到了很多数据分析常用的知识和经验。
通过数据预处理、缺失异常值的清理和填补,保证数据得以顺利下一步的分析。
在预测之前进行数数据分箱、变量的选择,然后运用logistic回归进行二分法预测。
最后构建预测客户是否违约的模型和评分表,建立了一个简单的信用评分系统,