【風控算法】一、變量分箱、WOE和IV值計算


一、變量分箱

變量分箱常見於邏輯回歸評分卡的制作中,在入模前,需要對原始變量值通過分箱映射成woe值。舉例來說,如”年齡“這一變量,我們需要找到合適的切分點,將連續的年齡打散到不同的”箱“中,並按年齡落入的“箱”對變量進行編碼。

關於變量分箱的作用,相關資料中的解釋有很多,我認為變量分箱最主要有三個作用:

  1. 歸一化:分箱且woe編碼映射后的變量,可以將變量歸一到近似尺度上;
  2. 引入非線性:對於邏輯回歸這類線性模型,引入變量分箱可以增強模型的擬合能力;
  3. 增強魯棒性:分箱可以避免異常數據對模型的影響

二、IV值和WOE

(1)WOE

WOE(Weight of Evidence),是一種對變量編碼的形式。通過對分箱后每一箱WOE值的計算,可以完成變量從原始數值->WOE數值的映射。

\[WOE_i = ln(\frac{y^1_i/y^1}{y^0_i/y^0})=ln(\frac{y^1_i/y^0_i}{y^1/y^0}) =ln(\frac{y^1_i}{y^1})-ln(\frac{y^0_i}{y^0})=ln(\frac{y^1_i}{y^0_i})-ln(\frac{y^1}{y^0}) \]

關於WOE的理解,主要有如下幾點:

  1. WOE可以理解成分箱區間內的正負樣本差異相對於整體的差異。機器學習二分類中,通常將分類任務中更關注的類label設為”1“,因此WOE越大說明該分箱內的樣本越可能為“1”類;
  2. 經過WOE編碼,實現了按WOE排序的區間正樣本比例呈單調趨勢。

(2)IV值

IV(Information Value)是基於WOE計算來的:

\[IV = \sum WOE_i*(\frac{y_i^1}{y^1}-\frac{y_i^0}{y^0}) \]

(3)KL散度

KL散度(相對熵)通常用於衡量兩個分布之間的差異,機器學習中,\(P\)往往代表樣本的真實分布,而\(Q\)代表樣本的預測分布,那么KL散度可以計算兩個分布之間的差異:

\[D_{KL}(p||q) =\sum_{i=1}^np(x_i)log(\frac{p(x_i)}{q(x_i)}) \]

如果\(P\)的分布和\(Q\)的分布越接近,KL散度的值就會越小。

KL散度通常被稱作KL距離,但卻只滿足距離的非負性和同一性,不滿足對稱性和直遞性,因此不是嚴格意義上的“距離"。

設分箱后,\(y=1\)的分布為\(p_1(x)\),\(y=0\)的分布為\(p_0(x)\),那么

\[\begin{aligned} KL(p_0,p_1)+KL(p_1,p_0) &= \sum p(x_i)*log(\frac{p(x_i)}{q(x_i)})+\sum q(x_i)*log(\frac{q(x_i)}{p(x_i)})\\ &=\sum{(p(x_i)-q(x_i))*(log(p(x_i))-log(q(x_i)))}\\ &= IV \end{aligned} \]

由此可知,\(IV=KL(p_0,p_1)+KL(p_1,p_0)\)

據此可以得到關於IV和KL散度更加深刻的理解:

  • IV值衡量了分組下好壞樣本分布差異,IV值越大分布差異越大,IV值越小分布差異越小
  • IV是KL散度的一種對稱化處理

三、分箱方法

變量分箱主要包括無監督方法和有監督方法,無監督的方法,在分箱中沒有用到y有關的信息,而有監督的分箱方法,在分箱時引入了y的分布,運用訓練集找到分箱的切點。

(1)無監督分箱

a.等頻分箱/等距分箱

如字面理解,等頻分箱和等距分箱可直接通過pandas的qcut和cut實現

等頻分箱:qcut IV=0.0158

image.png

等距分箱:cut IV=0.019

image.png

b.聚類分箱

無監督分箱中除了等頻分箱和等距分箱外,也可以使用KMeans算法實現聚類分箱,一個粗糙的代碼實現如下:

