貸款違約預測專案-資料分箱

ReidChenJX發表於2020-11-09

我們知道,在使用誤差平方和作為損失函式的模型中,離群點的存在會極大提高誤差值。但如果直接刪除離群點樣本,訓練資料減少也會降低模型的精度,特別是在本例中,離群點佔正例的概率很大。為了保留離群點,同時又能夠起到優化模型的效果,我們可採用資料分箱的技術。

特徵篩除

在分箱之前,我們先剔除掉資訊量少的特徵。在資訊理論中,我們學習到,如果一個訊號的方差很大,那麼其中包含的資訊量就多,同理,如果一個特徵的數值分佈廣,那麼包含的資訊就多。處理資料前,我們可以剔除單一變數的特徵,可認為該類特徵較少資訊。

# 特徵篩選:單一變數比重檢測,刪除比重超過95%的特徵
def drop_single_variable(data):
    drop_list = []
    for col in data.columns:
        precent = data[col].value_counts().max() / float(len(data))
        if precent >= 0.95:
            drop_list.append(col)
    data.drop(drop_list,axis=1,inplace=True)
    return drop_list
drop_list = drop_single_variable(data_train)
data_test.drop(drop_list,axis=1,inplace=True)

卡方分箱

最先想到的方法是卡方分箱。卡方分箱值利用卡方係數來合併資料分組,進而達到分箱的目的。
卡方統計的公式:卡方統計
其中fo指實際頻數,fe指期望頻數。卡方係數的計算過程如下:

  1. 統計正樣本個數佔所有樣本個數的比值,將其作為期望比例fp。
  2. 對特徵A的取值進行從大到小排序,並去重。通過計算樣本中A=a值的個數,乘以期望比例fp,當做特徵A為a時的期望頻數fe。通過統計樣本中A=a時,正樣本的個數作為實際頻數fo。運用上面公式計算特徵A=a時的卡方係數。
  3. 改變A的取值,遍歷特徵A所有的可能取值,可得到每一個取值下的卡方係數。

卡方係數的意義:如果特徵A=a樣本下,計算得到卡方係數接近於0,則意味著特徵A的值是否為a,與標籤Y是否為正,是兩個獨立條件。也意味著特徵A=a對標籤沒有貢獻。
以上步驟,我們對特徵A所有的可能情況計算了卡方係數,預設根據特徵A進行了多次分箱。接下來我們要考慮如何進行分箱合併。

  1. 選擇出卡方係數最小的一箱資料,向前或向後進行合併,合併規則為:選擇前一或後一個最小卡方係數的箱,進行合併。合併後計算新的卡方係數。
  2. 通過不斷的重複步驟4,直到滿足我們的分箱條件,如:滿足最小分箱個數,或者每個箱子滿足一定的卡方閾值。

根據順序對資料進行分箱後,我們即可根據資料區間實現樣本的分箱:利用 cut 函式。

卡方分箱實際操作程式碼如下:

# 計算資料特徵的卡方值
def get_chi2(data, col):
    # 計算樣本期望頻率
    pos_cnt = data['isDefault'].sum()
    all_cnt = data['isDefault'].count()
    expected_ratio = float(pos_cnt/all_cnt)
    
    # 對變數按照順序排序
    df = data[[col,'isDefault']]
    col_value = list(set(df[col]))    # 用set排除重複項
    col_value.sort()
    
    # 計算每一個區間的卡方統計量
    chi_list = []
    pos_list = []
    expected_pos_list = []
    
    for value in col_value:
        df_pos_cnt = df.loc[df[col]==value, 'isDefault'].sum()    # 實際頻數
        df_all_cnt = df.loc[df[col]==value, 'isDefault'].count()
        
        expected_pos_cnt = df_all_cnt * expected_ratio    # 期望頻數
        chi_square = (df_pos_cnt - expected_pos_cnt)**2 / expected_pos_cnt
        
        chi_list.append(chi_square)
        pos_list.append(df_pos_cnt)
        expected_pos_list.append(expected_pos_cnt)
    
    # 將結果匯入DataFrame格式
    chi_result = pd.DataFrame({col:col_value,'chi_square':chi_list,
                              'pos_cnt':pos_list,'expected_pos_cnt':expected_pos_list})
    return chi_result

