虽然大家都在努力防范信用风险,但实际违约的人并不多。并且,银行并不会真的一棒子打死所有会违约的人,很多人是会还钱的,只是忘记了还款日,很多人是不愿意欠人钱的,但是当时真的很困难,资金周转不过来,所以发生逾期,但一旦他有了钱,他就会把钱换上。对于银行来说,只要你最后能够把钱还上,我都愿意借钱给你,因为我借给你就有收入(利息)。所以,对于银行来说,真正想要被判别出来的其实是”
恶意违约
“
的人,而这部分人数非常非常少,样本就会不均衡。这一直是银行业建模的一个痛点:我们永远希望捕捉少数类。
之前提到过,逻辑回归中使用最多的是上采样方法来平衡样本。
2.4 划分训练集和测试集并保存
?
?
3 分箱
我们要制作评分卡,是要给各个特征进行分档,以便业务人员能够根据新客户填写的信息为客户打
分。因此在评分卡制作过程中,一个重要的步骤就是分箱。可以说,分箱是评分卡最难,也是最核心的思路,分箱的本质,其实就是离散化连续变量,好让拥有不同属性的人被分成不同的类别(打上不同的分数),其实本质比较类似于聚类。
离散化连续变量必然伴随着信息的损失,并且箱子越少,信息损失越大。为了衡量特征上的信息量以及特征对预测函数的贡献,银行业定义了概念Information value(IV)
:
其中
N
是这个特征上箱子的个数,
i
代表每个箱子,good%是这个箱内的优质客户(标签为0
的客户)占整个特征中所有优质客户的比例,bad%是这个箱子里的坏客户(就是那些会违约,标签为1
的那些客户)占整个特征中所有坏客户的比例。
这是我们在银行业中用来衡量违约概率的指标,中文叫做证据权重
(weight of Evidence)
,本质其实就是优质客户比上坏客户的比例的对数。WOE
是对一个箱子来说的,
WOE
越大,代表了这个箱子里的优质客户越多。而
IV
是对整个特征来说的,IV
代表的意义是我们特征上的信息量以及这个特征对模型的贡献。
?
IV
并非越大越好,我们想要找到
IV
的大小和箱子个数的平衡点。箱子越多,IV必然越小,因为信息损失会非常多,所以,我们会对特征进行分箱,然后计算每个特征在每个箱子数目下的WOE
值,利用
IV
值的曲线,找出合适的分箱个数。
我们希望不同属性的人有不同的分数,因此我们希望在同一个箱子内的人的属性是尽量相似的,而不同箱子的人的属性是尽量不同的,即业界常说的”
组间差异大,组内差异小
“
。对于评分卡来说,就是说我们希望一个箱子内的人违约概率是类似的,而不同箱子的人的违约概率差距很大,即WOE
差距要大,并且每个箱子中坏客户所占的比重也要不同。那我们,可以使用卡方检验来对比两个箱子之间的相似性,如果两个箱子之间卡方检验的P
值很大,则说明他们非常相似,那我们就可以将这两个箱子合并为一个箱子。
?
利用卡方检验画箱子数量的学习曲线,找出最适合的分箱个数。
?
?针对于age而言,选择箱子数据为13比较合适。
import pandas as pd
import numpy as np
import scipy
import matplotlib.pyplot as plt
import imblearn #imblearn是专门用来处理不平衡数据集的库,在处理样本不均衡问题中性能高过sklearn很多
from sklearn.linear_model import LogisticRegression as LR
from sklearn.ensemble import RandomForestRegressor as RFR
from sklearn.model_selection import train_test_split
from imblearn.over_sampling import SMOTE #从上采样导入smote方法
def fill_missing_rf(X,y,to_fill):
"""
使用随机森林填补一个特征的缺失值的函数
参数:
X:要填补的特征矩阵
y:完整的,没有缺失值的标签
to_fill:字符串,要填补的那一列的名称
返回值:
对缺失值的填补
"""
#构建新的特征矩阵和新标签
df=X.copy()
fill=df.loc[:,to_fill]
df=pd.concat([df.loc[:,df.columns != to_fill],pd.DataFrame(y)],axis=1) #将特征矩阵中不需要填充的部分和完整的标签做左右拼接
#找出随机森林的训练集 测试集
Ytrain=fill[fill.notnull()]
Ytest=fill[fill.isnull()]
Xtrain=df.iloc[Ytrain.index,:]
Xtest=df.iloc[Ytest.index,:]
#利用随机森林填补缺失值
rfr = RFR(n_estimators=100)
rfr = rfr.fit(Xtrain, Ytrain)
Ypredict = rfr.predict(Xtest)
return Ypredict
def graphforbestbin(DF, X, Y, n=5,q=20,graph=True):
"""
自动最优分箱函数,基于卡方检验的分箱
参数:
DF: 需要输入的数据
X: 需要分箱的列名
Y: 分箱数据对应的标签 Y 列名
n: 保留分箱个数
q: 初始分箱的个数
graph: 是否要画出IV图像
区间为前开后闭 (]
"""
DF = DF[[X,Y]].copy()
#分箱
DF["qcut"],bins = pd.qcut(DF[X], retbins=True, q=q,duplicates="drop")
coount_y0 = DF.loc[DF[Y]==0].groupby(by="qcut").count()[Y]#统计对应列结果为0的个数
coount_y1 = DF.loc[DF[Y]==1].groupby(by="qcut").count()[Y]#统计对应列结果为1的个数
num_bins = [*zip(bins,bins[1:],coount_y0,coount_y1)] #创建箱子列表 下限 上限 0的个数 1的个数
for i in range(q):
#如果第一个组没有包含正样本或负样本,向后合并
if 0 in num_bins[0][2:]:
num_bins[0:2] =[(num_bins[0][0],
num_bins[1][1],
num_bins[0][2]+num_bins[1][2],
num_bins[0][3]+num_bins[1][3])]
continue
#已经确认第一组中肯定包含两种样本了,如果其他组没有包含两种样本,就向前合并
#此时的num_bins已经被上面的代码处理过,可能被合并过,也可能没有被合并
#但无论如何,我们要在num_bins中遍历,所以写成in range(len(num_bins))
for i in range(len(num_bins)):
if 0 in num_bins[i][2:]:
num_bins[i-1:i+1] = [(num_bins[i-1][0],
num_bins[i][1],
num_bins[i-1][2]+num_bins[i][2],
num_bins[i-1][3]+num_bins[i][3])]
#即一旦合并发生,我们就让循环被破坏,使用break跳出当前循环
break
else:
break
#计算WOE和BAD RATE
#BAD RATE与bad%不是一个东西
#BAD RATE是一个箱中,坏的样本所占的比例 (bad/total)
#而bad%是一个箱中的坏样本占整个特征中的坏样本的比例
def get_woe(num_bins):
# 通过 num_bins 数据计算 woe
columns = ["min","max","count_0","count_1"]
df = pd.DataFrame(num_bins,columns=columns)
df["total"] = df.count_0 + df.count_1
df["percentage"] = df.total / df.total.sum()
df["bad_rate"] = df.count_1 / df.total
df["good%"] = df.count_0/df.count_0.sum()
df["bad%"] = df.count_1/df.count_1.sum()
df["woe"] = np.log(df["good%"] / df["bad%"])
return df
#计算IV值
def get_iv(df):
rate = df["good%"] - df["bad%"]
iv = np.sum(rate * df.woe)
return iv
IV = []#IV得分
axisx = []#箱子个数 画图时用作横坐标
while len(num_bins) > n:
pvs = []
for i in range(len(num_bins)-1):
x1 = num_bins[i][2:]
x2 = num_bins[i+1][2:]
pv = scipy.stats.chi2_contingency([x1,x2])[1]
pvs.append(pv)
#通过p值进行处理 合并p值最大的两组
#合并两个箱子时 取第一个箱子的下限 第二个箱子的上限 将两个箱子内0的值 1的值相加
i=pvs.index(max(pvs))
num_bins[i:i+2]=[(num_bins[i][0],
num_bins[i+1][1],
num_bins[i][3]+num_bins[i+1][3],
num_bins[i][2]+num_bins[i+1][2])]
bins_df = pd.DataFrame(get_woe(num_bins))
axisx.append(len(num_bins))
IV.append(get_iv(bins_df))
#画图
if graph:
plt.figure()
plt.plot(axisx,IV)
plt.xticks(axisx)
plt.xlabel("number of box")
plt.ylabel("IV")
plt.title(X)
plt.show()
return pd.DataFrame(get_woe(num_bins))
#df是表格,col是需要进行计算woe的特征,y是标签,bins是分箱的各个界限区间
def get_woe(df,col,y,bins):
#bins是分箱的左右端点
#cut是按照自己设定的进行分箱,qcut是等频分箱
df = df[[col,y]].copy()
df['cut'] = pd.cut(df.loc[:,col],bins)
bins_df = df.groupby('cut')[y].value_counts().unstack()
woe = np.log((bins_df[0]/bins_df[0].sum())/(bins_df[1]/bins_df[1].sum()))
df['woe'] = woe
return woe
data=pd.read_csv('rankingcard.csv',index_col=0)#不额外添加列
#查看数据
# print(data.head())
# print(data.info())
# print(data.shape) #10个特征 第一列为是否会违约(结果)
#删除重复的数据
data.drop_duplicates(inplace=True)#inplace=true表示直接对原数据进行操作
data.index=range(data.shape[0]) #删除重复数据以后 一定要恢复索引
#填补缺失值
# print(data.isnull().sum()/data.shape[0]) #查看缺失数据比率
#也可以这样写
# print(data.isnull().mean())
#使用均值填补家属人数
data['NumberOfDependents'].fillna(int(data['NumberOfDependents'].mean()),inplace=True)
#使用随机森林填补月收入缺失
X=data.iloc[:,1:]
y=data['SeriousDlqin2yrs']
y_pred=fill_missing_rf(X,y,'MonthlyIncome')
data.loc[data.loc[:,'MonthlyIncome'].isnull(),'MonthlyIncome']=y_pred
#print(data.isnull().mean()) #检测填充是否成功
#描述性统计处理异常值
data.describe([0.01,0.1,0.25,.5,.75,.9,.99]).T
#异常值也被我们观察到,年龄的最小值居然有0,这不符合银行的业务需求,
# 即便是儿童账户也要至少8岁,我们可以查看一下年龄为0的人有多少
data = data[data["age"] != 0] #删除年龄为0的用户
data = data[data.loc[:,"NumberOfTimes90DaysLate"] < 90]#不阔能超过90次
data.index=range(data.shape[0])#更新索引
#删除了异常值后的数据
X1=data.iloc[:,1:]
y1=data.iloc[:,0]
n_sample=X1.shape[0]#样本数量
n_1_sample = y.value_counts()[1]#结果为1数量
n_0_sample = y.value_counts()[0]#结果为0数量
# print('样本个数:{}; 1占{:.2%}; 0占{:.2%}'.format(n_sample,n_1_sample/n_sample,n_0_sample/n_sample))
#处理样本不平衡问题
sm=SMOTE()
X1,y1=sm.fit_resample(X1,y1)
n_sample_ = X1.shape[0]
n_1_sample = pd.Series(y1).value_counts()[1]
n_0_sample = pd.Series(y1).value_counts()[0]
# print('上采样后样本个数:{}; 1占{:.2%}; 0占{:.2%}'.format(n_sample_,n_1_sample/n_sample_,n_0_sample/n_sample_))
#分训练集和测试集并保存
X1=pd.DataFrame(X1)
y1=pd.DataFrame(y1)
X_train, X_vali, Y_train, Y_vali = train_test_split(X1,y1,test_size=0.3,random_state=420)
#训练数据 特征矩阵第一列设为标签
model_data = pd.concat([Y_train, X_train], axis=1)
model_data.index = range(model_data.shape[0]) #更新索引
model_data.columns = data.columns #更新列索引
#验证数据 特征矩阵第一列设为标签
vali_data = pd.concat([Y_vali, X_vali], axis=1)
vali_data.index = range(vali_data.shape[0])
vali_data.columns = data.columns
#导出文件
model_data.to_csv("model_data.csv")
vali_data.to_csv("vali_data.csv")
#可以自动分箱的特征
auto_col_bins = {"RevolvingUtilizationOfUnsecuredLines":6,
"age":5,
"DebtRatio":4,
"MonthlyIncome":3,
"NumberOfOpenCreditLinesAndLoans":5}
#不能使用自动分箱的变量
hand_bins = {"NumberOfTime30-59DaysPastDueNotWorse":[0,1,2,13]
,"NumberOfTimes90DaysLate":[0,1,2,17]
,"NumberRealEstateLoansOrLines":[0,1,2,4,54]
,"NumberOfTime60-89DaysPastDueNotWorse":[0,1,2,8]
,"NumberOfDependents":[0,1,2,3]}
#保证区间覆盖使用 np.inf替换最大值,用-np.inf替换最小值
#原因:比如一些新的值出现,例如家庭人数为30,以前没出现过,改成范围为极大值之后,这些新值就都能分到箱里边了
hand_bins = {k:[-np.inf,*v[:-1],np.inf] for k,v in hand_bins.items()}
#分箱的左右端点
bins_of_col = {}
for col in auto_col_bins:
bins_df = graphforbestbin(model_data,col
,'SeriousDlqin2yrs'
#使用字典的性质来取出每个特征所对应的箱的数量
,n=auto_col_bins[col]
,q=20,graph=False)
#将min和max转为集合,union是取两个集合的并集,bins_list就是各个区间的端点
bins_list = sorted(set(bins_df["min"]).union(bins_df["max"]))
#保证区间覆盖使用 np.inf 替换最大值 -np.inf 替换最小值
bins_list[0],bins_list[-1] = -np.inf,np.inf
bins_of_col[col] = bins_list
#合并手动分箱数据 ,将hand_bins所有的键值对更新到bins_of_col字典里
bins_of_col.update(hand_bins)
data = model_data.copy()
#将所有特征的WOE存储到字典当中
woeall = {}
for col in bins_of_col:
woeall[col] = get_woe(model_data,col,"SeriousDlqin2yrs",bins_of_col[col])
#不希望覆盖掉原本的数据,创建一个新的DataFrame,索引和原始数据model_data一模一样
model_woe = pd.DataFrame(index=model_data.index)
#对所有特征操作可以写成:
for col in bins_of_col:
model_woe[col] = pd.cut(model_data.loc[:,col],bins_of_col[col]).map(woeall[col])
#将标签补充到数据中
model_woe['SeriousDlqin2yrs'] = model_data['SeriousDlqin2yrs']
#这就是我们的建模数据了
model_woe.head()
#导入测试集
vail_data = pd.read_csv('vali_data.csv',index_col=0)
vail_woe = pd.DataFrame(index=vail_data.index)
for col in bins_of_col:
vail_woe[col] = pd.cut(vail_data.loc[:,col],bins_of_col[col]).map(woeall[col])
vail_woe["SeriousDlqin2yrs"] = vail_data["SeriousDlqin2yrs"]
#对训练集和测试集的特征和标签分离
x = model_woe.iloc[:,:-1]
y = model_woe.iloc[:,-1]
vail_x = vail_woe.iloc[:,:-1]
vail_y = vail_woe.iloc[:,-1]
lr = LR().fit(x,y)
print(lr.score(vail_x,vail_y))