互联网金融借款违约预测

本项目主要关注实现,数据分析、特征工程涉及较少,而且数据量较大,并没有进行多次调参。
另外,由于数据的分类极其不平衡,本项目尝试使用SMOTE增加偏少类的样本数量。

%matplotlib inline
import matplotlib.pyplot as plt
import pandas as pd
from dateutil.parser import parse
import datetime
import numpy as np
path = ''
lc = pd.read_csv(path + 'LC.csv')
lp = pd.read_csv(path + 'LP.csv')
lc.info()
<class 'pandas.core.frame.DataFrame'>
RangeIndex: 328553 entries, 0 to 328552
Data columns (total 21 columns):
ListingId    328553 non-null int64
借款金额         328553 non-null int64
借款期限         328553 non-null int64
借款利率         328553 non-null float64
借款成功日期       328553 non-null object
初始评级         328553 non-null object
借款类型         328553 non-null object
是否首标         328553 non-null object
年龄           328553 non-null int64
性别           328553 non-null object
手机认证         328553 non-null object
户口认证         328553 non-null object
视频认证         328553 non-null object
学历认证         328553 non-null object
征信认证         328553 non-null object
淘宝认证         328553 non-null object
历史成功借款次数     328553 non-null int64
历史成功借款金额     328553 non-null float64
总待还本金        328553 non-null float64
历史正常还款期数     328553 non-null int64
历史逾期还款期数     328553 non-null int64
dtypes: float64(3), int64(7), object(11)
memory usage: 52.6+ MB
lc.head().T
0 1 2 3 4
ListingId 126541 133291 142421 149711 152141
借款金额 18000 9453 27000 25000 20000
借款期限 12 12 24 12 6
借款利率 18 20 20 18 16
初始评级 C D E C C
借款类型 其他 其他 普通 其他 电商
是否首标
年龄 35 34 41 34 24
性别
手机认证 成功认证 未成功认证 成功认证 成功认证 成功认证
户口认证 未成功认证 成功认证 未成功认证 成功认证 成功认证
视频认证 成功认证 未成功认证 未成功认证 成功认证 成功认证
学历认证 未成功认证 未成功认证 未成功认证 未成功认证 未成功认证
征信认证 未成功认证 未成功认证 未成功认证 未成功认证 未成功认证
淘宝认证 未成功认证 未成功认证 未成功认证 未成功认证 未成功认证
历史成功借款次数 11 4 5 6 13
历史成功借款金额 40326 14500 21894 36190 77945
总待还本金 8712.73 7890.64 11726.3 9703.41 0
历史正常还款期数 57 13 25 41 118
历史逾期还款期数 16 1 3 1 14
lp.info()
<class 'pandas.core.frame.DataFrame'>
RangeIndex: 3203276 entries, 0 to 3203275
Data columns (total 10 columns):
ListingId     int64
期数            int64
还款状态          int64
应还本金          float64
应还利息          float64
剩余本金          float64
剩余利息          float64
到期日期          object
还款日期          object
recorddate    object
dtypes: float64(4), int64(3), object(3)
memory usage: 244.4+ MB
lp.head()
ListingId 期数 还款状态 应还本金 应还利息 剩余本金 剩余利息 到期日期 还款日期 recorddate
0 126541 1 1 1380.23 270.00 0.0 0.0 2015-06-04 2015-06-04 2017-02-22
1 126541 2 1 1400.94 249.29 0.0 0.0 2015-07-04 2015-07-04 2017-02-22
2 126541 3 1 1421.95 228.28 0.0 0.0 2015-08-04 2015-08-04 2017-02-22
3 126541 4 1 1443.28 206.95 0.0 0.0 2015-09-04 2015-09-04 2017-02-22
4 126541 5 1 1464.93 185.30 0.0 0.0 2015-10-04 2015-10-04 2017-02-22
# 提前去掉不需要的列
lc.drop('借款成功日期', axis=1, inplace=True)

合成label

目标是预测借款三个月内是否会逾期30天及以上。这只有在第一期和第二期逾期30天及以上时才会出现。下面开始提取label。

# 第1、2期未还款的提取出来。
u_data = lp.copy()
u12_late = u_data[((u_data['期数'] == 1) | (u_data['期数'] == 2)) \
                  & ((u_data['还款状态'] == 0) | (u_data['还款状态'] == 2) | (u_data['还款状态'] == 4))]

# 将没有还款日期的日期改为'2050-01-01'
u12_late['还款日期'] = u12_late['还款日期'].apply(lambda x : x if x !='\\N' else '2050-01-01')

