2020-12-10

一、写在前面的话

这是我的第一篇博客,希望写好。我几乎是一个编程小白,只有一点点C和Java的经验,一路懵头懵脑的成为了一名经济学渣硕(真的是渣)。研一下学期开了一门《数据挖掘与分析》的课,虽然我也不知道经济学为什么会开这种课,也许是因为学院顶了一个“大数据”的高大上(假大空)头衔,无论怎样,我与机器学习的缘分从此结下。之后开启了一路踩坑的自学之旅,到了今天总算能自己独立写出一个数据挖掘的流程了,虽然很烂,但我相信以后会更好。。。

二、赛题介绍

本次比赛是天池的学习赛,赛题为预测用户贷款是否违约,是一个典型的分类问题。数据来自某信贷平台的贷款记录,总数据量超过120w,包含47列变量信息,其中15列为匿名变量。为了保证比赛的公平性,将会从中抽取80万条作为训练集,20万条作为测试集A,20万条作为测试集B,同时对employmentTitle、purpose、postCode和title等信息进行脱敏。其中isDefault字段为标签。

数据集包含三个下载文件
train.csv:训练集
test.csv:测试集
sample_submit.csv:提交文件样式

三、数据探索&数据预处理

先导入一些必要的包,并读取数据集。

import numpy as np
import pandas as pd 
import matplotlib.pyplot as plt
import seaborn as sns
import datetime
from lightgbm.sklearn import LGBMClassifier
from sklearn.metrics import roc_auc_score
from sklearn.model_selection import StratifiedKFold

import warnings
warnings.filterwarnings('ignore')
%matplotlib inline

train=pd.read_csv('DataSet/贷款违约预测/train.csv')
test=pd.read_csv('DataSet/贷款违约预测/testA.csv')

我们一开始最关心的倒不是各个特征的分布,而是各特征的数据类型以及每个特征有多少种不同的取值。

train.info()
test.info()
for feature in train.columns:
    print("{}特征有个{}不同的值".format(feature,train[feature].nunique()))

output1:


RangeIndex: 800000 entries, 0 to 799999
Data columns (total 47 columns):
 #   Column              Non-Null Count   Dtype  
---  ------              --------------   -----  
 0   id                  800000 non-null  int64  
 1   loanAmnt            800000 non-null  float64
 2   term                800000 non-null  int64  
 3   interestRate        800000 non-null  float64
 4   installment         800000 non-null  float64
 5   grade               800000 non-null  object 
 6   subGrade            800000 non-null  object 
 7   employmentTitle     799999 non-null  float64
 8   employmentLength    753201 non-null  object 
 9   homeOwnership       800000 non-null  int64  
 10  annualIncome        800000 non-null  float64
 11  verificationStatus  800000 non-null  int64  
 12  issueDate           800000 non-null  object 
 13  isDefault           800000 non-null  int64  
 14  purpose             800000 non-null  int64  
 15  postCode            799999 non-null  float64
 16  regionCode          800000 non-null  int64  
 17  dti                 799761 non-null  float64
 18  delinquency_2years  800000 non-null  float64
 19  ficoRangeLow        800000 non-null  float64
 20  ficoRangeHigh       800000 non-null  float64
 21  openAcc             800000 non-null  float64
 22  pubRec              800000 non-null  float64
 23  pubRecBankruptcies  799595 non-null  float64
 24  revolBal            800000 non-null  float64
 25  revolUtil           799469 non-null  float64
 26  totalAcc            800000 non-null  float64
 27  initialListStatus   800000 non-null  int64  
 28  applicationType     800000 non-null  int64  
 29  earliesCreditLine   800000 non-null  object 
 30  title               799999 non-null  float64
 31  policyCode          800000 non-null  float64
 32  n0                  759730 non-null  float64
 33  n1                  759730 non-null  float64
 34  n2                  759730 non-null  float64
 35  n3                  759730 non-null  float64
 36  n4                  766761 non-null  float64
 37  n5                  759730 non-null  float64
 38  n6                  759730 non-null  float64
 39  n7                  759730 non-null  float64
 40  n8                  759729 non-null  float64
 41  n9                  759730 non-null  float64
 42  n10                 766761 non-null  float64
 43  n11                 730248 non-null  float64
 44  n12                 759730 non-null  float64
 45  n13                 759730 non-null  float64
 46  n14                 759730 non-null  float64
dtypes: float64(33), int64(9), object(5)
memory usage: 286.9+ MB

output2:

