1. 程式人生 > >ML - 貸款使用者逾期情況分析5 - 特徵工程2(特徵選擇)

ML - 貸款使用者逾期情況分析5 - 特徵工程2(特徵選擇)

文章目錄

特徵選擇 (判定貸款使用者是否逾期)

給定金融資料,預測貸款使用者是否會逾期。
(status是標籤:0表示未逾期,1表示逾期。)

Task8(特徵工程2 - 特徵選擇) - 分別用IV值和隨機森林挑選特徵,再構建模型,進行模型評估

1. IV值進行特徵選擇

1.1 基本介紹

在二分類問題中,IV值(Information Value)主要用來對輸入變數進行編碼和預測能力評估

IV 值的取值範圍是[0, \infty ),其大小表示該變數預測能力的強弱。通常認為:

IV值 預測能力
<0.02 無用
0.02—0.1 弱預測
0.1—0.3 中等預測
0.3—0.5 強預測
>0.5 可疑

一般選擇中等和強預測能力的變數用於模型開發,一些學派也只提倡具有中等IV值的變數來進行模型開發。

1.2 計算公式

1)WOE

WOE(weight of evidence,證據權重),是對原始變數的一種編碼形式。

對一個變數進行WOE編碼,首先要把變數進行分組處理(分箱或離散化)。常用離散化的方法有等寬分組,等高分組,或利用決策樹來分組。
分組後,對於第 i 組,WOE的計算公式見下式:
W O E i = ln p y 1 p y 0 = ln # B i / # B T # G i / # G T WO{E_i} = \ln {{{p_{{y_1}}}} \over {{p_{{y_0}}}}} = \ln {{\# {B_i}/\# {B_T}} \over {\# {G_i}/\# {G_T}}}

它衡量了"當前分組中響應使用者/所有響應使用者"和"當前分組中未響應使用者/所有未響應使用者"的差異。

2)IV值

IV值的計算以WOE為基礎,相當於是WOE值的一個加權求和。

假設變數分了n個組。對第i組,計算公式如下:
I V i = ( # B i # B T # G i # G T ) ln # B i / # B T # G i / # G T I{V_i} = \left( {{{\# {B_i}} \over {\# {B_T}}} - {{\# {G_i}} \over {\# {G_T}}}} \right)\ln {{\# {B_i}/\# {B_T}} \over {\# {G_i}/\# {G_T}}}
計算了變數各個組的 IV 值之後,我們就可以計算整個變數的 IV 值:
I V = i = 1 n I V i IV = \sum\limits_{i = 1}^n {I{V_i}}

IV值主要用於特徵選擇,如果想對變數的預測能力進行排序,可以按 IV 值從高到低篩選。

IV在WOE前多乘了一個因子:
1)保證了IV的值不是負數;
2)很好的考慮了分組中樣本佔整體的比例(比例越低,這個分組對變數整體預測能力的貢獻越低)。

2. 隨機森林進行特徵選擇

隨機森林提供了兩種特徵選擇的方法:mean decrease impurity和mean decrease accuracy。

2.1 平均不純度減少 mean decrease impurity

利用不純度可以確定節點(最優條件). 對於分類問題,常採用基尼不純度/資訊增益;對於迴歸問題,常採用方差/最小二乘擬合。

訓練決策樹時,可以計算每個特徵減少了多少樹的不純度。對於一個決策樹森林來說,可以算出每個特徵平均減少了多少不純度,並把它平均減少的不純度作為特徵選擇的值。
【缺點】
1)該方法存在偏向, 對具有更多類別的變數更有利;
2)label存在多個關聯特徵(任意一個都可以作為優秀特徵), 則一旦某個特徵被選擇, 其他特徵的重要性會急劇降低。這會造成誤解:錯誤的認為先被選中的特徵是很重要的,而其餘的特徵是不重要的。

2.2 平均精確率減少 Mean decrease accuracy

直接度量每個特徵對模型精確率的影響。

打亂每個特徵的特徵值順序,並且度量順序變動對模型的精確率的影響。
對於不重要的變數來說,打亂順序對模型的精確率影響不會太大,但是對於重要的變數來說,打亂順序就會降低模型的精確率。

3. 程式碼

import pickle
import pandas as pd
from sklearn.model_selection import train_test_split

