项目简介
信用风险:未履行合同的义务而造成的经济损失的风险。
评分卡:以分数的形式来衡量风险几率的一种手段,分数越高越安全。
数据来源:Kaggle
有15万条的样本数据,
– 基本属性:包括了借款人当时的年龄。
– 偿债能力:包括了借款人的月收入、负债比率。
– 信用往来:两年内35-59天逾期次数、两年内60-89天逾期次数、两年内90天或高于90天逾期的次数。
– 财产状况:包括了开放式信贷和贷款数量、不动产贷款或额度数量。
– 贷款属性:暂无。
– 其他因素:包括了借款人的家属数量(不包括本人在内)。
– 时间窗口:自变量的观察窗口为过去两年,因变量表现窗口为未来两年。
标号 | 变量标签 | 变量解释 |
---|---|---|
x0 | SeriousDlqin2yrs | 好客户和坏客户 |
x1 | RevolvingUtilizationOfUnsecuredLines | 无担保放款的循环利用 |
x2 | age | 借款人借款时的年龄 |
x3 | NumberOfTime30-59DaysPastDueNotWorse | 35-59天逾期但不糟糕次数 |
x4 | DebtRatio | 负债比率 |
x5 | MonthlyIncome | 月收入 |
x6 | NumberOfOpenCreditLinesAndLoans | 开放式信贷和贷款数量 |
x7 | NumberOfTimes90DaysLate | 90天逾期次数 |
x8 | NumberRealEstateLoansOrLines | 不动产贷款或额度数量 |
x9 | NumberOfTime60-89DaysPastDueNotWorse | 60-89天逾期但不糟糕次数 |
x10 | NumberOfDependents | 家属数量 |
import numpy as np
import matplotlib
import matplotlib.pyplot as plt
plt.rcParams['font.sans-serif'] =['Microsoft YaHei']
plt.rcParams['axes.unicode_minus']=False
import seaborn as sns
import pandas as pd
from sklearn.ensemble import RandomForestRegressor
from sklearn.linear_model import LogisticRegression
from sklearn.model_selection import train_test_split
from sklearn.metrics import roc_curve, auc
import os
os.chdir(r'\信用评分\GiveMeSomeCredit')
df=pd.read_csv('cs-training.csv')
states={"Unnamed: 0":"用户ID",
"SeriousDlqin2yrs":"好坏客户",
"RevolvingUtilizationOfUnsecuredLines":"可用额度比值",
"age":"年龄",
"NumberOfTime30-59DaysPastDueNotWorse":"逾期30-59天笔数",
"DebtRatio":"负债率",
"MonthlyIncome":"月收入",
"NumberOfOpenCreditLinesAndLoans":"信贷数量",
"NumberOfTimes90DaysLate":"逾期90天笔数",
"NumberRealEstateLoansOrLines":"固定资产贷款量",
"NumberOfTime60-89DaysPastDueNotWorse":"逾期60-89天笔数",
"NumberOfDependents":"家属数量"}
df.rename(columns=states,inplace=True)
df.head()
数据预处理
df.info()
def missing_values_table(df):
#全部缺失值
mis_val =df.isnull().sum()
#缺失值比例
mis_val_percent =100*df.isnull().sum()/len(df)
#能成一个表
mis_val_table=pd.concat([mis_val,mis_val_percent],axis=1)
#改列名
mis_val_table_ren_columns=mis_val_table.rename(
columns={0:'缺失值',1:'缺失比例'})
#对缺失值排序
mis_val_table_ren_columns=mis_val_table_ren_columns[mis_val_table_ren_columns.iloc[:,1]!=0]\
.sort_values('缺失比例',ascending=False).round(1)
#打印出表
print("数据有"+str(df.shape[1])+"列.\n"
"其中"+str(mis_val_table_ren_columns.shape[0])+
"列含有缺失值.")
#返回确实行列
return mis_val_table_ren_columns #显示缺失值和表示缺失值函数
missing_values_table(df)
缺失值
"家属数量"变量缺失值比较少,直接删除,对总体模型不会造成太大影响。对缺失值处理完之后,删除重复项。
df=df[df.家属数量.notnull()].drop_duplicates()
“月收入”缺失率比较大,采用随机森林法,根据变量之间的相关关系填补缺失值
# 用随机森林对缺失值预测填充函数
def set_missing(df):
# 把已有的数值型特征取出来
# process_df = df.iloc[:,1:]
process_df = df.iloc[:,[6,1,2,3,4,5,7,8,9,10,11]]
# 分成已知该特征和未知该特征两部分
known = process_df[process_df.月收入.notnull()].values
unknown = process_df[process_df.月收入.isnull()].values
# X为特征属性值
X = known[:,1:]
# y为结果标签值
y = known[:,0]
# 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)
print(predicted)
# 用得到的预测结果填补原缺失数据
df.loc[(df.月收入.isnull()), '月收入'] = predicted
return df
df=set_missing(df)
异常值
缺失值处理完毕后,我们还需要进行异常值处理。异常值是指明显偏离大多数抽样数据的数值,比如个人客户的年龄为0时,通常认为该值为异常值。这里用盖帽法处理。
盖帽法:整行替换数据框里99%以上和1%以下的点,将99%以上的点值=99%的点值;小于1%的点值=1%的点值。
df.年龄.hist(bins=50)
#盖帽法
def blk(floor,root):
def f(x):
if x<floor:
x=floor
elif x>root:
x=root
return x
return f
q1=df.年龄.quantile(0.01)
q99=df.年龄.quantile(0.99)
blk_tot=blk(floor=q1,root=q99)
df.年龄=df.年龄.map(blk_tot)
df.年龄.hist(bins=50)
探索性分析
单变量分析
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['总客户'])
ax1=df2[['总客户','坏客户']].plot.bar()
ax1.set_xticklabels(df2.index,rotation=15)
ax1.set_ylabel('客户数')
ax1.set_title('年龄与好坏客户数分布图')
ax11=df2['坏客户率'].plot()
ax11.set_ylabel('坏客户率')
ax11.set_title('坏客户率随年龄的变化趋势图')
多变量分析
corr = df.iloc[:,1:].corr() #计算各变量的相关性系数
xticks = list(corr.index) #x轴标签
yticks = list(corr.index) #y轴标签
fig = plt.figure(figsize=(10,10))
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':'k'})
ax1.set_xticklabels(xticks, rotation=30, fontsize=10)
ax1.set_yticklabels(yticks, rotation=0, fontsize=10)
plt.show()
WOE值替换和LR建模
woe全称叫Weight of Evidence,常用在风险评估、授信评分卡等领域。
IV全称是Information value,可通过woe加权求和得到,衡量自变量对应变量的预测能力。
WOE的计算公式是:ln[(违约/总违约)/(正常/总正常)]。
W
O
E
i
=
l
n
(
B
a
d
i
B
a
d
T
/
G
o
o
d
i
G
o
o
d
T
)
=
l
n
(
B
a
d
i
B
a
d
T
)
−
l
n
(
G
o
o
d
i
G
o
o
d
T
)
WOE_i=ln(\frac{Bad_i}{Bad_T} / \frac{Good_i}{Good_T})=ln(\frac{Bad_i}{Bad_T})-ln( \frac{Good_i}{Good_T})
WOEi=ln(BadTBadi/GoodTGoodi)=ln(BadTBadi)−ln(GoodTGoodi)
I V i = ( B a d i B a d T − G o o d i G o o d T ) ∗ W O E i IV_i=(\frac{Bad_i}{Bad_T} -\frac{Good_i}{Good_T})* WOE_i IVi=(BadTBadi−GoodTGoodi)∗WOEi
I V = ∑ i = 1 n ( I V i ) IV=\displaystyle \sum_{i=1}^n(IV_i) IV=i=1∑n(IVi)
pinf = float('inf') #正无穷大
ninf = float('-inf') #负无穷大
cut1=pd.qcut(df['可用额度比值'],4)
cut2=pd.qcut(df['年龄'],8)
bins3=[ninf, 0, 1, 3, 5, pinf]
cut3=pd.cut(df['逾期30-59天笔数'],bins3)
cut4=pd.qcut(df['负债率'],3)
cut5=pd.qcut(df['月收入'],4)
cut6=pd.qcut(df['信贷数量'],4)
bins7=[ninf, 0, 1, 3, 5, pinf]
cut7=pd.cut(df['逾期90天笔数'],bins7)
bins8=[ninf, 0,1,2, 3, pinf]
cut8=pd.cut(df['固定资产贷款量'],bins8)
bins9=[ninf, 0, 1, 3, pinf]
cut9=pd.cut(df['逾期60-89天笔数'],bins9)
bins10=[ninf, 0, 1, 2, 3, 5, pinf]
cut10=pd.cut(df['家属数量'],bins10)
#好坏客户比率
rate=df['好坏客户'].sum()/(df['好坏客户'].count()-df['好坏客户'].sum())
#定义woe计算函数
def get_woe_data(cut):
grouped=df['好坏客户'].groupby(cut,as_index = True).value_counts()
woe=np.log(pd.DataFrame(grouped).unstack().iloc[:,1]/pd.DataFrame(grouped).unstack().iloc[:,0]/rate) #计算每个分组的woe值
return(woe)
cut1_woe=get_woe_data(cut1)
cut2_woe=get_woe_data(cut2)
cut3_woe=get_woe_data(cut3)
cut4_woe=get_woe_data(cut4)
cut5_woe=get_woe_data(cut5)
cut6_woe=get_woe_data(cut6)
cut7_woe=get_woe_data(cut7)
cut8_woe=get_woe_data(cut8)
cut9_woe=get_woe_data(cut9)
cutl0_woe=get_woe_data(cut10)
#定义IV值计算函数
def get_IV_data(cut,cut_woe):
grouped=df['好坏客户'].groupby(cut,as_index = True).value_counts()
cut_IV=((pd.DataFrame(grouped).unstack().iloc[:,1]/df['好坏客户'].sum()-pd.DataFrame(grouped).unstack().iloc[:,0]/
(df['好坏客户'].count()-df['好坏客户'].sum()))*cut_woe).sum()
return(cut_IV)
#计算各分组的IV值
cut1_IV=get_IV_data(cut1,cut1_woe)
cut2_IV=get_IV_data(cut2,cut2_woe)
cut3_IV=get_IV_data(cut3,cut3_woe)
cut4_IV=get_IV_data(cut4,cut4_woe)
cut5_IV=get_IV_data(cut5,cut5_woe)
cut6_IV=get_IV_data(cut6,cut6_woe)
cut7_IV=get_IV_data(cut7,cut7_woe)
cut8_IV=get_IV_data(cut8,cut8_woe)
cut9_IV=get_IV_data(cut9,cut9_woe)
cut10_IV=get_IV_data(cut10,cutl0_woe)
#各组的IV值可视化
df_IV=pd.DataFrame([cut1_IV,cut2_IV,cut3_IV,cut4_IV,cut5_IV,cut6_IV,cut7_IV,cut8_IV,cut9_IV,cut10_IV],index=df.columns[2:])
df_IV.plot(kind='bar')
for a,b in zip(range(10),df_IV.values):
plt.text(a,b,'%.2f' % b, ha='center',va= 'bottom',fontsize=9)
LR建模
df_new=df.drop(['负债率', '月收入', '信贷数量','固定资产贷款量', '家属数量','用户ID'],axis=1)
def replace_data(cut,cut_woe):
a=[]
for i in cut.unique():
a.append(i)
a.sort()
for m in range(len(a)):
cut.replace(a[m],cut_woe.values[m],inplace=True)
return cut
#进行替换
df_new['可用额度比值']=replace_data(cut1,cut1_woe)
df_new['年龄']=replace_data(cut2,cut2_woe)
df_new['逾期30-59天笔数']=replace_data(cut3,cut3_woe)
df_new['逾期90天笔数']=replace_data(cut7,cut7_woe)
df_new['逾期60-89天笔数']=replace_data(cut9,cut9_woe)
x=df_new.iloc[:,1:]
y=df_new.iloc[:,0]
x_train,x_test,y_train,y_test=train_test_split(x,y,test_size=0.4,random_state=0)
model=LogisticRegression()
clf=model.fit(x_train,y_train)
print('测试成绩:{}'.format(clf.score(x_test,y_test)))
y_pred=clf.predict(x_test)
y_pred1=clf.decision_function(x_test)
print('y_pred\n',y_pred)
print('y_pred1\n',y_pred1)
#绘制ROC曲线以及计算AUC值
fpr,tpr,threshold = roc_curve(y_test, y_pred1)
roc_auc = auc(fpr,tpr)
plt.plot(fpr,tpr,color='darkorange',
label='ROC curve (area = %0.2f)' % roc_auc)
plt.plot([0, 1], [0, 1], color='navy', linestyle='--')
plt.xlim([0.0, 1.0])
plt.ylim([0.0, 1.0])
plt.xlabel('False Positive Rate')
plt.ylabel('True Positive Rate')
plt.title('ROC_curve')
plt.legend(loc="lower right")
plt.show()
LR模型转化为对应分数
在Logistic回归模型中,将概率发生比的对数表示成特征变量的线性组合,公式如下:
Logit(pi)是一个对数值,即为log(P(bad)/P(good)),由于P(bad)/P(good)的取值在0到∞ ,log (P(bad )/P (good))的取值在-∞到+∞之间。
为了使获得的评分更具“实用性”,需要对每个属性的分值需要进行线性比例变换,然后再加上一个偏移量。评分和用于逻辑回归(Logistic Regression )建模的好/坏比( good/bad odds )的对数成比例,而不是好/坏比( good/bad odds )本身。所以分值可以是负数,而且越小的分值代表风险越高。
每个属性对应的分值可以通过下面的公式计算:WOE乘该变量的回归系数,加上回归截距,再乘上比例因子,最后加上偏移量:
(
w
o
e
i
∗
β
i
+
a
n
)
∗
f
a
c
t
o
r
+
o
f
f
s
e
t
n
(woe_i* \beta_i+\frac{a}{n})*factor+\frac{offset}{n}
(woei∗βi+na)∗factor+noffset
对于评分卡的分值,可以这样计算:
依据以上论文资料得到:
a=log(p_good/P_bad)
Score = offset + factor * log(odds)
其中,比例因子factor和偏移量offset可以通过以下行业规则确定: 好:坏 = 20:1 时,评分刻度为600; 评分每增减20分,好坏比增加一倍。
在从log (odds)到score的转换过程中,前面的推导都是使用factor和offset作为转换参数。但在实际的应用当中,这两个参数难以解释,因此也不方便指定初始值。更为常用的是事先指定下述三个参数:
b: 基准分值 (base point)
o: 基准分值对应的odds (odds at base point)
p: 当odds增加一倍,评分增加的分数 (point double odds)
factor, offset和b、o,p之间的转换公式为:
f
a
c
t
o
r
=
p
l
o
g
2
factor=\frac{p}{log2}
factor=log2p
o
f
f
s
e
t
=
b
−
p
∗
l
o
g
o
l
o
g
2
offset=b-p*\frac{log o}{log2}
offset=b−p∗log2logo
无论是factor,offset,还是b、o、p,这些参数仅仅是为了将log (odds)转换成为合适的分数,与逻辑回归本身无关。不同的转换参数会得到不同的评分,但不会改变概率。
coe=model.coef_
# 一般行业规则,一般设定当odds为50时,score为600
# Odds翻倍时,score+20
factor = 20/np.log(2)
offset = 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
#计算每个变量得分
x1 = get_score(coe[0][0], cut1_woe, factor)
x2 = get_score(coe[0][1], cut2_woe, factor)
x3 = get_score(coe[0][2], cut3_woe, factor)
x7 = get_score(coe[0][3], cut7_woe, factor)
x9 = get_score(coe[0][4], cut9_woe, factor)
#打印输出每个特征对应的分数
print("可用额度比值对应的分数:{}".format(x1))
print("年龄对应的分数:{}".format(x2))
print("逾期30-59天笔数对应的分数:{}".format(x3))
print("逾期90天笔数对应的分数:{}".format(x7))
print("逾期60-89天笔数对应的分数:{}".format(x9))