kaggle实战-信贷违约预测
本文是基于一份Lendingclub上面的信贷数据,根据贷款人的历史数据和贷款信息,建立一个模型来预测借款人是否会偿还他们的贷款。
导入库
1 | import numpy as np |
数据基本信息
数据来自 LendingClub,它是美国一家个人对个人贷款公司,总部位于加利福尼亚州旧金山,号称是全球最大的个人对个人(p2p)借贷平台。
查看数据量、字段类型等:
缺失值处理
缺失值处理是本次数据处理的一项重要工作。由于存在字符型和数值型的变量,而且这两种类型下的数据都存在缺失值,所以在分开处理。
缺失值可视化
In [6]:
1 | # 4、缺失值情况 |
Out[6]:
1 | id 105451 |
In [7]:
1 | ms.bar(df,color="blue") |
如果取值是1,说明是完整的字段,没有缺失值;可以看到还是存在很多字段有缺失值。
删除全部缺失字段
1、部分字段是全部缺失的,将它们进行删除:
In [8]:
1 |
In [9]:
1 | # 在这里我们使用下面的【方法2】 |
Out[9]:
1 | id 1.0 |
In [10]:
1 | isnull_col_percent[:20] # 前20个 |
Out[10]:
1 | id 1.000000 |
在上面的结果中,比例为1的字段就是全部缺失的(前面18个字段),对于建模无用,我们将其删除:
In [11]:
1 | no_use_col = df.columns[df.isnull().all()].tolist() |
Out[11]:
1 | ['id', |
In [12]:
1 | df.drop(no_use_col,axis=1,inplace=True) |
In [13]:
1 | df.shape # 137-18 = 119,成功删除18个属性 |
Out[13]:
1 | (105451, 119) |
删除缺失值在30%以上的字段
2、有些字段是部分缺失值;缺失量过大也会对建模造成影响。
在这里我们设置删除缺失值在30%(可以设定其他值)以上的字段(按照列的方向来统计)
In [14]:
1 | # 最大的缺失值比例高达97.78% |
Out[14]:
1 | sec_app_mths_since_last_major_derog 0.977866 |
In [15]:
1 | thresh = len(df)*0.3 # 阀值(缺失值数量) |
In [16]:
1 | df.shape |
Out[16]:
1 | (105451, 102) |
In [17]:
1 | df.isnull().sum(axis=0).sort_values(ascending=False) / float(len(df)) |
Out[17]:
1 | mths_since_recent_revol_delinq 0.648405 |
新数据(删除缺失值)
新数据缺失值可视化
In [18]:
1 | ms.bar(df,color="blue") |
和之前的对比,发现缺失值的字段少了很多
处理后的数据量:只剩下102个字段
In [19]:
1 | df.shape |
Out[19]:
1 | (105451, 102) |
字段类型统计
In [20]:
1 | df.dtypes.value_counts() |
Out[20]:
1 | int64 54 |
唯一值字段
3、某些字段的取值是唯一的,比如全部是0,这样的字段对于建模也是无用的,我们要将这些字段进行删除。
首先找出这些字段:使用Pandas中的nunique函数来确定取值是否唯一。
In [21]:
1 | nunique_data = df.apply(pd.Series.nunique) != 1 # 不唯一的字段 |
Out[21]:
1 | loan_amnt True |
In [22]:
1 | # df1:删除唯一值字段后的数据 |
Out[22]:
1 | (105451, 97) |
字段从102变成了97,说明还是存在5个取值唯一的字段
In [23]:
1 | # 再次查看字段类型 |
Out[23]:
1 | int64 51 |
缺失值处理-分类型变量
针对分类型变量(取值为字符串类型)的缺失值处理,一般是采用类型编码、独热码、因子化等操作
In [24]:
1 | object_df = df1.select_dtypes(include="object") |
Out[24]:
1 | Index(['term', 'int_rate', 'grade', 'sub_grade', 'emp_title', 'emp_length', |
比如字段grade的取值是真实的Object类型:
In [25]:
1 | # grade |
Out[25]:
1 | C 36880 |
下面出现的字段虽然是object类型,但是实际上行它们的取值仍存在一定的大小关系
In [26]:
1 | # int_rate |
Out[26]:
1 | 16.02% 4956 |
In [27]:
1 | # revol_util |
Out[27]:
1 | 0% 468 |
分类型变量转成数值
- 将百分比的数据转成浮点型:
In [28]:
1 | # 将右侧的%去掉,同时强制转成float类型 |
- 关于年份的处理:转成数值,并且10+直接用10代替,<1的部分直接用0代替:
In [29]:
1 | # emp_length |
Out[29]:
1 | 10+ years 35438 |
In [30]:
1 | df1["emp_length"].isnull().sum() # 存在缺失值 |
Out[30]:
1 | 6697 |
现将缺失值填充为<1 year
:
In [31]:
1 | df1["emp_length"] = df1["emp_length"].fillna("< 1 year") |
In [32]:
1 | # 1、先将year或years去掉 |
In [33]:
1 | df1["emp_length"].value_counts() |
Out[33]:
1 | 10+ 35438 |
In [34]:
1 | # 2、转成数值型 |
In [35]:
1 | df1["emp_length"].value_counts() |
Out[35]:
1 | 10.0 35438 |
分类型变量可视化
In [36]:
1 | object_df = df1.select_dtypes(include="object") |
Out[36]:
1 | Index(['term', 'grade', 'sub_grade', 'emp_title', 'home_ownership', |
In [37]:
1 | ms.matrix(object_df) |
主要是两个字段缺失值:emp_title、next_pymnt_d
缺失值填充
In [38]:
1 | object_df.isnull().sum() |
Out[38]:
1 | term 0 |
在这里我们直接填充为“Unknown”:
In [39]:
1 | object_df.fillna("Unknown",inplace=True) |
再次查看发现已经没有缺失值:
In [40]:
缺失值处理——数值型变量
In [41]:
1 | number_df = df1.select_dtypes(exclude="object") |
Out[41]:
1 | Index(['loan_amnt', 'funded_amnt', 'funded_amnt_inv', 'int_rate', |
In [42]:
1 | number_df.shape |
Out[42]:
1 | (105451, 79) |
缺失值可视化
In [43]:
1 | ms.matrix(number_df) |
如何判断某行是否全部为空
查看缺失值情况
In [48]:
1 | number_df.isnull().sum().sort_values(ascending=False) |
Out[48]:
1 | mths_since_recent_revol_delinq 68375 |
找出存在缺失值的字段
In [49]:
1 | # 存在缺失值的为True,否则为False |
Out[49]:
1 | loan_amnt False |
In [50]:
1 | number_df.columns[number_df.isnull().sum() > 0] |
Out[50]:
1 | Index(['dti', 'mths_since_last_delinq', 'revol_util', 'mths_since_rcnt_il', |
缺失值填充
对于数值型变量的缺失值,我们采用每列的均值来填充
In [51]:
1 | # 方法1 |
In [52]:
1 | # 方法2 |
In [53]:
1 | number_df.isnull().sum().sort_values(ascending=False) |
Out[53]:
1 | loan_amnt 0 |
新建模数据new_data
手动过滤字段
有些字段对建模是无用的,即存在冗余的字段信息,我们需要手动删除。
In [54]:
1 | object_df.isnull().sum() |
Out[54]:
1 | term 0 |
In [55]:
1 | object_df["application_type"].value_counts() |
Out[55]:
1 | INDIVIDUAL 98619 |
- term:申请人的贷款偿还期数,通常是36或者60,对于建模没有指导意义,保留(1)
- grade和sub_grade:存在重复信息,选择删除字段sub_gracde,保留grade(12)
- emp_title:不能反映借款人的收入或者资产情况,可删除
- home_ownership:房屋所在状态,保留(3)
- verification_status:收入是否被LC验证、未验证或收入来源是否被验证,保留(4)
- issue_d:贷款发行时间,对于是否违规预测也没有指导意义,可删除
- loan_status:贷款的当前状态,保留(5)
- purpose:贷款目的,保留(6)
- title:和purpose重复,可删除
- zip_code:地址邮编,对于建模无效
- addr_state:申请人的地址所在州,建模无效
- earliest_cr_line:借款人最早公布的信用额度开通的月份,对于是否违规模型搭建无意义,考虑删除
- initial_list_status:贷款的初始上市状态,可谓W或者F,保留(7)
- last_pymnt_d、next_pymnt_d、last_credit_pull_d:贷款后的信息,对于是否违规预测为意义,可删除
- application_type:指示贷款是单独申请还是与两个共同借款人的联合申请,保留(8)
最终留下8个有用的字段信息:
In [56]:
1 | object_df = object_df[["term","grade","home_ownership","verification_status","loan_status", |
数据合并
将两个数据合并起来:
In [57]:
1 | new_data = pd.concat([object_df,number_df],axis=1) |
基本信息
In [58]:
1 | new_data.dtypes |
Out[58]:
1 | term object |
In [59]:
1 | new_data.shape |
Out[59]:
1 | (105451, 87) |
描述统计信息
针对数值型字段的描述统计信息,查看最值,四分位数等:
In [60]:
1 | new_data.describe() |
特征工程
特征衍生
In [61]:
installment / (annual_inc / 12):表示每个月的还款金额占据月收入的比例,数值越大,还款压力越大
1 | # installment:每月的还款金额 |
Out[61]:
贷款状态编码与可视化
编码
In [62]:
1 | new_data["loan_status"].value_counts() |
Out[62]:
1 | Current 99850 |
In [63]:
1 | # 违约=1, 正常=0 |
In [64]:
1 | new_data["loan_status"].value_counts() |
Out[64]:
1 | 0 103746 |
可视化
In [65]:
1 | # 贷款状态柱状图统计 |
可以看到违规和没有违规的比例差别是很大的,后面会通过采样的方法来解决。
有序变量编码
部分字段的取值是存在一定的顺序关系。比如服装的尺码大小,“XS”、“S”、“M”、"L"等,它们的取值本身是有大小关系的。
在这里我们实施硬编码:
In [66]:
1 | new_data.select_dtypes("object") |
无序特征编码
采用的独热码,通过get_dummies函数来实现
In [68]:
1 | df1 = new_data.select_dtypes("object") |
Out[68]:
特征缩放:标准化
针对数值型特征(数据df2)的特征缩放,将数据规范到一定范围内,便于后期加快算法的收敛速度
In [70]:
1 | # 目标变量load_status |
Out[70]:
loan_status | |
---|---|
0 | 0 |
1 | 0 |
2 | 0 |
3 | 0 |
4 | 0 |
In [71]:
1 | # df2中剩余的数值型变量构成df3 |
1 | # 对df3实施标准化处理 |
SMOTE采样(重点)
可以看到违规和没有违规的比例差别是很大的,也就是说样本及其不均衡,常用的处理方式有两种:
- 过采样(oversampling),增加正样本使得正、负样本数目接近,然后再进行学习
- 欠采样(undersampling),去除一些负样本使得正、负样本数目接近,然后再进行学习。
下面通过欠采样方法来处理:
In [75]:
1 | positive_data = new_df[new_df["loan_status"] == 1] |
In [76]:
1 | print("length of positive_data: ",len(positive_data)) |
In [77]:
1 | selected_data = negetive_data.sample(len(positive_data)) |
Out[77]:
1 | (1705, 104) |
下面生成的new_data就是最终用于建模的数据:
In [78]:
1 | new_data = pd.concat([positive_data,selected_data],axis=0) |
建模
特征和目标分离
In [79]:
1 | y = new_data[["loan_status"]] |
训练集和测试集切分
In [80]:
1 | from sklearn.model_selection import train_test_split |
逻辑回归分类器
基于逻辑回归的分类器
In [81]:
1 | from sklearn.linear_model import LogisticRegression |
Out[81]:
1 | LogisticRegression() |
In [82]:
1 | # 进行预测 |
Out[82]:
1 | array([1, 1, 0, 1, 1]) |
预测准确率
查看预测的准确率:
In [83]:
1 | from sklearn.metrics import accuracy_score |
Out[83]:
1 | 0.7067448680351907 |
混淆矩阵的信息:
In [84]:
1 | from sklearn.metrics import confusion_matrix |
Out[84]:
1 | array([[262, 87], |
In [85]:
1 | # 混淆矩阵可视化 |
上面可视化的结果表示:颜色越深代表的人数越多,也就是真阳性的人数是最多的;而假阳性是最少的
roc曲线
In [86]:
1 | from sklearn.metrics import roc_curve, auc |
Out[86]:
1 | 0.7056884965194421 |
In [87]:
1 | import matplotlib.pyplot as plt |
Out[87]:
1 | Text(0.5, 0, 'False Positive Rate') |
总结
整体方案的ROC值达到了71%,还是有一定的提升空间。后续可以优化的点:
- 特征属性过多:可以考虑降维或者特征的多重线性检验,找出更具有价值的特征
- 离散型变量的编码:目前是统一的标准化处理,没有编码工作;后续可以考虑加入编码工作,比如:因子化、特征分箱等
- 建模优化:尝试使用不同的分类模型,以及模型融合方法
- 参数调优:可以考虑做一个参数的调优,比如网格搜索、随机搜索等等