from sklearn.cluster import KMeans
def kmeansbin(data,x='x',bin_nums=5):
    minus_ = 999*(data[x].max() - data[x].min())
    bin_nums = 5
    clf = KMeans(n_clusters=bin_nums-1, random_state=999)
    _ = clf.fit_predict(data[[x,x]])
    cut_point = sorted(clf.cluster_centers_[:,0])
    #cut_point = [(clf_center[i]+clf_center[i+1])/2 for i in range(len(clf_center)-1)]
    return [data[x].min()-minus_,] + cut_point + [data[x].max()+minus_,]

效果如下: IV=0.0208

image.png

(2)有監督分箱

a.決策樹分箱

決策樹分箱利用單變量生成決策樹,利用決策樹的分裂規則完成變量分箱,因分箱速度快且效果比較穩定,同時也有sklearn接口可以調用,因此較為常用,一個簡單的代碼實現版本如下:

# 決策樹分箱
from sklearn.tree import DecisionTreeClassifier
def treebin(df,x='x',y='y',max_leaf_nodes=5,min_samples_leaf=0.05):
    # 訓練決策樹
    df = df.copy()
    df[x] = df[x].fillna(-9999)
    model = DecisionTreeClassifier(criterion='entropy',max_leaf_nodes=max_leaf_nodes,min_samples_leaf=min_samples_leaf)
    model.fit(df[[x]],df[[y]])
  
    # 從樹結構獲取決策邊界
    right_node = model.tree_.children_right
    left_node = model.tree_.children_left
    tree_threshold = model.tree_.threshold
  
    # sklearn樹結構,詳見:https://scikit-learn.org/stable/auto_examples/tree/plot_unveil_tree_structure.html#sphx-glr-auto-examples-tree-plot-unveil-tree-structure-py
    final_cut = [tree_threshold[i] for i,node in enumerate(zip(right_node,left_node)) if node[0]!=node[1]]
    minus_ = df[x].max() - df[x].min()
    # 返回分箱邊界
    return [df[x].min()-999*minus_,]+sorted(final_cut)+[df[x].max()+999*minus_]

分箱效果如下: IV=0.0286

image.png

b.卡方分箱

卡方分箱通過計算變量所有不同值之間的卡方值,並對卡方值最低的區間進行合並迭代,最終達到迭代要求的剩余箱數來完成分箱,簡要代碼實現如下:

def chibin(data,x='x',y='y',bin_nums=5,confidenceVal=6.635):
    def cal_chi(arr):
        sum_ = np.sum(arr[:,:2])
        denom_ = np.sum(arr[0,:2])*np.sum(arr[1,:2])*np.sum(arr[:,0])*np.sum(arr[:,1])
        chi_ = (arr[0][0]*arr[1][1] - arr[0][1]*arr[1][0])**2 * sum_ / denom_
        return chi_

    # 所有不同值,計數
    total_num = data.groupby([x])[y].apply(lambda x:{'nums':x.count()}).unstack() 
    # 正樣本計數
    total_num['x'] = total_num.index.tolist()
    total_num['pos_nums'] = data.groupby([x])[y].sum()
    total_num['neg_nums'] = data.groupby([x])[y].apply(lambda x:x.count()-x.sum())
    total_num = total_num[['pos_nums','neg_nums','x']]
    total_num = total_num.values

    # 第一步:合並連續的全正/全負區間
    i = 0
    while i<len(total_num)-1:
        if (total_num[i][0] == total_num[i+1][0] == 0) or (total_num[i][1] == total_num[i+1][1] == 0):
            total_num[i,2] = 0
            total_num[i] += total_num[i+1]
            total_num = np.delete(total_num,i+1,axis=0)
        else:
            i += 1

    # 第二步:計算相鄰區間的卡方值,並存入數組
    arr_chi = np.array([])
    for i in range(len(total_num)-1):
        arr_chi = np.append(arr_chi,cal_chi(total_num[i:i+2]))

    # 第三部:分箱合並
    while len(arr_chi)>bin_nums and min(arr_chi)<confidenceVal:
        idxmin = np.argmin(arr_chi)
        # 與下一個區間合並
        total_num[idxmin,2] = 0
        total_num[idxmin] += total_num[idxmin+1]
        total_num = np.delete(total_num,idxmin+1,axis=0)
        # 更新卡方值
        # 如果 最低卡方值是前兩個區間
        # 如果 最低卡方值是最后兩個區間
        # 如果 最低卡方值在中間

        if idxmin == 0:
            arr_chi[0] = cal_chi(total_num[:2])
            arr_chi = np.delete(arr_chi,1,axis=0)
        elif idxmin == len(arr_chi)-1:
            arr_chi[idxmin-1] = cal_chi(total_num[idxmin-1:])
            arr_chi = np.delete(arr_chi,idxmin,axis=0)
        else:
            arr_chi[idxmin-1] = cal_chi(total_num[idxmin-1:idxmin+1,:])
            arr_chi[idxmin+1] = cal_chi(total_num[idxmin:idxmin+2,:])
            arr_chi = np.delete(arr_chi,idxmin,axis=0)

    # 合並完畢,返回分箱邊界
    minus_ = total_num[:,2].max() - total_num[:,2].min()

    return [total_num[:2].min() - 999*minus_,] + sorted(total_num[:-1,2]) + [total_num[:2].max() + 999*minus_,]