# 匯入資料
data = pd.read_csv('data.csv')
data.drop_duplicates(inplace=True)

# 載入特徵
with open('feature.pkl', 'rb') as f:
    X = pickle.load(f)

# 提取標籤
y = data.status

# 劃分訓練集測試集
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.3,random_state=2333)
# 效能評估
from sklearn.metrics import accuracy_score, roc_auc_score

def model_metrics(clf, X_train, X_test, y_train, y_test):
    # 預測
    y_train_pred = clf.predict(X_train)
    y_test_pred = clf.predict(X_test)
    
    y_train_proba = clf.predict_proba(X_train)[:,1]
    y_test_proba = clf.predict_proba(X_test)[:,1]
    
    # 準確率
    print('[準確率]', end = ' ')
    print('訓練集:', '%.4f'%accuracy_score(y_train, y_train_pred), end = ' ')
    print('測試集:', '%.4f'%accuracy_score(y_test, y_test_pred))
    
    # auc取值:用roc_auc_score或auc
    print('[auc值]', end = ' ')
    print('訓練集:', '%.4f'%roc_auc_score(y_train, y_train_proba), end = ' ')
    print('測試集:', '%.4f'%roc_auc_score(y_test, y_test_proba))

3.1 IV值進行特徵選擇

stats.scoreatpercentile(x, 50) # 得到x在50%處的數值
np.in1d(B,A) # 在序列B中尋找與序列A相同的值,並返回一邏輯值(True,False)

處理上述特徵時, 遇到了IV的極端情況, 響應數為0或未響應數為0。
為簡單起見, 我們在程式碼中對極端值進行平滑處理。

import math
import numpy as np
from scipy import stats
from sklearn.utils.multiclass import type_of_target

def woe(X, y, event=1):  
    res_woe = []
    iv_dict = {}
    for feature in X.columns:
        x = X[feature].values
        # 1) 連續特徵離散化
        if type_of_target(x) == 'continuous':
            x = discrete(x)
        # 2) 計算該特徵的woe和iv
        # woe_dict, iv = woe_single_x(x, y, feature, event)
        woe_dict, iv = woe_single_x(x, y, feature, event)
        iv_dict[feature] = iv
        res_woe.append(woe_dict) 
        
    return iv_dict
        
def discrete(x):
    # 使用5等分離散化特徵
    res = np.zeros(x.shape)
    for i in range(5):
        point1 = stats.scoreatpercentile(x, i * 20)
        point2 = stats.scoreatpercentile(x, (i + 1) * 20)
        x1 = x[np.where((x >= point1) & (x <= point2))]
        mask = np.in1d(x, x1)
        res[mask] = i + 1    # 將[i, i+1]塊內的值標記成i+1
    return res

def woe_single_x(x, y, feature,event = 1):
    # event代表預測正例的標籤
    event_total = sum(y == event)
    non_event_total = y.shape[-1] - event_total
    
    iv = 0
    woe_dict = {}
    for x1 in set(x):    # 遍歷各個塊
        y1 = y.reindex(np.where(x == x1)[0])
        event_count = sum(y1 == event)
        non_event_count = y1.shape[-1] - event_count
        rate_event = event_count / event_total    
        rate_non_event = non_event_count / non_event_total
        
        if rate_event == 0:
            rate_event = 0.0001
            # woei = -20
        elif rate_non_event == 0:
            rate_non_event = 0.0001
            # woei = 20
        woei = math.log(rate_event / rate_non_event)
        woe_dict[x1] = woei
        iv += (rate_event - rate_non_event) * woei
    return woe_dict, iv
import warnings
warnings.filterwarnings("ignore")

iv_dict = woe(X_train, y_train)
iv = sorted(iv_dict.items(), key = lambda x:x[1],reverse = True)
iv

輸出

