一、前言

1.1 赛题背景

赛题以金融风控中的个人信贷为背景,要求选手根据贷款申请人的数据信息预测其是否有违约的可能,以此判断是否通过此项贷款,这是一个典型的分类问题

任务:预测用户贷款是否违约

比赛地址:https://tianchi.aliyun.com/competition/entrance/531830/introduction

1.2 赛题数据

数据来自某信贷平台的贷款记录,总数据量超过120w,包含47列变量信息,其中15列为匿名变量。

为了保证比赛的公平性,将会从中抽取80万条作为训练集,20万条作为测试集A,20万条作为测试集B,同时会对employmentTitle、purpose、postCode和title等信息进行脱敏。

数据集包含三个下载文件

  • train.csv:训练集
  • test.csv:测试集
  • sample_submit.csv:提交文件样式

train.csv数据
testA.csv数据

字段表

Field Description
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,为一些贷款人行为计数特征的处理

1.3 评价指标

提交结果为每个测试样本是1的概率,也就是y为1的概率。

评价方法为AUC评估模型效果(越大越好)。

注:AUC(Area Under Curve)被定义为 ROC曲线下与坐标轴围成的面积。

详细参见:
「机器学习」分类算法常见的评估指标
机器学习:评估指标

其次,除了要求的评价指标外,对于二分类问题其评价指标还有精确率、召回率、ROC、F值等

1.4 赛题整体流程

分析主要步骤如下

二、探索性的数据分析EDA

数据探索性分析是对数据进行初步分析,了解数据特征,观察数据类型,分析数据分布等等,为后续特征工程,以及建模分析都特别重要

例如

  • 分析数据中每个字段的含义、分布、缺失情况;
    字段表示什么含义、字段的类型是什么、字段的取值空间是什么、字段每个取值表示什么意义。
    字段整体的分布,分析字段在训练集/测试集中的分布情况。
    字段缺失值的分布比例,字段在训练集/测试集的缺失情况。
  • 分析数据中每个字段的与赛题标签的关系;
  • 分析数据字段两两之间,或者主者之间的关系;

引用图片:https://zhuanlan.zhihu.com/p/259788410

首先导入必要模块

import warnings
warnings.filterwarnings("ignore")import numpy as np
import pandas as pd
import seaborn as sns
import matplotlib.pyplot as pltimport statsmodels.formula.api as smffrom sklearn.preprocessing import LabelEncoder
from sklearn.feature_selection import SelectKBest
from sklearn.model_selection import train_test_split
from sklearn.model_selection import StratifiedKFold, KFold
from sklearn.feature_selection import SelectKBest
from sklearn.feature_selection import chi2
from sklearn.preprocessing import MinMaxScalerimport xgboost as xgb
import lightgbm as lgb
from catboost import CatBoostRegressor# 评价指标
from sklearn.metrics import accuracy_score, f1_score, roc_auc_score, log_lossplt.rcParams["font.sans-serif"]=["SimHei"]
plt.rcParams["axes.unicode_minus"]=False

使用pandas读入数据,包括训练集与测试集

导入数据集(数据集过大可以进行瘦身处理)

train = pd.read_csv('train.csv')
test = pd.read_csv('testA.csv')

查看部分数据

train.head()
id loanAmnt term interestRate installment grade subGrade employmentTitle employmentLength homeOwnership n5 n6 n7 n8 n9 n10 n11 n12 n13 n14
0 0 35000.0 5 19.52 917.97 E E2 320.0 2 years 2 9.0 8.0 4.0 12.0 2.0 7.0 0.0 0.0 0.0 2.0
1 1 18000.0 5 18.49 461.90 D D2 219843.0 5 years 0 NaN NaN NaN NaN NaN 13.0 NaN NaN NaN NaN
2 2 12000.0 5 16.99 298.17 D D3 31698.0 8 years 0 0.0 21.0 4.0 5.0 3.0 11.0 0.0 0.0 0.0 4.0
3 3 11000.0 3 7.26 340.96 A A4 46854.0 10+ years 1 16.0 4.0 7.0 21.0 6.0 9.0 0.0 0.0 0.0 1.0
4 4 3000.0 3 12.99 101.07 C C2 54.0 NaN 1 4.0 9.0 10.0 15.0 7.0 12.0 0.0 0.0 0.0 4.0

5 rows × 47 columns

2.1 总体分布

前面提到,整个数据包括80万条训练集,20万条测试集A,20万条测试集B

另外

训练集中有47列,其中包括46个特征列,1个标签列
测试集中只有46个特征列

#  样本个数和特征维度
train.shape
# (800000, 47)
test.shape
# (200000, 46)

查看特征名

train.columns
'''
Index(['id', 'loanAmnt', 'term', 'interestRate', 'installment', 'grade','subGrade', 'employmentTitle', 'employmentLength', 'homeOwnership','annualIncome', 'verificationStatus', 'issueDate', 'isDefault','purpose', 'postCode', 'regionCode', 'dti', 'delinquency_2years','ficoRangeLow', 'ficoRangeHigh', 'openAcc', 'pubRec','pubRecBankruptcies', 'revolBal', 'revolUtil', 'totalAcc','initialListStatus', 'applicationType', 'earliesCreditLine', 'title','policyCode', 'n0', 'n1', 'n2', 'n3', 'n4', 'n5', 'n6', 'n7', 'n8','n9', 'n10', 'n11', 'n12', 'n13', 'n14'],dtype='object')
'''

接下来查看数据集的一些基本信息(缺失情况、类型…)

train.info()
'''
<class 'pandas.core.frame.DataFrame'>
RangeIndex: 800000 entries, 0 to 799999
Data columns (total 47 columns):
id                    800000 non-null int64
loanAmnt              800000 non-null float64
term                  800000 non-null int64
interestRate          800000 non-null float64
installment           800000 non-null float64
grade                 800000 non-null object
subGrade              800000 non-null object
employmentTitle       799999 non-null float64
employmentLength      753201 non-null object
homeOwnership         800000 non-null int64
annualIncome          800000 non-null float64
verificationStatus    800000 non-null int64
issueDate             800000 non-null object
isDefault             800000 non-null int64
purpose               800000 non-null int64
postCode              799999 non-null float64
regionCode            800000 non-null int64
dti                   799761 non-null float64
delinquency_2years    800000 non-null float64
ficoRangeLow          800000 non-null float64
ficoRangeHigh         800000 non-null float64
openAcc               800000 non-null float64
pubRec                800000 non-null float64
pubRecBankruptcies    799595 non-null float64
revolBal              800000 non-null float64
revolUtil             799469 non-null float64
totalAcc              800000 non-null float64
initialListStatus     800000 non-null int64
applicationType       800000 non-null int64
earliesCreditLine     800000 non-null object
title                 799999 non-null float64
policyCode            800000 non-null float64
n0                    759730 non-null float64
n1                    759730 non-null float64
n2                    759730 non-null float64
n3                    759730 non-null float64
n4                    766761 non-null float64
n5                    759730 non-null float64
n6                    759730 non-null float64
n7                    759730 non-null float64
n8                    759729 non-null float64
n9                    759730 non-null float64
n10                   766761 non-null float64
n11                   730248 non-null float64
n12                   759730 non-null float64
n13                   759730 non-null float64
n14                   759730 non-null float64
dtypes: float64(33), int64(9), object(5)
memory usage: 286.9+ MB
'''

可以看到,许多特征存在缺失,特征的类型有dtypes: float64(33), int64(9), object(5)

对于缺失值的处理以及类型转换将在特征工程中说明


接下来查看一下数据的描述性分析

描述性统计加深对数据分布、数据结构等的理解看一下数据特征之间的两两关联关系数据中空值的个数、0的个数、正值或负值的个数,以及均值、方差、最小值、最大值、偏度、峰度等。
train.describe()
# train.describe().T

大致了解一下数据的分布、结构,简单的看一下特征值有没有什么异常