# 计算逾期天数
u12_late['逾期天数'] = u12_late['还款日期'].apply(parse)-u12_late['到期日期'].apply(parse)

# 提取出违约名单
u_label = u12_late[u12_late['逾期天数'] > datetime.timedelta(30)]
id_tar = u_label.ListingId.unique()
label_id = pd.DataFrame({'ListingId':id_tar,'label':np.ones(id_tar.size)})

# 联接,并删除不再需要的ListingId
all_data = pd.merge(lc,label_id,how='outer',on='ListingId')
all_data.drop('ListingId', axis=1, inplace=True)

# 后面就不需要lp了
del lp, u_data
all_data.head()
借款金额 借款期限 借款利率 初始评级 借款类型 是否首标 年龄 性别 手机认证 户口认证 视频认证 学历认证 征信认证 淘宝认证 历史成功借款次数 历史成功借款金额 总待还本金 历史正常还款期数 历史逾期还款期数 label
0 18000 12 18.0 C 其他 35 成功认证 未成功认证 成功认证 未成功认证 未成功认证 未成功认证 11 40326.0 8712.73 57 16 NaN
1 9453 12 20.0 D 其他 34 未成功认证 成功认证 未成功认证 未成功认证 未成功认证 未成功认证 4 14500.0 7890.64 13 1 NaN
2 27000 24 20.0 E 普通 41 成功认证 未成功认证 未成功认证 未成功认证 未成功认证 未成功认证 5 21894.0 11726.32 25 3 NaN
3 25000 12 18.0 C 其他 34 成功认证 成功认证 成功认证 未成功认证 未成功认证 未成功认证 6 36190.0 9703.41 41 1 NaN
4 20000 6 16.0 C 电商 24 成功认证 成功认证 成功认证 未成功认证 未成功认证 未成功认证 13 77945.0 0.00 118 14 NaN
# 缺失值分析
check_null = all_data.isnull()\
    .sum(axis=0)\
    .sort_values(ascending=False)/float(len(lc))
check_null
label       0.85136
历史逾期还款期数    0.00000
借款期限        0.00000
借款利率        0.00000
初始评级        0.00000
借款类型        0.00000
是否首标        0.00000
年龄          0.00000
性别          0.00000
手机认证        0.00000
户口认证        0.00000
视频认证        0.00000
学历认证        0.00000
征信认证        0.00000
淘宝认证        0.00000
历史成功借款次数    0.00000
历史成功借款金额    0.00000
总待还本金       0.00000
历史正常还款期数    0.00000
借款金额        0.00000
dtype: float64
# 对label,NaN的填0
all_data['label'].fillna(value=0,inplace=True)
# 数据探索与分析
# 大致观察数据统计指标情况,发现一些需要修正的outlier。另外,label的类型极度不平衡,在后面将采用smote扩展样本。
all_data.describe().T
count mean std min 25% 50% 75% max
借款金额 328553.0 4423.816906 11219.664024 100.0 2033.0 3397.00 5230.00 500000.00
借款期限 328553.0 10.213594 2.780444 1.0 6.0 12.00 12.00 24.00
借款利率 328553.0 20.601439 1.772408 6.5 20.0 20.00 22.00 24.00
年龄 328553.0 29.143042 6.624286 17.0 24.0 28.00 33.00 56.00
历史成功借款次数 328553.0 2.323159 2.922361 0.0 0.0 2.00 3.00 649.00
历史成功借款金额 328553.0 8785.856771 35027.363482 0.0 0.0 5000.00 10355.00 7405926.00
总待还本金 328553.0 3721.665361 8626.061205 0.0 0.0 2542.41 5446.81 1172652.87
历史正常还款期数 328553.0 9.947658 14.839899 0.0 0.0 5.00 13.00 2507.00
历史逾期还款期数 328553.0 0.423250 1.595681 0.0 0.0 0.00 0.00 60.00
label 328553.0 0.148640 0.355733 0.0 0.0 0.00 0.00 1.00

特征工程

