文章目錄
-
* * * 1、明確需求和目的 * 2、 數據收集 * 3、數據預處理 * * 3.1 數據整合 * * 3.1.1 加載相關庫和數據集 * 3.1.2 主要數據集概覽 * 3.2 數據清洗 * * 3.2.1 多余列的刪除 * 3.2.2 數據類型轉換 * 3.2.3 缺失值處理 * 3.2.4 異常值處理 * 3.2.5 重復值處理 * 4、數據分析 * * 4.1 相關系數分析 * 5、模型訓練 * * 5.1 數據標准化 * 5.2 使用邏輯回歸訓練 * 5.3 簡單優化 * 5.4 使用隨機森林訓練 * 6、總結
1、明確需求和目的
- 現代社會,越來越多的人使用信用卡進行消費,大部分人使用信用卡之后會按時還款,但仍然有少部分人不能在約定時間進行還款,這大大的增加了銀行或者金融機構的風險。
- 本文以某金融機構的歷史數據進行建模分析,對客戶的還款能力進行評估,以預測新客戶是否有信用卡的違約風險,從而決定是否貸款給新客戶使用。
- 本文使用AUC(ROC)作為模型的評估標准。
2、 數據收集
- 本文使用的數據集來源於kaggle平台,主要有兩份數據集。
- application_train , application_test :訓練集和測試集數據,包括每個貸款申請的信息。每筆貸款都有自己的行,並由特性SK_ID_CURR標識。訓練集的TARGET 0:貸款已還清,1:貸款未還清。
3、數據預處理
3.1 數據整合
3.1.1 加載相關庫和數據集
- 使用的庫主要有:pandas、numpy、matplotlib、seaborn
- 使用的數據集:kaggle平台提供的數據集文件
import numpy as np
import pandas as pd
import os
import matplotlib.pyplot as plt
import seaborn as sns
import warnings
warnings.filterwarnings('ignore')
print(os.listdir("../input/")) # List files available
-------------------------------------------------
['sample_submission.csv', 'credit_card_balance.csv', 'installments_payments.csv', 'HomeCredit_columns_description.csv', 'previous_application.csv', 'POS_CASH_balance.csv', 'bureau_balance.csv', 'application_test.csv', 'bureau.csv', 'application_train.csv']
3.1.2 主要數據集概覽
首先看一下訓練集數據:
# Training data
app_train = pd.read_csv('../input/application_train.csv')
print('Training data shape: ', app_train.shape)
--------------------------------------
Training data shape: (307511, 122)
app_train.head()

從上面可以看出,訓練集數據有122個特征,307511條數據。
再來看一下測試集數據:
# Testing data
app_test = pd.read_csv('../input/application_test.csv')
print('Testing data shape: ', app_test.shape)
---------------------------------------------
Testing data shape: (48744, 121)
app_test.head()

從上面可以看出,測試集數據有121個特征,48744條數據,相比訓練集,少了一個特征
TARGET,即我們需要預測的目標值(0表示貸款按時償還,1表示貸款未按時償還。)。
檢查一下TARGET列的分布,看看每一類貸款的數量:
app_train['TARGET'].value_counts()
----------------------------------------
0 282686
1 24825
Name: TARGET, dtype: int64
可以看出按時還款的類別明顯要比未按時還款的類別多, 屬於樣本不均衡的問題,后續可以考慮使用權重法或采樣法等辦法來進行解決。
轉化成圖形更直觀的對比一下:
app_train['TARGET'].astype(int).plot.hist()