id loanAmnt term interestRate installment employmentTitle homeOwnership annualIncome verificationStatus isDefault n5 n6 n7 n8 n9 n10 n11 n12 n13 n14
count 800000.000000 800000.000000 800000.000000 800000.000000 800000.000000 799999.000000 800000.000000 8.000000e+05 800000.000000 800000.000000 759730.000000 759730.000000 759730.000000 759729.000000 759730.000000 766761.000000 730248.000000 759730.000000 759730.000000 759730.000000
mean 399999.500000 14416.818875 3.482745 13.238391 437.947723 72005.351714 0.614213 7.613391e+04 1.009683 0.199513 8.107937 8.575994 8.282953 14.622488 5.592345 11.643896 0.000815 0.003384 0.089366 2.178606
std 230940.252015 8716.086178 0.855832 4.765757 261.460393 106585.640204 0.675749 6.894751e+04 0.782716 0.399634 4.799210 7.400536 4.561689 8.124610 3.216184 5.484104 0.030075 0.062041 0.509069 1.844377
min 0.000000 500.000000 3.000000 5.310000 15.690000 0.000000 0.000000 0.000000e+00 0.000000 0.000000 0.000000 0.000000 0.000000 1.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
25% 199999.750000 8000.000000 3.000000 9.750000 248.450000 427.000000 0.000000 4.560000e+04 0.000000 0.000000 5.000000 4.000000 5.000000 9.000000 3.000000 8.000000 0.000000 0.000000 0.000000 1.000000
50% 399999.500000 12000.000000 3.000000 12.740000 375.135000 7755.000000 1.000000 6.500000e+04 1.000000 0.000000 7.000000 7.000000 7.000000 13.000000 5.000000 11.000000 0.000000 0.000000 0.000000 2.000000
75% 599999.250000 20000.000000 3.000000 15.990000 580.710000 117663.500000 1.000000 9.000000e+04 2.000000 0.000000 11.000000 11.000000 10.000000 19.000000 7.000000 14.000000 0.000000 0.000000 0.000000 3.000000
max 799999.000000 40000.000000 5.000000 30.990000 1715.420000 378351.000000 5.000000 1.099920e+07 2.000000 1.000000 70.000000 132.000000 79.000000 128.000000 45.000000 82.000000 4.000000 4.000000 39.000000 30.000000

2.2 数据类型分析

2.2.1 数值类型(连续变量、离散型变量和单值变量)

这里引用文章观点:https://blog.csdn.net/qq_43401035/article/details/108648912

数值类型

# 数值类型
numerical_feature = list(train.select_dtypes(exclude=['object']).columns)
numerical_feature
['id', 'loanAmnt', 'term', 'interestRate', 'installment', 'employmentTitle','homeOwnership', 'annualIncome', 'verificationStatus', 'isDefault', 'purpose','postCode', 'regionCode', 'dti', 'delinquency_2years', 'ficoRangeLow','ficoRangeHigh', 'openAcc', 'pubRec', 'pubRecBankruptcies', 'revolBal','revolUtil', 'totalAcc', 'initialListStatus', 'applicationType','title', 'policyCode', 'n0', 'n1', 'n2', 'n3', 'n4', 'n5', 'n6', 'n7', 'n8','n9', 'n10', 'n11', 'n12', 'n13', 'n14']

一共有42个数值型变量(dtypes: float64(33), int64(9), object(5)

len(numerical_feature)  ## 42

由于数值类型又可以分为连续变量、离散型变量和单值变量

因此接下来把数值型中的连续型变量和离散型变量区分开来:

# 连续型变量
serial_feature = []
# 离散型变量
discrete_feature = []
# 单值变量
unique_feature = []for fea in numerical_feature:temp = train[fea].nunique()# 返回的是唯一值的个数if temp == 1:unique_feature.append(fea)# 自定义变量的值的取值个数小于10就为离散型变量    elif temp <= 10:discrete_feature.append(fea)else:serial_feature.append(fea)

(1)连续型变量

serial_feature
'''
['id', 'loanAmnt', 'interestRate', 'installment', 'employmentTitle','annualIncome', 'purpose', 'postCode', 'regionCode', 'dti','delinquency_2years', 'ficoRangeLow', 'ficoRangeHigh', 'openAcc','pubRec', 'pubRecBankruptcies', 'revolBal', 'revolUtil', 'totalAcc','title', 'n0', 'n1', 'n2', 'n3', 'n4', 'n5', 'n6', 'n7', 'n8','n9', 'n10', 'n13', 'n14']
'''

对于连续型变量

  • 查看某一个数值型变量的分布,查看变量是否符合正态分布,如果不符合正太分布的变量可以log化后再观察下是否符合正态分布。

  • 正态化的原因:一些情况下正态非正态可以让模型更快的收敛,一些模型要求数据正态(eg. GMM、KNN),保证数据不要过偏态即可,过于偏态可能会影响模型预测结果。

可视化呈现

#每个数字特征得分布可视化
f = pd.melt(train, value_vars=serial_feature)
g = sns.FacetGrid(f, col="variable",  col_wrap=3, sharex=False, sharey=False)
g = g.map(sns.distplot, "value")

可以单独查看一下贷款金额 loanAmnt 的分布情况

plt.figure(1 , figsize = (8 , 5))
sns.distplot(train.loanAmnt,bins=40)
plt.xlabel('loanAmnt')


对于违约与不违约两类样本的贷款金额分布情况

sns.kdeplot(train.loanAmnt[label[label==1].index], label='1', shade=True)#违约
sns.kdeplot(train.loanAmnt[label[label==0].index], label='0', shade=True)#没有违约
plt.xlabel('loanAmnt')
plt.ylabel('Density');


单独查看一下年收入的分布情况

plt.figure(1 , figsize = (8 , 5))
sns.distplot(train['annualIncome'])
plt.xlabel('annualIncome')

(2)离散型变量

discrete_feature
'''
['term', 'homeOwnership', 'verificationStatus', 'isDefault','initialListStatus', 'applicationType', 'n11', 'n12']
'''

离散型变量的类型数情况

for f in discrete_feature:print(f, '类型数:', train[f].nunique())
'''
term 类型数: 2
homeOwnership 类型数: 6
verificationStatus 类型数: 3
isDefault 类型数: 2
initialListStatus 类型数: 2
applicationType 类型数: 2
n11 类型数: 5
n12 类型数: 5
'''

离散型特征可视化呈现

df_ = train[discrete_feature]sns.set_style("whitegrid") # 使用whitegrid主题
fig,axes=plt.subplots(nrows=4,ncols=2,figsize=(8,10))
for i, item in enumerate(df_):plt.subplot(4,2,(i+1))#ax=df[item].value_counts().plot(kind = 'bar')ax=sns.countplot(item,data = df_,palette="Pastel1")plt.xlabel(str(item),fontsize=14)   plt.ylabel('Count',fontsize=14)plt.xticks(fontsize=13)plt.yticks(fontsize=13)#plt.title("Churn by "+ str(item))i=i+1plt.tight_layout()
plt.show()

查看一下每个特征的分布情况

(3)单值变量

单值变量表示该特征只有一种类别,对于数值全部都一样的特征,可以考虑直接删除

unique_feature
'''
['policyCode']
'''

2.2.2 分类型特征

# 分类型特征
category_feature = list(filter(lambda x: x not in numerical_feature,list(train.columns)))
category_feature['grade', 'subGrade', 'employmentLength', 'issueDate', 'earliesCreditLine']

对应info结果中的 (dtypes: float64(33), int64(9), object(5)

这里 "grade"为贷款等级,"subGrade"为贷款等级之子级,"employmentLength"为就业年限,"issueDate"为贷款发放的月份,"earliesCreditLine"为借款人最早报告的信用额度开立的月份,共有5个分类型特征。

查看一下这些分类型特征的结构,后面需要对其进行特征编码

train[category_feature]
'''grade subGrade employmentLength   issueDate earliesCreditLine
0          E       E2          2 years  2014-07-01          Aug-2001
1          D       D2          5 years  2012-08-01          May-2002
2          D       D3          8 years  2015-10-01          May-2006
3          A       A4        10+ years  2015-08-01          May-1999
4          C       C2              NaN  2016-03-01          Aug-1977...      ...              ...         ...               ...
799995     C       C4          7 years  2016-07-01          Aug-2011
799996     A       A4        10+ years  2013-04-01          May-1989
799997     C       C3        10+ years  2015-10-01          Jul-2002
799998     A       A4        10+ years  2015-02-01          Jan-1994
799999     B       B3          5 years  2018-08-01          Feb-2002[800000 rows x 5 columns]
'''

分类型特征可视化呈现

df_category = train[['grade', 'subGrade']]sns.set_style("whitegrid") # 使用whitegrid主题
color = sns.color_palette()
fig,axes=plt.subplots(nrows=2,ncols=1,figsize=(10,10))
for i, item in enumerate(df_category):plt.subplot(2,1,(i+1))#ax=df[item].value_counts().plot(kind = 'bar')ax=sns.countplot(item,data = df_category)plt.xlabel(str(item),fontsize=14)   plt.ylabel('Count',fontsize=14)plt.xticks(fontsize=13)plt.yticks(fontsize=13)#plt.title("Churn by "+ str(item))i=i+1plt.tight_layout()
plt.show()


可以看出对于grade特征中A\B\C等级的贷款占比比较大

employmentLength就业年限可视化呈现

plt.figure(1 , figsize = (10 , 8))
sns.barplot(train["employmentLength"].value_counts(dropna=False),train["employmentLength"].value_counts(dropna=False).keys())
plt.xticks(fontsize=13)
plt.yticks(fontsize=13)
plt.xlabel('employmentLength',fontsize=14)
plt.show()

可以看到,就业年限最多是 10+year

对于 issueDate 与 earliesCreditLine,统计一下每个类别的数量

for i in train[['issueDate', 'earliesCreditLine']]:print(train[i].value_counts())print()'''
2016-03-01    29066
2015-10-01    25525
2015-07-01    24496
2015-12-01    23245
2014-10-01    21461...
2007-08-01       23
2007-07-01       21
2008-09-01       19
2007-09-01        7
2007-06-01        1
Name: issueDate, Length: 139, dtype: int64Aug-2001    5567
Aug-2002    5403
Sep-2003    5403
Oct-2001    5258
Aug-2000    5246...
Jan-1946       1
Nov-1953       1
Aug-1958       1
Jun-1958       1
Oct-1957       1
Name: earliesCreditLine, Length: 720, dtype: int64
'''

2.3 目标变量(标签y)的分布

查看目标变量(标签)是否平衡

若分类问题中各类别样本数量差距太大,则会造成样本不均衡的问题。样本不均衡不利于建立与训练出正确的模型,且不能做出合理的评估。

label=train.isDefault
label.value_counts()/len(label)'''
0    0.800488
1    0.199513
Name: isDefault, dtype: float64
'''
sns.countplot(label)

可以看到,贷款违约与不违约的比例大约为1:4,样本较不平衡,这是金融风控模型评估的中常见的现象,大多数的人都是不会拖欠贷款的。

对于这种情况,考虑后续将进行采样等操作。

接下来,看一下目标变量和分类类别之间的分布关系

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))
# 目标变量为1时候grade的分布
train_loan_fr.groupby("grade").size().plot.bar(ax=ax1)
# 目标变量为0时候grade的分布
train_loan_nofr.groupby("grade")["grade"].count().plot.bar(ax=ax2)
# 目标变量为1时候employmentLength的分布
train_loan_fr.groupby("employmentLength").size().plot.bar(ax=ax3)
# 目标变量为0时候employmentLength的分布
train_loan_nofr.groupby("employmentLength")["employmentLength"].count().plot.bar(ax=ax4)
plt.xticks(rotation=90);

