08. BI - 万字长文,银行如何做贷款违约的预测,特征处理及学习

本文为 「茶桁的 AI 秘籍 - BI 篇 第 08 篇」

在这里插入图片描述

文章目录

    • 课程回顾
    • 案例分析
    • 案例实战

Hi, 你好。我是茶桁。

课程回顾

上节课,咱们讲了一个股票的指标:MACD。在趋势行情里面它应该还是有效的指标。它比较忌讳动荡行情,比如说它一会上升一会下降,那还没有等 12 天过完,就是均线还没有画好它又马上变成了另一个行线,这样 MACD 有可能会失效。

这个问题我们大家自己去思考一下,如果你采用这个策略在过去一段时间里面选择一些股票来进行购买的话,能不能让它的收益率大于 60%?有这种可能性,选好股的话确实在过去一年交易里面收益率是有可能大于 60%。

那我们之前的课程里,带来了 Fintech 的应用场景,同时又对其中一个量化交易的场景做了一个简单实验。今天,咱们来另一个 Fintech 的场景,同样也是有数据,这个数据是来自于一场比赛。

这个比赛是关于贷款违约预测的一个比赛,来,我们一起回想一下,在这个 BI 系列课程开始的几节课里咱们讲的模型、机器学习的神器。大家还记得是哪两个神器吗?其实严格来说的话应该是三个神器。

XGBoost 是第一个,LightGBM 是更快的,还有一个是跟分类相关的 CatBoost。

案例分析

接下来,我们来看一个问题:

零基础入门金融风控-贷款违约预测

这里有 47 个指标,120 万贷款记录。其中 15 列为匿名变量,并且对 employmentTitle、purpose、postCode 和 title 等字段进行了脱敏。

给你两个数据集, 一个是训练集,一个是测试集:

训练集为 train.csv
测试集为 testA.csv
提交格式 sample_submit.csv

我这里就不提供数据了,有需要的可以用阿里云帐号自行去获取:

https://tianchi.aliyun.com/competition/entrance/531830/information

我们来看看字段:

字段说明
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,为一些贷款人行为计数特征的处理

现在我们想要评估它,可以看一下到底哪一个是我们的 label? 贷款的 id、金额、期限、利率、等级等等,这些都是业务指标。最后有一个匿名特征,15 个。还有一个isDefault,代表违约,1 是违约,0 代表是正常。所以我们预测词段应该是最后一个词段叫isDefault

这是一个什么问题?我们可以思考一下,这是机器学习里面非常经典的一个问题,叫做分类问题。确切说这是个二分类问题,那么二分类问题可以提交结果是 0 和 1,也可以提交一个概率。如果你提交概率,评价指标是 AUC提交哪一个会更好?是提交 0 和 1 具体的分类结果好,还是提交一个概率结果。如果预测出来是个概率值的话,把它转化成了 0 和 1,有可能 AUC 是不高的,这是一个小的技巧性的问题。所以建议大家是以概率值来进行提交。

0 和 1 是实际的结果,从这个物理含义上来说的话它确实有违约和不违约两个最终的结果。但是我们要去预测,你做分类任务也可以得到一个概率值,这概率值是 0.95,就是他违约概率是 95%,还比较高。0.06 就是他不太违约,可以写 0 和 1,也可以写上它的概率值。概率值通常情况下 AUC 的结果会更大,就是你的排名会更靠前。

这些可以自己做个对比,因为它本身是一个在线的比赛,你可以把它转化成为一个分类结果再去看一看 AUC 会变成多少。

题目就是这样一个题目,去预测一个二分类的任务,是个跟贷款违约相关的场景。除了我们要知道这种分类模型可以用 XGBoost 的 LightGBM 以外,关键是要统计它的一些特征。

梳理一下整个过程,第一个要做数据加载,前期数据探索,探索一般来说我们要看字段 label,跟 label 相关的,还有就是有没有缺失值,唯一值的个数也可以做一些探索。

sns.countplot()函数,以 bar 的形式展示每个类别的数量。

在这里插入图片描述

探索以后可以做一些预处理,中间如果有缺失值要在模型里面进行补全,补全也会分两种情况,第一种情况叫做数值特征:num_features,第二种叫做类别特征: cat_features

以年龄为例,就是一个具体的数字。还有一个叫做收入,类似这种跟钱相关的指标用什么样的方式来做补全?两种方式,一般平均值和中位数。我个人更倾向于使用中位数。

举个例子,我们回头看一下刚才字段里的贷款金额,不同人的贷款金额可能不一样。不知道有没有人尝试过网络贷款,跟金额相关的其实差别是比较大的,均值会偏高,你想你借 500 块钱、1,000 块钱,有也会有人在贷款软件上借 20 万、30 万。这种如果你放到均值里面,差别会特别大。

再举个场景,收入,你们公司的收入平均收入是多少?如果你把马云放进去,你想想马云的收入高不高?一下子平均收入个人都是上亿的,这样大家都变成了异常值。

所以收入字段这种字段是不能用平均值的,用平均值做补全是没有意义的。因为那个缺失的值很有可能不是马云,你被平均了。所以用的是中位数,中位数会更加的过滤异常值,金额可以用中位数补全。

除了收入以外,一般来说年龄这种平均值差别不会太大。你可以自己做个实验,基本差不多。

第二种是类别特征,那众数在类别特征里做补全比较好,因为在类别特征里面不是一个连续值。

举个例子,gender 代表性别,请问性别能用平均值补全吗?肯定不行。因为性别只有两种情况,所以两种情况我们要找到那种情况最大的,如果他男性居多,我们的补全可以倾向于用男性来去做补全。

所以在类别特征里面一般来说是用众数好。

这是前期的数据处理,类别特征在整个程序里面还要做一个处理叫做数值编码。因为类别特征它原来是字母类型,数值编码有一个工具叫做labelEncoder,这个叫做标签编码,标签编码可以自动帮你进行标签。但有些情况下,有些类别是有方向的,什么是类别方向呢? 有一个叫做贷款等级:grade,贷款等级有方向之分,大小从 a、b、c、d、e,a 可能是优质的,e 可能是劣质的。贷款等级还不能直接让他用 labelEncoder,需要自己去写一个映射关系。