id特征有个800000不同的值
loanAmnt特征有个1540不同的值
term特征有个2不同的值
interestRate特征有个641不同的值
installment特征有个72360不同的值
grade特征有个7不同的值
subGrade特征有个35不同的值
employmentTitle特征有个248683不同的值
employmentLength特征有个11不同的值
homeOwnership特征有个6不同的值
annualIncome特征有个44926不同的值
verificationStatus特征有个3不同的值
issueDate特征有个139不同的值
isDefault特征有个2不同的值
purpose特征有个14不同的值
postCode特征有个932不同的值
regionCode特征有个51不同的值
dti特征有个6321不同的值
delinquency_2years特征有个30不同的值
ficoRangeLow特征有个39不同的值
ficoRangeHigh特征有个39不同的值
openAcc特征有个75不同的值
pubRec特征有个32不同的值
pubRecBankruptcies特征有个11不同的值
revolBal特征有个71116不同的值
revolUtil特征有个1286不同的值
totalAcc特征有个134不同的值
initialListStatus特征有个2不同的值
applicationType特征有个2不同的值
earliesCreditLine特征有个720不同的值
title特征有个39644不同的值
policyCode特征有个1不同的值
n0特征有个39不同的值
n1特征有个33不同的值
n2特征有个50不同的值
n3特征有个50不同的值
n4特征有个46不同的值
n5特征有个65不同的值
n6特征有个107不同的值
n7特征有个70不同的值
n8特征有个102不同的值
n9特征有个44不同的值
n10特征有个76不同的值
n11特征有个5不同的值
n12特征有个5不同的值
n13特征有个28不同的值
n14特征有个31不同的值

这里只列出训练集,测试集与此相似。考虑到训练集有80万个样本,可以考虑使用一些集成算法。结合赛题背景,我们发现:
1.employmentLength应为连续特征,我们需要把它变成数值类型。
2.issueDate和earliesCreditLine虽然是时间,但我们可以把它们减去一个基期时间,变成一个连续特征。
3.离散特征没有缺失,部分连续特征存在缺失,考虑回归填充。
4.id特征是无意义的,直接剔除。
5.policyCode特征只有一个取值,直接剔除。

接下来按照惯例继续查看数据集基本信息

train.describe()
test.describe()
train.head()
test.head()

好吧,并没有什么新的发现,果然还是info最有用。
通过上面的探索,结合金融背景,我们已经可以把离散特征和连续特征分开了

cat_features=['grade','subGrade','employmentTitle','homeOwnership','verificationStatus','purpose',
              'postCode','regionCode','initialListStatus','applicationType']

num_features=['loanAmnt','term','interestRate','installment','employmentLength','annualIncome',
              'dti','delinquency_2years','ficoRangeLow','ficoRangeHigh','openAcc','pubRec',
              'pubRecBankruptcies','revolBal','revolUtil','totalAcc','earliesCreditLine','n0',
              'n1','n2','n3','n4','n5','n6','n7','n8','n9','n10','n11','n12','n13','n14']

没啥用的特征我们先删了

train=train.drop(['id','policyCode'],axis=1)
test=test.drop(['id','policyCode'],axis=1)

我们打算使用lightgbm算法(别问我为什么不用catboost),lightgbm相比于xgboost一个大的改进就在于可以处理离散特征,不用再one_hot编码(据开发者说,这种方式比one_hot编码效果要好)。lightgbm对喂给模型的离散特征有一些要求,详见lightgbm离散特征处理。
接下来明确我们的路线:
1.检查非负且含0
2.label coding
3.转为category

for i in ['employmentTitle','homeOwnership','verificationStatus','purpose','initialListStatus','applicationType']:
    for j in train[i]:
        if int(j) < 0:
            print(i+'在训练集中存在负值')
for i in cat_features:
    for j in test[i]:
        if int(j) < 0:
            print(i+'测试集中存在负值')
            
for i in cat_features:
    for j in train[i]:
        if int(j) == 0:
            print(i+"在训练集中有效")
for i in cat_features:
    for j in train[i]:
        if int(j) == 0:
            print(i+"在训练集中有效")

并未发现非负,且都含0,之后我们继续进行label coding

for i in [train]:
    i['grade'] = i['grade'].map({'A':0,'B':1,'C':2,'D':3,'E':4,'F':5,'G':6})
    i['subGrade'] = i['subGrade'].map({'E2':0,'D2':1,'D3':2,'A4':3,'C2':4,'A5':5,'C3':6,'B4':7,'B5':8,'E5':9,
        'D4':10,'B3':11,'B2':12,'D1':13,'E1':14,'C5':15,'C1':16,'A2':17,'A3':18,'B1':19,
        'E3':20,'F1':21,'C4':22,'A1':23,'D5':24,'F2':25,'E4':26,'F3':27,'G2':28,'F5':29,
        'G3':30,'G1':31,'F4':32,'G4':33,'G5':34})