查看一下正负样本的数据差异

把数据集按正负样本分成两份,查看变量的分布差异

train_positve = train[train['isDefault'] == 1]
train_negative = train[train['isDefault'] != 1]
f, ax = plt.subplots(len(numerical_feature),2,figsize = (10,80))
for i,col in enumerate(numerical_feature):sns.distplot(train_positve[col],ax = ax[i,0],color = "blue")ax[i,0].set_title("positive")sns.distplot(train_negative[col],ax = ax[i,1],color = 'red')ax[i,1].set_title("negative")
plt.subplots_adjust(hspace = 1)

2.4 缺失值查看

如果缺失值过多会对整体的模型结果产生一定的影响,因此每次在建模之前都需要对数据的缺失值情况就行查看,若有缺失情况,需要在后续特征工程中进行填补

缺省值查看

# 去掉标签
X_missing = train.drop(['isDefault'],axis=1)# 查看缺失情况
missing = X_missing.isna().sum()
missing = pd.DataFrame(data={'特征': missing.index,'缺失值个数':missing.values})
#通过~取反,选取不包含数字0的行
missing = missing[~missing['缺失值个数'].isin([0])]
# 缺失比例
missing['缺失比例'] =  missing['缺失值个数']/X_missing.shape[0]
missing'''特征        缺失值个数 缺失比例
7      employmentTitle      1  0.000001
8     employmentLength  46799  0.058499
14            postCode      1  0.000001
16                 dti    239  0.000299
22  pubRecBankruptcies    405  0.000506
24           revolUtil    531  0.000664
29               title      1  0.000001
31                  n0  40270  0.050338
32                  n1  40270  0.050338
33                  n2  40270  0.050338
34                  n3  40270  0.050338
35                  n4  33239  0.041549
36                  n5  40270  0.050338
37                  n6  40270  0.050338
38                  n7  40270  0.050338
39                  n8  40271  0.050339
40                  n9  40270  0.050338
41                 n10  33239  0.041549
42                 n11  69752  0.087190
43                 n12  40270  0.050338
44                 n13  40270  0.050338
45                 n14  40270  0.050338
'''

可以看到employmentTitle、employmentLength、dti 以及匿名特征等字段存在缺省值,

从上面的结果可以看出train数据集中的47个字段有22个存在缺省值的情况。下面可视化一下缺省值数量占比。

一般对于缺失值,需要进行横纵对比

  • 纵向(从列方向):如果nan存在的过多,说明这一列对label的影响几乎不起作用了,可以考虑删掉。如果缺失值很小一般可以选择填充。比如占到总数的50%,理论上对分析作用不大,这样就可以省略该字段。

  • 横向(从行方向):如果在数据集中,某些样本数据的大部分列都是缺失的且样本足够的情况下可以考虑删除。

注意:对于一些模型,可以自动处理缺失值,例如 lightgbm 模型

# 可视化
(train.isnull().sum()/len(train)).plot.bar(figsize = (20,6),color=['#d6ecf0','#a3d900','#88ada6','#ffb3a7','#cca4e3','#a1afc9'])

缺失特征可视化呈现

可以看到,所有的特征缺失值都在10%以内,这里考虑全部保留。

总结

47列数据中有22列都缺少少量数据,存在一个唯一值特征 ‘policyCode’

2.5 数据相关关系

f, ax = plt.subplots(1,1, figsize = (20,20))
cor = train[numerical_feature].corr()
sns.heatmap(cor, annot = True, linewidth = 0.2, linecolor = "white", ax = ax, fmt =".1g" )

可以看到,有些变量之间的相关性还是很强的,比如贷款总额loanAmnt 和分期付款金额installment 相关性为1,ficoRangeLow he ficoRangeHigh 相关性为1…,这种情况后面再特征选择时考虑删除。

三、特征工程

基本的EDA探索完成后(还有一些可以继续探索),就可以进行特征工程啦,在数据挖掘中,大部分时间都是在做特征工程

特征工程包括数据预处理、缺失值以及异常值的处理、数据分桶处理以及特征交互、编码、选择

a) 数据清洗数据清洗主要的目的是提取原始数据中的噪音部分。(重复数据、异常数据、缺失数据等)
b)特征预处理(Feature Prepossess)特征预处理的目的是将数据的原始字段进行相应的编码(定性:独热编码、哈希编码等;定量:取整、截断、二值化、分箱、放缩)、变换(归一化、标准化、正态化),并进行缺失值的处理(插值、均值、中位数、众数、删除);
c)特征提取(Feature Extraction)特征提取的目的是从原始数据中提取出心的特征字段,并将特征转换成特定的格式;
d)特征筛选(Feature Selection)特征筛选的目的是筛选出较优的特征子集,以取得较好的泛化性能;

引用 贷款违约预测3-特征工程 观点:

3.1 重复值处理

重复值

train.duplicated().sum()

0

3.2 缺失值填补

在比赛中数据预处理是必不可少的一部分,对于缺失值的填充往往会影响比赛的结果。

缺失值的处理,请参见
【缺失值处理】拉格朗日插值法—随机森林算法填充—sklearn填充(均值/众数/中位数)

传统地,

  • 如果是分类型特征,采用众数进行填补。
  • 如果是连续型特征,采用均值进行填补。

还要考虑

  • 均值一般适用于近似正态分布数据,观测值较为均匀散布均值周围;
  • 中位数一般适用于偏态分布或者有离群点数据,中位数是更好地代表数据中心趋势;
  • 众数一般用于类别变量,无大小、先后顺序之分。

所以对于连续变量

  • 对于数据近似符合正态分布,用该变量的均值填补缺失。
  • 对于数据存在偏态分布的情况,采用中位数进行填补。

首先剔除标签列

label = 'isDefault'
Y_label = train['isDefault']
numerical_feature.remove(label)