3.2 數據清洗
3.2.1 多余列的刪除
首先刪除空白值超過一半的列 :
app_train = app_train.dropna(thresh=len(app_train) / 2 , axis=1)
定義一個函數,看一下剩余數據的缺失值情況:
# Function to calculate missing values by column# Funct
def missing_values_table(df):
# Total missing values
mis_val = df.isnull().sum()
# Percentage of missing values
mis_val_percent = 100 * df.isnull().sum() / len(df)
# Make a table with the results
mis_val_table = pd.concat([mis_val, mis_val_percent], axis=1)
# Rename the columns
mis_val_table_ren_columns = mis_val_table.rename(
columns = {0 : 'Missing Values', 1 : '% of Total Values'})
# Sort the table by percentage of missing descending
mis_val_table_ren_columns = mis_val_table_ren_columns[
mis_val_table_ren_columns.iloc[:,1] != 0].sort_values(
'% of Total Values', ascending=False).round(1)
# Print some summary information
print ("Your selected dataframe has " + str(df.shape[1]) + " columns.\n"
"There are " + str(mis_val_table_ren_columns.shape[0]) +
" columns that have missing values.")
# Return the dataframe with missing information
return mis_val_table_ren_columns
# Missing values statistics
missing_values = missing_values_table(app_train)
---------------------------------------------------
Your selected dataframe has 81 columns.
There are 26 columns that have missing values.
missing_values.head(10)