[(‘historical_trans_amount’, 2.6975301004625365),
(‘trans_amount_3_month’, 2.5633548887586746),
(‘pawns_auctions_trusts_consume_last_6_month’, 2.343990314630991),
(‘repayment_capability’, 2.31685232254565),
(‘first_transaction_day’, 2.10946672748192),
(‘abs’, 2.048054369415617),
(‘consfin_avg_limit’, 1.8005797778063934),
(‘consume_mini_time_last_1_month’, 1.4570522032774857),

3.2 隨機森林挑選特徵

首先網格調參,求得模型引數。

import warnings
warnings.filterwarnings("ignore")
from sklearn.model_selection import GridSearchCV
from sklearn.ensemble import RandomForestClassifier

# 觀察預設引數的效能
rf0 = RandomForestClassifier(oob_score=True, random_state=2333)
rf0.fit(X_train, y_train)
print('袋外分數:', rf0.oob_score_)
model_metrics(rf0, X_train, X_test, y_train, y_test)

輸出

袋外分數: 0.7342951608055305
[準確率] 訓練集: 0.9805 測試集: 0.7744
[auc值] 訓練集: 0.9996 測試集: 0.7289

# 網格法調參, 步驟省略...
"""
param_test = {'n_estimators':range(20,200,20)}
# param_test = {'max_depth':range(3,14,2), 'min_samples_split':range(50,201,20)}
# param_test = {'min_samples_split':range(10,100,20), 'min_samples_leaf':range(10,60,10)}
# param_test = {'max_features':range(3,17,2)}
gsearch = GridSearchCV(estimator = RandomForestClassifier(n_estimators=120, max_depth=9, min_samples_split=50, 
                                                          min_samples_leaf=20, max_features = 9,random_state=2333), 
                       param_grid = param_test, scoring='roc_auc', cv=5)

gsearch.fit(X_train, y_train)
# gsearch.grid_scores_, 
gsearch.best_params_, gsearch.best_score_
"""

最終引數及效能

rf = RandomForestClassifier(n_estimators=120, max_depth=9, min_samples_split=50,
                            min_samples_leaf=20, max_features = 9,oob_score=True, random_state=2333)
rf.fit(X_train, y_train)
print('袋外分數:', rf.oob_score_)
model_metrics(rf, X_train, X_test, y_train, y_test)

輸出

袋外分數: 0.7844905320108205
[準確率] 訓練集: 0.8115 測試集: 0.7954
[auc值] 訓練集: 0.8946 測試集: 0.7914

3.2.1 平均不純度減少 mean decrease impurity

對於每顆樹,按照impurity(此處是gini指數 )給特徵排序,然後整個森林取平均

rf.fit(X_train, y_train)
feature_impotance1 = sorted(zip(map(lambda x: '%.4f'%x, rf.feature_importances_), list(X_train.columns)), reverse=True)
feature_impotance1[:10]

輸出

[(‘0.1333’, ‘trans_fail_top_count_enum_last_1_month’),
(‘0.0818’, ‘loans_score’),
(‘0.0784’, ‘history_fail_fee’),
(‘0.0623’, ‘apply_score’),
(‘0.0580’, ‘latest_one_month_fail’),
(‘0.0424’, ‘loans_overdue_count’),
(‘0.0307’, ‘trans_fail_top_count_enum_last_12_month’),
(‘0.0237’, ‘trans_fail_top_count_enum_last_6_month’),
(‘0.0194’, ‘trans_day_last_12_month’),
(‘0.0184’, ‘max_cumulative_consume_later_1_month’)]

3.2.2 平均精確率減少 Mean decrease accuracy

打亂每個特徵的特徵值順序,並且度量順序變動對模型的精確率的影響。(也可以measure每個特徵加躁,看對結果的準確率的影響。)

import numpy as np
from collections import defaultdict
from sklearn.model_selection import cross_val_score, ShuffleSplit

scores = defaultdict(list)
rs = ShuffleSplit(n_splits=5, test_size=0.3, random_state=0)
for train_idx, test_idx in rs.split(X_train):
    x_train, x_test = X_train.values[train_idx], X_train.values[test_idx]
    Y_train, Y_test = y_train.values[train_idx], y_train.values[test_idx]
    r = rf.fit(x_train, Y_train)
    acc = accuracy_score(Y_test, rf.predict(x_test))
    for i in range(x_train.shape[1]):
        X_t = x_test.copy()
        np.random.shuffle(X_t[:, i])
        shuff_acc = accuracy_score(Y_test, rf.predict(X_t))
        scores[X_train.columns[i]].append((acc - shuff_acc) / acc)
        
feature_impotance2=sorted([('%.4f'%np.mean(score), feat) for feat, score in scores.items()], reverse=True)
feature_impotance2[:10<