数值型特征(连续型和离散型)用中位数填补(这里为了方便,都用中位数填补)

# 训练集
train[numerical_feature] = train[numerical_feature].fillna(train[numerical_feature].median())
# 测试集
test[numerical_feature] = test[numerical_feature].fillna(train[numerical_feature].median())

分类型特征用众数填补

分类型特征查看

train[category_feature]
'''grade subGrade employmentLength   issueDate earliesCreditLine
0          E       E2          2 years  2014-07-01          Aug-2001
1          D       D2          5 years  2012-08-01          May-2002
2          D       D3          8 years  2015-10-01          May-2006
3          A       A4        10+ years  2015-08-01          May-1999
4          C       C2              NaN  2016-03-01          Aug-1977...      ...              ...         ...               ...
799995     C       C4          7 years  2016-07-01          Aug-2011
799996     A       A4        10+ years  2013-04-01          May-1989
799997     C       C3        10+ years  2015-10-01          Jul-2002
799998     A       A4        10+ years  2015-02-01          Jan-1994
799999     B       B3          5 years  2018-08-01          Feb-2002[800000 rows x 5 columns]
'''

填补

# 训练集
train[category_feature] = train[category_feature].fillna(train[category_feature].mode())
# 测试集
test[category_feature] = test[category_feature].fillna(train[category_feature].mode())

填补之后,再次查看缺失值情况

train.isnull().sum()
'''id                        0
loanAmnt                  0
term                      0
interestRate              0
installment               0
grade                     0
subGrade                  0
employmentTitle           0
employmentLength      46799
homeOwnership             0
annualIncome              0
verificationStatus        0
issueDate                 0
isDefault                 0
purpose                   0
postCode                  0
regionCode                0
dti                       0
delinquency_2years        0
ficoRangeLow              0
ficoRangeHigh             0
openAcc                   0
pubRec                    0
pubRecBankruptcies        0
revolBal                  0
revolUtil                 0
totalAcc                  0
initialListStatus         0
applicationType           0
earliesCreditLine         0
title                     0
policyCode                0
n0                        0
n1                        0
n2                        0
n3                        0
n4                        0
n5                        0
n6                        0
n7                        0
n8                        0
n9                        0
n10                       0
n11                       0
n12                       0
n13                       0
n14                       0
issueDateDT               0
dtype: int64
'''

可以看到,employmentLength 列还存在缺失值

           特征        缺失值个数 缺失比例
8     employmentLength  46799  0.058499
train.employmentLength
'''
0           2 years
1           5 years
2           8 years
3         10+ years
4               NaN799995      7 years
799996    10+ years
799997    10+ years
799998    10+ years
799999      5 years
Name: employmentLength, Length: 800000, dtype: object
'''

采用决策树来填补就业年限(employmentLength)

from sklearn.tree import DecisionTreeClassifierempLenNotNullInd = train.employmentLength.notnull() # 不是空的行,返回True
columns = ['postCode','regionCode','employmentTitle','annualIncome'] # 用四个特征来预测employmentLength
train_empLen_X  = train.loc[empLenNotNullInd,columns]
train_empLen_y = train.employmentLength[empLenNotNullInd]DTC = DecisionTreeClassifier() # 实例化
DTC.fit(train_empLen_X ,train_empLen_y) # 训练
print(DTC.score(train_empLen_X ,train_empLen_y))# 0.9809320486828881
# 预测
for data in [train,test]:empLenIsNullInd = data.employmentLength.isnull()test_empLen_X  = data.loc[empLenIsNullInd,columns]empLen_pred = DTC.predict(test_empLen_X)data.employmentLength[empLenIsNullInd] = empLen_pred

填补完毕

train.isnull().any().sum()

0

train['employmentLength'][:20]
'''
0       2 years
1       5 years
2       8 years
3     10+ years
4       5 years
5       7 years
6       9 years
7        1 year
8       5 years
9       6 years
10    10+ years
11      3 years
12      2 years
13    10+ years
14      2 years
15      2 years
16      9 years
17     < 1 year
18    10+ years
19      9 years
Name: employmentLength, dtype: object
'''
train['employmentLength'].value_counts(dropna=False).sort_index()
'''
1 year        55034
10+ years    276853
2 years       76435
3 years       68888
4 years       50893
5 years       54038
6 years       39517
7 years       37175
8 years       37903
9 years       31463
< 1 year      71801
Name: employmentLength, dtype: int64
'''

总结

  • 如果采用 XGBoost(LightGBM) 模型,能够自动处理缺失值的模型,这样就无需处理缺失值;
  • 如果缺失值较多,可以直接删除
  • 如果发现某列特征信息可能是因为某种特定原因导致的,而不是随机缺失,可以考虑在特征工程时利用该假设来构架新特征。

3.3 异常值处理

异常值的存在很可能会影响模型的最终结果,但是当我们发现异常值的时候也不能马上就删除,应该先看看这个异常值是不是有特殊原因造成的,特别是在金融风控问题中,异常值的出现往往是存在意义的。

如果不是因为特殊原因造成的,可以先观察这个异常值出现的频率

  • 若异常值只出现了一次,多半是输入错误,直接把异常值删除即可

  • 若异常值出现了多次,可以和业务人员沟通,可能这是某种特殊表示,如果是人为造成的错误,留着是没有用,只要数据量不是太大,都可以删除

  • 若异常值占到总数据量的10%以上,不能轻易删除。可以考虑把异常值替换成非异常但是非干扰的项,比如说用0来进行替换,或者把异常当缺失值,用均值或者众数来进行替换

通常,在进行EDA的时候会利用描述统计的方法,查看特征的均值、极大值、极小值等信息,结合常识来判断是否存在异常值。

比如,年龄值出现负数,人的身高出现非常大的值等等

除此之外,还有其他判断异常值的方法,例如

3.3.1 方法一:均方差 3σ

在统计学中,如果一个数据分布近似正态,那么大约 68% 的数据值会在均值的一个标准差范围内,大约 95% 会在两个标准差范围内,大约 99.7% 会在三个标准差范围内。

def find_outliers_by_3segama(data,fea):data_std = np.std(data[fea])data_mean = np.mean(data[fea])outliers_cut_off = data_std * 3lower_rule = data_mean - outliers_cut_offupper_rule = data_mean + outliers_cut_offdata[fea+'_outliers'] = data[fea].apply(lambda x:str('异常值') if x > upper_rule or x < lower_rule else '正常值')return data

得到特征的异常值后可以进一步分析变量异常值和目标变量的关系

data_train = train.copy()
for fea in numerical_feature:data_train = find_outliers_by_3segama(data_train,fea)print(data_train[fea+'_outliers'].value_counts())print(data_train.groupby(fea+'_outliers')['isDefault'].sum())print('*'*10)

3.3.2 方法二:箱型图

箱型图包括上四


这里,没有对异常值进行处理…

3.4 时间数据处理

对于时间数据


对于本赛题,时间数据有 issueDate ,可以将其转化成时间格式(issueDateDT特征表示数据日期离数据集中日期最早的日期(2007-06-01)的天数)

首先看一下这个issueDate

train['issueDate']
'''
0         2014-07-01
1         2012-08-01
2         2015-10-01
3         2015-08-01
4         2016-03-01...
799995    2016-07-01
799996    2013-04-01
799997    2015-10-01
799998    2015-02-01
799999    2018-08-01
Name: issueDate, Length: 800000, dtype: object
'''
train.shape # (800000, 47)

训练集时间数据处理

import datetime
# 转化成时间格式 issueDateDT特征表示数据日期离数据集中日期最早的日期(2007-06-01)的天数
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.daystrain.shape # (800000, 48)

查看一下处理效果

train[['issueDate','issueDateDT']]

测试集时间数据处理

#转化成时间格式
test['issueDate'] = pd.to_datetime(train['issueDate'],format='%Y-%m-%d')
startdate = datetime.datetime.strptime('2007-06-01', '%Y-%m-%d')
test['issueDateDT'] = test['issueDate'].apply(lambda x: x-startdate).dt.days

可视化

plt.figure(1 , figsize = (10 , 8))
plt.hist(train['issueDateDT'], label='train');
plt.hist(test['issueDateDT'], label='test');
plt.legend();
plt.title('Distribution of issueDateDT dates');
#train 和 test issueDateDT 日期有重叠 所以使用基于时间的分割进行验证是不明智的

注:这里issueDate特征先暂时不删除

3.5 特征交叉

这里处理 earliesCreditLine,将利用到 issueDate

这两个特征含义如下

