项目背景:
自1995年4月16日上海发不了中国第一张信用卡以来已有26年了,近些年越来越多的人使用信用卡。信用卡要求人们按时还款,若未按时还款,银行将收取一定的利息获益,因此各大银行为了抢占市场,最大化发展客户。但有一些客户可能会违约使银行利益受损,故控制违约情况显得迫在眉睫。
本文通过预测某人在未来两年内遭遇财务困境的可能性,来提高自己在信用评分方面的水平。目标是建立一个借款人可以用来帮助做出最佳财务决策的模型。
本文主要从分析框架、数据处理和建立预测模型等几个方面进行分析和介绍:
个人大数据学习笔记和经典面试题整理,点击此处免费领取
一、明确分析需求
1.1数据介绍
本数据来kaggle数据集:
提供了25万借款人的历史数据,包括训练集、测试集以及数据集信息。
首先查看数据包含的文件:
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
import seaborn as sns
%matplotlib inline
import os
for dirname, _, filenames in os.walk('GiveMeSomeCredit'):
for filename in filenames:
print(os.path.join(dirname, filename))
结果:
GiveMeSomeCredit\cs-test.csv
GiveMeSomeCredit\cs-training.csv
GiveMeSomeCredit\Data Dictionary.xls
GiveMeSomeCredit\sampleEntry.csv
我们可以看到文件有训练集和测试集,以及一个资料字典,还有提交样例。
查看一下资料字典里的信息:
主要是讲的各字段所代表的名称,含义和类型。
各字段代表的含义:
1.2明确分析思路
我们看到这些数据,到底有什么用呢,如何去分析呢?
首先明确目的:信用卡数据的各个信息对我们分析有什么用,最终目的什么,可以得到什么结论?
目的:通过这些数据预测信用卡客户违约的预测概率。
思路:通过分析数据之间的相关性,关联性,进行特征分析,选择合适的特征进行建模。
具体方法:查看各数据的统计信息,与是否逾期的关系,进行可视化表示,并得到相应的一些结论。
1.3 数据探索
首先看一下训练集中的内容:
df = pd.read_csv("GiveMeSomeCredit//cs-training.csv")
df.drop('Unnamed: 0',axis =1,inplace = True)
df.head()
为了看起来方便,将英文名替换成中文名
# 中文名替换
zh_label = ['目标变量','信用卡余额百分比','年龄','逾期30-59天的次数','每月开支占比','月收入','未偿还债务','抵押贷款和房地产贷款的数量','借款人逾期60-89天的次数','家庭成员数目']
en_label = df.columns.values.tolist()
zh_label_dict = dict(zip(en_label,zh_label))
zh_label_dict
df.rename(columns =zh_label_dict ,inplace=True)
df.head()
查看一下数据的基本信息:
数据大小
df.shape
(150000, 11) 共有150000个数据,特征向量有11个。
查看数据类型
df.dtypes.value_counts()
int64 7
float64 4
dtype: int64
一共有7个特征是整型,4个特征是浮点型。
数据信息
df.info()
观察到部分数据有缺失,所以接下来需要对缺失值的处理
1.4数据清洗和处理
查看缺失值
df.isnull().sum()[df.isnull().sum() != 0]
月收入 29731
家庭成员数目 3924
可以看到月收入和家庭成员数目有缺失,一般数据缺失有几种处理方法:
直接删除
用众数、中位数等进行替换
根据变量之间的相关关系填补缺失值
不过考虑到总数为15万,家庭成员数目占比较小,缺失数据可以删除。月收入应当是一个重要的因素,且占比不算小,可以根据变量之间的相关关系填补缺失值。
采用随机森林法:
# 用随机森林对缺失值预测填充函数
def set_missing(df):
# 把已有的数值型特征取出来
process_df = df.iloc[:,[5,0,1,2,3,4,6,7,8,9]]
# 分成已知该特征和未知该特征两部分
# dataframe.values获取的是dataframe中的数据为数组array
known = process_df[process_df.月收入.notnull()].values
unknown = process_df[process_df.月收入.isnull()].values
# X为已知MonthlyIncome的特征属性值
X = known[:, 1:]
# y为结果标签值MonthlyIncome
y = known[:, 0]
# X与y用于训练随机森林模型,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:]).round(0)
# 用得到的预测结果填补原缺失数据
df.loc[(df.月收入.isnull()), '月收入'] = predicted
return df
df=set_missing(df) # 用随机森林填补比较多的缺失值
df=df.dropna() # 删除比较少的缺失值
df = df.drop_duplicates() # 删除重复项
df.info()
0 目标变量 145563 non-null int64
1 信用卡余额百分比 145563 non-null float64
2 年龄 145563 non-null int64
3 逾期30-59天的次数 145563 non-null int64
4 每月开支占比 145563 non-null float64
5 月收入 145563 non-null float64
6 未偿还债务 145563 non-null int64
7 逾期90天或以上的次数 145563 non-null int64
8 抵押贷款和房地产贷款的数量 145563 non-null int64
9 借款人逾期60-89天的次数 145563 non-null int64
10 家庭成员数目 145563 non-null float64
可以看到数量变成了145563个了。
观察正负样本数量是否大致相等
df.目标变量.value_counts()
0 135732
1 9831
可以看到大约为13比1的情况
个人大数据学习笔记和经典面试题整理,点击此处免费领取
二、异常值检测
缺失值处理完毕后,我们还需要进行异常值处理。异常值是指明显偏离大多数抽样数据的数值,比如个人客户的年龄为0时,通常认为该值为异常值。找出样本总体中的异常值,通常采用离群值检测的方法。
df["年龄"].describe()
count 145563.000000
mean 52.110701
std 14.567652
min 0.000000
25% 41.000000
50% 52.000000
75% 62.000000
max 107.000000
我们可以看到最小值为0,所以需要对其异常值处理。应当删掉此异常值。
df.drop(df[(df['年龄'] == 0)].index.tolist(),inplace = True)
df["年龄"].describe()
count 145562.000000
mean 52.111059
std 14.567062
min 21.000000
25% 41.000000
50% 52.000000
75% 62.000000
max 107.000000
删掉之后最小值为21,符合数据事实。
对于其它的数据,我们可以通过查看箱型图的方式查看:
import matplotlib.pyplot as plt #导入图像库
fig=plt.figure(figsize=(10,5))
plt.rcParams['font.sans-serif'] = ['SimHei'] # 步骤一(替换sans-serif字体)
ax1=fig.add_subplot(131)
ax1.boxplot(df['逾期30-59天的次数'])
ax1.set_title('逾期30-59天次数')
ax2=fig.add_subplot(132)
ax2.boxplot(df['逾期90天或以上的次数'])
ax2.set_title('逾期90天或以上的次数')
ax3=fig.add_subplot(133)
ax3.boxplot(df['借款人逾期60-89天的次数'])
ax3.set_title('借款人逾期60-89天的次数')
plt.show()
可以看到,逾期30-59天中出现了100天左右的数字,逾期90天和60-89天的次数也出现了这种情况。分析可知均存在96、98两个异常值,因此予以剔除。同时会发现剔除其中一个变量的96、98值,其他变量的96、98两个值也会相应被剔除。
df =df[df['逾期30-59天的次数'] < 60]
fig=plt.figure(figsize=(10,5))
plt.rcParams['font.sans-serif'] = ['SimHei'] # 步骤一(替换sans-serif字体)
ax1=fig.add_subplot(131)
ax1.boxplot(df['逾期30-59天的次数'])
ax1.set_title('逾期30-59天次数')
ax2=fig.add_subplot(132)
ax2.boxplot(df['逾期90天或以上的次数'])
ax2.set_title('逾期90天或以上的次数')
ax3=fig.add_subplot(133)
ax3.boxplot(df['借款人逾期60-89天的次数'])
ax3.set_title('借款人逾期60-89天的次数')
plt.show()
这些数据都正常了。
对于’信用卡余额百分比’,‘每月开支占比’,‘未偿还债务’,‘月收入’,‘抵押贷款和房地产贷款的数量’等去除单侧99%上部分异常值:
for variable in ['信用卡余额百分比','每月开支占比','月收入','未偿还债务','抵押贷款和房地产贷款的数量']:
df=df[df[variable]<df[variable].quantile(0.99)]
df.info()
三、探索性数据分析
3.1单变量分析
检验目标变量的正负样本数量是否大致相等。
grouped = df['目标变量'].value_counts()
print("逾期客户占比:{:.2%}".format(grouped[1]/grouped[0]))
逾期客户占比:6.29%,说明正负样本数量不平衡,这个可以作为后续测试集的分类标准。
查看客户年龄分布
fig = plt.figure()
fig,axes=plt.subplots(1,3,figsize=(10,5))
sns.distplot(df["年龄"],ax = axes[0],axlabel='所有客户年龄分布')
sns.distplot(df.loc[df["目标变量"] == 0]["年龄"],ax = axes[1],axlabel='非违约客户年龄分布')
sns.distplot(df.loc[df["目标变量"] == 1]["年龄"],ax = axes[2],axlabel='违约客户年龄分布')
可以观察到,所有的客户类型都基本符合正太分布,符合统计类型。
再查看用户是否违约和年龄段之间的关系:
import matplotlib.ticker as ticker
age_cut=pd.cut(df["年龄"],5)
age_cut_grouped=df["目标变量"].groupby(age_cut).count()
age_cut_grouped1=df["目标变量"].groupby(age_cut).sum()
df2=pd.merge(pd.DataFrame(age_cut_grouped), pd.DataFrame(age_cut_grouped1),right_index=True,left_index=True)
df2.rename(columns={"目标变量_x":"未违约客户","目标变量_y":"违约客户"},inplace=True)
df2.insert(2,"违约客户率",df2["违约客户"]/(df2["未违约客户"]+df2["违约客户"]))
ax2=df2["违约客户率"].plot()
ax2.set_xticklabels(df2.index,rotation=15)
ax2.set_ylabel("违约客户率")
ax2.set_title("违约客户率随年龄的变化趋势图")
plt.gca().xaxis.set_major_locator(ticker.MultipleLocator(1))
我们可以观察到随着年龄的增长,违约客户率在降低,且38-72之间降低最快.说明年龄对于客户是否违约是有相关性的.
月收入与是否违约的关系
income_cut=pd.cut(df["月收入"],18)
income_cut_grouped=df["目标变量"].groupby(income_cut).count()
income_cut_grouped1=df["目标变量"].groupby(income_cut).sum()
income_cut_grouped1
df3=pd.merge(pd.DataFrame(income_cut_grouped), pd.DataFrame(income_cut_grouped1),right_index=True,left_index=True)
df3.rename(columns={"目标变量_x":"未违约客户","目标变量_y":"违约客户"},inplace=True)
df3.insert(2,"违约客户率",df3["违约客户"]/(df3["未违约客户"]+df3["违约客户"]))
ax3=df3["违约客户率"].plot(figsize=(15,6))
ax3.set_xticklabels(df3.index,rotation=15)
ax3.set_ylabel("违约客户率")
ax3.set_title("违约客户率随月收入的变化趋势图")
plt.gca().xaxis.set_major_locator(ticker.MultipleLocator(1))
我们大概可以观察出这个趋势,随着月收入的增加,违约客户的比例在减少。
3.2 多变量分析
corr = df.corr()#计算各变量的相关性系数
xticks = list(corr.index)#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,linewidths=.5, annot_kws={'size': 9, 'weight': 'bold', 'color': 'blue'})
ax1.set_xticklabels(xticks, rotation=35, fontsize=10)
ax1.set_yticklabels(yticks, rotation=0, fontsize=10)
plt.show()
我们用热力图来表示不同变量之间的关系,单元格颜色越深,代表该单元格交叉的两个变量相关性越强。
变量借款人逾期30-59天次数和借款人逾期60-89天的次数的相关系数最大为0.3,说明逾期一两个月的人经常也会逾期两个月以上。
抵押贷款的数量和未偿还债务相关系数为0.41,说明抵押贷款越多,未偿还债务越多,符合逻辑。
四、变量选择
特征变量选择(排序)对于数据分析、机器学习从业者来说非常重要。好的特征选择能够提升模型的性能,更能帮助我们理解数据的特点、底层结构,这对进一步改善模型、算法都有着重要作用。至于Python的变量选择代码实现可以参考结合Scikit-learn介绍几种常用的特征选择方法。
在本文中,我们采用信用评分模型的变量选择方法,通过WOE分析方法,即是通过比较指标分箱和对应分箱的违约概率来确定指标是否符合经济意义。首先我们对变量进行离散化(分箱)处理。
4.1分箱处理
变量分箱(binning)是对连续变量离散化(discretization)的一种称呼。信用评分卡开发中一般有常用的等距分段、等深分段、最优分段。其中等距分段(Equval length intervals)是指分段的区间是一致的,比如年龄以十年作为一个分段;等深分段(Equal frequency intervals)是先确定分段数量,然后令每个分段中数据数量大致相等;最优分段(Optimal Binning)又叫监督离散化(supervised discretizaion),使用递归划分(Recursive Partitioning)将连续变量分为分段,背后是一种基于条件推断查找较佳分组的算法。
我们首先选择对连续变量进行最优分段,在连续变量的分布不满足最优分段的要求时,再考虑对连续变量进行等距分段。最优分箱的代码如下:
定义自动分箱函数
import matplotlib.pyplot as plt
import statsmodels.api as sm
import math
定义自动分箱函数
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)})
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_index(axis=1, level='min'))
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
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_index(axis=1, level='min'))
print("=" * 60)
print(d4)
woe = list(d4['woe'].round(3))
return d4, iv,woe
4.2 WOE
WoE分析, 是对指标分箱、计算各个档位的WoE值并观察WoE值随指标变化的趋势。其中WoE的数学定义是:
woe=ln(goodattribute/badattribute)
在进行分析时,我们需要对各指标从小到大排列,并计算出相应分档的WoE值。其中正向指标越大,WoE值越小;反向指标越大,WoE值越大。正向指标的WoE值负斜率越大,反响指标的正斜率越大,则说明指标区分能力好。WoE值趋近于直线,则意味指标判断能力较弱。若正向指标和WoE正相关趋势、反向指标同WoE出现负相关趋势,则说明此指标不符合经济意义,则应当予以去除。
woe函数实现在上一节的mono_bin()函数里面已经包含,这里不再重复。
接下来,我进一步计算每个变量的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值明显较低,所以予以删除。
五、模型分析
证据权重(Weight of Evidence,WOE)转换可以将Logistic回归模型转变为标准评分卡格式。引入WOE转换的目的并不是为了提高模型质量,只是一些变量不应该被纳入模型,这或者是因为它们不能增加模型值,或者是因为与其模型相关系数有关的误差较大,其实建立标准信用评分卡也可以不采用WOE转换。这种情况下,Logistic回归模型需要处理更大数量的自变量。尽管这样会增加建模程序的复杂性,但最终得到的评分卡都是一样的。
在建立模型之前,我们需要将筛选后的变量转换为WoE值,便于信用评分。
5.1 WOE转换
我们已经能获取了每个变量的分箱数据和woe数据,只需要根据各变量数据进行替换,实现代码如下:
替换成woe函数
def replace_woe(series,cut,woe):
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(woe[m])
i += 1
return list
我们将每个变量都进行替换,并将其保存到WoeData.csv文件中:
替换成woe
df['信用卡余额百分比'] = Series(replace_woe(df['信用卡余额百分比'], cutx1, woex1))
df['信用卡余额百分比']
df['年龄'] = Series(replace_woe(df['年龄'], cutx2, woex2))
df['逾期30-59天的次数'] = Series(replace_woe(df['逾期30-59天的次数'], cutx3, woex3))
df['每月开支占比'] = Series(replace_woe(data['每月开支占比'], cutx4, woex4))
df['月收入'] = Series(replace_woe(df['月收入'], cutx5, woex5))
df['未偿还债务'] = Series(replace_woe(df['未偿还债务'], cutx6, woex6))
df['逾期90天或以上的次数'] = Series(replace_woe(df['逾期90天或以上的次数'], cutx7, woex7))
df['抵押贷款和房地产贷款的数量'] = Series(replace_woe(df['抵押贷款和房地产贷款的数量'], cutx8, woex8))
df['借款人逾期60-89天的次数'] = Series(replace_woe(df['借款人逾期60-89天的次数'], cutx9, woex9))
df['家庭成员数目'] = Series(replace_woe(df['家庭成员数目'], cutx10, woex10))
df.to_csv('WoeData.csv', index=False)
5.2 Logisic模型建立
data = pd.read_csv('WoeData.csv')
应变量
Y=data['SeriousDlqin2yrs']
自变量,剔除对因变量影响不明显的变量
X=data.drop(['SeriousDlqin2yrs','DebtRatio','MonthlyIncome', 'NumberOfOpenCreditLinesAndLoans','NumberRealEstateLoansOrLines','NumberOfDependents'],axis=1)
X1=sm.add_constant(X)
logit=sm.Logit(Y,X1)
result=logit.fit()
print(result.summary())
通过上图可知,逻辑回归各变量都已通过显著性检验,满足要求。
5.3 模型检验
到这里,我们的建模部分基本结束了。我们需要验证一下模型的预测能力如何。我们使用在建模开始阶段预留的test数据进行检验。通过ROC曲线和AUC来评估模型的拟合能力。
在Python中,可以利用sklearn.metrics,它能方便比较两个分类器,自动计算ROC和AUC。
实现代码:
#应变量
Y_test = test['SeriousDlqin2yrs']
#自变量,剔除对因变量影响不明显的变量,与模型变量对应
X_test = test.drop(['SeriousDlqin2yrs', 'DebtRatio', 'MonthlyIncome', 'NumberOfOpenCreditLinesAndLoans','NumberRealEstateLoansOrLines', 'NumberOfDependents'], axis=1)
X3 = sm.add_constant(X_test)
resu = result.predict(X3)#进行预测
fpr, tpr, threshold = roc_curve(Y_test, resu)
rocauc = 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()
从上图可知,AUC值为0.85,说明该模型的预测效果还是不错的,正确率较高。
六、信用评分
我们已经基本完成了建模相关的工作,并用ROC曲线验证了模型的预测能力。接下来的步骤,就是将Logistic模型转换为标准评分卡的形式。
6.1 评分标准
依据以上论文资料得到:
a=log(p_good/P_bad)
Score = offset + factor * log(odds)
在建立标准评分卡之前,我们需要选取几个评分卡参数:基础分值、 PDO(比率翻倍的分值)和好坏比。 这里, 我们取600分为基础分值,PDO为20 (每高20分好坏比翻一倍),好坏比取20。
# 我们取600分为基础分值,PDO为20(每高20分好坏比翻一倍),好坏比取20。
p = 20 / math.log(2)
q = 600 - 20 * math.log(20) / math.log(2)
baseScore = round(q + p * coe[0], 0)
个人总评分=基础分+各部分得分
6.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[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)
我们可以得到各部分的评分卡如图所示
七、自动评分系统
根据变量计算分数
de f 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
我们来计算test里面的分数:
test1 = pd.read_csv('TestData.csv')
test1['BaseScore']=Series(np.zeros(len(test1)))+baseScore
test1['x1'] = Series(compute_score(test1['RevolvingUtilizationOfUnsecuredLines'], cutx1, x1))
test1['x2'] = Series(compute_score(test1['age'], cutx2, x2))
test1['x3'] = Series(compute_score(test1['NumberOfTime30-59DaysPastDueNotWorse'], cutx3, x3))
test1['x7'] = Series(compute_score(test1['NumberOfTimes90DaysLate'], cutx7, x7))
test1['x9'] = Series(compute_score(test1['NumberOfTime60-89DaysPastDueNotWorse'], cutx9, x9))
test1['Score'] = test1['x1'] + test1['x2'] + test1['x3'] + test1['x7'] +test1['x9'] + baseScore
test1.to_csv('ScoreData.csv', index=False)
批量计算的部分分结果:
好了, 本文就写到这了,都看到这里了不点个赞吗?
个人大数据学习笔记和经典面试题整理,点击此处免费领取