# 根據給定的自由度和顯著性水平, 計算卡方閾值
def cal_chisqure_threshold(dfree=4, cf=0.1):
    
    percents = [0.95, 0.90, 0.5, 0.1, 0.05, 0.025, 0.01, 0.005]
    
    ## 計算每個自由度,在每個顯著性水平下的卡方閾值
    df = pd.DataFrame(np.array([chi2.isf(percents, df=i) for i in range(1, 30)]))
    df.columns = percents
    df.index = df.index+1
    
    pd.set_option('precision', 3)
    return df.loc[dfree, cf]

# 給定資料集與特徵名稱,通過最大分箱數與卡方閾值,得出卡方表與最佳分箱區間
def chiMerge_chisqure(data, col, dfree=4, cf=0.1, maxInterval=7):

    chi_result = get_chi2(data, col)
    threshold = cal_chisqure_threshold(dfree, cf)
    min_chiSquare = chi_result['chi_square'].min()
    group_cnt = len(chi_result)
    
    # 如果變數區間的最小卡方值小於閾值,則繼續合併直到最小值大於等於閾值
    
    while(min_chiSquare < threshold or group_cnt > maxInterval):
        min_index = chi_result[chi_result['chi_square']==chi_result['chi_square'].min()].index.tolist()[0]
        
        # 如果分箱區間在最前,則向下合併
        if min_index == 0:
            chi_result = merge_chiSquare(chi_result, min_index+1, min_index)    # min_index+1, min_index的順序可保證最小值在前,便於切分割槽間
        
        # 如果分箱區間在最後,則向上合併
        elif min_index == group_cnt-1:
            chi_result = merge_chiSquare(chi_result, min_index-1, min_index)    # min_index-1, min_index的順序保證最大值在最後,便於切分割槽間
        
        # 如果分箱區間在中間,則判斷與其相鄰的最小卡方的區間,然後進行合併
        else:
            if chi_result.loc[min_index-1, 'chi_square'] > chi_result.loc[min_index+1, 'chi_square']:
                chi_result = merge_chiSquare(chi_result, min_index, min_index+1)
            else:
                chi_result = merge_chiSquare(chi_result, min_index-1, min_index)
        
        min_chiSquare = chi_result['chi_square'].min()
        
        group_cnt = len(chi_result)

    boundary = list(chi_result.iloc[:,0])
    
    return chi_result, boundary


#     按index進行合併,並計算合併後的卡方值,mergeindex 是合併後的序列值
def merge_chiSquare(chi_result, index, mergeIndex, a = 'expected_pos_cnt',b = 'pos_cnt', c = 'chi_square'):

    chi_result.loc[mergeIndex, a] = chi_result.loc[mergeIndex, a] + chi_result.loc[index, a]
    chi_result.loc[mergeIndex, b] = chi_result.loc[mergeIndex, b] + chi_result.loc[index, b]
    ## 兩個區間合併後,新的chi2值如何計算
    chi_result.loc[mergeIndex, c] = (chi_result.loc[mergeIndex, b] - chi_result.loc[mergeIndex, a])**2 /chi_result.loc[mergeIndex, a]
    
    chi_result = chi_result.drop([index])
    ## 重置index
    chi_result = chi_result.reset_index(drop=True)
    
    return chi_result

for fea in continuous_fea:
    chi_result, boundary = chiMerge_chisqure(data_train, fea)
    data_train[fea+'kf_bins'] = pd.cut(data_train[fea], bins= boundary, labels=False)
    data_test[fea+'kf_bins'] = pd.cut(data_test[fea], bins= boundary, labels=False)