Field Description
issueDate 贷款发放的月份
earliesCreditLine 借款人最早报告的信用额度开立的月份

将issueDate贷款发放时的年份减去借款人最早报告的信用额度开立的年份,得到新的特征,即开卡年限CreditLine

train[['issueDate','earliesCreditLine']]
'''issueDate earliesCreditLine
0       2014-07-01          Aug-2001
1       2012-08-01          May-2002
2       2015-10-01          May-2006
3       2015-08-01          May-1999
4       2016-03-01          Aug-1977...               ...
799995  2016-07-01          Aug-2011
799996  2013-04-01          May-1989
799997  2015-10-01          Jul-2002
799998  2015-02-01          Jan-1994
799999  2018-08-01          Feb-2002[800000 rows x 2 columns]
'''

参考:https://zhuanlan.zhihu.com/p/255105477

train_earliesCreditLine_year = train['earliesCreditLine'].apply(lambda x:x[-4:]).astype('int64')
test_earliesCreditLine_year = test['earliesCreditLine'].apply(lambda x:x[-4:]).astype('int64')train_issueDate_year = train['issueDate'].astype('str').apply(lambda x:x[:4]).astype('int64')
test_issueDate_year = test['issueDate'].astype('str').apply(lambda x:x[:4]).astype('int64')train['CreditLine'] = train_issueDate_year - train_earliesCreditLine_year
test['CreditLine'] = test_issueDate_year - test_earliesCreditLine_yeartrain = train.drop(['earliesCreditLine','issueDate'],axis=1)
test = test.drop(['earliesCreditLine','issueDate'],axis=1)

查看处理结果

train['CreditLine']
'''
0         13
1         10
2          9
3         16
4         39..
799995     5
799996    24
799997    13
799998    21
799999    16
Name: CreditLine, Length: 800000, dtype: int64
'''
train.shape ## (800000, 47)

目前新增两个特征 issueDateDT、CreditLine

earliesCreditLine和issueDate 已经删除

3.6 特征编码

对类别型特征进行转换,使其变为数值特征。具体有以下几种方法:

  • 序号编码:适用于类别间存在大小关系的特征。比如级别高中低,可以对应 321。
  • oneHot 编码:适用于不具有大小关系的特征。比如地名。
  • 二进制编码:先给每个类别赋予一个序号 ID,然后对 ID 进行二进制编码,最终得到和 OneHot 类似的 0-1 向量,但是维度更小。

首先将 employmentLength 进行简单的处理,再进行编码

这里将就业年限特征转换为数值(把数字后面的years去掉并且把10+改成10,<1改成0)

def employmentLength_to_int(s):if pd.isnull(s):return selse:return np.int8(s.split()[0])
for data in [train, test]:data['employmentLength'].replace(to_replace='10+ years', value='10 years', inplace=True)data['employmentLength'].replace('< 1 year', '0 years', inplace=True)data['employmentLength'] = data['employmentLength'].apply(employmentLength_to_int)

处理前效果

train['employmentLength'][:20]
'''
0       2 years
1       5 years
2       8 years
3     10+ years
4       5 years
5       7 years
6       9 years
7        1 year
8       5 years
9       6 years
10    10+ years
11      3 years
12      2 years
13    10+ years
14      2 years
15      2 years
16      9 years
17     < 1 year
18    10+ years
19      9 years
Name: employmentLength, dtype: object
'''

处理后效果

train['employmentLength']
'''
0          2
1          5
2          8
3         10
4          5..
799995     7
799996    10
799997    10
799998    10
799999     5
Name: employmentLength, Length: 800000, dtype: int64
'''

接下来,对其余分类型特征进行编码

像等级grade、subGrade这种类别特征,虽然是表示类别的数据,但是信用评级是有高低大小之分的,是有优先级的,所以可以直接自映射,转化为数值类型。(也可以使用labelencode编码)

a2z = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'
a2z_code = np.arange(1,27)
a2z_mapping = dict(zip(a2z, a2z_code))for data in [train,test]:data.loc[:,['grade','subGrade']] = data.loc[:,['grade','subGrade']].applymap(lambda g:g.replace(g[0], str(a2z.index(g[0])+1))).astype('int')

编码前结果

train[['grade','subGrade']]

编码后结果

train[['grade','subGrade']]

对于离散型特征,可以使用OneHotEncoder独热编码

要处理的特征有

train[['homeOwnership','verificationStatus','purpose']]
'''homeOwnership  verificationStatus  purpose
0                   2                   2        1
1                   0                   2        0
2                   0                   2        0
3                   1                   1        4
4                   1                   2       10...                 ...      ...
799995              1                   0        0
799996              0                   2        4
799997              1                   2        0
799998              0                   2        4
799999              0                   0        4[800000 rows x 3 columns]
'''

编码之前确定一下特征数

train.shape# (800000, 47)

独热编码

from sklearn.preprocessing import OneHotEncoder
oh = OneHotEncoder(sparse=False)
oh.fit(train[['homeOwnership','verificationStatus','purpose']])
OneHot1 = oh.transform(train[['homeOwnership','verificationStatus','purpose']])
OneHot2 = oh.transform(test[['homeOwnership','verificationStatus','purpose']])OneHot1.shape# (800000, 23)
'''
array([[0., 0., 1., ..., 0., 0., 0.],[1., 0., 0., ..., 0., 0., 0.],[1., 0., 0., ..., 0., 0., 0.],...,[0., 1., 0., ..., 0., 0., 0.],[1., 0., 0., ..., 0., 0., 0.],[1., 0., 0., ..., 0., 0., 0.]])
'''train = pd.concat([train, pd.DataFrame(OneHot1)], axis=1)
test = pd.concat([test, pd.DataFrame(OneHot2)], axis=1)train = train.drop(['homeOwnership','verificationStatus','purpose'],axis=1)
test = test.drop(['homeOwnership','verificationStatus','purpose'],axis=1)train.shape# (800000, 67)

3.7 数据分桶

引用 Datawhale零基础入门金融风控 Task3 特征工程 观点

常见的分箱方法有:

  • 固定宽度分箱
# 通过除法映射到间隔均匀的分箱中,每个分箱的取值范围都是loanAmnt/1000
train['loanAmnt_bin1'] = np.floor_divide(train['loanAmnt'], 1000)
## 通过对数函数映射到指数宽度分箱
train['loanAmnt_bin2'] = np.floor(np.log10(train['loanAmnt']))
  • 分位数分箱
train['loanAmnt_bin3'] = pd.qcut(train['loanAmnt'], 10, labels=False)
  • 方分箱及其他分箱方法的尝试

注:这里没有进行分箱

3.8 特征交互

参见引用文章

引用 Datawhale零基础入门金融风控 Task3 特征工程 观点

额,这里也懒得弄

3.9 特征选择

引用图片:https://blog.csdn.net/qq_38366112/article/details/114996847

1.人工判断与目标无关联特征为"id",需删除

train=train.drop(["id"],axis=1)
train.shape # (800000, 66)
test=test.drop(["id"],axis=1)
test.shape # (200000, 65)

2.求出各个特征与目标的相关系数,综合考虑排除corr小于0.01的特征

train.corr()["isDefault"].sort_values()

train=train.drop(["initialListStatus","n5","n11","n12","n8","postCode","policyCode"],axis=1)
test=test.drop(["initialListStatus","n5","n11","n12","n8","postCode","policyCode"],axis=1)train.shape# (800000, 59)

删除特征"initialListStatus",“n5”,“n11”,“n12”,“n8”,“postCode”,“policyCode”,剩余59个特征

3.特征间高相关过滤

两两特征之间高于0.6的