日期类型的处理,日期在数据集里面经常会出现,一般怎么处理呢?举个场景,2023 年的 7 月 5 号这是个日期,直接把它喂到模型里面是不能用的,我们要抽取出来它一个多尺度特征,多尺度可以按照年月日分别抽取出来,2023 一个特征,7 月一个特征,5 号一个特征。

那我们思考一下,还有没有其他多尺度特征可以抽取?那今天是周六(以我写这篇文章的日期问准,而非发布),周六在整个的时间里面也算是一个特殊的时间,你可以把这个特征叫做 weekday。还有哪个特征?是不是还可以再取一个叫做 holiday,或者叫 workday。因为有些周六它是个工作日。

所以从业务场景出发,weekday 是周几,还可以出一个叫 workday 或 holiday 来判断到底是工作还是不工作,这些也都有关系。可能跟贷款违约没啥关系,但是跟什么交通流量,或者跟商业里面的销售都是有关系的。举个例子,比如说电影票房就是个最明显的区别,工作日不会很高,但是假日的话是工作日的可能几倍都不止。

除了这种类型处理的方式我们还有一种处理的方法,我们把它称为 diff, 就是 different。

我们的日期可以做一个锚点,以一个最小值为例,什么时候开始的这项业务,假设是 2023 年的 1 月 1 号这个业务开始的,那么现在是 2023 年的 11 月 28 号,相比 1 月 1 号来说就会存在一个时间的 diff,这样就会把它转化成一个数值类型,也是一个统计特征。

所以时间类型有两种处理方式,一种叫做时间多尺度,一种叫做时间 diff。

这个方法还是比较常见的,未来你们在工作和比赛过程中遇到时间类型放到模型之前,都要采用这样的处理的方法。不一定两种策略都用,但是至少,如果要放时间特征的话,要用其中的一种。要不你用多尺度,要不你用时间 diff,否则这个时间是不能直接放到模型里去的。

那这个数据集里面就有跟时间类型相关的数据,比如说有个issueDate,贷款发方月份,这个是时间的,所以一会儿我们要采用提到的方式来进行处理。

然后就是第四个步骤了,我们要进行特征构造。大家要明白,模型的使用不是难点,因为大家都会用 XGBoost 和 LightGBM,参数也都是那些,区别在于前期的特征构造。我们之前有句话叫做特征决定模型的上限,而模型只是把上限跑出来而已。那么在特征构造里面有哪些技巧呢?特征构造里面有一些统计学的一些技巧。

对于那些类别变量的一些特征来说我们可以做一些统计的特征,代表这个类型的一些属性。

举个例子,比如说贷款等级我们有 a、b、c、d、e,是按照分组的方式。这个分组你可以数一下分组的个数,还可以把贷款等级和违约概率isDefault分别做一个对应。a 是 0,b 是 1…

这种对应到底是好还是不好我并不清楚,但是能把 a 的 default 的平均值计算出来,应该按照分组的方式求一下isDefault这个统计变量的平均值就好了,b、c、d、e 也都能算。算完以后就可以把它叫做grade_isDefault_mean。你加这个字段,假设 a 是 0.01,就是它的平均值。b 假设是 0.02,c 是 0.03,这样我们就可以给它统计一个特征出来,

就是说我们可以对每一个特征去跟我们的isDefault来做关联,做完关联以后就可以方便你去理解这个特征的一个含义。需要说明一点,这个特征一定是属于类别变量, 因为类别变量是一个离散的个数,它在贷款里面假设类别 a、b、c、d、e 只有 5 种,那只有 5 种每一种它的isDefault平均值才有价值。

如果它不是类别变量是一个数值变量,把每个数值都计算出来isDefault,请问它会发生什么样的一个结果呢?我们把这个结果叫做标签泄漏。为什么叫标签泄漏?因为在你的计算过程中我已经知道实际的答案了,知道实际答案的话就预测不出来这个结果。因为实际的情况下我们是不知道答案的,或者说实际情况下它的isDefault_mean没有一个稳定的衡量结果。

那么对于类别变量它才能相对稳定。对于数值变量他加isDefault能稳定吗?不能稳定。这个技巧是只限于类别变量,它才能得到一个相对稳定的一个状态特征。

然后就到了我们的第五个步骤,特征构造完了我们就上模型。模型这里用的是 LightGBM。那它的参数也很多,我们在之前的课程中给大家讲过,建议大家用祖传参数,因为祖传参数不需要把时间花到调参上面。那么这里我们也用的是祖传参数:

clf = LGBMClassifier(
num_leaves=2**5-1, reg_alpha=0.25, reg_lambda=0.25, objective='binary', max_depth=-1, learning_rate=0.005, min_child_samples=3, random_state=2023, n_estimators=2000, subsample=1, colsample_bytree=1
)

我们的数据比较多,100 多万。迭代的次数也比较多,用的是 2,000 轮。未来你都可以把这个轮数再进一步进行提升。

后面我们还可以做一个子模型的融合,这里叫五折交叉验证。子模型融合是什么含义呢?就是你列了 5 个模型,让这 5 个模型一起来去产生作用,这是一个五折交叉验证的一个策略。

案例实战

那现在就一起来写一写这个代码,一起来看一看这个流程。那这个预测是来自一个真实的业务比赛的一个数据,我们可以去将数据集下载下来,自己做一个测试。就去我上面提供的那个地址去下载就可以了,我这里就不进行提供了。

咱们这次的数据主要是 train 和 testA 两个数据集,先来把数据加载进来,然后简单的看一下,看这个数据长什么样:

train = pd.read_csv('dataset/train.csv')
test = pd.read_csv('dataset/testA.csv')
train.head()

我们今天简单写一写,给大家写个简单的 baseline,不会写那么完善。思路也是给大家梳理清楚。

第一个模块我们看一看唯一值的个数:比如我们要看一下其中isDefault这个特征的唯一值个数:

train['isDefault'].nunique()

---
2

