金融风控贷款预测之特征工程task3

import pandas as pd
import numpy as np
import warnings
warnings.filterwarnings('ignore')
import matplotlib.pyplot as plt
import seaborn as sns
train = pd.read_csv('data/train.csv')
testa = pd.read_csv('data/testA.csv')
train_copy = train.copy()
testa_copy = testa.copy()

提取不同数据类型数据

# 获取object对象
# 通过pandas.dataframe的select_dtypes方法
numerical_fea = list(train_copy.select_dtypes(exclude=['object']).columns)
# 通过filter函数实现差集运算
category_fea = list(filter(lambda x: x not in numerical_fea, list(train_copy.columns)))
# 在numerical_fea变量踢去标签列isDefault
label = 'isDefault'
# list的remove方法删除指定值
numerical_fea.remove(label)
# 打印不同数据类型的列名
print(numerical_fea)
print(category_fea)
print(len(numerical_fea), len(category_fea))
['id', 'loanAmnt', 'term', 'interestRate', 'installment', 'employmentTitle', 'homeOwnership', 'annualIncome', 'verificationStatus', 'purpose', 'postCode', 'regionCode', 'dti', 'delinquency_2years', 'ficoRangeLow', 'ficoRangeHigh', 'openAcc', 'pubRec', 'pubRecBankruptcies', 'revolBal', 'revolUtil', 'totalAcc', 'initialListStatus', 'applicationType', 'title', 'policyCode', 'n0', 'n1', 'n2', 'n2.1', 'n4', 'n5', 'n6', 'n7', 'n8', 'n9', 'n10', 'n11', 'n12', 'n13', 'n14']
['grade', 'subGrade', 'employmentLength', 'issueDate', 'earliesCreditLine']
41 5

填充空缺值

# 查看缺失值
# object类型只有employmentLength存在缺失值
for i in [numerical_fea, category_fea]:
    tmp = train_copy[i].isnull().sum()
    print(tmp[tmp>0])
    print('*'*50)
employmentTitle           1
postCode                  1
dti                     239
pubRecBankruptcies      405
revolUtil               531
title                     1
n0                    40270
n1                    40270
n2                    40270
n2.1                  40270
n4                    33239
n5                    40270
n6                    40270
n7                    40270
n8                    40271
n9                    40270
n10                   33239
n11                   69752
n12                   40270
n13                   40270
n14                   40270
dtype: int64
**************************************************
employmentLength    46799
dtype: int64
**************************************************
# 暂时不处理testa
############################
# numerical_fea
# 使用存在缺失值样本对应label类别的的中位数填充, 该方法没法给testa同样的处理。后续 会改变处理方法
# 这里仅尝试
for i in [0,1]:
    train_copy.loc[train_copy['isDefault']==i,numerical_fea] = train_copy.loc[train['isDefault']==i,numerical_fea]
                                                    .fillna(train_copy.loc[train_copy['isDefault']==i,numerical_fea].median())