# 显示相关性高于0.6的变量
def getHighRelatedFeatureDf(corr_matrix, corr_threshold):highRelatedFeatureDf = pd.DataFrame(corr_matrix[corr_matrix>corr_threshold].stack().reset_index())highRelatedFeatureDf.rename({'level_0':'feature_x', 'level_1':'feature_y', 0:'corr'}, axis=1, inplace=True)highRelatedFeatureDf = highRelatedFeatureDf[highRelatedFeatureDf.feature_x != highRelatedFeatureDf.feature_y]highRelatedFeatureDf['feature_pair_key'] = highRelatedFeatureDf.loc[:,['feature_x', 'feature_y']].apply(lambda r:'#'.join(np.sort(r.values)), axis=1)highRelatedFeatureDf.drop_duplicates(subset=['feature_pair_key'],inplace=True)highRelatedFeatureDf.drop(['feature_pair_key'], axis=1, inplace=True)return highRelatedFeatureDfgetHighRelatedFeatureDf(train.corr(),0.6)'''feature_x           feature_y      corr
2              loanAmnt         installment  0.953369
5          interestRate               grade  0.953269
6          interestRate            subGrade  0.970847
11                grade            subGrade  0.993907
22   delinquency_2years                 n13  0.658946
24         ficoRangeLow       ficoRangeHigh  1.000000
28              openAcc            totalAcc  0.700796
29              openAcc                  n2  0.658807
30              openAcc                  n3  0.658807
31              openAcc                  n4  0.618207
32              openAcc                  n7  0.830624
33              openAcc                  n8  0.646342
34              openAcc                  n9  0.660917
35              openAcc                 n10  0.998717
37               pubRec  pubRecBankruptcies  0.644402
44             totalAcc                  n5  0.623639
45             totalAcc                  n6  0.678482
46             totalAcc                  n8  0.761854
47             totalAcc                 n10  0.697192
53                   n1                  n2  0.807789
54                   n1                  n3  0.807789
55                   n1                  n4  0.829016
56                   n1                  n7  0.651852
57                   n1                  n9  0.800925
61                   n2                  n3  1.000000
62                   n2                  n4  0.663186
63                   n2                  n7  0.790337
64                   n2                  n9  0.982015
65                   n2                 n10  0.655296
70                   n3                  n4  0.663186
71                   n3                  n7  0.790337
72                   n3                  n9  0.982015
73                   n3                 n10  0.655296
79                   n4                  n5  0.717936
80                   n4                  n7  0.742157
81                   n4                  n9  0.639867
82                   n4                 n10  0.614658
86                   n5                  n7  0.618970
87                   n5                  n8  0.838066
97                   n7                  n8  0.774955
98                   n7                  n9  0.794465
99                   n7                 n10  0.829799
105                  n8                 n10  0.640729
113                  n9                 n10  0.660395
'''

结果分析:

1) "loanAmnt"贷款金额,"installment"分期付款金额两个特征间相关系数为0.95

2)"ficoRangeLow"fico所属的下限范围,"ficoRangeHigh"fico所属的上限范围两个特征间相关系数为1

3)"openAcc"未结信用额度的数量,“n10” 两个特征间相关系数为0.93

4)“n3”,"n2"两个特征间相关系数为1;“n3”,“n9” 两个特征间相关系数为0.98

根据高相关特征,综合考虑他们与目标的相关性,删除特征"installment",“ficoRangeHigh”,“openAcc”,“n3”,“n9”

col = ['installment','ficoRangeHigh','openAcc','n3','n9']
for data in [train,test]:data.drop(col,axis=1,inplace=True)
train.shape # (800000, 54)

