信用卡评分是以大量数据的统计结果为基础,根据客户提供的资料和历史数据对客户的信用进行评估,评分卡模型一般分为三类:A卡:申请评分卡,B卡:行为评分卡,C卡:催收评分卡。
本文主要涉及的为申请评分卡,申请评分卡的目标主要是区分好客户和坏客户,评分卡的结果为高分数的申请人意味着比低分数的申请人的风险低。
数据源分析
数据源自于kaggle Give Me Some Credit 15万条样本数据,10个影响客户好坏的特征值,特征属性主要包括:
==基本特征==:借款人的年龄
==偿还债务能力==:借款人收入,负债率等
==信用往来==:两年内35-59天逾期次数,60-89天逾期次数等;
==财产状况==:包括了开放式信贷和贷款数量、不动产贷款或额度数量;
==其他因素==:借贷人的家属数量;
1.首先导入数据,对数据集相关情况进行了解,是否存在缺失值;
datasets = pd.read_csv('./cs_training.csv')
del datasets['Unnamed: 0']
#查看缺失值
print(datasets.info())
#查看数据集的详细信息
print(datasets.describe())
其中MonthlyIncome和NumberOfDependents存在缺失值
2.对异常值进行分析,发现年龄和逾期还款次数存在异常值过滤掉为0 的数据,删除逾期还款次数异常值;
#对年纪进行分析发现其中有为0的异常值,将其过滤
fig,ax = plt.subplots(figsize=(5,4))
ax.boxplot(datasets['age'])
plt.xlabel('Age')
plt.ylabel('Values')
plt.show()
datasets = datasets[datasets['age']>0]
逾期还款次数
columns_worse = ['NumberOfTime30-59DaysPastDueNotWorse','NumberOfTimes90DaysLate','NumberOfTime60-89DaysPastDueNotWorse']
datasets.boxplot(column=columns_worse)
plt.xticks(rotation=90)
plt.show()
#剔除异常值后数值分布
datasets = datasets.drop(datasets[datasets['NumberOfTime30-59DaysPastDueNotWorse']>80].index)
columns_worse = ['NumberOfTime30-59DaysPastDueNotWorse','NumberOfTimes90DaysLate','NumberOfTime60-89DaysPastDueNotWorse']
datasets.boxplot(column=columns_worse)
plt.xticks(rotation=90)
plt.show()
3.对缺失值进行处理,对于NumberOfDependents数据,确实值相对较少,直接删除不影响整体数据,对于MonthlyIncome缺失值较多,采用随机森林预测的方法对缺失值进行填补;
#删除NumberOfDependents为空的数据
datasets = datasets.drop(datasets[datasets['NumberOfDependents'].isnull()].index)
#对MonthlyIncome缺失值预测
#数据集 剔除因变量
df = datasets.iloc[:,[1,2,3,4,5,6,7,8,9,10]]
#需要进行预测的数据集
uknown = df[df['MonthlyIncome'].isnull()].iloc[:,[0,1,2,3,5,6,7,8,9]]
#训练集数据
known = df[~df['MonthlyIncome'].isnull()]
#自变量数据
known_X = known.iloc[:,[0,1,2,3,5,6,7,8,9]]
#因变量数据
known_Y = known['MonthlyIncome']
#采用随机森林算法,首先是进行参数的选取
def params_fit(train_X,train_Y):
s_list = []
i_lists = []
for i in range(100,200,10):
rf = RandomForestRegressor(n_estimators=i,random_state=42)
scores = cross_val_score(rf,train_X,train_Y,cv=3).mean()
s_list.append(scores)
i_lists.append(i)
max_scores = max(s_list)
max_es = s_list.index(max_scores)*10+100
print(s_list)
print('====分割线=====')
# 获取最大深度
paras = {'max_depth':np.arange(3,10,1)}
rfs = RandomForestRegressor(n_estimators=max_es,random_state=42)
gs = GridSearchCV(rfs,param_grid=paras,cv=3)
fun = gs.fit(train_X,train_Y)
sc = fun.best_score_
para = fun.best_params_
result = rf.predict(test_X)
r2 = r2_score(test_Y,result)
print(para)
return max_es,para['max_depth']
paras = params_fit(known_X,known_Y)
#获取评估器个数和最大深度值
print(paras)
#对缺失值填补
def set_missing(known_X,known_Y,uknown,datasets):
rf = RandomForestRegressor(n_estimators=150,max_depth=3,random_state=42)
rf.fit(known_X,known_Y)
result = rf.predict(uknown)
datasets.loc[datasets['MonthlyIncome'].isnull(),'MonthlyIncome'] = result
return datasets
#缺失值填补后的数据集
dataset = set_missing(known_X,known_Y,uknown,datasets)
print(dataset.shape)
print(dataset.info())
查看年龄的分布
sns.distplot(dataset['age'])
plt.show()
统计好/坏客户分布
values = dataset['SeriousDlqin2yrs'].value_counts()
index = list(values.index)
value = list(values)
fig,ax = plt.subplots()
fig.set_size_inches(7,5)
sns.countplot(dataset['SeriousDlqin2yrs'])
for a,b in zip(index,value):
plt.text(a,b+0.01,b,ha='center',va='bottom')
plt.show()
根据正常的逻辑0为好客户 1为坏客户 将其转换成常规的表达方式 0为坏客户 1 为好客户,
在数据中0为好客户,1为坏客户,将其转换成常规的表示方法,1为好客户0为坏客户。
#根据正常的逻辑0为好客户 1为坏客户 将其转换成常规的表达方式 0为坏客户 1 为好客户
datasets['SeriousDlqin2yrs'] = 1- datasets['SeriousDlqin2yrs']
print(datasets['SeriousDlqin2yrs'].value_counts())
特征变量的选择即如何选择合适的变量,对于数据分析和数据建模非常重要,选择合适的变量对模型性能的提升扮演着至关重要的角色,在本文中是采用的是将模型变量进行WOE编码方式离散化,根据IV值进行变量选择然后建立逻辑回归模型。
5.1变量分箱
变量分箱是对连续变量离散化的一种称呼,信用卡评分中,常见的变量分箱有等距分段,等深分段,最优分段。等距分段:分段的区间是一致的;等深分段:先确定分段段数,每段的数量基本相同,最有分段:又叫监督离散化,使用递归划分,将连续变量分为分段,背后是一种基于条件推断查找较佳分组的算法。
WOE值是对原始变量的一种编码形式,当前分组中响应客户占所有样本中响应客户的比例和当前分租中未响应客户占所有样本中没有响应客户的比例。
其中pyi是这个组中响应客户(风险模型中,对应的是违约客户,总之,指的是模型中预测变量取值为“是”或者说1的个体)占所有样本中所有响应客户的比例,pni是这个组中未响应客户占样本中所有未响应客户的比例。
from scipy import stats
#最优分段
def optimal_part(Y,X,n=20):
good = Y.sum()
bad = Y.count()-good
n = 20
r=0
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()
d3['min'] = d2.min().X
d3['max'] = d2.max().X
d3['sum'] = d2.sum().Y #该分组下好客户量
d3['total'] = d2.count().X #该分组下的总数据量
d3['rate']=d2.mean().Y #该分组下好客户占比
# d3['woe'] = np.log(d3['rate']/(1-d3['rate'])/(good/bad))
d3['woe'] = np.log((d3['sum']/good)/((d3['total']-d3['sum'])/bad))
d3['goodattribute'] = d3['sum']/good
d3['badattribute'] =(d3['total']-d3['sum'])/bad
iv=round(((d3['goodattribute']-d3['badattribute'])*d3['woe']).sum(),4)
d4 = (d3.sort_values(by='min')).reset_index(drop=True)
woe = list(d4['woe'].round(3))
cut = []
ninf = float('-inf')
pinf = float('inf')
print('n',n)
cut.append(ninf)
for i in range(1,n+1):
#分位数函数
qua = X.quantile(i/(n+1))
cut.append(round(qua,4))
cut.append(pinf)
return d4,iv,cut,woe
x1 = optimal_part(dataset['SeriousDlqin2yrs'],dataset['RevolvingUtilizationOfUnsecuredLines'])
x2 = optimal_part(dataset['SeriousDlqin2yrs'],dataset['age'])
x4 = optimal_part(dataset['SeriousDlqin2yrs'],dataset['DebtRatio'])
x5 = optimal_part(dataset['SeriousDlqin2yrs'],dataset['MonthlyIncome'])
print(x1)
变量x3,x6,x7,x8,x9不适合最优分箱,采用等距分箱
#自定义等距分段
def auto_set(Y,X,cat):
good = Y.sum()
bad = Y.count() - good
d1 = pd.DataFrame({'Y':Y,'X':X,'Bucket':pd.cut(X,cat)})
d2 = d1.groupby('Bucket',as_index=True)
d3 = pd.DataFrame()
d3['min'] = d2.min().X
d3['max'] = d2.max().X
d3['sum'] = d2.sum().Y
d3['total'] = d2.count().X
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=round(((d3['goodattribute']-d3['badattribute'])*d3['woe']).sum(),4)
d4 = (d3.sort_values(by='min')).reset_index(drop=True)
woe = list(d3['woe'].round(3))
return d4,woe,iv
ninf = float('-inf')
pinf = float('inf')
#分段
cuts3 = [ninf, 0, 1, 3, 5, pinf]
cuts6=[ninf, 1, 2, 3, 5, pinf]
cuts7 = [ninf, 0, 1, 3, 5, pinf]
cuts8 =[ninf, 0,1,2, 3, pinf]
cuts9 = [ninf, 0, 1, 3, pinf]
cuts10 = [ninf, 0, 1, 2, 3, 5, pinf]
x3 = auto_set(dataset['SeriousDlqin2yrs'],dataset['NumberOfTime30-59DaysPastDueNotWorse'],cat=cuts3)
x6 = auto_set(dataset['SeriousDlqin2yrs'],dataset['NumberOfOpenCreditLinesAndLoans'],cat=cuts6)
x7 = auto_set(dataset['SeriousDlqin2yrs'],dataset['NumberOfTimes90DaysLate'],cat=cuts7)
x8 = auto_set(dataset['SeriousDlqin2yrs'],dataset['NumberRealEstateLoansOrLines'],cat=cuts8)
x9 = auto_set(dataset['SeriousDlqin2yrs'],dataset['NumberOfTime60-89DaysPastDueNotWorse'],cat=cuts9)
x10 = auto_set(dataset['SeriousDlqin2yrs'],dataset['NumberOfDependents'],cat=cuts10)
查看分段结果,iv值,woe值
#iv值
iv_lists=list(dataset.columns)[1:]
iv_values = [x1[1],x2[1],x3[2],x4[1],x5[1],x6[2],x7[2],x8[2],x9[2],x10[2]]
#分段
cuts = [x1[2],x2[2],cuts3,x4[2],x5[2],cuts6,cuts7,cuts8,cuts9,cuts10]
#分段对应的woe值
woes = [x1[3],x2[3],x3[1],x4[3],x5[3],x6[1],x7[1],x8[1],x9[1],x10[1]]
print(cuts)
print(woes)
5.2变量选取
相关性分析及IV值筛选
IV的全称是Information Value,中文意思为信息量或者信息价值,其主要作用是衡量变量的预测能力。
举例说明:目标变量的类别有两类:Y1,Y2。对于一个待预测的个体A,要判断A属于Y1还是Y2,我们是需要一定的信息的,假设这个信息总量是I,而这些所需要的信息,就蕴含在所有的自变量C1,C2,C3,……,Cn中,那么,对于其中的一个变量Ci来说,其蕴含的信息越多,那么它对于判断A属于Y1还是Y2的贡献就越大,Ci的信息价值就越大,Ci的IV就越大,它就越应该进入到入模变量列表中。
IV用来衡量自变量的预测能力
当IV <0.02时,该变量对预测目标值几乎无帮助;
当0.02<=IV<0.1时,该变量对目标变量具有一定的帮助;
当0.1<=IV <0.3时,该变量对预测目标变量有很大的帮助;
当IV >=0.3时,该变量对目标变量具有很大的帮助;
当IV >0.5时,改变量对目标变量有过度预测的倾向,应仔细检查是否选用了和目标变量很强因果关系的变量。
#IV值可视化展示
fig,ax = plt.subplots()
fig.set_size_inches(10,10)
bar_df = pd.DataFrame({'labels':iv_lists,'IV':iv_values})
# print(bar_df)
bar_df.plot.bar(x='labels',y='IV',ax=ax)
x = np.arange(len(iv_lists))
for a,b in zip(x,iv_values):
plt.text(a,b+0.01,b,ha='center',va='bottom')
plt.show()
变量之间的共线性检验
特征之间的共线性指的是两个或两个以上的的特征包含有相似的信息,它们之间存在着强烈的相关性,如若存在则会降低模型的稳定性和预测能力,因此对其进行相关性检验(通常判断标准为两个或者两个以上特征间的相关性系数高于0.8)
#变量相关性分析
fig,ax = plt.subplots()
fig.set_size_inches(10,10)
sns.heatmap(dataset.corr(),annot=True,cmap='rainbow',ax=ax)
plt.xticks(rotation=90)
plt.show()
变量之间不存在很强的相关性
根据iv值筛选出大于0.1的指标变量,其能对目标有较好的预测能力,因此选择RevolvingUtilizationOfUnsecuredLines,age,NumberOfTime30-59DaysPastDueNotWorse,NumberOfTimes90DaysLate,NumberOfTime60-89DaysPastDueNotWorse
5.3 woe值替换
#对应woe值转换
#cuts_x1为分段
#woe_x1分段对应的woe值
def woe_trans(series,cuts_x1,woe_x1):
lists = []
for values in series:
# print(values)
j = len(cuts_x1)-2
i=len(cuts_x1)-2
while i <= j:
if values > cuts_x1[i]:
lists.append(woe_x1[i])
i += 1
else:
i-= 1
j -= 1
if i == 0:
lists.append(woe_x1[0])
i= len(cuts_x1)-1
return lists
#根据对IV和变量之间的相关性分析,剔除iv值小于0.1的变量
woe1s = [x1[3],x2[3],x3[1],x7[1],x9[1]]
cuts = [x1[2],x2[2],cuts3,cuts7,cuts9]
#转换成woe值后的数据集
data_woe = pd.DataFrame()
data_woe['RevolvingUtilizationOfUnsecuredLines']=pd.Series(woe_trans(dataset['RevolvingUtilizationOfUnsecuredLines'],cuts[0],woe1s[0]))
data_woe['age']=pd.Series(woe_trans(dataset['age'],cuts[1],woe1s[1]))
data_woe['NumberOfTime30-59DaysPastDueNotWorse']=pd.Series(woe_trans(dataset['NumberOfTime30-59DaysPastDueNotWorse'],cuts[2],woe1s[2]))
data_woe['NumberOfTimes90DaysLate']=pd.Series(woe_trans(dataset['NumberOfTimes90DaysLate'],cuts[3],woe1s[3]))
data_woe['NumberOfTime60-89DaysPastDueNotWorse']=pd.Series(woe_trans(dataset['NumberOfTime60-89DaysPastDueNotWorse'],cuts[0],woe1s[0]))
data_woe['SeriousDlqin2yrs'] = pd.Series(dataset.loc[:,'SeriousDlqin2yrs'].values)
print(data_woe.shape)
#(145837, 6)
6.1建立回归模型
#建立回归模型
X = data_woe.iloc[:,:5]
Y = data_woe['SeriousDlqin2yrs']
from sklearn.model_selection import train_test_split
from sklearn.metrics import roc_curve
from sklearn.linear_model import LogisticRegressionCV
from sklearn.metrics import auc
X_train,X_test,Y_train,Y_test = train_test_split(X,Y,test_size=0.2,random_state=42)
clf=LogisticRegressionCV(Cs=[0.001,0.01,0.1,1,10,100,1000],#正则化强度备选集
cv=10,#10折交叉验证
class_weight='balanced',#自动调整类别权重
penalty='l2',#选用L2正则化
random_state=0,#设置一个固定的随机数种子
)#其余为默认参数
clf = clf.fit(X_train,Y_train)#训练模型
scores = y_score=clf.predict_proba(X_test)[:,1]
6.2模型评估
#模型评估
fpr,tpr,thresholds=roc_curve(Y_test,scores)
AUC=auc(fpr,tpr)
plt.figure(figsize=(7,7))
plt.plot(fpr,tpr,label='AUC=%.2f'%AUC)
plt.plot([0,1],[0,1],'--')
plt.xlim([0,1])
plt.ylim([0,1])
plt.xlabel('False Positive Rate',fontdict={'fontsize':12},labelpad=10)
plt.ylabel('True Positive Rate',fontdict={'fontsize':12},labelpad=10)
plt.title('ROC curve',fontdict={'fontsize':20})
plt.legend(loc=0,fontsize=11)
plt.show()
#auc值为0.85结果相对可以
6.3建立评分卡
根据以上得出 a = log(p_good/p_bad) Score = offset + factor* log(odds) 在建立标准评分卡之前,需要选取几个评分卡参数:基础分值、 PDO(比率翻倍的分值)和好坏比。 这里,采用600分为基础分值,PDO为20 (每高20分好坏比翻一倍),好坏比取20。
#模型输出系数
coes = clf.coef_[0]
#计算得分
import math
p =20/math.log(2)
q = 600 - 20*math.log(20)/math.log(2)
baseScore = round(q+p*coes[0],0)
print(baseScore)
print(coes)
#533.0
#[0.66090027 0.44773906 0.59112341 0.60396275 0.46359447]
#计算分数函数
def get_score(coe,woe,factor):
scores = []
for w in woe:
score = round(coe * w * factor,0)
scores.append(score)
return scores
#计算变量得分
x1_score = get_score(coes[0],woe1s[0],p)
print(x1_score)
x2_score = get_score(coes[1],woe1s[1],p)
print(x2_score)
x3_score = get_score(coes[2],woe1s[2],p)
print(x3_score)
x7_score = get_score(coes[3],woe1s[3],p)
print(x7_score)
x9_score = get_score(coes[4],woe1s[4],p)
print(x9_score)
计算 变量得分
#根据变量计算分数
def compute_score(series,cut,score):
lists = []
for values in series:
j = len(cut)-2
i=len(cut)-2
while i <= j:
if values > cut[i]:
lists.append(score[i])
i += 1
else:
i-= 1
j -= 1
if i == 0:
lists.append(score[0])
i= len(cut)-1
return lists
计算变量得分
dataset = dataset.reset_index()
dataset['BaseScore'] = pd.Series(np.zeros(len(dataset)))+ baseScore
dataset['x1'] = pd.Series(compute_score(dataset['RevolvingUtilizationOfUnsecuredLines'],cuts[0],x1_score))
dataset['x2'] = pd.Series(compute_score(dataset['age'],cuts[1],x2_score))
dataset['x3'] = pd.Series(compute_score(dataset['NumberOfTime30-59DaysPastDueNotWorse'],cuts3,x3_score))
dataset['x7'] = pd.Series(compute_score(dataset['NumberOfTimes90DaysLate'],cuts7,x7_score))
dataset['x9'] = pd.Series(compute_score(dataset['NumberOfTime60-89DaysPastDueNotWorse'],cuts9,x9_score))
dataset['Score'] = dataset['x1'] + dataset['x2'] + dataset['x3'] +dataset['x7'] +dataset['x9'] + baseScore
print(dataset.info())
dataset.to_excel(r'./score_result.xlsx',index=False)
#查看在好坏客户的前提下分数的分布图
dt = pd.read_excel(r'./score_result.xlsx')
good = dt[dt['SeriousDlqin2yrs']==1]['Score']
bad = dt[dt['SeriousDlqin2yrs']==0]['Score']
plt.figure(figsize=(7,5))
sns.distplot(bad,bins=50,hist=False,label='bad')
sns.distplot(good,bins=50,hist=False,label='good')
plt.xlabel('Score',fontdict={'fontsize':12},labelpad=10)
plt.legend(loc=0,fontsize=11)
plt.show()
评分卡模型在测试集中的应用
testdata = pd.read_excel(r'./test.xlsx')
testdata['BaseScore'] = pd.Series(np.zeros(len(testdata)))+ baseScore
testdata['x1'] = pd.Series(compute_score(testdata['RevolvingUtilizationOfUnsecuredLines'],cuts[0],x1_score))
testdata['x2'] = pd.Series(compute_score(testdata['age'],cuts[1],x2_score))
testdata['x3'] = pd.Series(compute_score(testdata['NumberOfTime30-59DaysPastDueNotWorse'],cuts3,x3_score))
testdata['x7'] = pd.Series(compute_score(testdata['NumberOfTimes90DaysLate'],cuts7,x7_score))
testdata['x9'] = pd.Series(compute_score(testdata['NumberOfTime60-89DaysPastDueNotWorse'],cuts9,x9_score))
testdata['Score'] = testdata['x1'] + testdata['x2'] + testdata['x3'] +testdata['x7'] +testdata['x9'] + baseScore
得分结果:
通过对信用卡用户相关数据的探索,清洗及预处理,根据woe变换和iv值筛的变量建立逻辑回归模型,在此基础上建立信用卡评分系统,根据评分结果可知大于550分的基本上为好客户,在500分左右的为坏客户,在525分左右好坏客户区分界限不明显。根据此评分系统对测试集数据进行评分预测。
评分系统如若长期使用,模型中涉及到的变量变化波动一定不能太大,例如收入,如若波动较大,不适合长期使用,模型的稳定性降低,如果不可缺少需将收入进行相关转化;后期监控中需长期监控模型的正确性及变量选择的有效性,以保证模型的正确性和稳定性。
参考
https://blog.csdn.net/njliaojiang817/article/details/90409799
https://blog.csdn.net/kingzone_2008/article/details/80449287
https://www.jianshu.com/p/159f381c661d
黎玉华,信用卡评分模型的建立[J],2010