# 分开attributes和label
y = all_data["label"].copy()
all_data = all_data.drop('label', axis=1)
# 特征的合并
# 1.加权利率 = 借款期限 * 借款的利率
# 2.还款期数比 = 历史正常还款期数 / (历史正常还款期数 + 逾期期数)
# 3.未还款比 = 总待还本金/历史成功借款金额
# 4.剩余还款压力 = 总待还本金 / 借款金额
all_data['加权利率'] = all_data['借款期限'] * all_data['借款利率']
all_data['还款期数比'] = all_data['历史正常还款期数'] / (all_data['历史正常还款期数'] + all_data['历史逾期还款期数'])
all_data['未还款比'] = all_data['总待还本金'] / all_data['历史成功借款金额']
all_data['剩余还款压力'] = all_data['总待还本金'] / all_data['借款金额']
# fillna
median = all_data[['还款期数比','未还款比','剩余还款压力']].mean()
all_data.fillna(median, inplace=True)
all_data.head().T
0 1 2 3 4
借款金额 18000 9453 27000 25000 20000
借款期限 12 12 24 12 6
借款利率 18 20 20 18 16
初始评级 C D E C C
借款类型 其他 其他 普通 其他 电商
是否首标
年龄 35 34 41 34 24
性别
手机认证 成功认证 未成功认证 成功认证 成功认证 成功认证
户口认证 未成功认证 成功认证 未成功认证 成功认证 成功认证
视频认证 成功认证 未成功认证 未成功认证 成功认证 成功认证
学历认证 未成功认证 未成功认证 未成功认证 未成功认证 未成功认证
征信认证 未成功认证 未成功认证 未成功认证 未成功认证 未成功认证
淘宝认证 未成功认证 未成功认证 未成功认证 未成功认证 未成功认证
历史成功借款次数 11 4 5 6 13
历史成功借款金额 40326 14500 21894 36190 77945
总待还本金 8712.73 7890.64 11726.3 9703.41 0
历史正常还款期数 57 13 25 41 118
历史逾期还款期数 16 1 3 1 14
加权利率 216 240 480 216 96
还款期数比 0.780822 0.928571 0.892857 0.97619 0.893939
未还款比 0.216057 0.544182 0.535595 0.268124 0
剩余还款压力 0.484041 0.834723 0.434308 0.388136 0
all_data.hist(bins=100, figsize=(20,15))
plt.show()

SMOTENC

# 首先numerise cat数据
# 划分每一列类型
row_ordial_attribs = ['初始评级']
cat_attribs = ['借款类型', '是否首标', '性别', '手机认证', '户口认证', '视频认证', '学历认证', '征信认证', '淘宝认证']
num_attribs = list(all_data.drop(list(row_ordial_attribs + cat_attribs), axis=1)) 

# ordinarise
# 下面方法自定义mapper
# key = list(np.sort(all_data['初始评级'].unique()))
# mapper = dict(zip(key, range(len(key))))
# all_data['初始评级'].replace(mapper, inplace=True)

# auto方式
for col_name in row_ordial_attribs + cat_attribs:
    if col_name in row_ordial_attribs:
        all_data[col_name] = pd.factorize(all_data[col_name],sort=True)[0]
    else:
        all_data[col_name] = pd.factorize(all_data[col_name])[0]

# one-hot
# cat_attribs = ['借款类型', '是否首标', '性别', '手机认证', '户口认证', '视频认证', '学历认证', '征信认证', '淘宝认证']
# all_data = pd.get_dummies(all_data, columns=cat_attribs)
# SMOTE抽样。由于label极度不平衡。
from imblearn.over_sampling import SMOTENC

cols = list(all_data.columns)
cat_index = []
for col in row_ordial_attribs + cat_attribs:
    cat_index.append(cols.index(col))
    
sm = SMOTENC(sampling_strategy=0.2, categorical_features=cat_index ,k_neighbors=5,n_jobs=4)
X_res, y_res = sm.fit_sample(all_data, y) # 返回ndarray

sm = SMOTENC(categorical_features=[1],k_neighbors=2)

# 合成回df
X_col = all_data.columns
X_data = pd.DataFrame(X_res, columns=X_col)
y_data = pd.DataFrame(y_res, columns=['label'])
resample_data = pd.concat([X_data, y_data], axis=1)
# 特征的离散化
# 借款金额,借款期限,借款利率,年龄

def money_2_cat(money):
    if money <= 10000:
        return money//3000
    elif money <= 100000:
        return money//10000 + 3
    elif money <= 1000000:
        return money//100000 + 13
    else:
        return 23


def due_2_cat(day):
    if day <= 6:
        return 0
    elif day <= 12:
        return 1
    else:
        return 2


def rate_2_cat(rate):
    if rate <= 13:
        return 0
    elif rate <= 17:
        return 1
    elif rate <= 21:
        return 2
    else:
        return 3


def age_2_cat(age):
    if age <= 22:
        return 0
    elif age <= 25:
        return 1
    elif age <= 30:
        return 2
    elif age < 40:
        return 3
    else:
        return 4


resample_data['借款金额'] = resample_data['借款金额'].apply(money_2_cat)
resample_data['借款期限'] = resample_data['借款期限'].apply(due_2_cat)
resample_data['借款利率'] = resample_data['借款利率'].apply(rate_2_cat)
resample_data['年龄'] = resample_data['年龄'].apply(age_2_cat)
final_data = resample_data.sample(frac=1)
final_data.to_csv(path+'final_data.csv', index=None, encoding='utf-8')
final_data.head().T
20332 99979 88800 311869 289215
借款金额 1.000000 0.000000 0.000000 1.000000 0.000000
借款期限 0.000000 1.000000 1.000000 1.000000 0.000000
借款利率 1.000000 3.000000 3.000000 3.000000 2.000000
初始评级 1.000000 3.000000 3.000000 2.000000 2.000000
借款类型 1.000000 3.000000 1.000000 1.000000 3.000000
是否首标 0.000000 0.000000 0.000000 0.000000 0.000000
年龄 3.000000 3.000000 0.000000 1.000000 1.000000
性别 1.000000 0.000000 0.000000 0.000000 0.000000
手机认证 0.000000 1.000000 1.000000 0.000000 1.000000
户口认证 0.000000 0.000000 0.000000 0.000000 0.000000
视频认证 1.000000 1.000000 0.000000 1.000000 1.000000
学历认证 0.000000 0.000000 0.000000 0.000000 1.000000
征信认证 0.000000 0.000000 0.000000 0.000000 0.000000
淘宝认证 0.000000 0.000000 0.000000 0.000000 0.000000
历史成功借款次数 1.000000 3.000000 2.000000 5.000000 3.000000
历史成功借款金额 7500.000000 5975.000000 3935.000000 14544.000000 5149.000000
总待还本金 5704.680000 4340.640000 3199.800000 4132.500000 3298.040000
历史正常还款期数 3.000000 8.000000 5.000000 33.000000 10.000000
历史逾期还款期数 0.000000 0.000000 0.000000 3.000000 0.000000
加权利率 96.000000 264.000000 264.000000 264.000000 120.000000
还款期数比 1.000000 1.000000 1.000000 0.916667 1.000000
未还款比 0.760624 0.726467 0.813164 0.284138 0.640520
剩余还款压力 1.503209 5.466801 2.461385 0.826500 4.704765
label 0.000000 0.000000 0.000000 1.000000 0.000000
final_data = pd.read_csv(path+'final_data.csv')

模型训练与调优

Y = final_data['label'].copy()
X = final_data.drop('label', axis=1)
from sklearn.model_selection import GridSearchCV
from sklearn.ensemble import GradientBoostingClassifier

param_grid = [
    {'n_estimators':[50], 'learning_rate': [0.1], 'min_samples_leaf': [100], 'min_samples_split': [100], 'max_depth':[7]}
  ]

gbc = GradientBoostingClassifier()
grid_search = GridSearchCV(gbc, param_grid, cv=4,
                           scoring='roc_auc', 
                           n_jobs=4,
                           return_train_score=True)
grid_search.fit(X, Y)
# 查看结果
grid_search.best_params_

grid_search.best_estimator_

cvres = grid_search.cv_results_
for mean_score, params in zip(cvres["mean_test_score"], cvres["params"]):
    print(mean_score, params)
    
pd.DataFrame(grid_search.cv_results_).T
0.7771415886609713 {'learning_rate': 0.1, 'max_depth': 7, 'min_samples_leaf': 100, 'min_samples_split': 100, 'n_estimators': 50}
0
mean_fit_time 170.069
std_fit_time 1.23085
mean_score_time 0.262221
std_score_time 0.0795939
param_learning_rate 0.1
param_max_depth 7
param_min_samples_leaf 100
param_min_samples_split 100
param_n_estimators 50
params {'learning_rate': 0.1, 'max_depth': 7, 'min_sa...
split0_test_score 0.776663
split1_test_score 0.776003
split2_test_score 0.777184
split3_test_score 0.778716
mean_test_score 0.777142
std_test_score 0.0010006
rank_test_score 1
split0_train_score 0.791537
split1_train_score 0.792024
split2_train_score 0.791077
split3_train_score 0.79236
mean_train_score 0.79175
std_train_score 0.0004864
posted @ 2019-01-19 22:50  justcodeit  阅读(817)  评论(0编辑  收藏  举报