一、写在前面的话
这是我的第一篇博客,希望写好。我几乎是一个编程小白,只有一点点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:
<class 'pandas.core.frame.DataFrame'>
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。
由于技能的生疏和硬件的限制,还有很多遗憾之处,希望下次可以在特征工程和网格调参上做的更好。