我们知道,这个特征那肯定是只有 2 个值,不是 0 就是 1,它是最后的一个结果。现在,我们要查看所有特征的唯一值,需要先统计它都有哪些特征,对它的 column 来做个遍历。某一个特征唯一值个数是多少用 format,然后看它的 column 唯一值个数咱们依然还是用nunique()

for col in train.columns:
print('{} 特征,唯一值个数{}'.format(col, train[col].nunique()))

---
id 特征,唯一值个数 800000
...
n14 特征,唯一值个数 31

数据集我们知道,训练集一共是 80 万,ID 的唯一值是 80 万。那我们想,这个 ID 会放到模型中去完成训练吗?ID 每个数值都不一样,而且它物理上面是不具备含义的,不要放到模型中。所以需要 drop 掉。

同时上面也能看出来,还有关注哪些?关注那些唯一值少的,isDefault这个比较明显,因为预测分类就是违约、不违约。

我们从打印结果来看,policyCode个数唯一,我们其实还可以单独写一句话来去做判断,判断唯一值个数是否为 1。

for col in train.columns:
print('{} 特征,唯一值个数{}'.format(col, train[col].nunique()))
# 判断唯一值个数是否为 1
if train[col].nunique() == 1:
    print(col, '唯一值为 1 ################')

---
id 特征,唯一值个数 800000
...
policyCode 特征,唯一值个数 1
policyCode 唯一值为 1 ####################
...

如果它的nunique等于 1 的话我们把它打印出来。

然后方便起见,加一个特殊的符号,也是为了在打印结果中一眼就可以分辨哪些是,让它做一个提示。

找到这个结果,它告诉我们policyCode唯一,那我们对这个数据,看一眼它的value_counts

train['policyCode'].value_counts()

---
policyCode
1.0    800000
Name: count, dtype: int64

80w 行数据都等于 1,那么我们还需要将它放到模型区吗?不要放,因为放进去和不放进去是没有任何区别的。我们就把这个给它去掉。

那我们的策略目前是去掉 ID 和policyCode,用 drop 方法。我们要去掉它,那训练集和测试集就都要去,不能只去一个,需要两个都执行:

train.drop(['id', 'policyCode'], axis=1, inplace=True)
test.drop(['id', 'policyCode'], axis=1, inplace=True)
print(len(train.columns), len(test.columns))

---
45, 44

去掉以后, 原来的列数是 47 列,现在的列数是 45 列。

接下来,我们需要进行数据的清洗,首先我们需要对缺失值来进行补全。我们先看一看缺失值的个数。

# 统计缺失值个数
train.isnull().sum()

---
loanAmnt                  0
...
employmentLength      46799
...
dti                     239
...
n11                   69752
n12                   40270
n13                   40270
n14                   40270
dtype: int64

统计缺失值个数这里用的是isnull().sum()来做统计,isnull()是对每个字段是否为空来做个判断,如果它为空的话把空的个数求和。求出来之后,现在可以看出来我们是有一些为空的词段,有些个数还挺高的。

比如employmentLength等等,这些资料其实都为空,还是蛮多的。先不着急直接补全,我们还可以在数据探索方面再去探索一下。可以看看这个不同类别特征与 label 之间的一些关系,我们简单看几个比较关键的特征。带大家一起来看一看这个该怎么去写。比如说grade,看它是不是类别特征,查看数据的类型。

# 查看数据的类型
train.info()

---
<class 'pandas.core.frame.DataFrame'>
RangeIndex: 800000 entries, 0 to 799999
Data columns (total 45 columns):
#   Column              Non-Null Count   Dtype  
---  ------              --------------   -----  
0   loanAmnt            800000 non-null  float64
1   term                800000 non-null  int64  
...
4   grade               800000 non-null  object 
...
44  n14                 759730 non-null  float64
dtypes: float64(32), int64(8), object(5)
memory usage: 274.7+ MB

这里用的是 info 来去做一个查看,我们可以看到grade词段是 object,一般来说就是字符串的类型,可以来对它的grade去求一下value_counts

train['grade'].value_counts()

---
grade
B    233690
C    227118
A    139661
D    119453
E     55661
F     19053
G      5364

可以看到是是 A、B、C、D、E,F、G,所以它应该是属于类别特征。那不同的类别和最终违约会有怎样的关系呢?我们以柱状图的形式展示每个类别的数量。呈现图的时候用的 seaborn 里的 sns 来做一个呈现:

# 以直方图的形式,展示每个类型的数量
sns.countplot(x='grade', hue='isDefault', data=train)

在这里插入图片描述

这是一个 grade 跟 isDefault 两者之间来做一个判断,数据是我们的训练数据。可以看一看这个结果,怎么看 ABCDEFG 这些类别跟 isDefault 之间的关系呢?因为 isDefault 有两种类型,所以每一个 ABCDE 都有这两种类型的柱状图。那么哪些特征是比较好,不容易违约。从这张图里面大概是能判断出来,0 是代表好人,1 代表坏人.所以我们看 A 是比较好的,A 的话就不太违约。其次是 B,然后再是 C,还有就是 D。然后到 E 后面违约的比例上基本差不多了,E 的比例已经比较高了,F 和 G 就会更高。所以 A 到 G 之间是有一个顺序关系的,越是字母靠前的 A、B、C 就越不太容易违约。

那应该还有其他一些特征都可以来去做个判断,比如我们再挑一个,homeOwnership, 借款人在登记时提供的房屋所有权状况,看它的value_counts是 0 到 5:

train['homeOwnership'].value_counts()

---
homeOwnership
0    395732
1    317660
2     86309
3       185
5        81
4        33
Name: count, dtype: int64

那我们觉得理论上它应该是属于什么类别的特征?看一下物理含义,借款人在登记时提供的房屋所有权的状态,它没有小数点,0-5 虽然是数值,但是它应该是个类别特征。

那类别特征的话我们用的过程和刚才是一样的,来做个对比,拿这个词段作为 x 轴

sns.countplot(x='homeOwnership', hue='isDefault', data=train)

在这里插入图片描述