for i in [test]:
    i['grade'] = i['grade'].map({'A':0,'B':1,'C':2,'D':3,'E':4,'F':5,'G':6})
    i['subGrade'] = i['subGrade'].map({'E2':0,'D2':1,'D3':2,'A4':3,'C2':4,'A5':5,'C3':6,'B4':7,'B5':8,'E5':9,
        'D4':10,'B3':11,'B2':12,'D1':13,'E1':14,'C5':15,'C1':16,'A2':17,'A3':18,'B1':19,
        'E3':20,'F1':21,'C4':22,'A1':23,'D5':24,'F2':25,'E4':26,'F3':27,'G2':28,'F5':29,
        'G3':30,'G1':31,'F4':32,'G4':33,'G5':34})

我们接下来要看看训练集和测试集中特征分布是否一致,不一致的话会影响模型泛化性能。

plt.figure(figsize=(16, 8))
i = 1
for fea in cat_features:
    if train[fea].nunique()<100:
        plt.subplot(2, 4, i)
        i += 1
        v = train[fea].value_counts()
        fig = sns.barplot(x=v.index, y=v.values)
        for item in fig.get_xticklabels():
            item.set_rotation(90)
        plt.title(fea)
plt.tight_layout()
plt.show()
plt.figure(figsize=(16, 8))
i = 1
for fea in cat_features:
    if test[fea].nunique()<100:
        plt.subplot(2, 4, i)
        i += 1
        v = test[fea].value_counts()
        fig = sns.barplot(x=v.index, y=v.values)
        for item in fig.get_xticklabels():
            item.set_rotation(90)
        plt.title(fea)
plt.tight_layout()
plt.show()

output3:


在这里插入图片描述

在这里插入图片描述

我们发现训练集和测试集的离散特征分布是一致的
最后我们转为category

train[cat_features]=train[cat_features].astype('category')
test[cat_features]=test[cat_features].astype('category')

接下来我们要看看不同的离散特征与标签是否有关系,我们只选择取值种类较少的离散特征

sns.countplot(x='grade',hue='isDefault',data=train)
在这里插入图片描述

我们发现grade与isDefault还是有很大关系的,grade越大,违约比例就越大,到了5、6级违约和不违约就基本是55开了,那可不,人家可是高级贷款,违个约那还不是常规操作?

sns.countplot(x='homeOwnership',hue='isDefault',data=train)
在这里插入图片描述

homeOwnership和isDefault的关系并不大


在这里插入图片描述

verificationStatus和isDefault的关系很大,verification越大,违约越多。

sns.countplot(x='purpose',hue='isDefault',data=train)
在这里插入图片描述

虽然purpose本身的分布很不均衡,但我们仍然可以看出不同purpose中违约比例大致是相同的。

sns.countplot(x='initialListStatus',hue='isDefault',data=train)
在这里插入图片描述

initialListStatus和isDefault没什么关系

applicationType分布不均衡,就不画了。
离散特征的分析处理就到这里啦,下面开始分析处理连续特征。

我们先转变一下employmentLength、earliesCrediLine的格式,它们应该是数值类型

