项目介绍:
根据借贷者的统计学特征、社会学特征、消费行为特征、兴趣爱好特征等一系列特征快速判断出一
个的信用情况,并为不同特征赋予不同的权重,整合得到可以供业务人员评判的借贷者信用得分情
况。
项目具体内容:
1、数据预处理,处理缺失值和异常值;使用上采样解决样本不均衡的问题。
2、离散特征分箱处理,获取到每个特征的最大 IV 值。
3、使用逻辑回归算法进行建模;将回归算法转化为银行使用的评分卡模型。
数据特征含义:
'SeriousDlqin2yrs':好坏客户
'RevolvingUtilizationOfUnsecuredLines':可用额度比值
'age':年龄
'NumberOfTime30-59DaysPastDueNotWorse': 逾期30-59天笔数
'DebtRatio':负债率
'MonthlyIncome':月收入
'NumberOfOpenCreditLinesAndLoans':信贷数量
'NumberOfTimes90DaysLate':逾期90天笔数
'NumberRealEstateLoansOrLines':固定资产贷款量
'NumberOfTime60-89DaysPastDueNotWorse':逾期60-89天笔数
'NumberOfDependents':家属数量
一、数据预处理
1、查看数据
查看数据信息,共有150000个样本,11个特征
data= pd.read_csv("cs-training.csv",index_col=0)
print(data.info())
print(data.shape)
2、重复数据删除
去除重复值,有609条是重复数据
print("查看是否有重复值")
print(data[data.duplicated().values==True].count())
data.drop_duplicates(inplace=True)
data.index= range(data.shape[0])# 重置索引
3、填补缺失值
共有两个特征有数据缺失:NumberOfDependents,缺失率为0.025;MonthlyIncome,缺失率为0.195601
print("查看缺失值:\n",data.isnull().mean())
3.1 使用均值填充NumberOfDependents
data["NumberOfDependents"].fillna(data["NumberOfDependents"].mean(),inplace=True)
3.2 使用随机森林填充随机森林填充MonthlyIncome
def fill_missing_rf(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,:]
# 利用随机森林回归树来填补
from sklearn.ensembleimport RandomForestRegressoras RFR
rfr= RFR(n_estimators=100)
rfr.fit(Xtrain,Ytrain)
Ypredict= rfr.predict(Xtest)
return Ypredict
X= data.iloc[:,1:]
y= data["SeriousDlqin2yrs"]
y_pred= fill_missing_rf(X,y,"MonthlyIncome")
data.loc[data.loc[:,"MonthlyIncome"].isnull(),"MonthlyIncome"]= y_pred
4. 异常值处理
使用箱线图查看异常值,年龄为0的样本有1个,申请信用卡的人年龄不可能为0,;违约次数超过80次的为异常。直接删除掉。
if True:
for iin range(1, data.shape[1]):
plt.figure()
plt.boxplot(data.iloc[:,i])
plt.xlabel(data.columns[i])
plt.savefig("explore/{}.png".format(data.columns[i]))
data= data[data.loc[:,"age"]>0]
data= data[data.loc[:,"NumberOfTime30-59DaysPastDueNotWorse"]<80]
data= data[data.loc[:,"NumberOfTime60-89DaysPastDueNotWorse"]<80]
data= data[data.loc[:,"NumberOfTimes90DaysLate"]<80]
5、样本不均衡的问题
正样本有104578,负样本有6644,使用上采样方法处理样本不均衡的问题
X= data.iloc[:,1:]
y= data.iloc[:,0]
n_0_sample= y.value_counts()[0]
n_1_sample= y.value_counts()[1]
print("样本分布情况",n_0_sample,n_1_sample)
from imblearn.over_samplingimport SMOTE
sm= SMOTE(random_state=42)#实例化
X,y= sm.fit_sample(X,y)
n_0_sample= y.value_counts()[0]
n_1_sample= y.value_counts()[1]
print("上采样后样本分布情况",n_0_sample,n_1_sample)
print(X.info())
二、数据探索
多变量分析就是对各个变量之间的相关性进行探索,线性回归模型中的特征之间由于存在精确相关关系或高度相关关系而使模型估计失真或难以估计准确,所以需要查看是否存在高度相关的变量,可以看到,各个特征间相关性都不高。
三、数据分箱处理
3.1 特征分箱离散化处理
计算每个特征中箱子的woe值
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(bins_df):
"""
通过箱子获取每个特征的iv值"""
rate= bins_df["good%"]- bins_df["bad%"]
iv= np.sum(rate* bins_df.woe)
return iv
自动合并箱子
def combine_bins(numbins_,n):
"""
自动合并箱子,得到最大的iv值
"""
bins_df= None
IV= []# 存储不同箱子的IV
axisx= []# 存储箱子个数
while len(numbins_)> n:
pvs= []
# 获取 num_bins_两两之间的卡方检验的置信度(或卡方值)
for iin range(len(numbins_)- 1):
x1= numbins_[i][2:]# (正样本数,负样本数) 做卡方检验
x2= numbins_[i+ 1][2:]# (正样本数,负样本数) 做卡方检验
# 0返回chi2值,1返回p值
pv= scipy.stats.chi2_contingency([x1, x2])[1]
pvs.append(pv)
# 通过p值进行处理,合并p值最大的两组
i= pvs.index(max(pvs))# p值越大,越不相关
numbins_[i:i+ 2]= [(
numbins_[i][0],
numbins_[i+ 1][1],
numbins_[i][2]+ numbins_[i+ 1][2],
numbins_[i][3]+ numbins_[i+ 1][3])]
bins_df= get_woe(numbins_)
IV.append(get_iv(bins_df))
axisx.append(len(numbins_))
return bins_df, IV, axisx
通过IV曲线确定最优箱子个数
def graphforbestbin(DF,x,y,n=5,q=20,graph=True):
"""
自动最优分箱,基于卡方检验的分箱"""
# 连续性变量离散化
DF= DF[[x,y]].copy()
DF["qcut"], updown= pd.qcut(DF[x],retbins=True,q=q,duplicates="drop")#等频分箱函数
print("{} 原始分箱 qcut:\n".format(x),DF["qcut"])
print("{} 原始分箱的组距 updown:\n".format(x),updown)
#统计每个分箱0、1的个数
coount_y0= DF[DF[y]== 0].groupby(by="qcut").count()[y]
coount_y1= DF[DF[y]== 1].groupby(by="qcut").count()[y]
numbins= [*zip(updown,updown[1:],coount_y0,coount_y1)]
print("{} 原始分箱:\n".format(x),numbins)
#确保每个箱子中都有至少一个0样本,一个1样本
#卡方检验、合并箱体、画出IV曲线
numbins_= numbins.copy()# 复制一份数据
bins_df, IV, axisx= combine_bins(numbins_,n)
if graph:
plt.figure()
plt.plot(axisx,IV,'mo:')
plt.xticks(axisx)
plt.title(x)
plt.xlabel("number of box")
plt.ylabel("IV")
plt.savefig("explore/iv_{}.png".format(x))
return bins_df,IV[0]
自动通过IV曲线得到的特征如下:
auto_col_bins= {
"RevolvingUtilizationOfUnsecuredLines":6,
"age":5,
"DebtRatio":4,
"MonthlyIncome":3,
"NumberOfOpenCreditLinesAndLoans":5
}
for colin auto_col_bins:
bins_df,iv= graphforbestbin(model_data,col,"SeriousDlqin2yrs",n=auto_col_bins[col],q=20,graph=False)
bins_list= sorted(set(bins_df["min"]).union(bins_df["max"]))
bins_list[0],bins_list[-1]= -np.inf, np.inf
print("{} 分箱上下限,正负无穷替换后:\n".format(col), bins_list)
#存放自动分箱的结果
bins_of_col[col]= bins_list
对于无法自动分箱的特征,采用手动分箱
# 手动分箱
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]
}
hand_bins= {k:[-np.inf,*v[:-1],np.inf]for k,vin hand_bins.items()}
合并自动和手动分箱
bins_of_col.update(hand_bins)
3.2 使用woe值替换原数据集
def get_woe1(df,col,y ,bins):
df= df[[col,y]].copy()
df["cut"]= pd.cut(df[col],bins)# 手动分箱
# print("{} 手动分箱:\n".format(col),df)
bins_df= df.groupby("cut")[y].value_counts().unstack()
bins_df["good%"]= bins_df[0]/ bins_df[0].sum()
bins_df["bad%"]= bins_df[1]/ bins_df[1].sum()
bins_df["woe"]= np.log(bins_df["good%"]/bins_df["bad%"])
return bins_df
woeall= {}
ivall= {}
for colin bins_of_col:
bins_df= get_woe1(model_data,col,"SeriousDlqin2yrs",bins_of_col[col])
woeall[col]= bins_df["woe"]
ivall[col]= get_iv(bins_df)
model_woe= pd.DataFrame(index=model_data.index)
for colin bins_of_col:
model_woe[col]= pd.cut(model_data[col],bins_of_col[col]).map(woeall[col])
model_woe["SeriousDlqin2yrs"]= model_data["SeriousDlqin2yrs"]
四、数据建模
使用逻辑回归进行数据建模
X= model_woe.iloc[:,:-1]
y= model_woe.iloc[:,-1]
vali_X= vali_woe.iloc[:,:-1]
vali_y= vali_woe.iloc[:,-1]
from sklearn.linear_modelimport LogisticRegressionas LR
lr= LR(max_iter=3000).fit(X,y)
score= lr.score(vali_X,vali_y)
vali_pred=lr.predict(vali_X)
print("线性回归评分1",score)
模型评估:测试在预测坏客户的召回率为0.7965
精准率:[0.78804166 0.75787516]
召回率:[0.74829608 0.79651945]
confusion_matrix=confusion_matrix(vali_y,vali_pred)
print("混淆矩阵:\n",confusion_matrix)
print("精准率:\n",precision_score(vali_y, vali_pred,average=None))
print("召回率:\n",recall_score(vali_y, vali_pred,average=None))
五、制作评分卡
建模完毕,接下来就是把逻辑回归转化成评分卡中的分数,评分卡分数由下面公式计算:
其中A与B 是常数,A叫做常数,B叫做"刻度"。log(odds)代表一个人违约的可能性。
1、指定某个特定违约率的分数
2、指定违约概率翻倍的分数(POD)
求解得:
B= 20/np.log(2)=28.85390081777927
A= 600+B*np.log(1/60)=481.8621880878296
file= "ScoreData.csv"
with open(file,"w")as fdata:
fdata.write("base_score,{}\n".format(base_score))
for i,colin enumerate(X.columns):
print(i,col,type(i),type(col))
score= woeall[col]*(-B*lr.coef_[0][i])
score.name= "Score"
score.index.name= col
score.to_csv(file,header=True,mode="a")