該案例為拍拍貸在“魔鏡杯” 風控算法大賽,比賽公開了國內網絡借貸行業的貸款風險數據,本著保護借款?人隱私以及拍拍貸知識產權的目的,數據字段已經過脫敏處理。數據及來源可自行在網上下載
讀取數據&了解數據
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%左右