Kaggle "$70000奖金池" 竞赛经历分享 — Home Credit 房屋信贷违约风险(一)

2019-07-22 14:43:00 浏览数 (1)

推荐导读:本文来源于知识星球中一位星友的投稿,主要分享前不久刚结束的一个Kaggle竞赛 “Home Credit Default Risk” 房屋借贷的违约预测分析。

本文适用于那些刚刚接触机器学习以及对数据挖掘竞赛感兴趣的小伙伴,通过阅读后能够掌握数据挖掘的整个流程,同时对房屋信贷违约风险这个项目能有较为深刻的认识。

文章分为上下两个部分,这是文章的上部分,在这篇中,我们将通过数据描述,数据探索以及特征相关性分析对整个数据有感性的认识,同时使用部分数据建立baseline model并且第一次提交我们的预测结果。

文本的主要内容有如下几个部分:

  • 项目背景介绍
  • 好坏定义以及样本概述与说明
  • 数据探索
  • 数据预处理
  • 数据合并
  • baseline model建立
  • 结果提交

▍项目背景介绍

Home Credit 公司是美国一家非常著名的借贷公司,他们通过使用机器学习的方法来进行放贷和判断用户的好坏。本次竞赛他们提供了7份数据供模型训练和测试,我们首先看看数据描述。

  • application_train | application_test : 模型的主要训练和测试数据,包含Home Credit的每笔贷款申请信息。每笔贷款都有自己的行,并由功能SK_ID_CURR标识。训练申请数据附有TARGET,表示0:贷款已偿还或1:贷款未偿还。
  • bureau : 所有客户之前由其他金融机构提供的信用报告给信用局(对于我们样本中有贷款的客户);对于我们样本中的每笔贷款,客户在申请日期之前在信用局拥有的信用数量与行数一样多。
  • bureau_balance:信贷局以往信贷的每月余额;该表每一行代表每个月向历史局报告的每个信用证的历史记录。
  • POS_CASH_balance: 有关以前销售点或客户现金贷款与Home Credit的月度数据。每行是前一个销售点或现金贷款的一个月,而前一个贷款可以有多行。
  • credit_card_balance: 客户对Home Credit的以前信用卡的月度数据。每行是信用卡余额的一个月,单个信用卡可以有多行。
  • previous_application: 每个在Home Credit公司贷款客户的先前申请记录;每一行代表之前与贷款相关的申请信息。
  • installments_payments:Home Credit的先前贷款的付款记录。每次付款都有一行,每次错过付款都有一行。

关于各表之间的联结关系图如下:

▍好坏定义以及样本概述说明

我们下载好数据之后开始读取数据(先说明一下,在本篇文章中只会使用三份数据进行处理,即application_train,test, bureau, bureau_balance, 也就是上图中的左边部分,因为我们一开始想要尽快的让baseline model跑起来,让我们对整个项目有比较清晰的认识)。

原始的训练数据有307511行,122列,测试数据有48744行,121列。

接下来我们查看具体的数据。

我们在训练集数据中很快找到了TARGET,这就是关于本次比赛的目标变量,具有两个取值0和1。'0' : 表示贷款已还 ;'1' : 表示贷款未偿还。我们这个项目的目的是根据之前客户的信用信息,申请信息以及客户的消费行为信息来预测测试集中的用户是否会偿还贷款。

总结一下:

  • 本次项目的数量集非常大,训练样本和测试样本分别为307511和48744,并且特征的维数达到121维;
  • 训练集中的TARGET即为训练的标签值,由0和1构成分别代表贷款是否偿还。
  • 本次问题其实是有监督学习中的二分类问题。

▍数据探索

3.1 数据类型探索

特征变量由number类和category类组成,首先我想查看一下训练集和测试集的特征类型分布,方便之后的编码操作。使用df.dtypes查看。

我们发现训练集和测试集中含有16列categorical 特征,查看一下分别是哪些:有些是涉及贷款的种类,比如cash或者revolving,有些是性别还有是否拥有车等等。我们现在有了一些认识,后面再进行处理。