從上面可以看出,還有好幾列的值缺失將近50%,缺失比例較高,我們一並將這些列進行刪除。當然,也可以選擇進行缺失值填充,這里選擇刪除。
drop_columns = ["FLOORSMAX_AVG","FLOORSMAX_MODE","FLOORSMAX_MEDI","YEARS_BEGINEXPLUATATION_AVG","YEARS_BEGINEXPLUATATION_MODE",
"YEARS_BEGINEXPLUATATION_MEDI","TOTALAREA_MODE","EMERGENCYSTATE_MODE"]
app_train = app_train.drop(drop_columns, axis=1)
刪除了缺失值較多的列之后,還有其它列的缺失值需要處理,我們可以先進行數據類型轉換,后面再統一采用填充的方式進行處理。
3.2.2 數據類型轉換
先來看一下各個類型的數據有多少:
# Number of each type of column
app_train.dtypes.value_counts()
----------------------------------
int64 41
float64 20
object 12
dtype: int64
object類型的有12個,需要將其轉換成數值類型,我們先看一下它們各自的特征類別有多少:
注意 nunique 不考慮空值。
# Number of unique classes in each object column
app_train.select_dtypes('object').apply(pd.Series.nunique, axis = 0)
-------------------------------------
NAME_CONTRACT_TYPE 2
CODE_GENDER 3
FLAG_OWN_CAR 2
FLAG_OWN_REALTY 2
NAME_TYPE_SUITE 7
NAME_INCOME_TYPE 8
NAME_EDUCATION_TYPE 5
NAME_FAMILY_STATUS 6
NAME_HOUSING_TYPE 6
OCCUPATION_TYPE 18
WEEKDAY_APPR_PROCESS_START 7
ORGANIZATION_TYPE 58
dtype: int64
- 標簽編碼 ( Label Encoding):它給類別一個任意的順序。分配給每個類別的值是隨機的,不反映類別的任何固有方面。所以我們在只有兩個類別的時候使用標簽編碼。例如上面的‘NAME_CONTRACT_TYPE’等,我們就可以使用標簽編碼。
- 獨熱編碼 (One-Hot Encoding):為分類變量中的每個類別創建一個新列。當類別>2的時候,我們將使用獨熱編碼。例如上面的‘CODE_GENDER’等。當然獨熱編碼的缺點也很明顯,就是特征可能會暴增,但我們可以使用PCA或其他降維方法來減少維數。
對於類別只有2個的特征,我們使用 Label Encoding 進行數據的轉換(注意測試集也同樣需要進行轉換):
from sklearn.preprocessing import LabelEncoder
# Create a label encoder object
le = LabelEncoder()
le_count = 0
# Iterate through the columns
for col in app_train:
if app_train[col].dtype == 'object':
# If 2 or fewer unique categories
if len(list(app_train[col].unique())) <= 2:
# Train on the training data
le.fit(app_train[col])
# Transform both training and testing data
app_train[col] = le.transform(app_train[col])
app_test[col] = le.transform(app_test[col])
# Keep track of how many columns were label encoded
le_count += 1
print('%d columns were label encoded.' % le_count)
-------------------------------------------------
3 columns were label encoded.
從上面可以看出3個類別全部完成了轉換,接下來使用 One-Hot Encoding
進行剩余數據的轉換,此處選擇使用pandas的get_dummies()函數,直接映射為數值型(測試集一並進行轉換):
# one-hot encoding of categorical variables
app_train = pd.get_dummies(app_train)
app_test = pd.get_dummies(app_test)
print('Training Features shape: ', app_train.shape)
print('Testing Features shape: ', app_test.shape)
----------------------------------------------------
Training Features shape: (307511, 182)
Testing Features shape: (48744, 239)
從上面可以看出,此時測試集列數多於訓練集,因為訓練集刪除了一些多余的列,我們對兩份數據取並集,只需要處理共同擁有的列即可:
train_labels = app_train['TARGET']
# Align the training and testing data, keep only columns present in both dataframes
app_train, app_test = app_train.align(app_test, join = 'inner', axis = 1)
# Add the target back in
app_train['TARGET'] = train_labels
print('Training Features shape: ', app_train.shape)
print('Testing Features shape: ', app_test.shape)
------------------------------------------------
Training Features shape: (307511, 179)
Testing Features shape: (48744, 178)
3.2.3 缺失值處理
數據類型轉換完成,我們就可以統一進行缺失值處理,可以采用中位數進行填充:
from sklearn.preprocessing import Imputer
imputer = Imputer(strategy = 'median')
train = app_train.drop(columns = ['TARGET'])
column_list = train.columns.tolist()
# fit with Training_data, fill both Training_data and Testing_data
imputer.fit(train)
train = imputer.transform(train)
test = imputer.transform(app_test)
train = pd.DataFrame(train, columns = column_list)
app_train = pd.concat([train, app_train['TARGET']], axis=1)
app_test = pd.DataFrame(test, columns = column_list)
print('Training data shape: ', app_train.shape)
print('Testing data shape: ', app_test.shape)
--------------------------------------------------
Training data shape: (307511, 179)
Testing data shape: (48744, 178)
檢查是否還有缺失值:
print(app_train.isnull().sum())
print(app_test.isnull().sum())
需要注意的是,此處"SK_ID_CURR" 經過處理之后變成float類型,需要重新轉換成 int類型:
app_train["SK_ID_CURR"] = app_train["SK_ID_CURR"].astype(int)
app_test["SK_ID_CURR"] = app_test["SK_ID_CURR"].astype(int)
3.2.4 異常值處理
對年齡進行異常值檢查(原始數據為天,需要除以365,並且取負數):
(app_train['DAYS_BIRTH'] / -365).describe()
-----------------------------------------------
count 307511.000000
mean 43.936973
std 11.956133
min 20.517808
25% 34.008219
50% 43.150685
75% 53.923288
max 69.120548
Name: DAYS_BIRTH, dtype: float64
看起來很正常,無異常,再看一下在職天數:
app_train['DAYS_EMPLOYED'].describe()
-----------------------------------------
count 307511.000000
mean 63815.045904
std 141275.766519
min -17912.000000
25% -2760.000000
50% -1213.000000
75% -289.000000
max 365243.000000
Name: DAYS_EMPLOYED, dtype: float64
最大值為365243天,換算成年即100年,明顯不合理,屬於異常值。
出於好奇,對異常客戶進行分析,看看他們的違約率比其他客戶高還是低。
anom = app_train[app_train['DAYS_EMPLOYED'] == 365243]
non_anom = app_train[app_train['DAYS_EMPLOYED'] != 365243]
print('The non-anomalies default on %0.2f%% of loans' % (100 * non_anom['TARGET'].mean()))
print('The anomalies default on %0.2f%% of loans' % (100 * anom['TARGET'].mean()))
print('There are %d anomalous days of employment' % len(anom))
-------------------------------------------------------------
The non-anomalies default on 8.66% of loans
The anomalies default on 5.40% of loans
There are 55374 anomalous days of employment
可以看到,這些異常值的客戶違約率比其他客戶還要低,且數量還不少。
我們需要對異常值進行處理,處理異常值取決於具體情況,沒有固定的規則。最安全的方法之一就是將異常值視為缺失值處理,然后在使用算法之前填充它們。這里我們將用(np.nan)填充異常值,然后創建一個新的布爾列,指示該值是否異常。
# Create an anomalous flag column
app_train['DAYS_EMPLOYED_ANOM'] = app_train["DAYS_EMPLOYED"] == 365243
# Replace the anomalous values with nan
app_train['DAYS_EMPLOYED'].replace({365243: np.nan}, inplace = True)
同樣對測試集進行異常值處理:
app_test['DAYS_EMPLOYED_ANOM'] = app_test["DAYS_EMPLOYED"] == 365243
app_test["DAYS_EMPLOYED"].replace({365243: np.nan}, inplace = True)
print('There are %d anomalies in the test data out of %d entries' % (app_test["DAYS_EMPLOYED_ANOM"].sum(), len(app_test)))
---------------------------------------------------
There are 9274 anomalies in the test data out of 48744 entries
使用中位數對異常值轉換后的缺失值進行填充:
from sklearn.preprocessing import Imputer
imputer = Imputer(strategy = 'median')
train = app_train['DAYS_EMPLOYED'].values.reshape(-1, 1)
imputer.fit(train)
train = imputer.transform(app_train['DAYS_EMPLOYED'].values.reshape(-1, 1))
test = imputer.transform(app_test['DAYS_EMPLOYED'].values.reshape(-1, 1))
app_train['DAYS_EMPLOYED'] = train
app_test['DAYS_EMPLOYED'] = test
print(app_train['DAYS_EMPLOYED'].describe())
print(app_test['DAYS_EMPLOYED'].describe())
---------------------------------------------
count 307511.000000
mean -2251.606131
std 2136.193492
min -17912.000000
25% -2760.000000
50% -1648.000000
75% -933.000000
max 0.000000
Name: DAYS_EMPLOYED, dtype: float64
count 48744.000000
mean -2319.063639
std 2102.150130
min -17463.000000
25% -2910.000000
50% -1648.000000
75% -1048.000000
max -1.000000
Name: DAYS_EMPLOYED, dtype: float64
從上面可以看出,已經沒有異常值了。
3.2.5 重復值處理
看一下有沒有重復值,有則直接刪除:
print(app_train.duplicated().sum()) # 查看重復值的數量
------------------------------------------
0
沒有重復值,不需要進行處理。
4、數據分析
4.1 相關系數分析
使用.corr方法計算每個變量與目標之間的相關系數。
相關系數並不是表示特征“相關性”的最佳方法,但它確實讓我們了解了數據中可能存在的關系。
# Find correlations with the target and sort
correlations = app_train.corr()['TARGET'].sort_values()
# Display correlations
print('Most Positive Correlations:\n', correlations.tail(15))
print('\nMost Negative Correlations:\n', correlations.head(15))
---------------------------------------------------------------
Most Positive Correlations:
OCCUPATION_TYPE_Laborers 0.043019
FLAG_DOCUMENT_3 0.044346
REG_CITY_NOT_LIVE_CITY 0.044395
FLAG_EMP_PHONE 0.045982
NAME_EDUCATION_TYPE_Secondary / secondary special 0.049824
REG_CITY_NOT_WORK_CITY 0.050994
DAYS_ID_PUBLISH 0.051457
CODE_GENDER_M 0.054713
DAYS_LAST_PHONE_CHANGE 0.055218
NAME_INCOME_TYPE_Working 0.057481
REGION_RATING_CLIENT 0.058899
REGION_RATING_CLIENT_W_CITY 0.060893
DAYS_EMPLOYED 0.063368
DAYS_BIRTH 0.078239
TARGET 1.000000
Name: TARGET, dtype: float64
Most Negative Correlations:
EXT_SOURCE_2 -0.160295
EXT_SOURCE_3 -0.155892
NAME_EDUCATION_TYPE_Higher education -0.056593
CODE_GENDER_F -0.054704
NAME_INCOME_TYPE_Pensioner -0.046209
DAYS_EMPLOYED_ANOM -0.045987
ORGANIZATION_TYPE_XNA -0.045987
AMT_GOODS_PRICE -0.039623
REGION_POPULATION_RELATIVE -0.037227
NAME_CONTRACT_TYPE -0.030896
AMT_CREDIT -0.030369
FLAG_DOCUMENT_6 -0.028602
NAME_HOUSING_TYPE_House / apartment -0.028555
NAME_FAMILY_STATUS_Married -0.025043
HOUR_APPR_PROCESS_START -0.024166
Name: TARGET, dtype: float64
從上面看出,和目標值有較大正相關性的有 DAYS_BIRTH 這個特征,有較大負相關性的有 EXT_SOURCE_2 和 EXT_SOURCE_3
這兩個特征,可以進一步進行分析:
plt.figure(figsize=(10, 8))
# KDE圖中按時償還的貸款
sns.kdeplot(app_train.loc[app_train['TARGET'] == 0, 'DAYS_BIRTH'] / -365, label = 'target == 0')
# KDE圖中未按時償還的貸款
sns.kdeplot(app_train.loc[app_train['TARGET'] == 1, 'DAYS_BIRTH'] / -365, label = 'target == 1')
plt.xlabel('Age (years)')
plt.ylabel('Density')
plt.title('Distribution of Ages')
plt.show()

