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