到后面其实这个值已经比较小了, 每个特征还是有一点明显的。0 是比较好的,然后 1 比较差,2 可能后面就逐渐的会下降。这些是属于跟房屋状态的一个特征相关的。

这些维度其实都可以做判断,我们也发现出来有些类别特征是可以找到 isDefault 的一个平均值的。

再看下面这个特征,这 0-13 看起来也像是类别特征

train['purpose'].value_counts()

---
purpose
0     464096
...
13       190

这是借款人在贷款时的贷款用途类别,跟刚才过程是一样的,依然是来看一个对比:

sns.countplot(x='purpose', hue='isDefault', data=train)

在这里插入图片描述

这个有点类似于像 A 到 G 的那个感觉。这个虽然是数值,但大家想想是不
是更倾向于看成一种类别的计算,我们想,类别最后也是要把它转化成一个数值类型的。因为这个统计个数没有小数点,所以我更倾向于把它看成是一种类别。就是已经转化成数值之后的一个类别。

基本上都看完之后我们看下一步,我们要设置一些状态,设置数值类型,然后来去做一些缺失值补全。

那下面我们就要找一找哪些是数值类型,哪些是类别特征。可以借助它的 dtype,那我们之前看过数据的 info, 一般来说 float 应该都是属于数值类型,这个是毫无疑问的。int 类型有些可能要属于类别之后的一个编码,就是它已经是类别加了 labelEncoder, 所以我们先用 float 来去做一个计算。

# 找到数值类型的特征
num_features = train.select_dtypes(include=float).columns
print('数值特征:{}'.format(num_features))

---
数值特征:Index(['loanAmnt', 'interestRate', 'installment', 'employmentTitle', 'annualIncome', 'postCode', 'dti''delinquency_2years', 'ficoRangeLow', 'ficoRangeHigh', 'openAcc', 'pubRec', 'pubRecBankruptcies', 'revolBal', 'revolUtil', 'totalAcc', 'title', 'n0', 'n1', 'n2', 'n3', 'n4', 'n5', 'n6', 'n7', 'n8', 'n9', 'n10', 'n11', 'n12', 'n13', 'n14'], dtype='object')

这些肯定是数值特征,因为它是属于 float。

计算完num_features,我们再计算 cat,找到类别。类别特征有一个简单的方法就是非 float 类型。包括了 int,包括了 object。那怎么去写?这个就和我们的数值类型的获取正好反过来:

cat_features = train.select_dtypes(exclude=float).columns
print('类别特征: {}'.format(cat_features))

---
类别特征: Index(['term', 'grade', 'subGrade', 'employmentLength', 'homeOwnership', 'verificationStatus', 'issueDate', 'isDefault', 'purpose', 'regionCode', 'initialListStatus', 'applicationType', 'earliesCreditLine'],dtype='object')

这回我们用的是exclude=float,include 是,exclude 就是。这样不是我们 float 类型的部分都等于我们的类别类型。这样类别特征就求出来了,这些都属于类别特征。

下一步就要做缺失值补全了。在模型预测之前,最好把缺失值给它补上,可以基于刚才统计好的类型和数值来去完成一个设置。

cat 是类别特征,然后我们来计算一下它当中有 null 值的都有多少:

train[cat_features].isnull().sum()

---
term                      0
...
employmentLength      46799
...
earliesCreditLine         0
dtype: int64

找到employmentLength是个缺失值,而且只有这一个。我们还是先来看看这个特征的value_counts

train['employmentLength'].value_counts()

---
employmentLength
10+ years    262753
2 years       72358
< 1 year      64237
3 years       64152
1 year        52489
5 years       50102
4 years       47985
6 years       37254
8 years       36192
7 years       35407
9 years       30272
Name: count, dtype: int64

看到这个内容,我们来看看怎么补充最好?对于这样一个类别特征,其实最好的办法是通过随机森林去计算,然后将缺失值分别补充进去,不过我们今天重点不在那里,所以现在我们拿众数来做补充。众数怎么求都不需要管,因为通过打印出来的结果,我们明显看到10+ years就是最多的。那我们就直接补进去就好了:

train['employmentLength'].fillna('10+ years', inplace=True)
train[cat_features].isnull().sum()

再查一遍类别特征的 isnull,全部都为 0 了。那我们类别特征就算是补全了。

相应的,测试集里我们也需要操作一遍:

test['employmentLength'].value_counts()
test['employmentLength'].fillna('10+ years', inplace=True)
test['employmentLength'].isnull().sum()

接下来是数值类型,跟刚才的过程原理是一样的。

train[num_features].isnull().sum()

---
...
employmentTitle           1
...
postCode                  1
dti                     239
...
pubRecBankruptcies      405
...
revolUtil               531
...
title                     1
n0                    40270
n1                    40270
n2                    40270
n3                    40270
n4                    33239
n5                    40270
n6                    40270
n7                    40270
...
n11                   69752
n12                   40270
n13                   40270
n14                   40270
dtype: int64

可以看到 num 里的缺失值还是蛮多的,似乎工作量不小。那我们怎么办,先拿employmentTitle这个来看,我们先查看一下它这个特征:

train['employmentTitle'].value_counts()

---
employmentTitle
54.0        51149
38.0        12644
32.0        11543
184.0        6112
151.0        5193
        ...  
56828.0         1
118913.0        1
208948.0        1
313793.0        1
134854.0        1
Name: count, Length: 248683, dtype: int64

特征值内全是浮点数,我们还是来看看它的中位数等于多少。

train['employmentTitle'].median()

---
7755.0

median 是等于 7,000 多,这个特征其实补 7000 多也可以,补 54 也可以,也就是众数。

那一个一个数值特征去处理有点太过麻烦了,我这里只是为了告诉大家思路而不是为了比赛,那我这里就简便的做一个批量处理。全部都用 median 来进行处理。

for col in num_features:
if train[col].isnull().sum() > 0:
    # print(col)
    train[col].fillna(train[col].median(), inplace=True)
if test[col].isnull().sum() > 0:
    test[col].fillna(train[col].median(), inplace=True)

train.isnull().sum()