3.2 缺失值探索

一般一个项目给的数据都会存在缺失值,接下来我们查看一下缺失值情况

这个结果看的不是很清楚,不知道每个含有缺失值的特征的缺失比例,因此写一个查看缺失值情况的函数。

代码语言:javascript复制
def count_missing(df,print_info=False):
    miss_value = df.isnull().sum()

    miss_percentage = round(miss_value / df.shape[0],3) 

    miss_df = pd.concat([miss_value,miss_percentage],axis=1)

    miss_df = miss_df.rename(columns={0:'miss_value',1:'% miss_percentage'})

    miss_df = miss_df.loc[miss_df['miss_value']!=0,:]

    miss_df = miss_df.sort_values(by='% miss_percentage',ascending=False)

    if print_info:
        print('There are {0} columns in total nThere are {1} columns have miss values'.format(df.shape[1],miss_df.shape[0]))
    return miss_df

得到的结果如下:一共有67个特征有缺失值,最多的几个特征的缺失比较差不多达到70%。

经过变量类型统计和缺失值探索两步,我们对数据有了基本的认识。

▍数据预处理

4.1 特征编码

从第三步数据类型探索后我们知道训练集和测试集都含有16个Object类型的特征,我们对其使用标签编码(Label Encoder)和独热编码(One-Hot Encoder)的方式。

我决定对只有两个值类型变量进行标签编码(因为用0和1表示两个类别较为清楚,但是当类型大于2时,用自然数1,2,3,4等来表示不同的类型会造成机器的误会,机器会认为这几个类型之间存在着大小关系),而其他类型变量使用独热编码。

编码之后查看训练集和测试集的shape分别为(307511,246),(48744,242)。

由于测试集和训练集在有些特征中的取值范围是不同的,所以编码后的维数存在差异,我们应该使用df.align对齐数据。

代码语言:javascript复制
train_df, test_df = train_df.align(test_df,join='inner',axis=1)
print(train_df.shape)
print(test_df.shape)

4.2 特征相关性分析

编码之后我们的数据都是number类型的了,所以我们可以使用corr( )来查看特征与目标变量之间的相关性。我们观察到与目标变量最相关的的几个特征分别是DAYS_BIRTH, EXT_SOURCE_3, EXT_SOURCE_2, EXT_SOURCE_1。

代码语言:javascript复制
correlation = train_df.corr()['TARGET'].sort_values()

print('最正相关的是:n',correlation.tail(10))
print('最负相关的是:n',correlation.head(10))

▍数据合并

通过之前的几个步骤,我们对于application_train和application_test数据已经有了比较清晰的认识,接下来看看剩下的两份数据,bureau和bureau_balance。

这里我们首先要理解一下SK_ID_CURR和SK_ID_BUREAU两个id, 通过之前的数据描述我们知道,每一个客户申请每笔贷款都会得到唯一的一个SK_ID_CURR标签,然后bureau这张表是记录的这个客户之前的信用(credit)记录, 而对于每一个客户会有多笔之前的信用记录,所以会发现SK_ID_CURR=215354的贷款客户有多笔信用(credit)记录,即SK_ID_BUREAU从5714462 - 5714466。

而bureau_balance数据表记录的为每条信用(credit)记录的月余额,有多少个月的记录,在bureau_balance表中就有多少行数据,并且不同的月份也是使用相同的SK_ID_BUREAU。

因为有了这些了解,我们接下来开始探究bureau和bureau_balance两份数据的有价值的信息。

bureau数据探索

我首先想知道每个客户有多少笔信用记录,所以使用groupby来计算。

接下来看看bureau中的number类特征,发现每个SK_ID_CURR都对应多笔数据,所以我决定将他们分类并把他们统计起来,计算出每个特征的【mean,sum,min,max】,结果如下:

图中标红的部分解释一下:代表100001这笔贷款的DAYS_CREDIT特征的几条信用(credit)记录的平均值,总数,最大值和最小值。这看起来非常有价值

