用户贷款风险预测是数字城堡网融360公司提供的一个项目,通过举办商提供的用户基本身份信息,消费行为,银行还款等数据,进行数据处理特征选取并建立逾期预测模型,来预测用户是否会逾期还款。
相关数据:融360-用户贷款风险预测
参考资料:https://zhuanlan.zhihu.com/p/34742822
首先总结下流程:
- 项目目标
- 数据解读
- 数据预处理
- 特征工程
- 模型建立
一、项目目标
通过举办商提供的关于用户基本身份信息,消费行为,银行还款等数据信息,以此建立准确的逾期预测模型,来预测用户是否会逾期还款。
二、数据解读
相关专业名词可以去举办方融360官网上搜索https://www.rong360.com/ask/
1)数据总体概述
提供的训练数据包括用户的基本属性user_info.txt、银行流水记录bank_detail.txt、用户浏览行为browse_history.txt、信用卡账单记录bill_detail.txt、放款时间loan_time.txt,以及这些顾客是否发生逾期行为的记录overdue.txt。(注意:并非每一位用户都有非常完整的记录,如有些用户并没有信用卡账单记录,有些用户却没有银行流水记录。)同时需要注意的是数据做过脱敏处理:(a) 隐藏了用户的id信息;(b) 将用户属性信息全部数字化;(c) 将时间戳和所有金额的值都做了函数变换。
2)数据详细描述
(1)用户的基本属性user_info.txt。共6个字段,其中字段性别为0表示性别未知。
用户id,性别,职业,教育程度,婚姻状态,户口类型
6346,1,2,4,4,2
(2)银行流水记录bank_detail.txt。共5个字段,其中,第2个字段,时间戳为0表示时间未知;第3个字段,交易类型有两个值,1表示支出、0表示收入;第5个字段,工资收入标记为1时,表示工资收入。
用户id,时间戳,交易类型,交易金额,工资收入标记
6951,5894316387,0,13.756664,0
(3)用户浏览行为browse_history.txt。共4个字段。其中,第2个字段,时间戳为0表示时间未知。
用户id,时间戳,浏览行为数据,浏览子行为编号
34724,5926003545,172,1
(4)信用卡账单记录bill_detail.txt。共15个字段,其中,第2个字段,时间戳为0表示时间未知。为方便浏览,字段以表格的形式给出。
文件示例如下: 用户id,账单时间戳,银行id,上期账单金额,上期还款金额,信用卡额度,本期账单余额,本期账单最低还款额,消费笔数,本期账单金额,调整金额,循环利息,可用金额,预借现金额度,还款状态 3147,5906744363,6,18.626118,18.661937,20.664418,18.905766,17.847133,1,0.000000,0.000000,0.000000,0.000000,19.971271,0
上期账单金额: 上月需要向信用卡还款的金额
上期还款金额:上月用户已还款的金额
信用卡额度:信用额度(即信用卡最高可以透支使用的限额)+ 存入信用卡的金额。
本期账单余额:指截止到出账单日,本期账单还未还的金额。
本期账单最低还款额:最低还款额=信用额度内消费款的10%+预借现金交易款的100%+前期最低还款额未还部分的100%+超过信用额度消费款的100%+费用和利息的100%
消费笔数:用户在账单期内的消费记录总数
本期账单金额:本期需要向信用卡还款的金额
调整金额:有可能是原先多还的款项, 在下期还款时会去掉这部分的金额
循环利息:当您偿还的金额等于或高于当期帐单的最低还款额,但低于本期应还金额时,剩余的延后还款的金额银行会计算相应的利息。
可用余额:信用额度-未还清的已出账金额-已使用未入账的累积金额
预借现金额度:是指持卡人使用信用卡通过ATM等自助终端提取现金的最高额度
还款状态:0--未还款,1--已还款
(5)放款时间信息loan_time.txt。共2个字段,用户id和放款时间。
用户id,放款时间
1,5914855887
银行发放贷款的时间
(6)顾客是否发生逾期行为的记录overdue.txt。共2个字段。样本标签为1,表示逾期30天以上;样本标签为0,表示逾期10天以内。注意:逾期10天~30天之内的用户,并不在此问题考虑的范围内。用于测试的用户,只提供id列表,文件名为testUsers.csv。
用户id,样本标签
1,1
此处可理解为1是逾期、0是未逾期
三、数据预处理
首先导入各个表,看下有无缺失以及用户id数。
导入数据,先看下银行流水记录表的信息情况:
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
import seaborn as sns
columns = ['user_id','time','loan_type','loan_amount','income']
df_bank_train = pd.read_table('E:\Datacastle\load\\train\\bank_detail_train.txt',names = columns,
sep=',' ) #银行流水记录表
df_bank_train.head()
看下缺失值情况:
df_bank_train.isnull().sum()
发现数据完整没有缺失。再看一下用户id的总数:
len(df_bank_train.user_id.unique())
有9294条不同用户在银行的流水记录。
同理导入其他5张表并看下基本信息:
columns1 = ['user_id','time','bank_id','last_bill_amount','last_repay_amount','credit_amount','cur_bill_bal','cur_bill_minrepay','cons_num','cur_bill_amount','adjust_amount','cyclic_accr','avail_bal','borrow_cash','repay_status']
columns2 = ['user_id','time','brows_beh','brows_num']
columns3 = ['user_id','time']
columns4 = ['user_id','overdue']
columns5 = ['user_id','sex','job','eduction','marriage','residence']
df_bill_train=pd.read_table('E:\Datacastle\load\\train\\bill_detail_train.txt',names = columns1,sep=',' ) #账单记录表
df_browse_train=pd.read_table('E:\Datacastle\load\\train\\browse_history_train.txt',names = columns2,sep=',' ) #浏览信息表
df_loan_train=pd.read_table('E:\Datacastle\load\\train\\loan_time_train.txt',names = columns3,sep=',' ) #放款时间表
df_overdue_train=pd.read_table('E:\Datacastle\load\\train\\overdue_train.txt',names = columns4,sep=',' ) #逾期记录表
df_user_train=pd.read_table('E:\Datacastle\load\\train\\user_info_train.txt',names = columns5,sep=',' ) #用户信息表
df_bill_train.head()
账单记录表:
浏览信息表:
df_browse_train.head()
放款时间表:
df_loan_train.head()
逾期记录表:
df_overdue_train.head()
用户信息表:
df_user_train.head()
看下各个表的用户id数
print(len(df_bank_train.user_id.unique()),
len(df_bill_train.user_id.unique()),
len(df_browse_train.user_id.unique()),
len(df_loan_train.user_id.unique()),
len(df_overdue_train.user_id.unique()),
len(df_user_train.user_id.unique()))
发现用户信息表,是否逾期表,放款时间表这三张表的id数目都是55,596,银行流水表为9,294,浏览信息表为47,330,信用卡账单表为53,174。
通过用户id数得到并非每个用户都有银行流水记录、信用卡账单等信息,所以这里我们取6个表共同用户的记录筛选后组成完整的表。
进一步筛选,这里加入时间范围
如图, 在本课题中是否逾期是在放款时间那一天之后开始计算的,放款时间之后三十天以上没有还款将视为逾期,我们要预测的测试集都是还没有放款的用户特征,所以训练数据这里我们也选取放款时间之前的特征,将存在时间戳的表与放款时间表进行交叉,只筛选此时间范围内的用户id。
通过上面各个表的信息情况,可以看到除了放款时间表外有时间字段的是银行流水记录表、账单记录表、浏览信息表。
(1)筛选符合条件的银行流水记录表的用户id:
df = pd.merge(left = df_bank_train, right= df_loan_train, how = 'left',on = 'user_id')
time_x表示流水记录时间,time_y表示放款时间,然后筛选出流水时间<放款时间的记录:
t = df[(df['time_x']<=df['time_y'])]
t.head()
筛选数用户id这一列并进行去重处理:
bank_user = t[['user_id']]
bank_user = bank_user.drop_duplicates(subset = 'user_id',keep='first')
bank_user.info()
可以看到在银行流水记录表中符合条件的还有9271个不同用户。
(2)筛选符合条件的账单记录表的用户id:
df1 = pd.merge(left = df_bill_train, right= df_loan_train, how = 'left',on = 'user_id')
t1 = df1[(df1['time_x']<=df1['time_y'])]
bill_user = t1[['user_id']]
bill_user = bill_user.drop_duplicates(subset = 'user_id',keep='first')
bill_user.info()
账单记录表中符合条件的还有46739个不同用户。
(3)筛选符合条件的浏览信息表的用户id:
df2 = pd.merge(left = df_browse_train, right= df_loan_train, how = 'left',on = 'user_id')
t2= df2[(df2['time_x']<=df2['time_y'])]
browse_user = t2[['user_id']]
browse_user= browse_user.drop_duplicates(subset = 'user_id',keep='first')
browse_user.info()
浏览信息表中符合条件的还有44945个不同用户。
筛选出这6张表共有的用户id(实际为上述的三张表共同的用户id即可)
user1 = pd.merge(left=bank_user,right=browse_user,how='inner',on = 'user_id')
user2 = pd.merge(left=user1,right=bill_user,how='inner',on = 'user_id')
user2.info()
得出5735个用户的记录是完整的。然后通过这5735个id筛选每张表的记录,并进行字段预处理。
(1)银行记录表
先筛选出id为‘user2’的记录,根据收入,支出以及工资这三个字段的交易类型进行筛选,并按id进行分组统计,每个分组中生成新的变量。
bank_select = pd.merge(left=df_bank_train,right = user2,how='inner',on='user_id')
b1= bank_select[(bank_select['loan_type'] == 0)].groupby(['user_id'], as_index=False) # 收入统计
b2 = bank_select[(bank_select['loan_type'] == 1)].groupby(['user_id'], as_index=False) # 支出统计
b3 = bank_select[(bank_select['income'] == 1)].groupby(['user_id'], as_index=False) # 工资收入统计
c1 = b1['loan_amount'].agg({'income_num': 'count', 'income_amount': 'sum'}) #三表共有用户在放款之前的收入次数和收入总额
c2 = b2['loan_amount'].agg({'expen_num': 'count', 'expen_amount': 'sum'}) #支出次数和支出额
c3 = b3['loan_amount'].agg({'wages_num': 'count', 'wages_amount': 'sum'}) #工资收入次数和工资收入额
将生成的字段分别与user2 id交叉组合到一起,由于是分组统计生成的字段,分组外用户id对应的数据是缺失的,这里缺失的数据全部填0
d1 = pd.merge(left=user2,right =c1,how = 'left',on='user_id')
d1 = d1.fillna(0)
d2 = pd.merge(left=user2,right =c2,how = 'left',on='user_id')
d2 = d2.fillna(0)
d3 = pd.merge(left=user2,right =c3,how = 'left',on='user_id')
d3 = d3.fillna(0)
将填充完整的六个字段组合到一起
bank_train = d1.merge(d2)
bank_train = bank_train.merge(d3)
bank_train.head()
(2)账单信息表
去掉了时间、银行id、还款状态这几个变量,按用户id分组后对每个字段均值化处理
bill_select = pd.merge(left=df_bill_train,right = user2,how='inner',on='user_id')
bill_select.drop(['time','bank_id','repay_status'], axis=1, inplace=True)
e1=bill_select.groupby(['user_id'], as_index=False)
f1= e1['last_bill_amount','last_repay_amount','credit_amount','cur_bill_bal','cur_bill_minrepay','cons_num','cur_bill_amount','adjust_amount','cyclic_accr','avail_bal','borrow_cash'].agg(np.mean)
bill_train = pd.merge(left=user2,right =f1,how = 'left',on='user_id')
bill_train.head()
(3)浏览记录表
去掉浏览子行为这个变量,对浏览行为数据变量作了按用户id做分组计数统计。
browse_select = pd.merge(left=df_browse_train,right = user2,how='inner',on='user_id')
g1= browse_select.groupby(['user_id'], as_index=False)
h1= g1['brows_beh'].agg({'brows_beh': 'count', })
browse_train = pd.merge(left=user2,right =h1,how = 'left',on='user_id')
browse_train.head()
对剩下的用户信息表和逾期记录表筛选
overdue_train = pd.merge(left=df_overdue_train,right = user2,how='inner',on='user_id')
user_train = pd.merge(left=df_user_train,right = user2,how='inner',on='user_id')
将筛选后的五个表进行合并
df_train = user_train.merge(bank_train)
df_train = df_train.merge(bill_train)
df_train = df_train.merge(browse_train)
df_train = df_train.merge(overdue_train)
df_train.head()
df_train.info()
可见现在还有25个字段
四、特征工程
先看下相关性
(1)银行流水记录特征:
internal_chars = ['income_num','income_amount','expen_num','expen_amount','wages_num','wages_amount']
corrmat = bank_train[internal_chars].corr()
plt.subplots(figsize=(8,8))
sns.heatmap(corrmat, square=True, linewidths=.5, annot=True)
可以看出收入/支出/工资的笔数和金额数目是线性关系,同时收入支出相关性同样较强。
这样,我们保留支出笔数、支出金额、工资笔数、工资金额这四个特征,并且利用这四个特征重新创建两个新的特征:每笔支出平均金额expen_avg=支出金额/支出笔数,每笔工资平均收入wages_avg=工资金额/工资笔数
(2)信用卡记录特征:
internal_chars = ['last_bill_amount','last_repay_amount','credit_amount','cur_bill_bal','cur_bill_minrepay'
,'cons_num','cur_bill_amount','adjust_amount','cyclic_accr','avail_bal','borrow_cash']
corrmat = df_train[internal_chars].corr()
f, ax = plt.subplots(figsize=(10, 8))
plt.xticks(rotation='0')
sns.heatmap(corrmat, square=False, linewidths=.5, annot=True)
可以看出上期账单金额last_bill_amount和上期还款金额last_repay_amount、本期账单余额cur_bill_bal 和本期账单最低还款额 cur_bill_minrepay、上期还款金额last_repay_amount和本期账单金额cur_bill_amount的相关性较强。
还款公式:
本期应还金额 = 上期账单金额-上期还款金额 + 本期账单金额 - 本期调整金额 + 循环利息
根据上期还款金额的多少将上式分为三种情况:
上期还款金额 (或者没还)<上期账单的最低还款额,就视为逾期,而且本期的还款要加上循环利息和上期未还款的那部分
上期最低还款额< 上期还款金额<上期账单金额,不视为逾期,但本期的还款要加上循环利息和上期未还款的那部分
上期还款金额 >上期账单金额,也就是说用户还多了,那么本期的还款会减去一个调整金额(多还的那部分),循环利息和上期未还的部分也就为0。
由此也解释了上期还款金额last_repay_amount和本期账单金额cur_bill_amount的相关性较强。
综合以上筛选下信用卡记录的特征:
针对上期账单金额last_bill_amount和上期还款金额last_repay_amount引入一个新特征:上期还款差额 =上期账单金额 - 上期还款金额;
信用额度,字面上感觉与还款是否逾期相关性比较大,所以先保留;
本期的账单余额与最低还款额具有高度共线性,决定只选用最低还款额;
消费笔数,目前来看与其他特征相关性均不高,所以先保留;
本期账单金额,这个根据还款公式可知会影响本期应还金额,所以先保留;
调整金额和循环利息是跟“上期的还款差额”有关的:
还款差额>0,需要计算循环利息,调整金额不计
还款差额<0,需要计算调整金额,循环利息不计
所以可以将还款差额进行“特征二值化”来代替这两个特征0表示有调整金额,1表示有循环利息,引入新特征last_diff_label;
可用余额:信用额度-未还清的已出账金额-已使用未入账的累积金额,应该跟信用额度相关性较大,但是在图中相关性与其他几个特征均存在较弱关系,所以暂时先保留;
预借现金额度,是指持卡人使用信用卡通过ATM等自助终端提取现金的最高额度,取现额度包含于信用额度之内,一般是信用额度的50%左右,通过图中也可以看出跟信用额度等相关性较强,所以可以不用这个特征,选择信用额度即可。
最后,筛选剩下的信用卡记录特征还有:last_repay_diff(上期还款差额),credit_amount,cur_bill_minrepay,cur_bill_amount、cons_num、avail_bal、last_diff_label(上期差额标签)这7个特征。
剩下用户信息和用户浏览行为的特征先保留观察。
我们把需要分析分特征罗列出来:
整理下特征:
df_train['expen_avg'] = df_train.apply(lambda x:x.expen_amount/x.expen_num,axis=1)
df_train['wages_avg'] = df_train.apply(lambda x:x.wages_amount/x.wages_num,axis=1)
df_train['last_repay_diff'] = df_train.apply(lambda x:x.last_bill_amount - x.last_repay_amount,axis=1)
df_select = df_train.loc[:,['user_id','sex','job','education','marriage','residence','expen_avg','wages_avg','last_repay_diff',
'credit_amount','cur_bill_amount','cur_bill_minrepay','cons_num','avail_bal','brows_beh','overdue']].fillna(0)
在添加新特征上期差额标记
df_select['last_diff_label'] = df_select.apply(lambda x: 0 if x.last_repay_diff < 0 else 1,axis=1)
df_select.info()
其中,用户属性的几个特征和上期差额标记为分类数据,其他9个位连续型数值变量。
这里用Filter法和Wrapper法对特征进行过滤,分别选取性能最好的5个特征进行交叉检验,再综合判断使用哪个模型。
特征工程多特征选择参考 https://blog.csdn.net/u012102306/article/details/52299427
https://www.jianshu.com/p/b3056d10a20f
https://www.jianshu.com/p/f5f54a39cb19
先使用Filter法的方差分析筛选:
x = df_select.drop(['user_id','overdue'],axis=1)
x = np.array(x)
y = df_select[['overdue']]
y = np.array(y)
from sklearn.feature_selection import SelectKBest, f_classif
selector = SelectKBest(f_classif, k=5)
a=selector.fit(x,y)
print(np.array(a.scores_),'\n',a.get_support())
筛选的结果为:'sex' ,'job', 'expen_avg', 'wages_avg', 'cur_bill_minrepay'为5个F值(组间方差/组内方差)最大的变量。
Wrapper法:
这里使用递归消除特征法,使用一个基模型来进行多轮训练,每轮训练后,消除若干权值系数的特征,再基于新的特征集进行下一轮训练。使用feature_selection库的RFE类来选择特征
from sklearn.tree import DecisionTreeClassifier
from sklearn.feature_selection import RFE
model1 = DecisionTreeClassifier()
rfe = RFE(model1,5)
rfe = rfe.fit(x,y)
print(rfe.support_)
print(rfe.ranking_)
筛选的结果为:
'expen_avg', 'last_repay_diff', 'credit_amount', 'cur_bill_minrepay', 'brows_beh'
可以看出两个方法,除了特征expen_avg、cur_bill_minrepay,其他选择的特征都不同,且Wrapper法选择的都是连续型变量。
交叉验证:
from sklearn.model_selection import cross_val_score
x_test1 = df_select[['sex','job','expen_avg','wages_avg','cur_bill_minrepay']]
x_test1 = np.array(x_test1)
y_test1 = df_select[['overdue']]
y_test1 = np.array(y_test1)
m1 = DecisionTreeClassifier()
m1.fit(x_test1,y_test1)
scores = -cross_val_score(m1, x_test1, y_test1, cv=5, scoring= 'neg_mean_absolute_error')
print(np.mean(scores))
filter法结果为:0.23976097225177595
from sklearn.model_selection import cross_val_score
x_test2 = df_select[['expen_avg','last_repay_diff','credit_amount','cur_bill_minrepay','brows_beh']]
x_test2 = np.array(x_test2)
y_test2 = df_select[['overdue']]
y_test2 = np.array(y_test2)
m2 = DecisionTreeClassifier()
m2.fit(x_test2,y_test2)
scores = -cross_val_score(m2, x_test2, y_test2, cv=5, scoring= 'neg_mean_absolute_error')
print(np.mean(scores))
Wrapper法结果为:0.24132633413428106
交叉检验的结果表明还是用Filter法好一点,接下来就是建模了,既然筛选的5个特征既有分类型,又有连续型,用决策树是最合适了。
五、模型建立
1.拆分训练集和测试集数据:比例为4:1
from sklearn.model_selection import train_test_split
x_train, x_test, y_train, y_test = train_test_split(x, y, test_size = 0.2, random_state = 0)
- 设定决策树参数,进行建模
这里采用的是决策树里的CART法,之所以不用ID3和C4.5,是因为这两个方法不能处理连续型数据,必须转换成离散型。
from sklearn import tree
clf = tree.DecisionTreeClassifier(criterion='gini',
splitter='best',
max_depth=3,
min_samples_split=10,
min_samples_leaf=5
)
clf = clf.fit(x_train,y_train)
y_pred = clf.predict(x_test)
y_pred = y_pred[:, np.newaxis]
y_pred
3.模型结果评价
from sklearn.metrics import classification_report
print(classification_report(y_test, y_pred))
精确率0.82,召回率0.86,f1_score为0.81
这里把特征变量缩减一些,改成Filter法得到的那5个变量
from sklearn.model_selection import train_test_split
x_train, x_test, y_train, y_test = train_test_split(x_test1, y, test_size = 0.2, random_state = 0)
from sklearn import tree
clf = tree.DecisionTreeClassifier(criterion='gini',
splitter='best',
max_depth=3,
min_samples_split=10,
min_samples_leaf=5
)
clf = clf.fit(x_train,y_train)
y_pred = clf.predict(x_test)
y_pred = y_pred[:, np.newaxis]
from sklearn.metrics import classification_report
print(classification_report(y_test, y_pred))
精确率0.84,召回率0.86,f1_score为0.81,这样的话精确度有提高。