---
loanAmnt              0
...
n14                   0
dtype: int64

这里我们将所有的 num 进行循环,用于处理每一个数值特征,当 null 的个数大于 0 的时候,也就是有 null 值的时候,我们就将其用 median 进行处理,inplace 打开。不要忘了除了 train 特征集之外,test 也做同样的处理。这样每个有缺失值的部分我们都可以给它做一个补全。

然后查看一下 train 和 test 的 null 值是否还有。这样,我们就完成了所有缺失值的一个补全。

之后我们要做的事情,就是要转化数值编码了。那要做处理的类别特征有哪些呢?我们之前查看 info 的时候,有很多的 object 类型,这就是我们现在要处理的内容。因为 object 类型,基本上都是字符串的形式。

train.select_dtypes(include=object).columns

---
Index(['grade', 'subGrade', 'employmentLength', 'issueDate', 'earliesCreditLine'], dtype='object')

找到这些特征,grade是其中一个需要处理的类别特征。这些类别特征其实都需要给它转换成数值编码,我们将其打印出来一遍后续查看

print('需要处理的类别特征: {}'.format(train.select_dtypes(include=object).columns))

然后我们先来处理grade,看看它是什么样的一个特征,以及特征个数都分别有多少:

train['grade'].value_counts()

---
grade
B    233690
C    227118
A    139661
D    119453
E     55661
F     19053
G      5364
Name: count, dtype: int64

刚刚我们应该能知道它是有一定的顺序的,就是我们之前看到过那张柱状图,可以发现 A 的等级是最好的,所以需要给它指定出来一个顺序,那这里的指定关系可以自己手写一下,去做一个类别编码。按照指定顺序进行类别编码,通过 map 的方式来去做一个指定。

train['grade'] = train['grade'].map({'A':0, 'B':1, 'C':2, 'D':3, 'E':4, 'F':5, 'G':6})
test['grade'] = test['grade'].map({'A':0, 'B':1, 'C':2, 'D':3, 'E':4, 'F':5, 'G':6})

那测试集一样,我们也需要做这样一个处理,编码规则一定是要一致。

做完以后我们再去对比一下它的value_counts

train['grade'].value_counts()

---
grade
1    233690
2    227118
0    139661
3    119453
4     55661
5     19053
6      5364
Name: count, dtype: int64

可以看到,目前就把它转化成了 0-6 之间,就是我们的 A 到 G 的一个转化。

我们回顾之前的 object 特征列表,第二个是subGrade,那现在就来处理这个特征,还是一样,先查看一下:

train['subGrade'].value_counts()

---
subGrade
C1    50763
...
G5      645
Name: count, dtype: int64

这个似乎有点麻烦。前面这个 ABCD…应该是属于大的类别,然后后面的 12345 属于小的类别。那现在怎么弄呢,我们创建一个临时变量,然后用这个临时变量做一个排序,再来观察下它的规律:

temp = train['subGrade'].value_counts()
temp.sort_index(ascending=True)

---
subGrade
A1    25909
A2    22124
A3    22655
A4    30928
A5    38045
...
G5      645
Name: count, dtype: int64

可以发现,这个 A 到 G,每一个大类别里都有 5 个小类别,还是比较规律的。那这样的话就比较好做了,我个人的做法,是干脆从 A1 到 G5 全部编成不同的数值,从 0 开始向后进行排列。

可以自己来去手工写,最笨的办法的话是一个手工的办法来去完成,简单写一个逻辑。

好,先来做一些前期工作,我们先写个 A 到 G 的列表,然后定义一个 index 和一个 map,用于作为中间值好做处理:

grades = ['A', 'B', 'C', 'D', 'E', 'F', 'G']
subgrade_index = 0
sub_map = {}

接着,我们就可以来做个循环了,将我们定义好的映射值放入sub_map:

for grade in grades:
for i in range(5):
    subgrade = grade+str(i+1)
    sub_map[subgrade]  = subgrade_index
    subgrade_index += 1

接下来呢,直接将subGrade这个特征做一个 map,然后我们来查看一下:

train['subGrade'] = train['subGrade'].map(sub_map)
train['subGrade'].value_counts().sort_index(ascending=True)

---
subGrade
0     25909
...
34      645
Name: count, dtype: int64

为了查看方便,给它做一个排序。这样,我们可以看到从 0 到 34,一个 35 个特征值就转化好了。

这个逻辑是我们现在 0-34 这 35 个类别,有这个类别我们再统一给它做一个 map。当然,之后测试集也是要做一样的处理:

test['subGrade'] = test['subGrade'].map(sub_map)

接着,我们再来查看一下目前的 object 特征还有哪些:

train.select_dtypes(include=object).columns

---
Index(['employmentLength', 'issueDate', 'earliesCreditLine'], dtype='object')

之前我们知道issueDate是属于日期类型,一共现在还有 3 个类别变量。那现在我们还是一个一个来,先从employmentLength开始,我们查看一下:

train['employmentLength'].value_counts()

---
employmentLength
10+ years    309552
2 years       72358
< 1 year      64237
3 years       64152
1 year        52489
5 years       50102
4 years       47985
6 years       37254
8 years       36192
7 years       35407
9 years       30272
Name: count, dtype: int64

这个顺序关系怎么去写呢,找一找看有没有一些比较巧的方法来去做一下。排下顺序看看:

temp = train['employmentLength'].value_counts()
temp.sort_index(ascending=True)

---
employmentLength
1 year        52489
10+ years    309552
2 years       72358
3 years       64152
4 years       47985
5 years       50102
6 years       37254
7 years       35407
8 years       36192
9 years       30272
< 1 year      64237
Name: count, dtype: int64

似乎也并没有特别好的方式,那我们还是用最笨的方法,直接用 map 来手工写一个好了:

train['employmentLength'] = train['employmentLength'].map({'< 1 year':0,  '1 year':1, '2 years':2, '3 years':3, '4 years':4, '5 years':5, '6 years':6, '7 years':7, '8 years':8, '9 years':9, '10+ years':10})
train['employmentLength'].value_counts().sort_index(ascending=True)