其余高相关的特征可以使用PCA进行降维处理
(参考:https://zhuanlan.zhihu.com/p/255105477?utm_source=wechat_session)

注:这里不处理了

4.低方差过滤

train.var().sort_values()

结合相关性过滤方差小于0.1的特征"applicationType"

col = ['applicationType']
for data in [train,test]:data.drop(col,axis=1,inplace=True)
train.shape  # (800000, 53)

总结

特征选择中对高相关性的特征进行删除、PCA降维,处理的可能不太合适,可尝试使用过滤法、包装法、嵌入法等特征选择方法进行特征的筛选

3.10 样本不平衡处理

若分类问题中各类别样本数量差距太大,则会造成样本不均衡的问题。样本不均衡不利于建立与训练出正确的模型,且不能做出合理的评估。

label.value_counts()/len(label)
'''
0    0.800488
1    0.199513
Name: isDefault, dtype: float64
'''

1.上采样

过抽样(也叫上采样、over-sampling)方法通过增加分类中少数类样本的数量来实现样本均衡,最直接的方法是简单复制少数类样本形成多条记录

参考:https://zhuanlan.zhihu.com/p/255105477?utm_source=wechat_session

import imblearn
from imblearn.over_sampling import SMOTE
over_samples = SMOTE(random_state=1234)
train_over,label_over = over_samples.fit_sample(train, label)train_over.to_csv('train_over.csv',index=False)
label_over.to_csv('label_over.csv',index=False)print(label_over.value_counts()/len(label_over))
print(train_over.shape)

2.下采样

欠抽样(也叫下采样、under-sampling)方法通过减少分类中多数类样本的样本数量来实现样本均衡,最直接的方法是随机地去掉一些多数类样本来减小多数类的规模

参考:https://zhuanlan.zhihu.com/p/255105477?utm_source=wechat_session

from imblearn.under_sampling import RandomUnderSampler
under_samples = RandomUnderSampler(random_state=1234)
train_under, label_under = under_samples.fit_sample(train,label)train_under.to_csv('train_under.csv',index=False)
label_under.to_csv('label_under.csv',index=False)print(label_under.value_counts()/len(label_under))
print(train_under.shape)

注:这里没有进行采样,如果做了可以分别利用上采样后的数据跑模型和下采样后的数据跑模型

四、建模分析

在完成相关的特征处理后,接下来进行建模分析,通过调节参数得到性能更强的模型

4.1 LightGBM

参考:https://zhuanlan.zhihu.com/p/256310383

X = train.drop(['isDefault'], axis=1)
y = train.loc[:,'isDefault']kf = KFold(n_splits=5, shuffle=True, random_state=525)
X_train_split, X_val, y_train_split, y_val = train_test_split(X, y, test_size=0.2)

使用5折交叉验证法对数据进行验证和训练

import lightgbm as lgb
cv_scores = []
for i, (train_index, val_index) in enumerate(kf.split(X, y)):X_train, y_train, X_val, y_val = X.iloc[train_index], y.iloc[train_index], X.iloc[val_index], y.iloc[val_index]train_matrix = lgb.Dataset(X_train, label=y_train)valid_matrix = lgb.Dataset(X_val, label=y_val)params = {'boosting_type': 'gbdt','objective': 'binary','learning_rate': 0.1,'metric': 'auc','min_child_weight': 1e-3,'num_leaves': 31,'max_depth': -1,'seed': 525,'nthread': 8,'silent': True,}model = lgb.train(params, train_set=train_matrix, num_boost_round=20000, valid_sets=valid_matrix, verbose_eval=1000, early_stopping_rounds=200)val_pred = model.predict(X_val, num_iteration=model.best_iteration)cv_scores.append(roc_auc_score(y_val, val_pred))print(cv_scores)print("lgb_scotrainre_list:{}".format(cv_scores))
print("lgb_score_mean:{}".format(np.mean(cv_scores)))
print("lgb_score_std:{}".format(np.std(cv_scores)))

lgb_scotrainre_list:[0.7303837315833632, 0.7258692125145638, 0.7305149209921737, 0.7296117869375041, 0.7294438695369077]
lgb_score_mean:0.7291647043129024
lgb_score_std:0.0016998349834934656


ROC曲线

from sklearn import metrics
from sklearn.metrics import roc_auc_scoreal_pre_lgb = model.predict(X_val, num_iteration=model.best_iteration)
fpr, tpr, threshold = metrics.roc_curve(y_val, val_pred)
roc_auc = metrics.auc(fpr, tpr)
print('AUC:{}'.format(roc_auc))plt.figure(figsize=(8, 8))
plt.title('Validation ROC')
plt.plot(fpr, tpr, 'b', label = 'Val AUC = %0.4f' % roc_auc)
plt.ylim(0,1)
plt.xlim(0,1)
plt.legend(loc='best')
plt.title('ROC')
plt.ylabel('True Positive Rate')
plt.xlabel('False Positive Rate')
# 画出对角线
plt.plot([0,1],[0,1],'r--')
plt.show()

AUC得分为0.7338

4.2 XGBoost

X = train.drop(['isDefault'], axis=1)
y = train.loc[:,'isDefault']Xtrain, Xtest, Ytrain, Ytest = train_test_split(X,y,test_size=0.3)

用XGBClassifier模型跑一下(具体的xgboost 参数设置可以参考官网)

from xgboost.sklearn import XGBClassifier
clf1 = XGBClassifier(n_jobs=-1)
clf1.fit(Xtrain,Ytrain)
clf1.score(Xtest,Ytest)

0.8068791666666667

计算模型结构的AUC面积

from sklearn.metrics import roc_curve, aucpredict_proba = clf1.predict_proba(Xtest)
false_positive_rate, true_positive_rate, thresholds = roc_curve(Ytest, predict_proba[:,1])
auc(false_positive_rate, true_positive_rate)

0.7326304866618416

4.3 三个模型比较

gra=GradientBoostingClassifier()
xgb=XGBClassifier()
lgb=LGBMClassifier()
models=[gra,xgb,lgb]
model_names=["gra","xgb","lgb"]#交叉验证看看上述3个算法评分
for i,model in enumerate(models):score=cross_val_score(model,X,y,cv=5,scoring="accuracy",n_jobs=-1)print(model_names[i],np.array(score).round(3),round(score.mean(),3))

其他建模方法

参见:

金融风控-贷款违约预测
数据竞赛入门-金融风控(贷款违约预测)四、建模与调参

以及

数据挖掘实践(金融风控-贷款违约预测)(四):建模与调参

尝试多种模型

五、模型调参

5.1 调参方法

(1)贪心调参

参考:https://www.jianshu.com/p/cdf0a9ffec6f

(2)网格搜索

参考:https://www.jianshu.com/p/cdf0a9ffec6f

sklearn 提供GridSearchCV用于进行网格搜索,只需要把模型的参数输进去,就能给出最优化的结果和参数。相比起贪心调参,网格搜索的结果会更优,但是网格搜索只适合于小数据集,一旦数据的量级上去了,很难得出结果。

(3)贝叶斯调参

参考:https://www.jianshu.com/p/cdf0a9ffec6f

贝叶斯调参的主要思想是:给定优化的目标函数(广义的函数,只需指定输入和输出即可,无需知道内部结构以及数学性质),通过不断地添加样本点来更新目标函数的后验分布(高斯过程,直到后验分布基本贴合于真实分布)。简单的说,就是考虑了上一次参数的信息,从而更好的调整当前的参数。

5.2 XGboost调参

参考:https://zhuanlan.zhihu.com/p/255105477?utm_source=wechat_session

1.优化max_depth,min_child_weight

from xgboost.sklearn import XGBClassifier
from sklearn.model_selection import GridSearchCV# 其余参数
other_params = {'learning_rate': 0.1, 'n_estimators': 100, 'max_depth': 5, 'min_child_weight': 1, 'seed': 0,'subsample': 0.8, 'colsample_bytree': 0.8, 'gamma': 0, 'reg_alpha': 0, 'reg_lambda': 1}# 待调参数
param_test1 = {'max_depth':list(range(4,9,2)),'min_child_weight':list(range(1,6,2))
}xgb1 = XGBClassifier(**other_params)
# 网格搜索
gs1 = GridSearchCV(xgb1,param_test1,cv = 5,scoring = 'roc_auc',n_jobs = -1,verbose=2)
best_model1=gs1.fit(Xtrain,Ytrain)
print('最优参数:',best_model1.best_params_)
print('最佳模型得分:',best_model1.best_score_)

最优参数:{‘max_depth’:4,,‘min-childweight’:5}
最佳模型得分:0.7185495198261862


2.优化gamma参数

other_params = {'learning_rate': 0.1, 'n_estimators': 100, 'max_depth': 4, 'min_child_weight': 5, 'seed': 0,'subsample': 0.8, 'colsample_bytree': 0.8, 'gamma': 0, 'reg_alpha': 0, 'reg_lambda': 1}param_test = {'gaama':[0,0.05,0.1,0.2,0.3]
}xgb = XGBClassifier(**other_params)
gs = GridSearchCV(xgb,param_test,cv = 5,scoring = 'roc_auc',n_jobs = -1,verbose=2)
best_model=gs.fit(Xtrain,Ytrain)
print('最优参数:',best_model.best_params_)
print('最佳模型得分:',best_model.best_score_)

最优参数:{‘gaama’:0}
最模得分:0.7185495198261862


3.subsample和colsample_bytree

other_params = {'learning_rate': 0.1, 'n_estimators': 100, 'max_depth': 4, 'min_child_weight': 5, 'seed': 0,'subsample': 0.8, 'colsample_bytree': 0.8, 'gamma': 0, 'reg_alpha': 0, 'reg_lambda': 1}param_test = {'subsample':[0.6,0.7,0.8,0.9],
'colsample_bytree':[0.6,0.7,0.8,0.9]
}xgb = XGBClassifier(**other_params)
gs = GridSearchCV(xgb,param_test,cv = 5,scoring = 'roc_auc',n_jobs = -1,verbose=2)
best_model=gs.fit(Xtrain,Ytrain)
print('最优参数:',best_model.best_params_)
print('最佳模型得分:',best_model.best_score_)

最优参数:{‘colsample-bytree’:0.7,‘subsample’:0.7}
最佳模得分:0.7187964885978947


4.reg_alpha和reg_lambda

other_params = {'learning_rate': 0.1, 'n_estimators': 100, 'max_depth': 4, 'min_child_weight': 5, 'seed': 0,'subsample': 0.7, 'colsample_bytree': 0.7, 'gamma': 0, 'reg_alpha': 0, 'reg_lambda': 1}param_test = {'reg_alpha': [4,5,6,7],
'reg_lambda': [0,0.01,0.05, 0.1]
}xgb = XGBClassifier(**other_params)
gs = GridSearchCV(xgb,param_test,cv = 5,scoring = 'roc_auc',n_jobs = -1,verbose=2)
best_model=gs.fit(Xtrain,Ytrain)
print('最优参数:',best_model.best_params_)
print('最佳模型得分:',best_model.best_score_)

最优参数:{‘reg-alpha’:5,‘reg-lambda’:0.01}
最佳模型得分:0.7194153615536154


5. learning_rate和n_estimators

other_params = {'learning_rate': 0.1, 'n_estimators': 100, 'max_depth': 4, 'min_child_weight': 5, 'seed': 0,'subsample': 0.7, 'colsample_bytree': 0.7, 'gamma': 0, 'reg_alpha': 5, 'reg_lambda': 0.01}param_test = {'learning_rate': [0.01, 0.05, 0.07, 0.1, 0.2],
'n_estimators': [100,200,300,400,500]
}xgb = XGBClassifier(**other_params)
gs = GridSearchCV(xgb,param_test,cv = 5,scoring = 'roc_auc',n_jobs = -1,verbose=2)
best_model=gs.fit(Xtrain,Ytrain)
print('最优参数:',best_model.best_params_)
print('最佳模型得分:',best_model.best_score_)

最优参数:{‘learning-rate’:0.05,‘n-estimators’:400}
最佳模型得分:0.7207082359918353


通过调参后的最终模型

from xgboost.sklearn import XGBClassifier
clf = XGBClassifier(learning_rate= 0.05, n_estimators= 400, max_depth= 4, min_child_weight= 5, seed= 0,subsample= 0.7, colsample_bytree= 0.7, gamma= 0, reg_alpha= 5, reg_lambda=0.01,n_jobs = -1)clf.fit(Xtrain,Ytrain)
clf.score(Xtest,Ytest)

0.80934521

AUC面积

from sklearn.metrics import roc_curve, aucpredict_proba = clf.predict_proba(Xtest)
false_positive_rate, true_positive_rate, thresholds = roc_curve(Ytest, predict_proba[:,1])
auc(false_positive_rate, true_positive_rate)

0.74512067

这里做完后,还可以得出特征重要性

from xgboost import plot_importance
plot_importance(clf)
plt.show()

总结

调参过程综合要点:

(1)"n_estimators"基分类器数量越大,偏差越小,但时间有限,这里初步可选30

(2)"max_depth"越大偏差越小,方差越大,需综合考虑时间及拟合性

(3)"learning_rate"学习速率一般越小越好,只是耗时会更长

(4)"subsample"采样比例一般在[0.5,0.8]之间比较好


六、模型融合

模型融合是比赛后期上分的重要手段,模型融合后结果会有大幅提升,以下是模型融合的方式。

【机器学习】模型融合方法概述

1)平均法(Averaging)-针对回归问题

2)投票法(Voting)- 针对分类问题

  • 简单投票法

  • 加权投票法

  • 硬投票法

模型 1:A - 99%、B - 1%,表示模型 1 认为该样本是 A 类型的概率为 99%,为 B 类型的概率为 1%

python实现