接下来看看bureau中的categorical类特征。

显然我们对其应该使用独热法编码使其变成number类型。

接下来我们同样对其分类,并且agg【count, mean】。

标红部分的含义:贷款号为100001的信用(credit)记录关于CREDIT_ACITVE这个特征一共有7笔记录,其中Active有3笔,占0.4285;Closeed有3笔,占0.5715。其他的也是这个意思,就不一一解释了。

通过上面两步我们得到了每一笔贷款(即每一个SK_ID_CURR)在bureau表中的有用信息,即bureau_num和bureau_cate。然后我们使用pd.merge,合并键为'SK_ID_CUR'R 将bureau_num和bureau_cate加入到train和test数据中。

代码语言:javascript复制
train_df = train_df.merge(bureau_num, on='SK_ID_CURR', how='left')
test_df = test_df.merge(bureau_cate, on='SK_ID_CURR', how='left')

合并进去的特征

合并完数据之后我们的数据shpe达到了(307511,243)和(48744,242)。

bureau_balance 数据探索

首先解释一下这张表的含义: bureau_balance记录的是每条信用(credit)记录每月的月余额,图中截取的几笔数据代表SK_ID_BUREAU=5715448这笔信用记录第一月收支平衡,后面几个月依次为-1,-2,-3,-4......

所以我决定对SK_ID_BUREAU进行groupby,计算出每条信用记录的关于月份的统计数据(mean, sum, max, min)。

标红部分表示sk_id_bureau=5001709这条信用记录十几个月下来months_balance的平均值是-48,总计-4656。从这个数据来看,拥有这条记录的客户申请贷款的时候应该不是那么容易。

我们对其categorical变量编码后也进行同样的分类加统计操作。

标红部分解释:SK_ID_BUREAU=5001710这条信用记录的所有月份里status为0的有5笔,占0.06。

同样的我们通过对bureau_balance探索,得到了两份数据bureau_num和bureau_cate,接下来我们以SK_ID_BUREAU合并键,将其拼接起来得到每条信用记录的统计数据。

我们假设SK_ID_BUREAU=5001709,5001710,5001711这三条信用记录都属于SK_ID_CURR=100001这条申请贷款的客户的,那么我们为了最终得到关于申请贷款客户的统计数据,应该将SK_ID_CURR添加进该表并且重新按SK_ID_CURR分类。

代码语言:javascript复制
bureau_balance_bycredit = bureau_balance_bycredit.merge(
    bureau_balance.loc[:,['SK_ID_BUREAU','SK_ID_CURR']],
        on='SK_ID_BUREAU',how='left')
bureau_balance_bycredit.head()

标红部分表示SK_ID_CURR=162368客户拥有三条统计信用记录,我们使用groupby ('SK_ID_CURR')的方式就得到了关于每个贷款客户的bureau_balance统计数据。代码也是和上面的一样,这里就不一一列举了,分类后得到的结果如下:

表中的第一列表示:SK_ID_CURR=100001贷款这位客户所有信用(credit)记录中months_balance的平均数是-11.78多。

有了关于每笔贷款的统计数据,我们就可以将其以SK_ID_CURR为主键,合并进之前的训练集和测试集,最终得到的训练集和测试集的shape分别为(307511,296),(48744,295)

到此我们application_train | test 和 bureau, bureau_balance这几份数据就处理完了,接下来进行建模和预测。

▍Baseline model建立

建模部分我使用lightgbm 并且 使用k 折交叉验证使训练的模型更稳定,废话不多说,直接上代码:

代码语言:javascript复制
def model(features, test_features, encoding = 'ohe', num_folds = 5):

    train_ids = features['SK_ID_CURR']
    test_ids = test_features['SK_ID_CURR']

    labels = features['TARGET']
    features = features.drop(columns=['SK_ID_CURR','TARGET'])
    test_features = test_features.drop(columns=['SK_ID_CURR'])


    #One-Hot Encoding
    if encoding == 'ohe':
        features = pd.get_dummies(features)
        test_features = pd.get_dummies(test_features)


    #align data
    features,test_features = features.align(test_features,join='inner',axis=1)
    print('Training Data shape:{0}'.format(features.shape))
    print('Testing Data shape:{0}'.format(test_features.shape))

    #Create array to store output
    feature_names = list(features.columns)

    train_scores = []
    valid_scores = []

    oof_preds = np.zeros(features.shape[0])
    sub_preds = np.zeros(test_features.shape[0])
    feature_importance_value = np.zeros(len(feature_names))


    k_fold = KFold(n_splits=num_folds,shuffle=False,random_state=50)

    for n_fold, (train_idx,valid_idx) in enumerate(k_fold.split(features,labels)):

        X_train,y_train = features.iloc[train_idx],labels.iloc[train_idx]
        X_valid,y_valid = features.iloc[valid_idx],labels.iloc[valid_idx]

        clf = lgb.LGBMClassifier(
            nthread=4,
            n_estimators=10000,
            learning_rate=0.02,
            num_leaves=34,
            colsample_bytree=0.9497036,
            subsample=0.8715623,
            max_depth=8,
            reg_alpha=0.041545473,
            reg_lambda=0.0735294,
            min_split_gain=0.0222415,
            min_child_weight=39.3259775,
            silent=-1,
            verbose=-1, )

        clf.fit(X_train,y_train,eval_set=[(X_train,y_train),(X_valid,y_valid)],
                eval_names = ['train', 'valid'],eval_metric ='auc',
                verbose= 200, early_stopping_rounds= 200)

        oof_preds[valid_idx] = clf.predict_proba(X_valid,num_iteration=clf.best_iteration_)[:,1]
        sub_preds  = clf.predict_proba(test_features,num_iteration=clf.best_iteration_)[:,1] / k_fold.n_splits
        feature_importance_value  = clf.feature_importances_ / k_fold.n_splits

        #计算auc值
        valid_auc = clf.best_score_['valid']['auc']
        train_auc = clf.best_score_['train']['auc']

        #valid_score = model.best_score_['valid']['auc']
        #train_score = model.best_score_['train']['auc']

        train_scores.append(train_auc)
        valid_scores.append(valid_auc)

        #释放内存
        gc.enable()
        del clf,X_train,X_valid,y_train,y_valid
        gc.collect()

    #输出信息
    #submission
    submission = pd.DataFrame({'SK_ID_CURR':test_ids,'TARGET':sub_preds})

    #feature_importance_df
    feature_importance_df = pd.DataFrame({'feature':feature_names,
                                         'importance_value':feature_importance_value})

    #train_scores and valid_scores metrics
    all_valid_score = roc_auc_score(labels,oof_preds)

    valid_scores.append(all_valid_score)
    train_scores.append(np.mean(train_scores))

    fold_names = np.array(range(num_folds)) 1
    fold_names = np.append(fold_names,'overall')

    metrics = pd.DataFrame({'num_fold':fold_names,
                           'train_auc':train_scores,
                           'valid_auc':valid_scores})

    return submission,feature_importance_df,metrics

等模型训练后我们得到返回值。

代码语言:javascript复制
submission,fi,metrics = model(train_df,test_df)

submission

特征重要性排序,我们发现几个重要特征是EXT_SOURCE_1,EXT_SOURCE_2,EXT_SOURCE_3,DAYS_BIRTH,这和我们之前进行相关性分析得到的结果是一样的。

feature importance value sort

训练和验证auc分数

▍结果提交

将得到的submission保存成csv文件并且提交到Kaggle得到最终的分数为0.763

代码语言:javascript复制
submission.to_csv('submission.csv',index=False)

▍总结

这一篇我们只使用了application_train,test, bureau, bureau_balance三份数据进行数据探索,挖掘并进行建模。

下一篇文章我们将剩下的几份数据POS_CASH_balance,credit_card_balance,previous_application,installments_payments充分利用,加入我们的训练集和测试集,看看最终的结果会如何。

0 人点赞