# 查看填充后的结果
print(train_copy.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
n2.1                      0
n4                        0
n5                        0
n6                        0
n7                        0
n8                        0
n9                        0
n10                       0
n11                       0
n12                       0
n13                       0
n14                       0
dtype: int64
# 查看employmentLength
# 10+的最多,缺失的数据有4w+
print(train['employmentLength'].value_counts(dropna=False).sort_values())
print(train['employmentLength'].value_counts(dropna=False).sort_index())
9 years       30272
7 years       35407
8 years       36192
6 years       37254
NaN           46799
4 years       47985
5 years       50102
1 year        52489
3 years       64152
< 1 year      64237
2 years       72358
10+ years    262753
Name: employmentLength, dtype: int64





1 year        52489
10+ years    262753
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
NaN           46799
Name: employmentLength, dtype: int64
# 转换employmentLength
# 10+ -> 10 
# 1 -> 0
# NaN 暂时不动
def employmentLength_to_int(s):
    if pd.isnull(s):
        return s 
    else:
        return np.int8(s.split()[0])
# for data in [train_copy, testa_copy]: # 暂不处理testa
train_copy['employmentLength'].replace(to_replace='10+ years', value='10 years', inplace=True)
train_copy['employmentLength'].replace(to_replace='< 1 year', value='1 year', inplace=True)

转换

# employmentLength
train_copy.employmentLength = train_copy.employmentLength.apply(employmentLength_to_int)
print(train_copy['employmentLength'].value_counts(dropna=False).sort_values())
9.0      30272
7.0      35407
8.0      36192
6.0      37254
NaN      46799
4.0      47985
5.0      50102
3.0      64152
2.0      72358
1.0     116726
10.0    262753
Name: employmentLength, dtype: int64
pd.set_option('max_row',1000)
# 查看  issueDate值得类型  : str
# 因此issue为标准的时间字符,先不用动
# 最早发放贷款日期为2007-06-01; 
type(train_copy.issueDate[0])
(train_copy.issueDate.value_counts().sort_index())
2007-06-01        1
2007-07-01       21
2007-08-01       23
2007-09-01        7
2007-10-01       26
2007-11-01       24
2007-12-01       55
2008-01-01       91
2008-02-01      105
2008-03-01      130
2008-04-01       92
2008-05-01       38
2008-06-01       33
2008-07-01       52
2008-08-01       38
2008-09-01       19
2008-10-01       62
2008-11-01      113
2008-12-01      134
2009-01-01      145
2009-02-01      160
2009-03-01      162
2009-04-01      166
2009-05-01      190
2009-06-01      191
2009-07-01      223
2009-08-01      231
2009-09-01      270
2009-10-01      305
2009-11-01      376
2009-12-01      362
2010-01-01      355
2010-02-01      394
2010-03-01      418
2010-04-01      481
2010-05-01      578
2010-06-01      600
2010-07-01      654
2010-08-01      677
2010-09-01      623
2010-10-01      670
2010-11-01      646
2010-12-01      765
2011-01-01      855
2011-02-01      812
2011-03-01      850
2011-04-01      917
2011-05-01     1019
2011-06-01     1087
2011-07-01     1096
2011-08-01     1139
2011-09-01     1238
2011-10-01     1258
2011-11-01     1343
2011-12-01     1310
2012-01-01     1566
2012-02-01     1566
2012-03-01     1740
2012-04-01     1951
2012-05-01     1980
2012-06-01     2299
2012-07-01     2774
2012-08-01     3265
2012-09-01     3661
2012-10-01     3693
2012-11-01     3849
2012-12-01     3551
2013-01-01     4016
2013-02-01     4462
2013-03-01     4918
2013-04-01     5627
2013-05-01     6116
2013-06-01     6424
2013-07-01     7052
2013-08-01     7490
2013-09-01     7733
2013-10-01     8409
2013-11-01     8748
2013-12-01     8948
2014-01-01     9273
2014-02-01     9105
2014-03-01     9645
2014-04-01    10830
2014-05-01    10886
2014-06-01     9665
2014-07-01    16355
2014-08-01    10648
2014-09-01     5898
2014-10-01    21461
2014-11-01    13793
2014-12-01     5528
2015-01-01    19254
2015-02-01    12881
2015-03-01    13549
2015-04-01    18929
2015-05-01    17119
2015-06-01    15236
2015-07-01    24496
2015-08-01    18750
2015-09-01    14950
2015-10-01    25525
2015-11-01    19453
2015-12-01    23245
2016-01-01    16792
2016-02-01    20571
2016-03-01    29066
2016-04-01    14248
2016-05-01    10680
2016-06-01    12270
2016-07-01    12835
2016-08-01    13301
2016-09-01    10165
2016-10-01    11245
2016-11-01    11172
2016-12-01    11562
2017-01-01     9757
2017-02-01     8057
2017-03-01    10068
2017-04-01     7746
2017-05-01     9620
2017-06-01     9005
2017-07-01     8861
2017-08-01     9172
2017-09-01     8100
2017-10-01     7129
2017-11-01     7306
2017-12-01     5915
2018-01-01     5176
2018-02-01     3995
2018-03-01     4228
2018-04-01     4160
2018-05-01     3933
2018-06-01     2878
2018-07-01     2550
2018-08-01     2108
2018-09-01     1427
2018-10-01     1252
2018-11-01      962
2018-12-01      746
Name: issueDate, dtype: int64
# 查看 earliesCreditLine
# 共720个不同值
print(len(train_copy.earliesCreditLine.value_counts()))
print(train_copy.earliesCreditLine.value_counts().head())
# 使用calendar模块,将月份英文简写转换为数字在拼接如200108
# calendar属性month_abbr可以得到月份英文简写和数字得转换
import calendar
month_abbr = list(calendar.month_abbr)
720
Aug-2001    5567
Sep-2003    5403
Aug-2002    5403
Oct-2001    5258
Aug-2000    5246
Name: earliesCreditLine, dtype: int64
train_copy.earliesCreditLine[0].split('-')
['Aug', '2001']
def transform_earliesCreditLine(s):
    tmp = s.strip().split('-')
    tmp[0] = str(month_abbr.index(tmp[0]))
    # 将1位得数字,如8,前面补零。 08
    if len(tmp[0]) == 1:
        tmp[0] = '0' + tmp[0]
    
    return ''.join([tmp[1],tmp[0]])
#  这样转换是便于后面时间上得特征构造
train_copy.earliesCreditLine = train_copy.earliesCreditLine.apply(transform_earliesCreditLine)
train_copy[['earliesCreditLine','issueDate']].head()
earliesCreditLine issueDate
0 200108 2014-07-01
1 200205 2012-08-01
2 200605 2015-10-01
3 199905 2015-08-01
4 197708 2016-03-01
# 类别型数据还剩grade ,subGrade 没处理
# 另外 employmentLength 得空缺值还未处理
# 这样先将grade, subGrade 标签化
# grade明显有等级区别
train_copy.grade = train_copy.grade.map({'A':1,'B':2,'C':3,'D':4,'E':5,'F':6,'G':7})
# subGrade也有等级区别
# 查看subGrade
# a-g, 每个等级有5个小等级区别,因此准备转换,如 c1,c2 -> 3.1,3.2
# train_copy.subGrade.value_counts()
transform_subGrade_map = {'A':1,'B':2,'C':3,'D':4,'E':5,'F':6,'G':7}
def transform_subGrade(s):
    return float(str(transform_subGrade_map.get(s.strip()[0])) + '.' + s[1:])

train_copy.subGrade = train_copy.subGrade.apply(transform_subGrade)
print(train_copy.subGrade.head())
0    5.2
1    4.2
2    4.3
3    1.4
4    3.2
Name: subGrade, dtype: float64
pd.set_option('max_column', 1000)
train_copy.head()
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 n2.1 n4 n5 n6 n7 n8 n9 n10 n11 n12 n13 n14
0 0 35000.0 5 19.52 917.97 5 5.2 320.0 2.0 2 110000.0 2 2014-07-01 1 1 137.0 32 17.05 0.0 730.0 734.0 7.0 0.0 0.0 24178.0 48.9 27.0 0 0 200108 1.0 1.0 0.0 2.0 2.0 2.0 4.0 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 4 4.2 219843.0 5.0 0 46000.0 2 2012-08-01 0 0 156.0 18 27.83 0.0 700.0 704.0 13.0 0.0 0.0 15096.0 38.9 18.0 1 0 200205 1723.0 1.0 0.0 3.0 5.0 5.0 10.0 7.0 7.0 7.0 13.0 5.0 13.0 0.0 0.0 0.0 2.0
2 2 12000.0 5 16.99 298.17 4 4.3 31698.0 8.0 0 74000.0 2 2015-10-01 0 0 337.0 14 22.77 0.0 675.0 679.0 11.0 0.0 0.0 4606.0 51.8 27.0 0 0 200605 0.0 1.0 0.0 0.0 3.0 3.0 0.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 1 1.4 46854.0 10.0 1 118000.0 1 2015-08-01 0 4 148.0 11 17.21 0.0 685.0 689.0 9.0 0.0 0.0 9948.0 52.6 28.0 1 0 199905 4.0 1.0 6.0 4.0 6.0 6.0 4.0 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 3 3.2 54.0 NaN 1 29000.0 2 2016-03-01 0 10 301.0 21 32.16 0.0 690.0 694.0 12.0 0.0 0.0 2942.0 32.0 27.0 0 0 197708 11.0 1.0 1.0 2.0 7.0 7.0 2.0 4.0 9.0 10.0 15.0 7.0 12.0 0.0 0.0 0.0 4.0
# 查看非数值类型列
# 只有两个时间列,且值都为str, 为之后特征构造打下基础
train_copy.select_dtypes(include=['object']).head()
issueDate earliesCreditLine
0 2014-07-01 200108
1 2012-08-01 200205
2 2015-10-01 200605
3 2015-08-01 199905
4 2016-03-01 197708

查看异常值

在统计学中,如果一个数据分布近似正态,那么大约 68% 的数据值会在均值的一个标准差

范围内,大约 95% 会在两个标准差范围内,大约 99.7% 会在三个标准差范围内。

  1. 采用均方差检测异常值
    • 在这个区间外的样本为异常值[mean-3std, mean+3std]
  2. 箱型检测异常值
    • 在这个区间外的样本为异常值[q1-3 * iqr, q3+3 * iqr],iqr=q3-q1
#  均方差检测
def find_outliers_by_3segama(data, fea):
    data_std = np.std(data[fea])
    data_mean = np.mean(data[fea])
    outliers_cut_off = data_std * 3
    lower_off = data_mean - outliers_cut_off
    upper_off = data_mean + outliers_cut_off
    data[fea + '_outliers'] = data[fea].apply(lambda x:str('异常值') if x<lower_off or x>upper_off else str('正常值'))
    return data

  • 没有异常值的变量:id,loanAmnt, term ,employmentTitle ,verificationStatus ,initialListStatus ,policyCode
  • 异常值在label=1上的分布>或<整体分布一个百分点的变量:interestRate,dti ,ficoRangeLow ,ficoRangeHigh ,n14
print('在label=1上的整体分布:',round(train_copy.isDefault.sum()/train_copy.shape[0],2))
for fea in numerical_fea:
    train_copy = find_outliers_by_3segama(train_copy, fea)
    print(fea,'异常值在label=1上的分布:',round(train_copy.groupby(fea + '_outliers')['isDefault'].sum()/train_copy[train_copy[fea + '_outliers']=='异常值'].shape[0],2)[0])

在label=1上的整体分布: 0.2
id 异常值在label=1上的分布: inf
loanAmnt 异常值在label=1上的分布: inf
term 异常值在label=1上的分布: inf
interestRate 异常值在label=1上的分布: 0.51
installment 异常值在label=1上的分布: 0.27
employmentTitle 异常值在label=1上的分布: inf
homeOwnership 异常值在label=1上的分布: 0.21
annualIncome 异常值在label=1上的分布: 0.13
verificationStatus 异常值在label=1上的分布: inf
purpose 异常值在label=1上的分布: 0.21
postCode 异常值在label=1上的分布: 0.21
regionCode 异常值在label=1上的分布: 0.17
dti 异常值在label=1上的分布: 0.3
delinquency_2years 异常值在label=1上的分布: 0.23
ficoRangeLow 异常值在label=1上的分布: 0.07
ficoRangeHigh 异常值在label=1上的分布: 0.07
openAcc 异常值在label=1上的分布: 0.24
pubRec 异常值在label=1上的分布: 0.23
pubRecBankruptcies 异常值在label=1上的分布: 0.24
revolBal 异常值在label=1上的分布: 0.14
revolUtil 异常值在label=1上的分布: 0.44
totalAcc 异常值在label=1上的分布: 0.2
initialListStatus 异常值在label=1上的分布: inf
applicationType 异常值在label=1上的分布: 0.25
title 异常值在label=1上的分布: 0.16
policyCode 异常值在label=1上的分布: inf
n0 异常值在label=1上的分布: 0.2
n1 异常值在label=1上的分布: 0.26
n2 异常值在label=1上的分布: 0.29
n2.1 异常值在label=1上的分布: 0.29
n4 异常值在label=1上的分布: 0.22
n5 异常值在label=1上的分布: 0.19
n6 异常值在label=1上的分布: 0.23
n7 异常值在label=1上的分布: 0.24
n8 异常值在label=1上的分布: 0.21
n9 异常值在label=1上的分布: 0.29
n10 异常值在label=1上的分布: 0.24
n11 异常值在label=1上的分布: 0.2
n12 异常值在label=1上的分布: 0.23
n13 异常值在label=1上的分布: 0.22
n14 异常值在label=1上的分布: 0.3

虽然删除异常值没标签整体分布没有影响,但还是先不删除

# # 查看删除异常值后标签整体分布有无影响
# 从结果看,删除异常值没有影响标签整体分布
tmp = train_copy.copy()
for fea in numerical_fea:
    tmp = tmp[tmp[fea + '_outliers']=='正常值']
print(train_copy.shape)
print(tmp.shape)


print(train_copy.isDefault.sum()/train_copy.shape[0])
print(tmp.isDefault.sum()/tmp.shape[0])
(800000, 88)
(612742, 88)
0.1995125
0.19509189838463822

连续数值分箱(或者分桶)

  • 特征分箱的目的:

    • 从模型效果上来看,特征分箱主要是为了降低变量的复杂性,减少变量噪音对模型的影响,提高自变量和因变量的相关度。从而使模型更加稳定。
  • 数据分桶的对象:

    • 将连续变量离散化
    • 将多状态的离散变量合并成少状态
  • 分箱的原因:

    • 数据的特征内的值跨度可能比较大,对有监督和无监督中如k-均值聚类它使用欧氏距离作为相似度函数来测量数据点之间的相似度。都会造成大吃小的影响,其中一种解决方法是对计数值进行区间量化即数据分桶也叫做数据分箱,然后使用量化后的结果。
  • 分箱的优点:

    • 处理缺失值:当数据源可能存在缺失值,此时可以把null单独作为一个分箱。
    • 处理异常值:当数据中存在离群点时,可以把其通过分箱离散化处理,从而提高变量的鲁棒性(抗干扰能力)。例如,age若出现200这种异常值,可分入“age > 60”这个分箱里,排除影响。
    • 业务解释性:我们习惯于线性判断变量的作用,当x越来越大,y就越来越大。但实际x与y之间经常存在着非线性关系,此时可经过WOE变换。
  • 特别要注意一下分箱的基本原则:

    • (1) 最小分箱占比不低于5%
    • (2) 箱内不能全部是好客户
    • (3) 连续箱单调
# 

# 10分位分箱
pd.qcut(train_copy['loanAmnt'], 10, labels=False)
# 卡方分箱
0         9
1         6
2         4
3         4
4         0
         ..
799995    8
799996    6
799997    1
799998    7
799999    3
Name: loanAmnt, Length: 800000, dtype: int64
from scipy.stats import chi2


简单的特征构造(特征交互)

  • 代价是非常耗时,效果还未知;最好是基于业务和数据探索结论来做特征构造或交互,不要一上来就各种统计量堆叠。

特征处理

  • (不同算法输入的数据要求也不同,此处特征的处理需要适应所选算法。过程会很繁琐,不要让变量混乱分不清,做好变量管理)

特征选择

  • 特征选择方法很多。选择原则应耗时尽可能少,逻辑尽可能合理。 此处不存在哪种特征选择方法会让模型结果精度高的说法,精度取决于数据质量(就是之前关于数据各种操作)

算法选择

  • 算法都有一个假设前提。 因此输入模型的数据需要尽可能处理成符合假设前提。

原文地址:https://www.cnblogs.com/Alexisbusyblog/p/13709487.html