from xgboost import XGBClassifier
from sklearn.linear_model import LogisticRegression
from sklearn.ensemble import RandomForestClassifier
from sklearn.ensemble import VotingClassifier
from sklearn.model_selection import train_test_split,cross_val_score    #划分数据 交叉验证clf1 = XGBClassifier(learning_rate=0.1, n_estimators=150, max_depth=3, min_child_weight=2, subsample=0.7,colsample_bytree=0.6, objective='binary:logistic')
clf2 = RandomForestClassifier(n_estimators=50, max_depth=1, min_samples_split=4,min_samples_leaf=63,oob_score=True)
clf3 = SVC(C=0.1)# 硬投票
eclf = VotingClassifier(estimators=[('xgb', clf1), ('rf', clf2),('svc', clf3)], voting='hard')
# 比较模型融合效果
for clf, label in zip([clf1, clf2, clf3, eclf], ['XGBBoosting', 'Random Forest', 'SVM', 'Ensemble']):scores = cross_val_score(clf, x, y, cv=5, scoring='accuracy')print("Accuracy: %0.2f (+/- %0.2f) [%s]" % (scores.mean(), scores.std(), label))
  • 软投票法

将所有模型预测样本为某一类别的概率的平均值作为标准

from xgboost import XGBClassifier
from sklearn.linear_model import LogisticRegression
from sklearn.ensemble import RandomForestClassifier
from sklearn.ensemble import VotingClassifier
from sklearn.model_selection import train_test_split,cross_val_score    #划分数据 交叉验证clf1 = XGBClassifier(learning_rate=0.1, n_estimators=150, max_depth=3, min_child_weight=2, subsample=0.7,colsample_bytree=0.6, objective='binary:logistic')
clf2 = RandomForestClassifier(n_estimators=50, max_depth=1, min_samples_split=4,min_samples_leaf=63,oob_score=True)
clf3 = SVC(C=0.1)# 软投票
eclf = VotingClassifier(estimators=[('xgb', clf1), ('rf', clf2), ('svc', clf3)], voting='soft', weights=[2, 1, 1])
# 比较模型融合效果
for clf, label in zip([clf1, clf2, clf3, eclf], ['XGBBoosting', 'Random Forest', 'SVM', 'Ensemble']):scores = cross_val_score(clf, x, y, cv=5, scoring='accuracy')print("Accuracy: %0.2f (+/- %0.2f) [%s]" % (scores.mean(), scores.std(), label))

3)综合法

  • 排序融合
  • log融合

4)stacking/blending:

  • stacking(构建多层模型,并利用预测结果再拟合预测)
  • blending(选取部分数据预测训练得到预测结果作为新特征,带入剩下的数据中预测。Blending只有一层,而Stacking有多层)

5) boosting/bagging

多树的提升方法,在xgboost,Adaboost,GBDT中已经用到


介绍完上述方法之后,回到赛题

这里使用之前的训练的lgb和xgb模型作为基分类器,逻辑回归作为目标分类器做stacking

from mlxtend.classifier import StackingClassifiergra=GradientBoostingClassifier()
xgb=XGBClassifier()
lgb=LGBMClassifier()
lr = LogisticRegression()
sclf = StackingClassifier(classifiers=[gra, xgb, lgb], use_probas=True,meta_classifier=lr)sclf.fit(Xtrain,Ytrain)
pre =sclf.predict_proba(Xtest)[:,1]
fpr, tpr, thresholds = roc_curve(Ytest, pre)
score = auc(fpr, tpr)
print(score)

总结

  • 简单平均和加权平均是常用的两种比赛中模型融合的方式。其优点是快速、简单。
  • stacking融合速度非常慢,同时stacking多层提升幅度并不能抵消其带来的时间和内存消耗,所以实际环境中应用还是有一定的难度。

七 结果部署

a) 预测评估数据集(通过验证数据集来验证被优化过的模型)
b) 利用整个数据集生产模型(通过整个数据集来生成模型)
c) 序列化模型(将模型序列化,以便于预测新数据)

当有新数据产生时,就可以采用这个模型来预测新数据。


至此
2021.3.29

「机器学习」天池比赛:金融风控贷款违约预测相关推荐

  1. 天池比赛-金融风控贷款违约预测

    一.赛题背景 以金融风控中的个人信贷为背景,要求根据贷款申请人的数据信息预测其是否有违约的可能,以此判断是否通过此项贷款.这是一个典型的分类问题,但也涉及一些金融的业务背景知识. 二.赛题数据 赛题以 ...

  2. 【天池】金融风控-贷款违约预测(五)—— 模型融合

    [天池]金融风控-贷款违约预测(五)-- 模型融合 前言 内容介绍 stacking\blending详解 代码示例 总结 前言 [天池]金融风控-贷款违约预测(赛题链接). 上一篇进行数据建模和模型 ...

  3. 【天池】金融风控贷款违约预测task5

    [天池]金融风控贷款违约预测task5 task5学习总结: 1)简单平均和加权平均是常用的两种比赛中模型融合的方式.其优点是快速.简单. 2)stacking在众多比赛中大杀四方,但是跑过代码的小伙 ...

  4. 基于机器学习与深度学习的金融风控贷款违约预测

    基于机器学习与深度学习的金融风控贷款违约预测 目录 一.赛题分析 1. 任务分析 2. 数据属性 3. 评价指标 4. 问题归类 5. 整体思路 二.数据可视化分析 1. 总体数据分析 2. 数值型数 ...

  5. 阿里天池学习赛-金融风控-贷款违约预测

    阿里天池学习赛-金融风控-贷款违约预测 1 赛题理解 1.1 赛题数据 1.2 评测标准 2 探索性分析(EDA) 2.1 初窥数据 2.2 查看缺失值占比 2.3 数值型变量 2.3.1 数据分布 ...

  6. DataWhale天池-金融风控贷款违约预测-Task01赛题理解

    目录 一.赛题概况 二.数据集介绍 三.预测指标 理解 通过ROC曲线评估分类器 最佳阈值点选择 一.赛题概况 本次新人赛是Datawhale与天池联合发起的0基础入门系列赛事第四场 -- 零基础入门 ...

  7. 零基础入门金融风控-贷款违约预测-机器学习-数据分析

    零基础入门金融风控-贷款违约预测 一.赛题数据 赛题以预测用户贷款是否违约为任务,数据集报名后可见并可下载,该数据来自某信贷平台的贷款记录,总数据量超过120w,包含47列变量信息,其中15列为匿名变 ...

  8. Datawhale学习笔记【阿里云天池 金融风控-贷款违约预测】Task2 数据分析

    阿里云天池学习赛[金融风控-贷款违约预测] 赛题数据及背景 python库的导入 国内镜像源网址及使用方法 镜像使用方法 文件读取 数据的总体了解 查看数据集中特征缺失值,唯一值等 检查缺失值 缺失值 ...

  9. 1.天池金融风控-贷款违约预测新人赛之预备知识

    比赛链接:金融风控-贷款违约预测 因为这是一个金融风控专题的数据挖掘实战,在开始之前先引入一些预备知识. 1.预备知识 1.1预测指标 本次竞赛用AUC作为评价指标,AUC为ROC曲线下与坐标轴围成的 ...

最新文章

  1. 任意的android程序,Android任意位置获取应用Context
  2. 教育部:2006年江西高职(专科)院校名单
  3. 实现一个简单的基于码云(Gitee) 的 Storage
  4. layui导航栏页面滚动固定_帮你搞定长滚动网页的设计!这5种设计策略需谨记!...
  5. EMLOG复制网站文字提醒弹窗源码美化版
  6. 简单的webservice发布和测试
  7. python画图代码大全-简单画图 - python代码库 - 云代码
  8. YetAnotherKeyDisplayer(YAKD屏幕上显示键盘操作)源码下载及编译(Win10,VS2022)
  9. python行业中性_知乎
  10. 如何使用Reviewboard进行代码Review?
  11. 销售业务处理流程之 分期收款
  12. Oracle 软件包及补丁包免费下载及简单说明
  13. 表空间的相关查询命令
  14. 淘宝短视频,为什么搬运的短视频没有流量?从算法角度分析
  15. WindowsCE目录解析
  16. 使用adb的时候出现 adb不是内部或者外部命令如何解决
  17. import * as 是什么?
  18. css引入第三方字体(等宽非衬线)
  19. 【VJudge】【Legilimens Contest 1】
  20. 数据规范化与数据离散化

热门文章

  1. matlab实现手绘风格(简笔画风格、漫画风格)的曲线绘图
  2. HackerRank 题目目录
  3. 卷积神经网络之前向传播算法
  4. 快速入门开发实现订单类图片识别结果抽象解析
  5. Jzxx.oj:4068: 数组元素的查找新
  6. 利用XML制作UGUI登陆界面
  7. 计算机英语第3版课后单词,PC电脑相关词汇三
  8. pcl::compute3DCentroid()计算质心算法原理
  9. Java-用星号打印菱形
  10. VMware: 虚拟机启动没有IP地址