---
employmentLength
0      64237
1      52489
2      72358
3      64152
4      47985
5      50102
6      37254
7      35407
8      36192
9      30272
10    309552
Name: count, dtype: int64

这样就把这个数值呢给它 map 好以后再去映射回去,test 的应该也是一样的逻辑。

test['employmentLength'] = test['employmentLength'].map({'< 1 year':0,  '1 year':1, '2 years':2, '3 years':3, '4 years':4, '5 years':5, '6 years':6, '7 years':7, '8 years':8, '9 years':9, '10+ years':10})

处理好这个特征之后,我们就只剩下['issueDate', 'earliesCreditLine']这两个特征需要进行处理了。之前我们知道,issueDate是一个日期特征,也讲到了,处理日期特征要么就是将其分拆,做多尺度,要么就是使用 diff 的方式。这里,我们为了方便,选择 diff 的方式来处理。

首先我们是要将其转化为 Pandas 中的日期格式:

# 转化为 Pandas 中的日期格式
train['issueDate'] = pd.to_datetime(train['issueDate'])
test['issueDate'] = pd.to_datetime(test['issueDate'])
print('train min date: {}, test min date: {}'.format(train['issueDate'].min(), test['issueDate'].min()))

---
train min date: 2007-06-01 00:00:00, test min date: 2007-07-01 00:00:00

那最小的日期是 2007 年 6 月 1 号,那我们就设置一个起始时间,就将其设置为这个时间点:

# 设置起始时间
base_time = datetime.datetime.strptime('2007-06-01', '%Y-%m-%d')

其实时间设置好之后,就可以将特征值设定为 diff 的形式了。这里,我们写一个简单的匿名函数来完成:

# 设置日期类型特征值为 diff
train['issueDate'] = train['issueDate'].apply(lambda x: x-base_time).dt.days
test['issueDate'] = test['issueDate'].apply(lambda x: x-base_time).dt.days

train 和 test 都做了一个处理,然后我们来看看,特征打印出来现在是什么样。

train['issueDate']

---
0         2587
      ... 
799999    4079
Name: issueDate, Length: 800000, dtype: int64

这样就好了。我们接下来就还剩下最后一个特征需要进行处理,就是earliesCreditLine,先来看看它是什么样的:

train['earliesCreditLine']

---
0         Aug-2001
        ...   
799999    Feb-2002

竟然也是一个时间类型的特征,正好,之前处理issueDate的逻辑放在这里还是可以用:

train['earliesCreditLine'] = pd.to_datetime(train['earliesCreditLine'])
test['earliesCreditLine'] = pd.to_datetime(test['earliesCreditLine'])

base_time = datetime.datetime.strptime('1944-01-01', '%Y-%m-%d')

train['earliesCreditLine'] = train['earliesCreditLine'].apply(lambda x: x-base_time).dt.days
test['earliesCreditLine'] = test['earliesCreditLine'].apply(lambda x: x-base_time).dt.days

train['earliesCreditLine'].value_counts()

---
earliesCreditLine
21032    5567
      ... 
731         1
Name: count, Length: 720, dtype: int64

那现在呢,我们就将所有的数据都清洗好了,前期是需要花一点时间去做一个数据清洗的工作。大家可以仔细看一下我的整个写法的一个过程,重点还是要看思路,如果不是很熟悉没有关系,一点点来。第一次我可以带你来去写,后面你逐渐熟悉以后就知道它整个的过程原理了。

接着,我们就可以使用corr()来查看相关性了,因为所有特征都变成了数值类型。顺便,我们为了更直观一些,用一个热力图将其打印出来:

train.corr()
plt.figure(figsize=(32, 26))
sns.heatmap(train.corr(), annot=True)

在这里插入图片描述

annot=True,是将数值也写上去。那这张图我们的 figsize 就要设置的大一些才可以,因为图太小数字写上去可能有重影。

类别确实很很多,关注哪些值?一般怎么看呢?关注颜色高亮的值,0.95,0.9 这些值就比较有含义。可以自己对照一下,它们是一个高度相关性。还有就是绝对值负的这种数也很重要,它会给你高亮出来。

什么是相关和负相关我这里就不一一给大家看了,你回去可以自己来看一看,这个图都帮你高亮出来了。这是它的特征相关性。

在所有数据处理的工作做完之后呢,就轮到我们的模型上长了。这里我们用的是 LightGBM。

开头,我给到大家的一个祖传参数,这里可以拿过来创建模型:

clf = LGBMClassifier(
num_leaves=2**5-1, reg_alpha=0.25, reg_lambda=0.25, objective='binary', max_depth=-1, learning_rate=0.005, min_child_samples=3, random_state=2023, n_estimators=2000, subsample=1, colsample_bytree=1
)

那数据集我们是要进行分割的

train_X, test_X, train_y, test_y = train_test_split(train.drop(['isDefault'], axis=1), train['isDefault'], test_size=0.2, random_state=2023)

我们把这个数据集给它做个切分,看一看这个数据的效果,然后再把整个的全量数据拿去做个训练。因为原始数据,我们是要应该把isDefault给它 drop 掉。

然后现在去 fit 一下

clf.fit(train_X, train_y)
y_pred = clf.predict(test_X)
y_pred

---
[LightGBM] [Info] Number of positive: 127712, number of negative: 512288
[LightGBM] [Info] Auto-choosing row-wise multi-threading, the overhead of testing was 0.031736 seconds.
You can set `force_row_wise=true` to remove the overhead.
And if memory is not enough, you can set `force_col_wise=true`.
[LightGBM] [Info] Total Bins 3940
[LightGBM] [Info] Number of data points in the train set: 640000, number of used features: 44
[LightGBM] [Info] [binary:BoostFromScore]: pavg=0.199550 -> initscore=-1.389109
[LightGBM] [Info] Start training from score -1.389109
CPU times: user 3min 34s, sys: 15.3 s, total: 3min 50s
Wall time: 59 s

array([0, 0, 0, ..., 0, 0, 0])

