拍拍贷风控违约预测项目
背景介绍
国内网络借贷行业的贷款风险数据
- 1.包括信用违约标签(因变量)
- 2.建模所需的基础与加⼯字段(自变量)
- 3.相关用户的网络行为原始数据
本着保护借款⼈隐私以及拍拍贷知识产权的目的,数据字段已经过脱敏处理。
数据信息
Master(每一行代表一个成功成交借款样本,每个样本包含200多个各类字段。
- 1.idx:每笔贷款的unique key,可与另外2个文件里的idx相匹配。
- 2.UserInfo_*:借款人特征字段
- 3.WeblogInfo_*:Info网络行为字段
- 4.Education_Info*:学历学籍字段
- 5.ThirdParty_Info_PeriodN_*:第三方数据时间段N字段
- 6.SocialNetwork_*:社交网络字段
- 7.LinstingInfo:借款成交时间
- 8.Target:违约标签(1 = 贷款违约,0 = 正常还款)。测试集里 不包含target字段。
**Log_Info(借款人的登陆信息)**
- 1.ListingInfo:借款成交时间
- 2.LogInfo1:操作代码
- 3.LogInfo2:操作类别
- 4.LogInfo3:登陆时间
- 5.idx:每一笔贷款的unique key
Userupdate_Info(借款⼈修改信息)
- 1.ListingInfo1:借款成交时间
- 2.UserupdateInfo1:修改内容
- 3.UserupdateInfo2:修改时间
- 4.idx:每⼀笔贷款的unique key
案例流程
数据清洗
缺失值的多维度处理;
- 对于每一列来说,统计每一列的数据缺失比例:
- 对于每一行来说,也统计一下缺失值的个数并按缺失值大小从小到大排序,训练集和测试集表现出近乎一样的分布,但在右上角区域略有不多,于是我们猜测,这部分的数据之所以分布与测试集不同,应该是离群点的缘故,这样我们通过对比训练集和测试集上数据缺失值的数量分布区分出了明显的离群点。
- 还可以利用Xgboost在原始的训练集上训练数据得出每一个原始特征的重要程度列表,排序以后,取前40个重要的原始特征(根据原始特征的数量,可以调整抽取重要特征的个数),将有缺失值的样本拿出来进行统计,如果其缺失的原始特征中达到10个特征都是前20个重要特征,那就说么这个样本为异常值,应该剔除掉。
剔除常变量
- 原始数据中有190维数值型特征,计算出这些数值型特征的标准差,标准差几乎为零的说明其,在这一维度上的取值几乎为常数,故其没有区分度,没有价值,我们以小于deng为界限,剔除掉标准差小于0.1的特征项。
其余的文本处理
- 字符大小写转换
- Userupdate_Info表中的UserupdateInfo1字段,属性取值为英文字符,包含了大小写,如“_QQ”和“_qQ”,很明显是同一种取值,我们将所有字符统一转换为小写。
- 空格符号处理
- Master 表中 UserInfo_9字段的取值包含了空格字符,如“中国移动”和“中国移动 ”,它们是同一种取值,需要将空格符去除。
- 城市名处理
- UserInfo_8 包含有“重庆”、“重庆市”等取值,它们实际上是同一个城市,需要把字符中的“市”全部去掉。去掉“市”之后,城市数由 600 多下降到 400 多。
特征工程
-
(省份)地理位置的处理方法
- UserInfo_7和UserInfo_19是省份信息,其余为城市信息。统计每个省份和城市的违约率。
- 选择违约率超过%的省份或直辖市,例如四川,湖南,湖北,吉林,天津,山东等等,用这些省份或直辖市构造几个二值特征:“是否为四川省”,“是否为湖南省”…“是否为山东省”,取值为 0或1 。
-
(市级)地理位置处理
- (1)按照城市等级合并
- 由于市级城市数量过多,如果按照类别型特征直接处理,进行独热编码后,会得到很高的维度的稀疏特征,这样训练的时候,每一维度的城市特征是学不到什么有用的权重的。故不可采取这种办法,除了上述按违约率较高的省份或直辖市单独成为一个维度,违约率过低的合并成一个维度以为,还可以将所有城市按照经济上对于城市等级的经济等级分层划分成不同的层次,这样既科学,也能很好的降低城市这一特征维度。具体操作如下,例如一线城市北京,上海,广州,深圳合并,赋值为1,同样的,二线城市合并为2,三线城市合并为3,以此类推。
- (2)经纬度特征的引入
- 上述几种对地理位置信息的处理都是基于类别型的,我们还可以把类别型特征转化为数值型特征,通过引入经纬度来实现,具体操作:将地理位置一个特征变为经度和纬度两个特征,比如:我们把北京市,用经度39.92,纬度116.46,两个特征替换掉。
-(3)构建地理位置的组合特征,地理位置差异特征 - 例如UserInfo_2,UserInfo_4,UserInfo_7,UserInfo_8,UserInfo_20,都是城市地理信息,我们可以两两比较,构造diff_24(UserInfo_2,UserInfo_4),当这两个特征值一样时,diff_12为1,否则为0,一次类推,可以构造类似diff_27,diff_28,…等特征。
以上几种对地理位置的处理方法,可以根据效果进行选择,从而选择最适合的方法。
- 上述几种对地理位置信息的处理都是基于类别型的,我们还可以把类别型特征转化为数值型特征,通过引入经纬度来实现,具体操作:将地理位置一个特征变为经度和纬度两个特征,比如:我们把北京市,用经度39.92,纬度116.46,两个特征替换掉。
- (1)按照城市等级合并
-
3.成交时间
- (1)将成交时间字段Listinginfo处理成数值型的特征,直接当成连续值来处理。
- (2)将成交时间离散化,按照一定的间隔(如以10天为一个区间),即将日期010离散为1,日期1120离散为2,以此类推。
-
4.类别型特征
- 除去上述的特征处理方法外,其余的都做独热向量编码。
-
5.其他特征
- UpadteInfo表特征
- 根据这个表提供的信息,我们可以从中抽取用户修改信息次数,修改信息时间到成交时间的跨度,每种信息的修改次数等等特征,至于提取的哪个特征具有区别性可以做个简单的统计分析,每种特征下违约率的分布是否由于不同的取值发生明显的变化。
- LogInfo表特征
- 类似的从登录信息表里提取了用户的登录信息特征,比如登录天数,平均登录间隔,以及每一种操作代码次数等等特征。
- UpadteInfo表特征
特征选择
比较高效的一种方法是基于模型的特征排序方法, 这种方法有一个好处:模型学习的过程和特征选择的过程是同时进行的,因此采用这种 方法,基于 xgboost 来做特征选择,xgboost模型训练完成后可以输出特征的重要性,据此可以保留 TopN个特征,从而达到特征选择的目的。
类别不平衡问题
数据的类别比例接近13:1,采用在训练模型时设置类别权重等来解决类别不平衡问题的方法。
建模与模型融合
实现过程
数据读取
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
train_master = pd.read_csv('data/PPD-First-Round-Data-Update/Training Set/PPD_Training_Master_GBK_3_1_Training_Set.csv',encoding='gbk')
train_userupdateinfo = pd.read_csv(u'data/PPD-First-Round-Data-Update/Training Set/PPD_Userupdate_Info_3_1_Training_Set.csv',encoding='gbk')
train_loginfo = pd.read_csv('data/PPD-First-Round-Data-Update/Training Set/PPD_LogInfo_3_1_Training_Set.csv',encoding='gbk')
%config ZMQInteractiveShell.ast_node_interactivity='all'
%pprint
Pretty printing has been turned OFF
借款人信息
train_master.head() ## 借款人的一些信息
train_master.info()
train_master.shape
Idx | UserInfo_1 | UserInfo_2 | UserInfo_3 | UserInfo_4 | WeblogInfo_1 | WeblogInfo_2 | WeblogInfo_3 | WeblogInfo_4 | WeblogInfo_5 | ... | SocialNetwork_10 | SocialNetwork_11 | SocialNetwork_12 | SocialNetwork_13 | SocialNetwork_14 | SocialNetwork_15 | SocialNetwork_16 | SocialNetwork_17 | target | ListingInfo | |
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
0 | 10001 | 1.0 | 深圳 | 4.0 | 深圳 | NaN | 1.0 | NaN | 1.0 | 1.0 | ... | 222 | -1 | 0 | 0 | 0 | 0 | 0 | 1 | 0 | 2014/3/5 |
1 | 10002 | 1.0 | 温州 | 4.0 | 温州 | NaN | 0.0 | NaN | 1.0 | 1.0 | ... | 1 | -1 | 0 | 0 | 0 | 0 | 0 | 2 | 0 | 2014/2/26 |
2 | 10003 | 1.0 | 宜昌 | 3.0 | 宜昌 | NaN | 0.0 | NaN | 2.0 | 2.0 | ... | -1 | -1 | -1 | 1 | 0 | 0 | 0 | 0 | 0 | 2014/2/28 |
3 | 10006 | 4.0 | 南平 | 1.0 | 南平 | NaN | NaN | NaN | NaN | NaN | ... | -1 | -1 | -1 | 0 | 0 | 0 | 0 | 0 | 0 | 2014/2/25 |
4 | 10007 | 5.0 | 辽阳 | 1.0 | 辽阳 | NaN | 0.0 | NaN | 1.0 | 1.0 | ... | -1 | -1 | -1 | 0 | 0 | 0 | 0 | 0 | 0 | 2014/2/27 |
5 rows × 228 columns
<class 'pandas.core.frame.DataFrame'>
RangeIndex: 30000 entries, 0 to 29999
Columns: 228 entries, Idx to ListingInfo
dtypes: float64(38), int64(170), object(20)
memory usage: 52.2+ MB
(30000, 228)
借款成交时间 , 修改内容 ,修改时间
train_userupdateinfo.head(20) ###借款成交时间 , 修改内容 ,修改时间
train_userupdateinfo.info()
train_userupdateinfo.shape
Idx | ListingInfo1 | UserupdateInfo1 | UserupdateInfo2 | |
---|---|---|---|---|
0 | 10001 | 2014/03/05 | _EducationId | 2014/02/20 |
1 | 10001 | 2014/03/05 | _HasBuyCar | 2014/02/20 |
2 | 10001 | 2014/03/05 | _LastUpdateDate | 2014/02/20 |
3 | 10001 | 2014/03/05 | _MarriageStatusId | 2014/02/20 |
4 | 10001 | 2014/03/05 | _MobilePhone | 2014/02/20 |
5 | 10001 | 2014/03/05 | _MobilePhone | 2014/02/20 |
6 | 10001 | 2014/03/05 | _QQ | 2014/02/20 |
7 | 10001 | 2014/03/05 | _ResidenceAddress | 2014/02/20 |
8 | 10001 | 2014/03/05 | _ResidencePhone | 2014/02/20 |
9 | 10001 | 2014/03/05 | _ResidenceTypeId | 2014/02/20 |
10 | 10001 | 2014/03/05 | _ResidenceYears | 2014/02/20 |
11 | 10002 | 2014/02/26 | _age | 2013/06/21 |
12 | 10002 | 2014/02/26 | _educationId | 2013/06/21 |
13 | 10002 | 2014/02/26 | _gender | 2013/06/21 |
14 | 10002 | 2014/02/26 | _hasBuyCar | 2013/06/21 |
15 | 10002 | 2014/02/26 | _idNumber | 2013/06/21 |
16 | 10002 | 2014/02/26 | _lastUpdateDate | 2013/06/21 |
17 | 10002 | 2014/02/26 | _lastUpdateDate | 2013/07/08 |
18 | 10002 | 2014/02/26 | _lastUpdateDate | 2013/07/08 |
19 | 10002 | 2014/02/26 | _lastUpdateDate | 2013/07/08 |
<class 'pandas.core.frame.DataFrame'>
RangeIndex: 372463 entries, 0 to 372462
Data columns (total 4 columns):
Idx 372463 non-null int64
ListingInfo1 372463 non-null object
UserupdateInfo1 372463 non-null object
UserupdateInfo2 372463 non-null object
dtypes: int64(1), object(3)
memory usage: 11.4+ MB
(372463, 4)
借款成交时间 ,操作代码 ,操作类别 ,登陆时间
train_loginfo.head(20) ##借款成交时间 ,操作代码 ,操作类别 ,登陆时间
train_loginfo.shape
train_loginfo.isnull().sum().sort_values(ascending=False).head(10) # 缺失值统计
Idx | Listinginfo1 | LogInfo1 | LogInfo2 | LogInfo3 | |
---|---|---|---|---|---|
0 | 10001 | 2014-03-05 | 107 | 6 | 2014-02-20 |
1 | 10001 | 2014-03-05 | 107 | 6 | 2014-02-23 |
2 | 10001 | 2014-03-05 | 107 | 6 | 2014-02-24 |
3 | 10001 | 2014-03-05 | 107 | 6 | 2014-02-25 |
4 | 10001 | 2014-03-05 | 107 | 6 | 2014-02-27 |
5 | 10001 | 2014-03-05 | 107 | 6 | 2014-03-04 |
6 | 10001 | 2014-03-05 | 1 | 1 | 2014-02-20 |
7 | 10001 | 2014-03-05 | 1 | 20 | 2014-02-20 |
8 | 10001 | 2014-03-05 | 12 | 0 | 2014-02-20 |
9 | 10001 | 2014-03-05 | 1 | 2 | 2014-02-20 |
10 | 10001 | 2014-03-05 | 2 | 1 | 2014-02-20 |
11 | 10001 | 2014-03-05 | 4 | 1 | 2014-02-20 |
12 | 10001 | 2014-03-05 | -4 | 6 | 2014-02-20 |
13 | 10001 | 2014-03-05 | -4 | 6 | 2014-02-20 |
14 | 10001 | 2014-03-05 | -4 | 6 | 2014-02-23 |
15 | 10001 | 2014-03-05 | -4 | 6 | 2014-02-24 |
16 | 10001 | 2014-03-05 | -4 | 6 | 2014-02-25 |
17 | 10001 | 2014-03-05 | -4 | 6 | 2014-02-27 |
18 | 10001 | 2014-03-05 | -4 | 6 | 2014-03-04 |
19 | 10002 | 2014-02-26 | 10 | 0 | 2013-06-21 |
(580551, 5)
LogInfo3 0
LogInfo2 0
LogInfo1 0
Listinginfo1 0
Idx 0
dtype: int64
数据探索分析
用户登录信息和用户更新信息没有缺失值,不用处理
list(train_master.columns)
%matplotlib inline
n_null_rate = train_master.isnull().sum().sort_values(ascending=False)/30000
n_null_rate.head(20)
## 去掉缺失比例接近百分之百的字段
train_master.drop(['WeblogInfo_1' ,'WeblogInfo_3'],axis=1,inplace=True)
## 处理UserInfo_12缺失
train_master['UserInfo_12'].unique()
#fig = plt.figure()
#fig.set(alpha=0.2)
target_UserInfo_12_not = train_master.target[train_master.UserInfo_12.isnull()].value_counts()
target_UserInfo_12_ = train_master.target[train_master.UserInfo_12.notnull()].value_counts()
df_UserInfo_12 = pd.DataFrame({
'missing':target_UserInfo_12_not,'not_missing':target_UserInfo_12_})
df_UserInfo_12
df_UserInfo_12.plot(kind='bar', stacked=True)
plt.title(u'有无这个特征对结果的影响')
plt.xlabel(u'有无')
plt.ylabel(u'违约情况')
plt.show()
train_master.loc[(train_master.UserInfo_12.isnull() , 'UserInfo_12')] = 2.0
#train_master['UserInfo_11'].fillna(2.0)
#train_master['UserInfo_12'] =train_master['UserInfo_12'].astype(np.int32)
train_master['UserInfo_12'].dtypes
train_master['UserInfo_12'].unique()
## 处理UserInfo_11缺失
train_master['UserInfo_11'].unique()
#fig = plt.figure()
#fig.set(alpha=0.2)
target_UserInfo_11_not = train_master.target[train_master.UserInfo_11.isnull()].value_counts()
target_UserInfo_11_ = train_master.target[train_master.UserInfo_11.notnull()].value_counts()
df_UserInfo_11 = pd.DataFrame({
'no_have':target_UserInfo_11_not,'have':target_UserInfo_11_})
df_UserInfo_11
df_UserInfo_11.plot(kind='bar', stacked=True)
plt.title(u'有无这个特征对结果的影响')
plt.xlabel(u'有无')
plt.ylabel(u'违约情况')
plt.show()
#train_master['UserInfo_11'] =train_master['UserInfo_11'].astype(str)
train_master.loc[(train_master.UserInfo_11.isnull() , 'UserInfo_11')] = 2.0
train_master['UserInfo_11'].unique()
## 处理UserInfo_13缺失
train_master['UserInfo_13'].unique()
#fig = plt.figure()
#fig.set(alpha=0.2)
target_UserInfo_13_not = train_master.target[train_master.UserInfo_13.isnull()].value_counts()
target_UserInfo_13_ = train_master.target[train_master.UserInfo_13.notnull()].value_counts()
df_UserInfo_13 = pd.DataFrame({
'no_have':target_UserInfo_13_not,'have':target_UserInfo_13_})
df_UserInfo_13
df_UserInfo_13.plot(kind='bar', stacked=True)
plt.title(u'有无这个特征对结果的影响')
plt.xlabel(u'有无')
plt.ylabel(u'违约情况')
plt.show()
#train_master['UserInfo_13'] =train_master['UserInfo_13'].astype(str)
train_master.loc[(train_master.UserInfo_13.isnull() , 'UserInfo_13')] = 2.0
train_master['UserInfo_13'].unique()
## 处理WeblogInfo_20 缺失
train_master['WeblogInfo_20'].unique()
#fig = plt.figure()
#fig.set(alpha=0.2)
target_WeblogInfo_20_not = train_master.target[train_master.WeblogInfo_20.isnull()].value_counts()
target_WeblogInfo_20_ = train_master.target[train_master.WeblogInfo_20.notnull()].value_counts()
df_WeblogInfo_20 = pd.DataFrame({
'no_have':target_WeblogInfo_20_not,'have':target_WeblogInfo_20_})
df_WeblogInfo_20
df_WeblogInfo_20.plot(kind='bar', stacked=True)
plt.title(u'有无这个特征对结果的影响')
plt.xlabel(u'有无')
plt.ylabel(u'违约情况')
plt.show()
#train_master['WeblogInfo_20'] =train_master['WeblogInfo_20'].astype(str)
train_master.loc[(train_master.WeblogInfo_20.isnull() , 'WeblogInfo_20')] = u'不详'
train_master['WeblogInfo_20'].unique()
train_master['WeblogInfo_19'].unique()
#fig = plt.figure()
#fig.set(alpha=0.2)
target_WeblogInfo_19_not = train_master.target