如今在银行、消费金融公司等各种贷款业务机构,普遍使用信用评分,对客户实行打分制,以期对客户有一个优质与否的评判。评分卡分为三类分别为:
A卡(Application score card)申请评分卡
B卡(Behavior score card)行为评分卡
C卡(Collection score card)催收评分卡
评分机制的区别在于:
1.使用的时间不同。分别侧重贷前、贷中、贷后;
2.数据要求不同。A卡一般可做贷款0-1年的信用分析,B卡则是在申请人有了一定行为后,有了较大数据进行的分析,一般为3-5年,C卡则对数据要求更大,需加入催收后客户反应等属性数据。
3.每种评分卡的模型会不一样。在A卡中常用的有逻辑回归,AHP等,而在后面两种卡中,常使用多因素逻辑回归,精度等方面更好。
对于建立评分卡模型,我们参照以下的流程:
此次的数据来源于Kaggle的Give Me Some Credit项目,首先来看一下数据:
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
from sklearn.ensemble import RandomForestRegressor
import seaborn as sns
from scipy import stats
import copy
%matplotlib inline
train_data = pd.read_csv('cs-training.csv')
train_data = train_data.iloc[:,1:]
train_data.info()
可以看到数据方面,对于缺失比较多的MonthlyIncome,在此建立随机森林模型进行填补,而缺失较少的NumberOfDependts,则直接删除缺样本。
mData = train_data.iloc[:,[5,0,1,2,3,4,6,7,8,9]]
train_known = mData[mData.MonthlyIncome.notnull()].as_matrix()
train_unknown = mData[mData.MonthlyIncome.isnull()].as_matrix()
train_X = train_known[:,1:]
train_y = train_known[:,0]
rfr = RandomForestRegressor(random_state=0,n_estimators=200,max_depth=3,n_jobs=-1)
rfr.fit(train_X,train_y)
predicted_y = rfr.predict(train_unknown[:,1:]).round(0)
train_data.loc[train_data.MonthlyIncome.isnull(),'MonthlyIncome'] = predicted_y
train_data = train_data.dropna()
train_data = train_data.drop_duplicates()
缺失值处理后,来处理异常值。异常值一般是指偏离数据较大的值。例如在统计学中,常把低于 Q1-1.5IQR的值和高于Q3+1.5IQR的值作为异常值。通过绘制箱型图能很明显的看到异常值,例如:
train_box = train_data.iloc[:,[3,7,9]]
train_box.boxplot()
很明显可以看到,在这三个特征之中有两组样本偏离了其他样本的分布,可以将其去除,此外,我们发现在age为0的样本,这很明显是不符合常识的,应同样作为异常值舍弃:
train_data = train_data[train_data['NumberOfTime30-59DaysPastDueNotWorse']<90]
train_data = train_data[train_data.age>0]
train_data['SeriousDlqin2yrs'] = 1-train_data['SeriousDlqin2yrs'] #使好客户为1,违约客户为0
为了使得能够更好地检验模型效果,我们将数据切分化为训练集和测试集。测试集取原数据的30%:
from sklearn.cross_validation import train_test_split
y = train_data.iloc[:,0]
X = train_data.iloc[:,1:]
train_X,test_X,train_y,test_y = train_test_split(X,y,test_size =0.3,random_state=0)
ntrain_data = pd.concat([train_y,train_X],axis=1)
ntest_data = pd.concat([test_y,test_X],axis=1)
age = ntrain_data['age']
sns.distplot(age)
可以看到,年龄的分布大致呈正态分布,符合统计分析假设。
mi = ntrain_data[['MonthlyIncome']]
sns.distplot(mi)
同样,收入的分布也大致呈正态分布。
首先,需要将特征进行分箱处理。分箱是将连续特征离散化的一种方式,一般有等距,等频,卡方分箱的等多种方式,合理的分箱可以使模型更加精准。在此,我使用的是一种常见于SAS上的单调分箱,python代码由这为大神提供。
def mono_bin(Y, X, n=10):
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']/good)/((1-d3['rate'])/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(by = 'min')).reset_index(drop=True)
woe=list(d4['woe'].round(3))
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'))
return d4,iv,cut,woe
x1_d,x1_iv,x1_cut,x1_woe = mono_bin(train_y,train_X.RevolvingUtilizationOfUnsecuredLines)
x2_d,x2_iv,x2_cut,x2_woe = mono_bin(train_y,train_X.age)
x4_d,x4_iv,x4_cut,x4_woe = mono_bin(train_y,train_X.DebtRatio)
x5_d,x5_iv,x5_cut,x5_woe = mono_bin(train_y,train_X.MonthlyIncome)
对于
RevolvingUtilizationOfUnsecuredLines、age、DebtRatio和MonthlyIncome我们使用这种方式进行分类。
然而,其他的变量无法通过这种方式分箱,故我们使用人工选择的方式进行:
def woe_value(d1):
d2 = d1.groupby('Bucket', as_index = True)
good=train_y.sum()
bad=train_y.count()-good
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']/good)/((1-d3['rate'])/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(by = 'min')).reset_index(drop=True)
woe=list(d4['woe'].round(3))
return d4,iv,woe
d1 = pd.DataFrame({"X": train_X['NumberOfTime30-59DaysPastDueNotWorse'], "Y": train_y})
d1['Bucket'] = d1['X']
d1_x1 = d1.loc[(d1['Bucket']<=0)]
d1_x1.loc[:,'Bucket']="(-inf,0]"
d1_x2 = d1.loc[(d1['Bucket']>0) & (d1['Bucket']<= 1)]
d1_x2.loc[:,'Bucket'] = "(0,1]"
d1_x3 = d1.loc[(d1['Bucket']>1) & (d1['Bucket']<= 3)]
d1_x3.loc[:,'Bucket'] = "(1,3]"
d1_x4 = d1.loc[(d1['Bucket']>3) & (d1['Bucket']<= 5)]
d1_x4.loc[:,'Bucket'] = "(3,5]"
d1_x5 = d1.loc[(d1['Bucket']>5)]
d1_x5.loc[:,'Bucket']="(5,+inf)"
d1 = pd.concat([d1_x1,d1_x2,d1_x3,d1_x4,d1_x5])
x3_d,x3_iv,x3_woe= woe_value(d1)
x3_cut = [float('-inf'),0,1,3,5,float('+inf')]
在分箱的过程中,同时计算了WOE(Weight of Evidence)和IV(Information Value),前者在建立逻辑回归模型是需要将所有的变量转为WOE,而后者则可以很好的展示变量的预测能力。这两个值的计算方式如下:
在通过IV值判断之前可以先检查一下变量之间的相关性,对变量有个直观的了解:
corr = train_data.corr()
xticks = ['x0','x1','x2','x3','x4','x5','x6','x7','x8','x9','x10']
yticks = list(corr.index)
fig = plt.figure()
ax1 = fig.add_subplot(1, 1, 1)
sns.heatmap(corr, annot=True, cmap='rainbow', ax=ax1, annot_kws={'size': 5, 'color': 'blue'})
ax1.set_xticklabels(xticks, rotation=0, fontsize=10)
ax1.set_yticklabels(yticks, rotation=0, fontsize=10)
plt.show()
可以看到 NumberOfTime30-59DaysPastDueNotWorse,NumberOfOpenCreditLinesAndLoans和NumberOfTime60-89DaysPastDueNotWorse这三个特征对于我们所要预测的值有较强的相关性。
接下来,看一下各个变量的IV值:
informationValue = []
informationValue.append(x1_iv)
informationValue.append(x2_iv)
informationValue.append(x3_iv)
informationValue.append(x4_iv)
informationValue.append(x5_iv)
informationValue.append(x6_iv)
informationValue.append(x7_iv)
informationValue.append(x8_iv)
informationValue.append(x9_iv)
informationValue.append(x10_iv)
informationValue
index=['x1','x2','x3','x4','x5','x6','x7','x8','x9','x10']
index_num = range(len(index))
ax=plt.bar(index_num,informationValue,tick_label=index)
plt.show()
通过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
可以看到,对于X4,X5,X6,X8,以及X10而言,IV值都比较低,因此可以舍弃这些预言能力较差的特征
接下来,将所有的需要的特征woe化,并将不需要的特征舍弃,仅保留WOE转码后的变量:
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'
train_X = trans_woe(train_X,x1_name,x1_woe,x1_cut)
train_X = trans_woe(train_X,x2_name,x2_woe,x2_cut)
train_X = trans_woe(train_X,x3_name,x3_woe,x3_cut)
train_X = trans_woe(train_X,x7_name,x7_woe,x7_cut)
train_X = trans_woe(train_X,x9_name,x9_woe,x9_cut)
train_X = train_X.iloc[:,-5:]
此时数据如下所示:
通过调用STATSMODEL包来建立逻辑回归模型:
import statsmodels.api as sm
X1=sm.add_constant(train_X)
logit=sm.Logit(train_y,X1)
result=logit.fit()
print(result.summary())
结果如下:
模型建立后,可以通过导入测试集的数据,画出ROC曲线来判断模型的准确性:
1.对测试集进行woe转化
test_X = trans_woe(test_X,x1_name,x1_woe,x1_cut)
test_X = trans_woe(test_X,x2_name,x2_woe,x2_cut)
test_X = trans_woe(test_X,x3_name,x3_woe,x3_cut)
test_X = trans_woe(test_X,x7_name,x7_woe,x7_cut)
test_X = trans_woe(test_X,x9_name,x9_woe,x9_cut)
test_X = test_X.iloc[:,-5:]
2.拟合模型,画出ROC曲线得到AUC值
from sklearn import metrics
X3 = sm.add_constant(test_X)
resu = result.predict(X3)
fpr, tpr, threshold = metrics.roc_curve(test_y, resu)
rocauc = metrics.auc(fpr, tpr)
plt.plot(fpr, tpr, 'b', label='AUC = %0.2f' % rocauc)
plt.legend(loc='lower right')
plt.plot([0, 1], [0, 1], 'r--')
plt.xlim([0, 1])
plt.ylim([0, 1])
plt.ylabel('TPR')
plt.xlabel('FPR')
plt.show()
可以看到,ACU=0.85,是可以接受的。
5.1 评分标准
依据以上论文资料得到:
a=log(p_good/P_bad)
Score = offset + factor * log(odds)
在建立标准评分卡之前,我们需要选取几个评分卡参数:基础分值、 PDO(比率翻倍的分值)和好坏比。 这里, 我们取600分为基础分值,PDO为20 (每高20分好坏比翻一倍),好坏比取20。
p = 20/np.log(2)
q = 600 - 20*np.log(20)/np.log(2)
def get_score(coe,woe,factor):
scores=[]
for w in woe:
score=round(coe*w*factor,0)
scores.append(score)
return scores
x_coe = [2.6084,0.6327,0.5151,0.5520,0.5747,0.4074]
baseScore = round(q + p * x_coe[0], 0)x1_score = get_score(x_coe[1], x1_woe, p)
x1_score = get_score(x_coe[1], x1_woe, p)
x2_score = get_score(x_coe[2], x2_woe, p)
x3_score = get_score(x_coe[3], x3_woe, p)
x7_score = get_score(x_coe[4], x7_woe, p)
x9_score = get_score(x_coe[5], x9_woe, p)
x_coe是之前逻辑回归模型得到的系数。最后BaseScore等于589分。
建立一个函数使得当输入x1,x2,x3,x7,x9的值时可以返回评分数
cut_t = [x1_cut,x2_cut,x3_cut,x7_cut,x9_cut]
def compute_score(x): #x为数组,包含x1,x2,x3,x7和x9的取值
tot_score = baseScore
cut_d = copy.deepcopy(cut_t)
for j in range(len(cut_d)):
cut_d[j].append(x[j])
cut_d[j].sort()
for i in range(len(cut_d[j])):
if cut_d[j][i] == x[j]:
tot_score = score[j][i-1] +tot_score
return tot_score
来测试一下:
至此此次基于python制作的行为评分卡就此完成。本文通过对于Kaggle上项目的数据进行分析,利用逻辑回归制作了一个简单的评分卡。在建立评分卡的过程中,首先进行了数据清洗,对缺失值和异常值进行了处理并对数据分布进行了宏观展示。然后对特征值进行了处理,将连续的变量分箱,同时计算了woe和iv值,并保留了iv值较高的变量对其woe转化。最后将woe转化后的数据进行逻辑回归分析,利用得到变量系数并自行拟定了评分标准建立了评分卡。
在整体过程中,并没有对数据进行过多的挖掘。例如:只舍弃了个别变量的异常值,亦或是对于不能自动分箱的变量采取了直观分箱的方式,并没有过多的去探究其可能对于模型的影响。这可以为后续的模型优化奠定方向。