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