fit 以后去 predict,然后我们将赋值后的y_pred打印出来,看它运行的结果。我在前面加了一个%%time,所以最后将整个的运行时间打印了出来,我们可以看到,一共用时是 3min 50s,那这样一个 80W 的数据,我们用了 2000 轮来进行 fit,最后的时间还是比较快的,可以看到 LightGBM 确实在速度上还是非常有优势的。那最后的array就是我们的y_pred

我们先拿它在提交结果之前看一下这个结果会怎么样,如果这个结果还可以,你可以直接用个全量的数据来去做个训练和预测,然后把这个结果输出来。

结果是打印出来了,我们想看看评分,这次的评分来看看两个结果,一个 Accuracy, 一个比赛要求的 AUC:

print('Accuracy: {}'.format(accuracy_score(y_pred, test_y)))
print('AUC : {}'.format(roc_auc_score(y_pred, test_y)))

---
Accuracy: 0.8058875
AUC : 0.6956198372904435

这个结果并不是很好,那我们现在来用一个全量的数据跑一下看看:

%%time
clf.fit(train.drop(['isDefault'], axis=1), train['isDefault'])
result = clf.predict_proba(test)[:, 1]
result

---
array([0.07173634, 0.33175495, 0.50728124, ..., 0.18494668, 0.27338017,
    0.03319145])

然后呢,我们从新读取一下 testA 的数据,我们只要它的 id,然后将结果放到里面去作为isDefault这个特征。最后输出一个 csv 文件,用于进行提交:

test2 = pd.read_csv('~/mount/Sync/data/AI_Cheats/531830/testA.csv')
test2 = test2[['id']]
test2['isDefault'] = result
test2.to_csv('dataset/baseline_lgb_2000.csv', index=False)

然后到比赛的页面上,我们在提交结果这个页面内将刚才输出的文件提交上去:

在这里插入图片描述

在等待一段时间之后,会给一个提交结果:

在这里插入图片描述

em… 这个分数就不要指望在比赛中有什么好的排名了,基本预测分数应该都是在 0.74 以上的,那这个分数预估在所有参赛人员名单里,也就是在 50%左右吧,反正排名名单里是基本找不到。

好,这样我们整个流程就完成了,当然,如果你看重比赛结果,想自己排名更好一点的话,可以尝试的方式就是更改自己使用的模型,可以事实 catBoost, XGBoost 等等,然后调整一下参数。更重要的,你可以自己用随机森林把之前我们要填补的缺失值都计算填写进去,而不是像我们目前这样随意的使用中位数来进行填充。

简单总结一下,我们的操作就是把类别变量和数值变量做一个区分,对数据做了一个清洗,然后让类别变量做数据编码的时候有一定的顺序,对时间类型求一个 diff,仅此而已。然后就用祖传参数来去完成训练。整个的结果是 0.72 分,基本上应该会在 3000 多名左右的位置
在整个 7,000 多个人队伍中呢也就是中游水平。

刚才我们整个的这个求解这套问题的思路,从数据加载、数据探索,再到数据的预处理、补全这样一套思路,最后到 LGBM 的计算这套过程。这个问题的思路跟我们之前课程中给大家讲 LGBM 基本上是一致的,LGBM 就是帮你来做这种分类任务的。具体做的过程中更多的时间都是在数据预处理的环节中。

好,我们来留个作业,就是大家自己去比赛的这个页面,完成我们今天的一个练习,然后自己提交一下,看看你的分数是多少。可以在我这篇文章下面进行留言,把你的分数告诉我。

在你做练习过程中可能会遇到各种问题,不论是咱们本文中讲解的一些概念原理还是在代码实战过程中调包使用等等,如果你遇到任何问题都可以和我来做一个交流。

那最后也非常感谢大家能学习我的这个教程,绝对有帮助的话,可以分享出去给其他更多的小伙伴看到。

今天的课程就到这里。那我们下节课再见,下节课还是关于 BI 的一些知识内容。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mfbz.cn/a/353729.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

###C语言程序设计-----C语言学习(5)#

前言&#xff1a;感谢您的关注哦&#xff0c;我会持续更新编程相关知识&#xff0c;愿您在这里有所收获。如果有任何问题&#xff0c;欢迎沟通交流&#xff01;期待与您在学习编程的道路上共同进步&#xff01; 一. 主干知识的学习 1.switch语句 switch语句可以处理多分支选…

ubuntu 22 安装 node,npm,vue

1:安装 nodejs sudo apt update curl -fsSL https://deb.nodesource.com/setup_20.x | sudo -E bash - sudo apt update && sudo apt install -y nodejs node -v 2:安装npm sudo npm install n -g npm -v 3:安装vite npm install vite -g 4:运行vue 把项目拷贝到…

公考之判断推理(一、图形推理)

一、前言 判断推理这一题型主要具体分为四种题型&#xff1a; 1.图形推理 2.类比推理 3.定义判断 4.逻辑判断每种题型做题方法又不一样。 才本文采用总分的形式结构。 每一小标题的下面紧接着就是总结。二、图形推理常见的命题形式 图形推理常见的命题形式&#xff1a; 1.…

鸿蒙ArkUI 宫格+列表+HttpAPI实现

鸿蒙ArkUI学习实现一个轮播图、一个九宫格、一个图文列表。然后请求第三方HTTPAPI加载数据&#xff0c;使用了axios鸿蒙扩展库来实现第三方API数据加载并动态显示数据。 import {navigateTo } from ../common/Pageimport axios, {AxiosResponse } from ohos/axiosinterface IDa…

ASP.NET Core基础之用扩展方法封装服务配置

阅读本文你的收获 了解C#中的扩展方法机制学会在ASP.NET Core 中&#xff0c;用扩展方法封装服务配置&#xff0c;使得代码更加简洁 一、什么是扩展方法 扩展方法使能够向现有类型添加方法&#xff0c;而无需创建新的派生类型、重新编译或以其他方式修改原始类型。 扩展方法…

从 React 到 Qwik:开启高效前端开发的新篇章

1. Qwik Qwik 是一个为构建高性能的 Web 应用程序而设计的前端 JavaScript 框架,它专注于提供即时启动性能,即使是在移动设备上。Qwik 的关键特性是它采用了称为“恢复性”的技术,该技术消除了传统前端框架中常见的 hydration 过程。 恢复性是一种序列化和恢复应用程序状态…

