kaggle上的Give Me Some Credit一个8年前的老项目,网上的分析说明有很多,但本人通过阅读后,也发现了很多的问题。比如正常随着月薪越高,违约率会下降。但对于过低的月薪,违约率却为0等。
因此,本人写这个项目的目的是按照自己对数据的理解(可能有的地方是错的,希望大家指正),对网上相关的分析进行改进。(主要集中在数据预处理)
由于整篇文章的篇幅过长,因此分为上下两部分。
随着人们的消费观念的升级,所谓的“花明天的钱,圆今天的梦”。银行以及私营企业推出了各种各样的消费金融服务,具有代表性的是各大银行的信用卡,支付宝的花呗、京东白条,还有一些专门针对针对学生群体的平台,比如趣分期哈、分期乐之类的,把这些统称为信用卡用户。
只要涉及到金融借贷的,就有可能有坏账的存在。 每个公司都在用各种手段来降低坏账的发生,最常见的方法就是根据一定的规则,给每个用户打分进行预测,哪些用户可能会发生坏账,针对预测结果采取相应的措施。
本篇将针对历史坏账用户进行分析,分析坏账用户都有哪些特征,为后续的建模做准备。数据来自kaggle上的Give Me Some Credit根据信用评分建立原理,构建一个简易的信用评分卡模型——申请评分卡(A卡),对用户自动评分。
现在kaggle上获取数据好像要注册手机号,但是国外……所以,把数据的链接贴在这里了。
链接:https://pan.baidu.com/s/1jl9-r3FItlpHX-HP3-7d_A
提取码:mtq0
# 需要导入的包
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
import seaborn as sns
import datetime
import scipy
%matplotlib inline
sns.set(style="ticks")
pd.set_option("display.max_columns",None)#展示所有列数据
pd.set_option("display.max_rows",None)#展示所有行数据
# 画图显示中文
plt.rcParams['font.sans-serif']=['SimHei']
plt.rcParams['axes.unicode_minus'] = False
from sklearn.ensemble import RandomForestRegressor as rfr
from imblearn.over_sampling import SMOTE
from sklearn.linear_model import LogisticRegression as LR
import scikitplot as skplt
train = pd.read_csv('./cs-training.csv',index_col=0)
test = pd.read_csv('./cs-test.csv',index_col=0)
train.head()
train.shape
(150000, 11)
test.shape
(101503, 11)
训练集数据150000个样本,11个特征。
测试集数据101503个样本,11个特征。
train.describe([0.01,0.03,0.05,0.07,0.1,0.25,.5,.75,.9,.99]).T
train.info()
这是之前写的数据探索性分析,在分析时可以当做个参考,提供些思路。
figure,ax = plt.subplots(figsize=(12,4))
train['SeriousDlqin2yrs'].value_counts().plot.pie(autopct='%1.1f%%')
plt.show()
fig= plt.figure(figsize=(14,4))
ax1=fig.add_subplot(1,2,1)
sns.distplot(train['RevolvingUtilizationOfUnsecuredLines'],kde=True)
ax1=fig.add_subplot(1,2,2)
sns.boxplot(y=train['RevolvingUtilizationOfUnsecuredLines'])
plt.show()
一般认为贷款额度与总额度比例小于1是合理的情况。因此先分析比例小于时,贷款额度与总额度比例和好坏客户之间的关系。
cut_num=[0,0.3,0.5,0.7,1,10,100,1000,10000]
get_compare_plot(train,feature_plot="RevolvingUtilizationOfUnsecuredLines",cut_num=cut_num,is_qcut=False)
cut_num=[0,1,10,30,50,70,100,1000,10000,100000]
get_compare_plot(train,feature_plot="RevolvingUtilizationOfUnsecuredLines",cut_num=cut_num,is_qcut=False)
cut_num=[]
for i in np.arange(-1,11,1):
cut_num.append(i)
get_compare_plot(train,feature_plot="RevolvingUtilizationOfUnsecuredLines",cut_num=cut_num,is_qcut=False)
cut_num=[]
for i in np.arange(0.6,3.2,0.2):
cut_num.append(i)
get_compare_plot(train,feature_plot="RevolvingUtilizationOfUnsecuredLines",cut_num=cut_num,is_qcut=False)
print("贷款以及信用卡可用额度与总额度比例大于2的比重为:",100*(train['RevolvingUtilizationOfUnsecuredLines']>2).sum()/len(train),"%")
贷款以及信用卡可用额度与总额度比例大于2的比重为: 0.24733333333333332 %
cut_num=[-1,0,1,2,100000]
get_compare_plot(train,feature_plot="RevolvingUtilizationOfUnsecuredLines",cut_num=cut_num,is_qcut=False)
以上是对Revol这个特征的一个比较全面的分析,通过不断分组探索数据的阈值,找出比较适合的切分点。这里最后的数据处理,仍然存在问题,即可能会引入数据噪声,因为不清楚总额度具体值为多少。
以下的特征依旧会这么处理,分析数据的合理性,找出阈值,对异常值进行处理。
fig= plt.figure(figsize=(14,4))
ax1=fig.add_subplot(1,2,1)
sns.distplot(train['age'],kde=True)
ax1=fig.add_subplot(1,2,2)
sns.boxplot(y=train['age'])
plt.show()
age_mean=train['age'].mean()
age_std=train['age'].std()
age_lowlimit=age_mean-3*age_std
age_uplimit=age_mean+3*age_std
print('异常值下限:',age_lowlimit,'异常值上限:',age_uplimit)
异常值下限: 7.979609077364238 异常值上限: 96.6108042559691
print('峰度:',train['age'].skew(),'偏度:',train['age'].kurt())
峰度: 0.18899454512676198 偏度: -0.4946688326403583
cut_num=[20,30,40,50,60,70,80,90,100,110]
get_compare_plot(train,feature_plot="age",cut_num=cut_num,is_qcut=False)
fig,[[ax1,ax2],[ax3,ax4],[ax5,ax6]] = plt.subplots(3,2,figsize=(20,15))
sns.distplot(train['NumberOfTime30-59DaysPastDueNotWorse'],ax=ax1)
sns.boxplot(y=train['NumberOfTime30-59DaysPastDueNotWorse'],ax=ax2)
sns.distplot(train['NumberOfTime60-89DaysPastDueNotWorse'],ax=ax3)
sns.boxplot(y=train['NumberOfTime60-89DaysPastDueNotWorse'],ax=ax4)
sns.distplot(train['NumberOfTimes90DaysLate'],ax=ax5)
sns.boxplot(y=train['NumberOfTimes90DaysLate'],ax=ax6)
plt.show()
这里找出在限定范围内(两年内2*365),每种情况出现的最多次数。
print('两年内30~59天违规次数超过24次的样本数为:',(train['NumberOfTime30-59DaysPastDueNotWorse']>24).sum())
print('两年内60-89天违规次数超过12次的样本数为:',(train['NumberOfTime60-89DaysPastDueNotWorse']>12).sum())
print('两年内90天以上违规次数超过8次的样本数为:',(train['NumberOfTimes90DaysLate']>8).sum())
两年内30~59天违规次数超过24次的样本数为: 269
两年内60-89天违规次数超过12次的样本数为: 269
两年内90天以上违规次数超过8次的样本数为: 312
train["qcut_30-59"], updown = pd.qcut(train["NumberOfTime30-59DaysPastDueNotWorse"], retbins=True, q=20,duplicates='drop')
train["qcut_60-89"], updown = pd.qcut(train["NumberOfTime60-89DaysPastDueNotWorse"], retbins=True, q=20,duplicates='drop')
train["qcut_90"], updown = pd.qcut(train["NumberOfTimes90DaysLate"], retbins=True, q=20,duplicates='drop')
fig = plt.figure(figsize=(18,4))
ax1 = fig.add_subplot(131)
(train.groupby('qcut_30-59')['SeriousDlqin2yrs'].sum()/train.groupby('qcut_30-59')['SeriousDlqin2yrs'].count()).plot()
ax2 = fig.add_subplot(132)
(train.groupby('qcut_60-89')['SeriousDlqin2yrs'].sum()/train.groupby('qcut_60-89')['SeriousDlqin2yrs'].count()).plot()
ax3 = fig.add_subplot(133)
(train.groupby('qcut_90')['SeriousDlqin2yrs'].sum()/train.groupby('qcut_90')['SeriousDlqin2yrs'].count()).plot()
plt.show()
fig= plt.figure(figsize=(14,4))
ax1=fig.add_subplot(1,2,1)
sns.distplot(train['DebtRatio'],kde=True)
ax1=fig.add_subplot(1,2,2)
sns.boxplot(y=train['DebtRatio'])
plt.show()
cut_num=[-1,0,0.3,0.5,0.7,1,10,100,1000,10000]
get_compare_plot(train,feature_plot="DebtRatio",cut_num=cut_num,is_qcut=False)
cut_num=[10,30,50,70,100]
get_compare_plot(train,feature_plot="DebtRatio",cut_num=cut_num,is_qcut=False)
cut_num=[]
for i in np.arange(0,11,1):
cut_num.append(i)
get_compare_plot(train,feature_plot="DebtRatio",cut_num=cut_num,is_qcut=False)
a=(len(train)-len(train_nM))/len(train)*100
print('家属人数缺失值比例为%.2f%%'%(a))
家属人数缺失值比例为19.82%。
fig= plt.figure(figsize=(14,4))
ax1=fig.add_subplot(1,2,1)
sns.distplot(train_nM['MonthlyIncome'],kde=True)
ax1=fig.add_subplot(1,2,2)
sns.boxplot(y=train_nM['MonthlyIncome'])
plt.show()
cut_num=[0,1000,5000,10000,15000,20000,100000]
get_compare_plot(train,feature_plot="MonthlyIncome",cut_num=cut_num,is_qcut=False)
train_nM_2=train_nM.loc[(train_nM['MonthlyIncome']>1)&(train_nM['MonthlyIncome']<=3000),:]
sns.scatterplot(x='MonthlyIncome',y='DebtRatio',data=train_nM_2,hue='SeriousDlqin2yrs')
train_nM_2=train_nM.loc[(train_nM['MonthlyIncome']>1)&(train_nM['MonthlyIncome']<700),:]
sns.scatterplot(x='MonthlyIncome',y='DebtRatio',data=train_nM_2,hue='SeriousDlqin2yrs')
注:
另外,通过分析还发现了月薪为0,1和NaN值的,负债率也特别高。这里不把1当做金额输入错误的值是因为月薪为1的有605个,因此怀疑其跟0和NaN的含义一样,都是“空值”。
之前有过想法是这些负债率在计算的时候,由于月薪未知,所以计算出来的负债率其实是每个月需要偿还的钱。但这个想法也只是个假设,具体的问题还要跟业务相关联。所以,不对这些月薪进行这个假设处理,而是进行分箱处理。
fig= plt.figure(figsize=(14,4))
ax1=fig.add_subplot(1,2,1)
sns.distplot(train['NumberOfOpenCreditLinesAndLoans'],kde=True)
ax1=fig.add_subplot(1,2,2)
sns.boxplot(y=train['NumberOfOpenCreditLinesAndLoans'])
plt.show()
cut_num=[-1,0,1,3,5,7,10,20,30,40]
get_compare_plot(train,feature_plot="NumberOfOpenCreditLinesAndLoans",cut_num=cut_num,is_qcut=False)
fig= plt.figure(figsize=(14,4))
ax1=fig.add_subplot(1,2,1)
sns.distplot(train['NumberRealEstateLoansOrLines'],kde=True)
ax1=fig.add_subplot(1,2,2)
sns.boxplot(y=train['NumberRealEstateLoansOrLines'])
plt.show()
cut_num=[-1,0,1,3,5,7,10,15,20,25,50]
get_compare_plot(train,feature_plot="NumberRealEstateLoansOrLines",cut_num=cut_num,is_qcut=False)
b=(len(train)-len(train_nN))/len(train)*100
print('家属人数缺失值比例为%.2f%%'%(b))
家属人数缺失值比例为2.62%
fig= plt.figure(figsize=(14,4))
ax1=fig.add_subplot(1,2,1)
sns.distplot(train_nN['NumberOfDependents'],kde=True)
ax1=fig.add_subplot(1,2,2)
sns.boxplot(y=train_nN['NumberOfDependents'])
plt.show()
cut_num=[-1,0,1,3,5,7,9,15,20]
get_compare_plot(train,feature_plot="NumberOfDependents",cut_num=cut_num,is_qcut=False)
数据预处理,这里包含一些之前整理过的异常值处理,填补缺失值和数据不平衡处理的方法。
train.duplicated(keep='first').sum()
609
train.drop_duplicates(keep='first',inplace=True)
train.duplicated(keep='first').sum()
0
train.index = range(train.shape[0])
def data_deal(data):
#data=data[data['RevolvingUtilizationOfUnsecuredLines']<=2]
data=data[(data['age']>=18)&(data['age']<=100)]
data=data[data['NumberOfTime30-59DaysPastDueNotWorse']<24]
data=data[data['NumberOfTime60-89DaysPastDueNotWorse']<12]
data=data[data['NumberOfTimes90DaysLate']<8]
data=data[data['DebtRatio']<=1]
#data=data[data['NumberOfOpenCreditLinesAndLoans']<50]
data=data[data['NumberRealEstateLoansOrLines']<20]
data=data[data['NumberOfDependents']<20]
return data
train_data = data_deal(train)
test_data = data_deal(test)
train_data.loc[:,"NumberOfDependents"] = train_data.loc[:,"NumberOfDependents"].fillna(train_data.loc[:,"NumberOfDependents"].median())
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.loc[Ytrain.index,:]
Xtest = df.loc[Ytest.index,:]
rfr = rfr(n_estimators=200)
rfr = rfr.fit(Xtrain,Ytrain)
Ypredict = rfr.predict(Xtest)
return Ypredict
X = train_data.iloc[:,1:]
y = train_data["SeriousDlqin2yrs"]
y_pred = fill_missing_rf(X,y,"MonthlyIncome")
train_data.loc[train_data.loc[:,"MonthlyIncome"].isnull(),"MonthlyIncome"] = y_pred
之前提到过数据中正负样本比例不均衡,为了尽可能捕捉坏样本。这里采用SMOTE算法对数据进行上采样。
X = train_data.iloc[:,1:]
y = train_data.iloc[:,0]
n_sample = X.shape[0]
n_1_sample = y.value_counts()[1]
n_0_sample = y.value_counts()[0]
print('样本个数:{}; 坏样本(1)占{:.2%}; 好样本(0)占{:.2%}'.format(n_sample,n_1_sample/n_sample,n_0_sample/n_sample))
样本个数:149101; 坏样本(1)占6.59%; 好样本(0)占93.41%
sm = SMOTE(random_state=42)
X,y = sm.fit_sample(X,y)
n_sample_ = X.shape[0]
n_1_sample = pd.Series(y).value_counts()[1]
n_0_sample = pd.Series(y).value_counts()[0]
print('样本个数:{}; 坏样本(1)占{:.2%}; 好样本(0)占{:.2%}'.format(n_sample_,n_1_sample/n_sample_,n_0_sample/n_sample_))
样本个数:278540; 坏样本(1)占50.00%; 好样本(0)占50.00%
通过对数据进行探索性分析,更加了解数据,从中观察出数据的一些规律和问题,这样在后续的预处理过程中,也能有些思路。
以上是对数据分析和处理的过程,但其中还有很多的不足,比如对月薪和负债率的分析处理,数据预处理的方式等,大家有好的建议可以多多指正,感谢!
接下来是对数据进行特征工程,如特征衍生,数据分箱等。构建逻辑回归模型来进行预测,并利用ROC-AUC和KS进行评估。最后建立评分卡。
数据挖掘项目:银行信用评分卡建模分析(下篇)