卡方分箱優點:

  1. 減少離群點與異常點對模型的影響。將上萬資料區分為個位數的資料箱,可有效防止異常資料的影響。在分箱過程中,還可以對缺失資料進行單獨分箱,替代缺失值填充的過程。
  2. 防止過擬合,模型目的為二分類,整合資料能有效防止過擬合。
  3. 對採用梯度下降演算法的模型,能加速擬合過程,採用分箱後的資料代替原始資料,可實現資料標準化的功能,避免量綱對模型的干擾。

卡方分箱缺點:分箱過程較慢。分箱涉及大量重複性計算過程。當然可以採用設定初始箱數的方法來加數計算過程,如:開始以100數為整體,進行初始分箱。

決策樹分箱

分箱的實際意義在於:選擇合適的切分點,對資料集進行切分。
在傳統機器學習演算法中,決策樹剛好是最直觀的進行資料切分的模型。
我們構造一顆以資訊熵為指標的決策樹,決策樹的葉子節點數就是我們需要的分箱數。訓練決策樹後,通過獲取樹生成過程中的切分點,即可獲得分箱區間,過程如下:

from sklearn.tree import DecisionTreeClassifier
# 利用決策樹獲得最優分箱的邊界值列表
def optimal_binning_boundary(x: pd.Series, y: pd.Series, nan: float = -999.) -> list:
    
    boundary = []  # 待return的分箱邊界值列表
    
    x = x.fillna(nan).values  # 填充缺失值
    y = y.values
    
    clf = DecisionTreeClassifier(criterion='entropy',    #“資訊熵”最小化準則劃分
                                 max_leaf_nodes=6,       # 最大葉子節點數
                                 min_samples_leaf=0.05)  # 葉子節點樣本數量最小佔比

    clf.fit(x.reshape(-1, 1), y)  # 訓練決策樹
    
    n_nodes = clf.tree_.node_count
    children_left = clf.tree_.children_left
    children_right = clf.tree_.children_right
    threshold = clf.tree_.threshold
    
    for i in range(n_nodes):
        if children_left[i] != children_right[i]:  # 獲得決策樹節點上的劃分邊界值
            boundary.append(threshold[i])

    boundary.sort()

    min_x = x.min() - 0.1  
    max_x = x.max() + 0.1  # -0.1 +0.1是為了考慮後續groupby操作時,能包含特徵最小值,最大值的樣本
    boundary = [min_x] + boundary + [max_x]
    return boundary

for fea in continuous_fea:
    boundary = optimal_binning_boundary(x=data_train[fea],y=data_train['isDefault'])
    data_train[fea+'_tr_bins'] = pd.cut(data_train[fea], bins= boundary, labels=False)
    data_test[fea+'_tr_bins'] = pd.cut(data_test[fea], bins= boundary, labels=False)

採用決策樹分箱,明顯比卡方分箱更快。並且能獲得不弱於卡方分箱的WOE值。在實際過程中,更推薦使用決策樹來進行分箱。

WOE值與IV值

分箱後,為進一步利用資料,可進行WOE與IV值的轉換。

# 計算特徵的WOE與IV值
def call_WOE_IV(data, var, target):
    eps = 0.0001
    gbi = pd.crosstab(data[var], data[target]) + eps
    gb = data[target].value_counts() + eps
    gbri = gbi / gb
    gbri.rename(columns={'0':'0_i','1':'1_i'},inplace=True)

    gbri['WOE'] = np.log(gbri[1] / gbri[0])
    gbri['IV'] = (gbri[1] - gbri[0]) * gbri['WOE']
    
    congb = pd.concat([gbi,gbri],axis=1)
    return congb
# 計算分箱後的WOE值,並生成新的特徵
for col in data_train.columns:
    if 'tr_bins' in col:
        WOE_table = dict(call_WOE_IV(data_train,col,'isDefault')['WOE'])
        data_train[col+'_woe'] = data_train[col].map(WOE_table)
        data_test[col+'_woe'] = data_test[col].map(WOE_table)

WOE值與IV值的意義有很多部落格進行解釋,這裡不再贅述。我們將資料的WOE值作為新的特徵帶入模型,在一定程度上能夠提高模型的精確度。

相關文章