HbuilderX报错“Error: Fail to open IDE“,以及运行之后没有打开微信开发者,或者运行没有反应的解决办法

开始 问题:HbuilderX启动时,打开微信开发者工具报错"Error: Fail to open IDE",以及运行之后没有打开微信开发者,或者运行没有反应的解决办法! 解决办法: 按照步骤一步一步完成分析,除非代码报错,否则都是可以启动的 第一步:检查HbuildX是否登录账号 第二步:检查微信…

背后的魔术师----jsp

作为一名对技术充满热情的学习者&#xff0c;我一直以来都深刻地体会到知识的广度和深度。在这个不断演变的数字时代&#xff0c;我远非专家&#xff0c;而是一位不断追求进步的旅行者。通过这篇博客&#xff0c;我想分享我在某个领域的学习经验&#xff0c;与大家共同探讨、共…

嵌入式软件工程师面试题——2025校招社招通用(C/C++)(四十四)

说明&#xff1a; 面试群&#xff0c;群号&#xff1a; 228447240面试题来源于网络书籍&#xff0c;公司题目以及博主原创或修改&#xff08;题目大部分来源于各种公司&#xff09;&#xff1b;文中很多题目&#xff0c;或许大家直接编译器写完&#xff0c;1分钟就出结果了。但…

Linux编译实时内核和打补丁

目录 1.Linux内核2.实时内核3.编译实时内核3.1 准备3.2 获取内核源码3.3 编译3.4 设置GRUB确保启动到实时内核 4.给内核打补丁5.安装新的内核 1.Linux内核 https://github.com/torvalds/linux Linux内核是Linux操作系统的核心部分&#xff0c;它是操作系统的基本组成部分&…

研发日记,Matlab/Simulink避坑指南(七)——数据溢出钳位Bug

文章目录 前言 背景介绍 问题描述 分析排查 解决方案 总结归纳 前言 见《研发日记&#xff0c;Matlab/Simulink避坑指南(二)——非对称数据溢出Bug》 见《研发日记&#xff0c;Matlab/Simulink避坑指南(三)——向上取整Bug》 见《研发日记&#xff0c;Matlab/Simulink避坑…

棋盘(来源:第十四届蓝桥杯省赛JavaA/C/研究生组 , 第十四届蓝桥杯省赛PythonC组)

小蓝拥有 nn大小的棋盘&#xff0c;一开始棋盘上全都是白子。 小蓝进行了 m 次操作&#xff0c;每次操作会将棋盘上某个范围内的所有棋子的颜色取反(也就是白色棋子变为黑色&#xff0c;黑色棋子变为白色)。 请输出所有操作做完后棋盘上每个棋子的颜色。 输入格式 输入的第…

Qt扩展-QXlsx读写Excel配置使用

QXlsx读写Excel配置使用 一、概述1. 功能概述2. 其他维护 二、安装1. 下载源码2. 配置项目3. 测试代码4. 运行结果 一、概述 项目介绍&#xff1a;https://qtexcel.github.io/QXlsx/Example.html GitHub&#xff1a;https://github.com/QtExcel/QXlsx/tree/master QXlsx 是一个…

【算法】闇の連鎖(树上差分,LCA)

题目 传说中的暗之连锁被人们称为 Dark。 Dark 是人类内心的黑暗的产物&#xff0c;古今中外的勇者们都试图打倒它。 经过研究&#xff0c;你发现 Dark 呈现无向图的结构&#xff0c;图中有 N 个节点和两类边&#xff0c;一类边被称为主要边&#xff0c;而另一类被称为附加边…

C++设计模式介绍:优雅编程的艺术

物以类聚 人以群分 文章目录 简介为什么有设计模式&#xff1f; 设计模式七大原则单一职责原则&#xff08;Single Responsibility Principle - SRP&#xff09;开放封闭原则&#xff08;Open/Closed Principle - OCP&#xff09;里氏替换原则&#xff08;Liskov Substitution …

【C++修行之道】STL(初识list、stack)

目录 一、list 1.1list的定义和结构 以下是一个示例&#xff0c;展示如何使用list容器: 1.2list的常用函数 1.3list代码示例 二、stack 2.1stack的定义和结构 stack的常用定义 2.2常用函数 2.3stack代码示例 一、list 1.1list的定义和结构 list的使用频率不高&#…

常见的核函数

在机器学习中&#xff0c;特别是在支持向量机&#xff08;SVM&#xff09;和其他基于核的方法中&#xff0c;核函数是一种用来计算数据点在高维空间中相对位置的方法。核函数能够使得算法在不显式地映射数据到高维空间的情况下&#xff0c;仍然能够处理线性不可分的数据。常见的…

RPC教程 6.负载均衡

1.负载均衡策略 假设有多个服务实例&#xff0c;而每个实例都提供相同的功能&#xff0c;为了提高整个系统的吞吐量&#xff0c;每个实例部署在不同的机器上。客户端可以选择任意一个实例进行调用&#xff0c;获取想要的结果。那如何选择呢&#xff1f;取决于负载均衡的策略。…

【WPF.NET开发】WPF中的双向功能

本文内容 FlowDirectionFlowDocumentSpan 元素非文本元素的 FlowDirection数字替换 与其他任何开发平台不同&#xff0c;WPF 具有许多支持双向内容快速开发的功能&#xff0c;例如&#xff0c;同一文档中混合了从左到右和从右到左的数据。 同时&#xff0c;WPF 也为需要双向功…

JVM基础知识汇总篇

☆* o(≧▽≦)o *☆嗨~我是小奥&#x1f379; &#x1f4c4;&#x1f4c4;&#x1f4c4;个人博客&#xff1a;小奥的博客 &#x1f4c4;&#x1f4c4;&#x1f4c4;CSDN&#xff1a;个人CSDN &#x1f4d9;&#x1f4d9;&#x1f4d9;Github&#xff1a;传送门 &#x1f4c5;&a…