卡方值和KL散度一樣,都是用於衡量分布之間差異的指標,因此處不是重點,所以不再詳細說明

效果如下: IV=0.0303

image.png

c.BestKS分箱

BestKS分箱通過不斷計算所有可能切分點的KS,每次分箱選擇讓KS最大的切分點,最終達到要求的分箱數來完成分箱,具體的實現思路其他文章中都有較為詳細的介紹,因此此處也不再贅述。

一個簡要的代碼實現:

def bestksbin(data,x='x',y='y',bin_nums=5,stopl=0.05):
    cut_point = []
    minus_ = 999*(data[x].max() - data[x].min())

    if len(data[x].unique())<=bin_nums:
        cut_point = data[x].unique()
        return [data[x].min()-minus_,] + cut_point + [data[x].max()+minus_,]

    cut_point.append(binks(data,x,y)[0])

    while len(cut_point) < bin_nums-1:
        bestks = -999
        bestcut = None
        icnt = 0
        while icnt <= len(cut_point):
            if icnt == 0:
                tmpcp,tmpks = binks(data[data[x]<=cut_point[icnt]],x,y,l=data.shape[0],stopl=stopl)
            elif icnt == len(cut_point):
                tmpcp,tmpks = binks(data[data[x]>cut_point[icnt-1]],x,y,l=data.shape[0],stopl=stopl)
            else:
                tmpcp,tmpks = binks(data[(data[x]>cut_point[icnt-1])&(data[x]<=cut_point[icnt])],x,y,l=data.shape[0],stopl=stopl)
            if tmpks > bestks:
                bestcut,bestks = tmpcp,tmpks
            icnt += 1
        if not bestcut:
            break
        cut_point.append(bestcut)
        cut_point = sorted(cut_point)
    return [data[x].min()-minus_,] + cut_point + [data[x].max()+minus_,]


def binks(data,x='x',y='y',l=10000,stopl=0.05):
    if (len(data[x].unique()) == 1) or (data.shape[0]/l<stopl) or (data[y].sum()==data.shape[0]) or (data[y].sum()==0):
        return None,-9999
    tmp = data.groupby(x)[y].apply(lambda x:{'count':x.count(),'bad':x.sum()}).unstack()
    tmp['x'] = tmp.index.tolist()
    tmp['good'] = tmp['count'] - tmp['bad']
    tmp['cumgood'] = tmp['good'].cumsum()
    tmp['cumbad'] = tmp['bad'].cumsum()
    tmp['cumgood_ratio'] = tmp['cumgood'] / (data.shape[0] - data['y'].sum())
    tmp['cumbad_ratio'] = tmp['cumbad'] / data['y'].sum()
    tmp['KS'] = abs(tmp['cumgood_ratio']-tmp['cumbad_ratio'])
    tmp = tmp.reset_index(drop=True)
    tmp = tmp[tmp['x'] != tmp['x'].max()]
    maxidx = tmp['KS'].argmax()
    return tmp.loc[maxidx ,'x'],tmp.loc[maxidx,'KS']
  

分箱效果如下: IV=0.0281

image.png

總結

本文主要記錄了變量分箱、WOE和IV值計算,其中包括了有監督分箱的幾種方法的代碼實現,代碼寫的倉促可能其中有一些疏漏,在未來的學習和研究中可能會優化其中代碼。

參考資料

特征工程之特征分箱

KL散度


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM