拍拍贷违约预测案例(GBDT)

该案例为拍拍贷在“魔镜杯” 风控算法大赛,比赛公开了国内网络借贷行业的贷款风险数据,本着保护借款⼈人隐私以及拍拍贷知识产权的目的,数据字段已经过脱敏处理。数据及来源可自行在网上下载

读取数据&了解数据

import numpy as np
import pandas as pd
%matplotlib inline
# 用户行为数据
train = pd.read_csv('PPD_Training_Master_GBK_3_1_Training_Set.csv', encoding='gbk')
train.head()
train.shape  # (30000, 228)
# 用户登录数据
train_log = pd.read_csv('PPD_LogInfo_3_1_Training_Set.csv')
train_log.head()
# 用户修改信息数据
train_update = pd.read_csv('/Users/henrywongo/Desktop/Code_Python/PPD-RiskContral/data/first round train data/PPD_Userupdate_Info_3_1_Training_Set.csv')
train_update.head()

1. 数据处理

1.1 缺失值处理

import matplotlib.pyplot as plt
# 统计每一个字段的缺失值比率
train_isnull = train.isnull().mean()
train_isnull = train_isnull[train_isnull > 0].sort_values(ascending=False)
train_isnull.plot.bar(figsize=(12, 8))

有两个字段确实值达到了97%, 三个字段达到60%, 部分字段缺失值在10%以下,对缺失值比率不同的字段,根据业务情况,进行处理

# 统计每一条记录的缺失值个数
plt.figure(figsize=(12, 8))
plt.scatter(np.arange(train.shape[0]),
            train.isnull().sum(axis=1).sort_values().values)
plt.show()

有部分记录的缺失值达25个以上,最大不超过40个,该数据集共228字段,最大缺失值比例不超过25%,在能容忍的范围内,在这里,就不对又确实值得字段进行处理

# 通过观察原数据,对于缺失值达90%以上的字段,无法知晓其业务的实际含义,在这里直接删除
train = train.loc[:, train.isnull().mean() < 0.9]

通过观察,对于缺失值在60%左右的字段,都为二分类型的数值,使用0填补

# 通过观察,对于缺失值在60%左右的字段,都为二分类型的数值,使用0填补
col_6 = []
for col in train.columns:
    if train[col].isnull().mean() > 0.6:
        col_6.append(col)

col_6  # 缺失值在0.6以上的字段名称列表

train.loc[:, col_6].info()  

train.loc[:, col_6] = train.loc[:, col_6].fillna(0)

还未处理的有缺失值的字段

# 统计余下字段的缺失值比率
train_isnull2 = train.isnull().mean()
train_isnull2 = train_isnull2[train_isnull2 > 0].sort_values(ascending=False)
train_isnull2.plot.bar(figsize=(12, 8))

由于无了解到以上字段的实际业务含义,在这里对数值型的字段,统一使用-1填补,把其归为一类

# 由于无了解到以上字段的实际业务含义,在这里对数值型的字段,统一使用-1填补
for col in train_isnull2.index:
    if train[col].dtype in ['float', 'int']:
        train[col] = train[col].fillna(-1)

还剩余未处理的字段,这些字段均为字符型字段

# 统计余下字段的缺失值比率
train_isnull3 = train.isnull().mean()
train_isnull3 = train_isnull3[train_isnull3 > 0].sort_values(ascending=False)
train_isnull3.plot.bar(figsize=(12, 8))
# 采用'Unkonw'填补
train['WeblogInfo_20'] = train['WeblogInfo_20'].fillna('Unknow')
train['WeblogInfo_21'] = train['WeblogInfo_21'].fillna('Unknow')
train['WeblogInfo_19'] = train['WeblogInfo_19'].fillna('Unknow')
train['UserInfo_2'] = train['UserInfo_2'].fillna('Unknow')
train['UserInfo_4'] = train['UserInfo_4'].fillna('Unknow')

1.2 异常值处理

本数据仅未发现异常值点,故不作处理

1.3 文本处理

Userupdate_Info 表中的 UserupdateInfo1 字段,属性取值为英⽂文字符, 包含了⼤大小写,如 “QQ”和“qQ”,很明显是同⼀一种取值,我们将所有 字符统⼀一转换为小写。

train_update['UserupdateInfo1'] = train_update['UserupdateInfo1'].apply(lambda x: np.char.lower(x))

train中 UserInfo_9 字段的取值包含了空格字符,如“中国移 动”和“中国移动 ”, 它们是同⼀一种取值,需要将空格符去除。

train['UserInfo_9'] = train['UserInfo_9'].apply(lambda x: x.strip())

UserInfo_8 包含有“重庆”、“重庆市”等取值,它们实际上是同⼀一个城 市,需要把 字符中的“市”全部去掉。去掉“市”之后,城市数由 600 多下 降到 400 多

train['UserInfo_8'] = train['UserInfo_8'].apply(lambda x: x[:-1] if x[-1] == '市' else x)

2. 特征工程

2.1 成交时间

# 将时间转换为时间型数据
train['ListingInfo'] = pd.to_datetime(train['ListingInfo'])
# 获取日其所在的周数,周数为所在年份的第几周
train['Week'] = train['ListingInfo'].dt.week

以数据集起始时间为第一周,本数据集起始时间为2013年的第44周,所以2013年周数减去43,2014年周数加上9,即可把日期变量按周离散化

week = []
for i in range(train.shape[0]):
    if train['ListingInfo'].dt.year[i] == 2013:
        if train['ListingInfo'][i] in pd.to_datetime(['2013-12-30', '2013-12-31']):
            # 2013-12-30,2013-12-31为2014年第一周
            week.append(9)
        else:
            week.append(-43)    
    else:
        week.append(9)

train['Weeks'] = week + train['Week']
train.drop(['Week'], axis=1, inplace=True)
train.drop(['ListingInfo'], axis=1, inplace=True)
# 以周为维度,计算每周违约人数以及未违约人数
train_by_week = train.groupby('Weeks')
plt.figure(figsize=(12, 8))
plt.plot(train_by_week.target.sum().index, train_by_week.target.sum().values)
plt.plot(train_by_week.target.sum().index, train_by_week.target.count().values - train_by_week.target.sum().values)
plt.xlabel('Weeks(20131101-20141109)')
plt.ylabel('Count')
plt.legend(['Count_1', 'Count_0'], loc='upper left')
plt.show()

从图中估计可看出,随着时间的移动,违约人数在一定方位内浮动,未违约人数稳定增长,浮动均呈规律性变化,可能与该金融机构的缴款日有关

2.2 衍生特生

统计Log_info、Updat_info表中的用户登录次数以及用户更新信息的次数,并命名为Log_count和Updat_count加入到数据中

# 统计Log登录次数
log_count = train_log.pivot_table(values=['LogInfo3'], index=['Idx'], aggfunc=['count'])
log_count = log_count.reset_index()
log_count.columns = log_count.columns.droplevel(1)
log_count.rename(columns={'count':'Log_count'}, inplace=True)

# 统计Update更改次数
updat_count = train_update.pivot_table(values='UserupdateInfo1', index='Idx', aggfunc=['count'])
updat_count = updat_count.reset_index()
updat_count.columns = updat_count.columns.droplevel(1)
updat_count.rename(columns={'count':'Updat_count'}, inplace=True)

# 将新的衍生字段加入到数据中
train = pd.merge(train, log_count, how='left', on=['Idx'])
train = pd.merge(train, updat_count, how='left', on=['Idx'])
# 用0天不信的字段的缺失值
train['Log_count'] = train['Log_count'].fillna(0)
train['Updat_count'] = train['Updat_count'].fillna(0)

3. 特征选择

3.1 方差分析

# 通过计算每个数值型特征的标准差,删除方差很小的字段,尤其是只有唯一值的字段
train_var = train.var().sort_values()
train_var_index = train_var[train_var < 0.1].index[:-1]  # 保留target,因为target是因变量
train.drop(train_var_index, axis=1, inplace=True)

3.2 GBDT重要度排序

X = train.drop('target', axis=1).copy()
y = train['target'].copy()
X = pd.get_dummies(X)  # 对X进行独热编码

from sklearn.ensemble import GradientBoostingClassifier
clf = GradientBoostingClassifier()
clf.fit(X, y)
print(clf.feature_importances_) 

[0.03835858 0.00768559 0.00235331 ... 0. 0. 0. ]
删除重要度为0的字段

X_new = X.loc[:, clf.feature_importances_ > 0]
X_new.shape  # (30000, 259)

4. 类别不均衡处理

from collections import Counter
Counter(y)  # 正负样本比例接近13:1

Counter({0: 27802, 1: 2198})

# 采取过采样的方法解决类别不均衡问题,使用SMOTE
from imblearn.over_sampling import SMOTE
X_resampled, y_resampled = SMOTE().fit_sample(X_new, y)

sorted(Counter(y_resampled).items())

[(0, 27802), (1, 27802)]

5. 模型优化设计

# 实例化一个GBDT分类器
from sklearn import metrics
clf2 = GradientBoostingClassifier()
clf2.fit(X_resampled, y_resampled)
clf2.predict(X_resampled)
metrics.accuracy_score(y_resampled, clf2.predict(X_resampled))

调参

# n_estimators
n_estimators = np.arange(20, 200, 20)

for n in n_estimators:
    clf = GradientBoostingClassifier(n_estimators=n)
    clf.fit(X_resampled, y_resampled)
    print('n_estimators为{}, AUC为{}'.format(n, metrics.accuracy_score(y_resampled, clf.predict(X_resampled))))

运行发现n_estimators参数基本在92%-96%之间,n_estimators为60后,AUC提升基本不明显

learning_rate = np.arange(0.1, 1, 0.1)
for n in learning_rate:
    clf4 = GradientBoostingClassifier(n_estimators=60, learning_rate=n)
    clf4.fit(X_resampled, y_resampled)
    print('learning_rate为{}, AUC为{}'.format(n, metrics.accuracy_score(y_resampled, clf4.predict(X_resampled))))

在不同的learning_rate取值下,AUC的变化不大,之才采用默认参数
使用交叉验证评估模型稳定性

from sklearn.model_selection import cross_val_score
clf3 = GradientBoostingClassifier(n_estimators=60)
clf3.fit(X_resampled, y_resampled)
scores = cross_val_score(clf3, X_resampled, y_resampled, cv=10)
scores

经过交叉验证的评估,模型AUC基本稳定在98%左右

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 213,014评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,796评论 3 386
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 158,484评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,830评论 1 285
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,946评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,114评论 1 292
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,182评论 3 412
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,927评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,369评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,678评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,832评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,533评论 4 335
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,166评论 3 317
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,885评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,128评论 1 267
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,659评论 2 362
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,738评论 2 351

推荐阅读更多精彩内容