target ==
1曲線向范圍的較年輕端傾斜,說明隨着客戶年齡的增長,他們往往會更經常地按時償還貸款。雖然這不是一個顯著的相關性(-0.07相關系數),但這個變量很可能在機器學習模型中有用,因為它確實會影響目標。
對於其余兩個特征,也可以進行相應的分析,並可以利用這幾個特征進行特征工程的選擇。(本文為簡單起見,未進行特征工程)
5、模型訓練
5.1 數據標准化
- 在使用算法之前,進行 數據標准化處理,將數據集中的特征轉換成相同的量綱,從而消除不同量綱對算法造成的負面影響。
from sklearn.preprocessing import Imputer, MinMaxScaler
imputer = Imputer(strategy = 'median')
scaler = MinMaxScaler(feature_range = [0,1])
train = app_train.drop(columns = ['TARGET'])
scaler.fit(train)
train = scaler.transform(train)
test = scaler.transform(app_test)
print('Training data shape: ', train.shape)
print('Testing data shape: ', test.shape)
--------------------------------------------
Training data shape: (307511, 183)
Testing data shape: (48744, 183)
5.2 使用邏輯回歸訓練
- 直接使用簡單的邏輯回歸
from sklearn.linear_model import LogisticRegression
log_reg = LogisticRegression()
log_reg.fit(train, train_labels)
log_reg_pred = log_reg.predict_proba(test)[:, 1]
submit = app_test[['SK_ID_CURR']]
submit['TARGET'] = log_reg_pred
submit.head()
submit.to_csv('log_reg_baseline.csv', index = False)
提交之后,分數如下:
Private Score:0.72683, Public Score:0.73322
- 使用網格交叉驗證計算出最佳參數的邏輯回歸:
from sklearn.linear_model import LogisticRegression
from sklearn.model_selection import GridSearchCV
param_grid = {'C' : [0.01,0.1,1,10,100],
'penalty' : ['l1','l2']}
log_reg = LogisticRegression()
grid_search = GridSearchCV(log_reg, param_grid, scoring = 'roc_auc', cv = 5)
grid_search.fit(train, train_labels)
# Train on the training data
log_reg_best = grid_search.best_estimator_
log_reg_pred = log_reg_best.predict_proba(test)[:, 1]
submit = app_test[['SK_ID_CURR']]
submit['TARGET'] = log_reg_pred
submit.head()
submit.to_csv('log_reg_baseline_gridsearch2.csv', index = False)
使用交叉驗證,效果有一點提升,分數如下:
Private Score:0.72770, Public Score:0.73452
5.3 簡單優化
增加領域知識特征,我們可以創建幾個特性,試圖捕捉我們認為對於判斷客戶是否會拖欠貸款可能很重要的信息。
- CREDIT_INCOME_PERCENT: 信貸金額占客戶收入的百分比
- ANNUITY_INCOME_PERCENT: 貸款年金占客戶收入的百分比
- CREDIT_TERM: 以月為單位支付的期限(因為年金是每月到期的金額)
- DAYS_EMPLOYED_PERCENT: 就職天數占客戶年齡的百分比
app_train_domain = app_train.copy()
app_train['CREDIT_INCOME_PERCENT'] = app_train_domain['AMT_CREDIT'] / app_train_domain['AMT_INCOME_TOTAL']
app_train['ANNUITY_INCOME_PERCENT'] = app_train_domain['AMT_ANNUITY'] / app_train_domain['AMT_INCOME_TOTAL']
app_train['CREDIT_TERM'] = app_train_domain['AMT_ANNUITY'] / app_train_domain['AMT_CREDIT']
app_train['DAYS_EMPLOYED_PERCENT'] = app_train_domain['DAYS_EMPLOYED'] / app_train_domain['DAYS_BIRTH']
測試集進行同樣處理:
app_test_domain = app_test.copy()
app_test['CREDIT_INCOME_PERCENT'] = app_test_domain['AMT_CREDIT'] / app_test_domain['AMT_INCOME_TOTAL']
app_test['ANNUITY_INCOME_PERCENT'] = app_test_domain['AMT_ANNUITY'] / app_test_domain['AMT_INCOME_TOTAL']
app_test['CREDIT_TERM'] = app_test_domain['AMT_ANNUITY'] / app_test_domain['AMT_CREDIT']
app_test['DAYS_EMPLOYED_PERCENT'] = app_test_domain['DAYS_EMPLOYED'] / app_test_domain['DAYS_BIRTH']
增加領域特征之后,再次使用簡單的邏輯回歸進行訓練(交叉驗證太耗時間,用簡單的比較看看效果):
from sklearn.linear_model import LogisticRegression
log_reg = LogisticRegression()
log_reg.fit(train, train_labels)
log_reg_pred = log_reg.predict_proba(test)[:, 1]
submit = app_test[['SK_ID_CURR']]
submit['TARGET'] = log_reg_pred
submit.head()
submit.to_csv('log_reg_baseline_domain.csv', index = False)
增加領域特征之后,效果有一點提升,分數如下:
Private Score:0.72805, Public Score:0.73434
5.4 使用隨機森林訓練
- 增加領域特征之后,使用隨機森林訓練,看看效果怎么樣
from sklearn.ensemble import RandomForestClassifier
# 隨機森林
random_forest = RandomForestClassifier(n_estimators = 100, random_state = 50, verbose = 1, n_jobs = -1)
random_forest.fit(train, train_labels)
predictions = random_forest.predict_proba(test)[:, 1]
submit = app_test[['SK_ID_CURR']]
submit['TARGET'] = predictions
submit.to_csv('random_forest_baseline_domain.csv', index = False)
使用隨機森林,效果比邏輯回歸還要差一些:
Private Score:0.70975, Public Score:0.70120
6、總結
- 本文僅僅使用一個數據集進行模型訓練,並使用邏輯回歸和隨機森林分別預測,並通過簡單優化提升了模型效果。
- 如果進一步進行特征分析,並且使用其它數據集進行訓練的話,應該會得到更好的訓練模型。