本文主要根据自某信贷平台的贷款记录进行预测,以金融风控中的个人信贷为背景,要求根据贷款申请人的数据信息预测其是否有违约的可能,以此判断是否通过此项贷款,是一个典型的多分类的问题。本文完整的陈述了从数据探索到特征工程到构建模型的全过程。
一、背景
总数据量超过120w,包含47列变量信息,其中15列为匿名变量。其中80万条作为训练集,20万条作为测试集A,20万条作为测试集B,同时会对employmentTitle、purpose、postCode和title等信息进行脱敏。
二、数据描述
train.csv
- id 为贷款清单分配的唯一信用证标识
- loanAmnt 贷款金额
- term 贷款期限(year)
- interestRate 贷款利率
- installment 分期付款金额
- grade 贷款等级
- subGrade 贷款等级之子级
- employmentTitle 就业职称
- employmentLength 就业年限(年)
- homeOwnership 借款人在登记时提供的房屋所有权状况
- annualIncome 年收入
- verificationStatus 验证状态
- issueDate 贷款发放的月份
- purpose 借款人在贷款申请时的贷款用途类别
- postCode 借款人在贷款申请中提供的邮政编码的前3位数字
- regionCode 地区编码
- dti 债务收入比
- delinquency_2years 借款人过去2年信用档案中逾期30天以上的违约事件数
- ficoRangeLow 借款人在贷款发放时的fico所属的下限范围
- ficoRangeHigh 借款人在贷款发放时的fico所属的上限范围
- openAcc 借款人信用档案中未结信用额度的数量
- pubRec 贬损公共记录的数量
- pubRecBankruptcies 公开记录清除的数量
- revolBal 信贷周转余额合计
- revolUtil 循环额度利用率,或借款人使用的相对于所有可用循环信贷的信贷金额
- totalAcc 借款人信用档案中当前的信用额度总数
- initialListStatus 贷款的初始列表状态
- applicationType 表明贷款是个人申请还是与两个共同借款人的联合申请
- earliesCreditLine 借款人最早报告的信用额度开立的月份
- title 借款人提供的贷款名称
- policyCode 公开可用的策略代码=1新产品不公开可用的策略代码=2
- n系列匿名特征 匿名特征n0-n14,为一些贷款人行为计数特征的处理
三、数据探索
3.1 导入库以及数据读取
import pandas as pd
pd.set_option('display.max_rows',None)
pd.set_option('display.max_columns',None)
import numpy as np
import matplotlib.pyplot as plt
import seaborn as sns
import datetime
from tqdm import tqdm
from sklearn.preprocessing import LabelEncoder
from sklearn.feature_selection import SelectKBest
from sklearn.feature_selection import chi2
from sklearn.preprocessing import MinMaxScaler
import xgboost as xgb
import lightgbm as lgb
from catboost import CatBoostRegressor
import warnings
from sklearn.model_selection import StratifiedKFold, KFold
from sklearn.metrics import accuracy_score, f1_score, roc_auc_score, log_loss
warnings.filterwarnings('ignore')
#数据导入
train = pd.read_csv('D:/myP/financial_risk/train.csv')
testA = pd.read_csv('D:/myP/financial_risk/testA.csv')
print('Train data shape:',train.shape)
print('TestA data shape:',testA.shape)
3.2 查看数据包含的特征
print('the columns name of training dataset:\n',train.columns)
print('the columns name of test dataset:\n',testA.columns)
分析:
- train文件比testA文件多了特征“isDefault”;
- isDefault是需要预测的目标变量;
- 数据探索时着重分析train文件,testA文件类似
3.3 查看数据信息
train.info()
分析:
- trian.csv文件包含80万行数据,47个特征;
- 表中展示了每个特征的数据类型和非空数值;
- n0 ~ n14都有较多的空值,约4万个。
3.4 查看数据集各个特征的一些基本统计量
train.describe()
3.5 具体的查看缺失特征及缺失率
# 缺失值可视化
missing = train.isnull().sum()/len(train)
missing = missing[missing > 0]
missing.sort_values(inplace=True) #排个序
missing.plot.bar()
- 纵向了解哪些列存在 “nan”, 并可以把nan的个数打印,主要的目的在于查看某一列nan存在的个数是否真的很大,如果nan存在的过多,说明这一列对label的影响几乎不起作用了,可以考虑删掉。如果缺失值很小一般可以选择填充。
- 另外可以横向比较,如果在数据集中,某些样本数据的大部分列都是缺失的且样本足够的情况下可以考虑删除。
查看训练集测试集中特征属性只有一值的特征
#找出训练数据集(train)和测试数据集(tset)中所有特征值唯一的特征
one_value_fea = [col for col in train.columns if train[col].nunique() <= 1]
one_value_fea_test = [col for col in testA.columns if testA[col].nunique() <= 1]
print(one_value_fea, one_value_fea_test)
分析:
- 47列数据中有22列都有缺值;
- ‘policyCode’具有一个唯一值(或全部缺失)
3.6 查看特征的数值类型有哪些,对象类型有哪些
- 特征一般都是由类别型特征和数值型特征组成,而数值型特征又分为连续型和离散型。
- 类别型特征有时具有非数值关系,有时也具有数值关系。比如‘grade’中的等级A,B,C等,是否只是单纯的分类,还是A优于其他要结合业务判断。
- 数值型特征本是可以直接入模的,但往往风控人员要对其做分箱,转化为WOE编码进而做标准评分卡等操作。从模型效果上来看,特征分箱主要是为了降低变量的复杂性,减少变量噪音对模型的影响,提高自变量和因变量的相关度。从而使模型更加稳定。
#判断并打印数值型特征(除object外的所有特征)
numerical_fea = list(train.select_dtypes(exclude=['object']).columns)
print(numerical_fea)
#打印属性为object的特征值
category_fea = list(filter(lambda x: x not in numerical_fea, list(train.columns)))
print(category_fea)
3.7 数值型变量分析
#写一个函数来区分连续性变量和离散型变量(依据变量的个数)
def get_numerical_serial_fea(data,feas):
numerical_serial_fea = []
numerical_noserial_fea = []
for fea in feas:
temp = data[fea].nunique()
if temp <= 10:
numerical_noserial_fea.append(fea)
continue
numerical_serial_fea.append(fea)
return numerical_serial_fea,numerical_noserial_fea
#对测试集进行处理
numerical_serial_fea,numerical_noserial_fea = get_numerical_serial_fea(train,numerical_fea)
#打印连续型变量及其个数
print(numerical_serial_fea,len(numerical_serial_fea))
#打印离散型变量
print(numerical_noserial_fea, len(numerical_noserial_fea))
分析:一共有 9个离散型变量和33个连续型变量
3.7.1 数值离散型变量分析(9个)
#离散型变量
for fea in numerical_noserial_fea:
a = train[fea].value_counts()
print([fea],'的取值与取值数量为:\n',a)
3.7.2 连续型变量分析(33个)
f = pd.melt(train, value_vars=numerical_serial_fea)
g = sns.FacetGrid(f, col="variable", col_wrap=2, sharex=False, sharey=False)
g = g.map(sns.displot, "value")
- 查看某一个数值型变量的分布,查看变量是否符合正态分布,如果不符合正太分布的变量可以log化后再观察下是否符合正态分布。
- 如果想统一处理一批数据变标准化 必须把这些之前已经正态化的数据提出
- 正态化的原因:一些情况下正态非正态可以让模型更快的收敛,一些模型要求数据正态(eg. GMM、KNN),保证数据不要过偏态即可,过于偏态可能会影响模型预测结果。
#绘制交易金额值分布
plt.figure(figsize=(16,12))
plt.suptitle('Transaction Values Distribution', fontsize=22)
plt.subplot(221) ##2代表行,2代表列,所以一共有4个图,1代表此时绘制第一个图。
sub_plot_1 = sns.distplot(train['loanAmnt'])
sub_plot_1.set_title("loanAmnt Distribuition", fontsize=18)
sub_plot_1.set_xlabel("")
sub_plot_1.set_ylabel("Probability", fontsize=15)
plt.subplot(222)
sub_plot_2 = sns.distplot(np.log(train['loanAmnt']))
sub_plot_2.set_title("loanAmnt (Log) Distribuition", fontsize=18)
sub_plot_2.set_xlabel("")
sub_plot_2.set_ylabel("Probability", fontsize=15)
Text(0, 0.5, 'Probability')
3.7.3 非数值类别型变量分析(5个)
for fea in category_fea:
a = train[fea].value_counts()
print([fea],'的取值与取值数量为:\n',a)
分析:
- 上面用value_counts()等函数看了特征属性的分布,但是图表是概括原始信息最便捷的方式。
- 同一份数据集,在不同的尺度刻画上显示出来的图形反映的规律是不一样的。python将数据转化成图表,但结论是否正确需要由你保证。
3.8 变量分布可视化
3.8.1 单一变量分布可视化
plt.figure(figsize=(8, 8))
sns.barplot(train["employmentLength"].value_counts(dropna=False)[:20],
train["employmentLength"].value_counts(dropna=False).keys()[:20])
plt.show()
3.8.2 根据y值不同可视化x某个特征的分布
- 首先查看类别型变量在不同目标值'isDefault'上的分布,演示了grade和employmentLength
train_loan_fr = train.loc[train['isDefault'] == 1]
train_loan_nofr = train.loc[train['isDefault'] == 0]
fig, ((ax1, ax2), (ax3, ax4)) = plt.subplots(2, 2, figsize=(15, 8))
train_loan_fr.groupby('grade')['grade'].count().plot(kind='barh', ax=ax1, title='Count of grade fraud')
train_loan_nofr.groupby('grade')['grade'].count().plot(kind='barh', ax=ax2, title='Count of grade non-fraud')
train_loan_fr.groupby('employmentLength')['employmentLength'].count().plot(kind='barh', ax=ax3, title='Count of employmentLength fraud')
train_loan_nofr.groupby('employmentLength')['employmentLength'].count().plot(kind='barh', ax=ax4, title='Count of employmentLength non-fraud')
plt.show()
- 其次查看连续型变量在不同目标值'isDefault'上的log分布
fig, ((ax1, ax2)) = plt.subplots(1, 2, figsize=(15, 6))
#对训练数据定位目标值为1,再定位特征值为"loanAmnt",再应用log函数,再画图
train.loc[train['isDefault'] == 1] \
['loanAmnt'].apply(np.log) \
.plot(kind='hist',
bins=100,
title='Log Loan Amt - Fraud',
color='r',
xlim=(-3, 10),
ax= ax1)
train.loc[train['isDefault'] == 0] \
['loanAmnt'].apply(np.log) \
.plot(kind='hist',
bins=100,
title='Log Loan Amt - Not Fraud',
color='b',
xlim=(-3, 10),
ax=ax2)
total = len(train) #数据行数80万
#对特征值'loanAmnt'按目标值'isDefault'分组累加2次
total_amt = train['loanAmnt'].sum()
plt.figure(figsize=(12,5))
plt.subplot(121)
#计数图,展示目标值"isDefault"的类别分布
plot_tr = sns.countplot(x='isDefault',data=train)
plot_tr.set_title("Fraud Loan Distribution \n 0: good user | 1: bad user", fontsize=14)
plot_tr.set_xlabel("Is fraud by count", fontsize=16)
plot_tr.set_ylabel('Count', fontsize=16)
for p in plot_tr.patches:
height = p.get_height()
plot_tr.text(p.get_x()+p.get_width()/2.,
height + 3,
'{:1.2f}%'.format(height/total*100),
ha="center", fontsize=15)
percent_amt = (train.groupby(['isDefault'])['loanAmnt'].sum())
percent_amt = percent_amt.reset_index()
plt.subplot(122)
plot_tr_2 = sns.barplot(x='isDefault', y='loanAmnt', dodge=True, data=percent_amt)
plot_tr_2.set_title("Total Amount in loanAmnt \n 0: good user | 1: bad user", fontsize=14)
plot_tr_2.set_xlabel("Is fraud by percent", fontsize=16)
plot_tr_2.set_ylabel('Total Loan Amount Scalar', fontsize=16)
for p in plot_tr_2.patches:
height = p.get_height()
plot_tr_2.text(p.get_x()+p.get_width()/2.,
height + 3,
'{:1.2f}%'.format(height/total_amt * 100),
ha="center", fontsize=15)
3.9 时间格式数据处理及查看
将"issueDate"转化为"issueDateDT"("issueDate"中最早的一天为2007-06-01,所以"startdate"为2007-06-01)
#train的特征值"issueDate"转化成时间格式,转化为离开始的天数
train['issueDate'] = pd.to_datetime(train['issueDate'],format='%Y-%m-%d')
startdate = datetime.datetime.strptime('2007-06-01', '%Y-%m-%d')
train['issueDateDT'] = train['issueDate'].apply(lambda x: x-startdate).dt.days
#testA的"issueDate"转化成时间格式,转化为离开始的天数
testA['issueDate'] = pd.to_datetime(testA['issueDate'],format='%Y-%m-%d')
testA['issueDateDT'] = testA['issueDate'].apply(lambda x: x-startdate).dt.days
#作图
plt.hist(train['issueDateDT'], label='train');
plt.hist(testA['issueDateDT'], label='test');
plt.legend();
plt.title('Distribution of issueDateDT dates');
#train 和 test issueDateDT 日期有重叠 所以使用基于时间的分割进行验证是不明智的
3.10 掌握透视图可以让我们更好的了解数据
index,columns和values分别为行,列和数据
pivot = pd.pivot_table(train, index=['grade'], columns=['issueDateDT'], values=['loanAmnt'], aggfunc=np.sum)
pivot
3.10.1 用pandas_profiling生成数据报告
import pandas_profiling
pfr = pandas_profiling.ProfileReport(train)
pfr.to_file("./example.html")
四、特征工程
4.1 目录
- 数据预处理:
- 缺失值的填充
- 时间格式处理
- 对象类型特征转换到数值
- 异常值处理:
- 基于3segama原则
- 基于箱型图
- 数据分箱
- 固定宽度分箱
- 分位数分箱
- 离散数值型数据分箱
- 连续数值型数据分箱
- 卡方分箱
- 特征交互
- 特征和特征之间组合
- 特征和特征之间衍生
- 其他特征衍生的尝试
- 特征编码
- one-hot编码
- label-encode编码
- 特征选择
- 1 Filter
- 2 Wrapper (RFE)
- 3 Embedded
4.2 缺失值填充
- 把所有缺失值替换为指定的值0
- 向用缺失值上面的值替换缺失值
- 纵向用缺失值下面的值替换缺失值,且设置最多只填充两个连续的缺失值
4.2.1 查看缺失值的情况:
#将目标值从数值型变量中去除
numerical_fea.remove('isDefault')
train.isnull().sum()
#按照train训练数据的中位数填充数值型特征的缺失值
train[numerical_fea] = train[numerical_fea].fillna(train[numerical_fea].median())
testA[numerical_fea] = testA[numerical_fea].fillna(train[numerical_fea].median())
4.2.2 时间格式处理
['issueDate']为时间格式特征,需要先转化为时间格式。
#转化成时间格式
for data in [train, testA]:
data['issueDate'] = pd.to_datetime(data['issueDate'],format='%Y-%m-%d')
startdate = datetime.datetime.strptime('2007-06-01', '%Y-%m-%d')
#构造时间特征
data['issueDateDT'] = data['issueDate'].apply(lambda x: x-startdate).dt.days
4.2.2.1 对象类型特征‘employmentLength’转换到数值
train['employmentLength'].value_counts(dropna=False).sort_index()
#定义一个函数:非零值取空格前的数字,且设置为int8格式
def