for i in [train]:
    i['earliesCreditLine']=i['earliesCreditLine'].apply(lambda x: int(x[-4:]))
    i['employmentLength']=i['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})
for i in [test]:
    i['earliesCreditLine']=i['earliesCreditLine'].apply(lambda x: int(x[-4:]))
    i['employmentLength']=i['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可以减去一个基期时间转变成连续特征

train['issueDate']=pd.to_datetime(train['issueDate'],format='%Y-%m-%d')
base_time=datetime.datetime.strptime('2007-01-01','%Y-%m-%d')
train['issueDate']=train['issueDate'].apply(lambda x:x-base_time).dt.days

test['issueDate']=pd.to_datetime(test['issueDate'],format='%Y-%m-%d')
base_time=datetime.datetime.strptime('2007-01-01','%Y-%m-%d')
test['issueDate']=test['issueDate'].apply(lambda x:x-base_time).dt.days

train['earliesCreditLine']=train['earliesCreditLine'].apply(lambda x:x-1940)
test['earliesCreditLine']=test['earliesCreditLine'].apply(lambda x:x-1940)

接着用中位数填充缺失值

train[num_features]=train[num_features].fillna(train[num_features].median())
test[num_features]=test[num_features].fillna(test[num_features].median())

与离散特征相同地,我们需要先看看训练集和测试集的连续特征分布是否一致,这里我们用KDE(核密度估计,直方图的加窗平滑)`

dist_cols=6
dist_rows=len(num_features)
plt.figure(figsize=(5*dist_cols,5*dist_rows))
i=1
for col in num_features:
    ax=plt.subplot(dist_rows,dist_cols,i)
    ax=sns.kdeplot(train[col],color='Red',shade=True)
    ax=sns.kdeplot(test[col],color='Blue',shade=True)
    ax.set_xlabel(col)
    ax.set_ylabel('Frequency')
    ax=ax.legend(['train','test'])
    i+=1
plt.show()
在这里插入图片描述

太多了就只展示一部分,训练集和测试集分布是一致的。
部分特征因为取值种类十分有限所以看起来是空白的。
因为lightgbm对特征的尺度不敏感,所以我们就不做归一化和异常值处理了。

最后我们看一下相关性矩阵和热力图

pd.set_option('display.max_rows',10)
pd.set_option('display.max_columns',10)
train_corr=train.corr()
train_corr

output4:

在这里插入图片描述

然后是热力图

plt.figure(figsize=(10,8))
sns.heatmap(train_corr,vmax=0.8,linewidths=0.05,cmap=sns.cm.rocket_r)
在这里插入图片描述

除了term、interestRate与标签相关性稍大一点,其余特征与标签的相关性都较小。n系列之间存在较大的相关性。

三、特征工程

前面得出term、interestRate与标签相关性比较大,做一些简单的组合

related_col=['term','interestRate']

for i in related_col:
    for j in related_col:
        train['new'+i+'*'+j]=train[i]*train[j]
        
for i in related_col:
    for j in related_col:
        train['new'+i+'+'+j]=train[i]+train[j]

for i in related_col:
    for j in related_col:
        train['new'+i+'-'+j]=train[i]-train[j]
        
for i in related_col:
    for j in related_col:
        train['new'+i+'/'+j]=train[i]/train[j]
        
for i in related_col:
    for j in related_col:
        test['new'+i+'*'+j]=test[i]*test[j]
        
for i in related_col:
    for j in related_col:
        test['new'+i+'+'+j]=test[i]+test[j]

for i in related_col:
    for j in related_col:
        test['new'+i+'-'+j]=test[i]-test[j]
        
for i in related_col:
    for j in related_col:
        test['new'+i+'/'+j]=test[i]/test[j]

再随便做一些特征交互

df=pd.concat([train,test],axis=0)
cat_col=['grade','subGrade','homeOwnership','verificationStatus','regionCode','initialListStatus']
for col in cat_col:
    t=train.groupby(col,as_index=False)['isDefault'].agg({col+'_count':'count',col+'_mean':'mean',col+'_std':'std'})
    df=pd.merge(df,t,on=col,how='left')
train=df[df['isDefault'].notnull()]
test=df[df['isDefault'].isnull()]

剔除错误的特征,再画出热力图

train=train.drop(['newterm-term','newterm/term','newinterestRate-interestRate','newinterestRate/interestRate'],axis=1)
test=test.drop(['newterm-term','newterm/term','newinterestRate-interestRate','newinterestRate/interestRate','isDefault'],axis=1)
train_corr=train.corr()
plt.figure(figsize=(10,8))
sns.heatmap(train_corr,vmax=0.8,linewidths=0.03,cmap=sns.cm.rocket_r)
在这里插入图片描述

也不做特征选择了。。。
特征工程是一项庞杂反复的工程,我的知识很有限,这里只是抛砖引玉,就不多做了。

四、建模

设置数据集

train_x=train.drop(['isDefault'],axis=1)
train_y=train['isDefault']

硬件太差不能网格调参,就凭感觉随便填填参数了

clf=LGBMClassifier(boosting_type='gbdt',
                   n_estimators=500,
                   metric='auc',
                   learning_rate=0.1,
                   random_state=2020
                  )

五折交叉验证

prob=[]
mean_auc=0 
sk=StratifiedKFold(n_splits=5,shuffle=True,random_state=0)
for k,(train_index,val_index) in enumerate(sk.split(train_x,train_y)):
    train_x_real=train_x.iloc[train_index]
    train_y_real=train_y.iloc[train_index]
    val_x=train_x.iloc[val_index]
    val_y=train_y.iloc[val_index]
    clf=clf.fit(train_x_real,train_y_real,categorical_feature=cat_features)
    val_y_pred=clf.predict(val_x)
    auc_val=roc_auc_score(val_y,val_y_pred)
    print('第{}次验证auc{}'.format(k,auc_val))
    mean_auc+=auc_val/5
    test_y_pred=clf.predict_proba(test)[:,-1]
    prob.append(test_y_pred)
print(mean_auc)
mean_prob=sum(prob)/5
submit=pd.DataFrame({'id':range(800000,1000000),'isDefault':mean_prob})
submit.to_csv('贷款预测结果.csv',index=False)

最后的结果应该在0.73左右,如果进行更多的特征迭代和参数调优,应该可以达到0.74以上。下面是我的成绩,大约是排在60/3800。


在这里插入图片描述

由于技能的生疏和硬件的限制,还有很多遗憾之处,希望下次可以在特征工程和网格调参上做的更好。

你可能感兴趣的:(2020-12-10)