ALINK(二十二):特征工程(一)特征離散化簡介(一)


來源:https://blog.csdn.net/weixin_39552874/article/details/112325629

1 特征離散化方法和實現

特征離散化指的是將連續特征划分離散的過程:將原始定量特征的一個區間一一映射到單一的值。

在下文中,我們也將離散化過程表述為 分箱(Binning) 的過程。

特征離散化常應用於邏輯回歸和金融領域的評分卡中,同時在規則提取、特征分類中同樣有應用價值。

特征離散化后將帶來如下優勢:

  數據被規約和簡化,便於理解和解釋,同時有助於模型部署和應用,加快了模型迭代;
  增強模型魯棒性:對於異常值、無效值、缺失值、未見值可統一歸為一類,降低噪聲,從而避免此類極端值對模型的影響;
  增加非線性表達能力:連續特征不同區間對模型貢獻或重要度不一樣時,分箱后不同的權重能直接體現這種差異,當離散化后的特征再進行特征交叉衍生能進一步加強這種表達能力;
  提升模型的泛化能力:總體來說分箱后模型表現更為穩定;
  此外,還能擴展數據在不同類型算法中的應用范圍;減少存儲空間和加速計算等。
當然,離散化也有缺點,例如:

  信息損失:分箱必然造成一定程度的信息損失;
  增加流程:建模過程加入額外的離散化步驟;
  影響模型穩定性:當一個特征值處在分箱點的邊緣,微小的偏差會造成該特征值的歸屬從一箱躍遷到另外一箱,從而影響穩定性。

離散化/分箱可以從不同的角度進行划分。當分箱方法中使用了目標y的信息,那么該分箱方法就屬於有監督的分箱方法,反之為無監督的分箱方法。

當分箱的算法策略是不斷增加分隔點達到分箱的目的,那么稱之為分裂式;反之,當策略是不斷的合並原始或粗分箱時則稱合並式

相應的,算法層面上,有監督的方法一般屬於貪心算法;分裂式稱為自上而下的算法;合並式則稱為自底向上的算法。

6.3.1節將講述常用的分箱算法,圖6-5總結了這些算法的分類。

分箱算法的流程總結如下:

  預設分箱條件,例如箱數、停止閾值等;
  選擇某個點作為候選分隔點,由算法中的評價指標衡量該分隔點的效用;
  根據該效用是否滿足預設的條件,選擇分裂/合並或放棄;
  當滿足預設的停止條件時停止。
算法不同的評價指標代表不同的分箱算法,除了書中給出的分箱算法外,讀者完全可以嘗試新的方法和策略。

筆者認為離散化的本質是箱/組內差異小,組間差異大。但以下兩個不同的目標有不同的權衡:當分箱是作為特征處理方法時,分箱的目標應是為建模服務;
當分箱的目標直接作為規則和預測時則可追求箱內的純度。

實際上,即使嘗試多種分箱方法,我們依然難以找到一個最優解。下文的分箱方法來自工程實踐,實驗數據依然使用“Breast Cancer Wisconsin (Diagnostic) Database”數據集。

from sklearn.datasets import load_breast_cancer
bc = load_breast_cancer()
y = bc.target
X = pd.DataFrame.from_records(data=bc.data, columns=bc.feature_names)
# 轉化為df
df = X
df['target'] = y

取其中的字段 “mean radius”,其原始分布可視化如圖6-6所示。

 

 

2 等寬和等頻離散法

等寬、等頻分箱都屬於無監督分箱方法,不需要標簽y的信息。由於兩者實現簡單,應用廣泛,例如,便於快速粗篩特征、監控和可視化等。

等寬分箱(Equal-Width Binning):指的是每個分隔點/划分點的距離一樣(等寬)。實踐中一般指定分隔的箱數,等分計算后得到每個分隔點,例如將數據序列分為n份,則分隔點的寬度計算式為(6-4):

一般情況下,等寬分箱的每個箱內的樣本數量不一致,使用Pandas中的cut為例說明如下:

按上述分隔點划分后,數據分布如圖6-7所示。

value,cutoff = pd.cut(df['mean radius'],bins=8,retbins=True,precision=2)

# cutoff 輸出
array([ 6.959871,  9.622125, 12.26325 , 14.904375, 17.5455  , 20.186625,
       22.82775 , 25.468875, 28.11    ])
       
       
c2 = cutoff.copy()

c2[1:] - cutoff[:-1]

# 輸出:可以看出每個分隔點距離相等
array([2.662254, 2.641125, 2.641125, 2.641125, 2.641125, 2.641125,
       2.641125, 2.641125])

等寬方便計算,但是如果數值有很大的差距時,可能會出現沒有數據的空箱(如圖6-7所示的最后一箱數量非常少)。這個問題可以通過自適應數據分布的分箱方法——等頻分箱避免。

3 等頻分箱(Equal-Frequency Binning)

等頻分箱理論上分隔后的箱內數據量大小一致,但當某個值出現次數較多時,會出現等分邊界是同一個值,導致同一數值分到不同的箱內,這是不正確的。具體實現時可去除分界處的重復值,但這也導致每箱的數量不一致。

同樣,我們對df['mean radius']使用等頻分箱,該數據分布正常,等頻分箱后每箱數量基本一致。value的分布如圖6-8所示。

value = pd.qcut(df['mean radius'],8,precision=1)

等頻分箱的一個特例為分位數分箱,下面以4分位數為例進行說明:

#兩者數據的分隔點都是:
# array([ 6.981, 11.7  , 13.37 , 15.78 , 28.11 ])
np.array(np.percentile(df['mean radius'], [0,25, 50, 75,100])) 

# cutoff和上面的分隔點相同
value,cutoff = pd.qcut(df['mean radius'],4,retbins=True)

上述的等寬和等頻分箱容易出現的問題是每箱中信息量變化不大。

例如等寬分箱不太適合分布不均勻的數據集、離群值;

等頻方法不太適合特定的值占比過多的數據集,如長尾分布。

此外,當將“年齡”這樣的特征等頻分箱后,極有可能出現每箱中好壞樣本的差異不顯著,對算法的幫助有限,那么如何才能得到更有意義的分箱呢

4 信息熵分箱原理與實現

以往離散化的方式常由等寬等頻、專家建議、直覺經驗等確定,但這對模型構建和效果優化有限。

如果分箱后箱內樣本對y的區分度好,那么這是一個較好的分箱。我們從信息論理論出發,可知信息熵衡量了這種區分能力。當特征按某個點划分成上下兩部分后能達到最大的信息增益,那么這是一個優選的分箱點。

圖6-9演示了信息增益的計算過程,該示例中特征x=(1,2,3,4,5),y=(0,1,0,1,1),以x為3的點作為分隔點,帶來的信息增益為0.420。

 

 

類似的可以繼續計算其他分隔點的信息增益,最終選取信息增益最大時對應的點為分隔點。

同時我們也可以看出,當分箱后某個箱中因變量y各類別(二分類時:0和1)的比例相等時,那么其熵值達到最大,相反,當某個箱中因變量為單個類別值時,那么該箱的熵值達到最小的0(純度最純)。

從結果上看,最大信息增益對應分箱后的總熵值最小,即最大信息增益划分和最小熵分箱的含義是一致的.

關於信息熵和信息增益,下面給出一個實現參考:

class entropy:
    '''計算離散隨機變量的熵
    x,y:pd.Series 類型
    '''
    @staticmethod
    def entropy(x):
        '''信息熵'''
        p = x.value_counts(normalize=True)
        p = p[p > 0]
        e = -(p * np.log2(p)).sum()
        return e

    @staticmethod
    def cond_entropy(x, y):
        '''條件熵'''
        p = y.value_counts(normalize=True)
        e = 0
        for yi in y.unique():
            e += p[yi] * entropy.entropy(x[y == yi])
        return e

    @staticmethod
    def info_gain(x, y):
        '''信息增益'''
        g = entropy.entropy(x) - entropy.cond_entropy(x, y)
        return g

使用上面的數據計算:

y= pd.Series([0,1,0,1,1])
entropy.entropy(y)

# 輸出
0.97095


x = pd.Series([1,2,3,4,5])
entropy.info_gain(y, (x 4).astype(int))

#輸出
0.41997

基於Sklearn決策樹離散化

經典的決策樹算法中,信息熵是特征選擇和划分的背后實現:

from sklearn.tree import DecisionTreeClassifier

# max_depth=3,表示進行3次划分構造3層的樹結構
def dt_entropy_cut(x, y, max_depth=3, criterion='entropy'):  # gini
    dt = DecisionTreeClassifier(criterion=criterion, max_depth=max_depth)
    dt.fit(x.values.reshape(-1, 1), y)
    qts = dt.tree_.threshold[np.where(dt.tree_.children_left > -1)]
    if qts.shape[0] == 0:
        qts = np.array([np.median(data[:, feature])])
    else:
        qts = np.sort(qts)
    return qts

使用該方法對數據划分示例如下,cutoff中的點即是Sklearn的決策樹基於信息熵的分隔點。

cutoff  = dt_entropy_cut(df['mean radius'],df['target'] )

cutoff = cutoff.tolist()

[np.round(x,3) for x in cutoff]

# 輸出
[10.945, 13.095, 13.705, 15.045, 17.8, 17.88]

最小熵離散化

DecisionTreeClassifier的初衷並不是數據的離散化。我們並不能自由控制分箱的過程,比如倒數第2箱的數據很少,但無法干預。

下文給出了一個自定義的基於信息熵的數據離散化方法,供讀者參考。

1.計算原始信息熵;
2.對於數據中的每個潛在分割點計算以該點划分后各部分的信息熵;
3.計算信息(熵)增益;
4.選擇信息增益最大值對應的分割點;
5.遞歸地對每個分箱后的部分執行2、3、4,直到滿足終止條件為止。終止條件一般為:
 a.到達指定的分箱數。
 b.當信息增益小於某一閾值。
 c.其他經驗條件。
算法結束后得到一系列的分隔點,這些點作為最終的分箱點(Cutoff),由這些分箱點得到分箱(Bins)。

停止條件直接指定箱數即可,但考慮到算法遞歸實現,每進行一次遞歸,分隔點將翻倍,代碼設計時使用了遞歸的輪數,這樣實現比較直接。該算法實現參考6.3.2.3。

5 Best-KS分箱原理與實現

介紹Best-KS的資料非常少,筆者暫未找到詳細資料,但業內確實有許多應用實踐,也許它是實踐得出的特征工程方法吧。

 KS(Kolmogorov-Smirnov)是模型區分能力的一個常用評價指標,該指標衡量的是好壞樣本累計之間的差異。

KS原理:由蘇聯數學家Kolmogorow和Smirnov聯合發明的這一統計方法。Kolmogorov-Smirnov檢驗(K-S檢驗)基於累積分布函數,用於檢驗一個經驗分布是否符合某種理論分布或比較兩個經驗分布是否有顯著性差異,屬於非參數檢驗方法,其優點是不假設數據的分布。

該檢驗的統計量稱為KS值,在幾何上可直觀解釋為:累計好、壞樣本分布曲線的最大間隔。

在數值上可解釋為:累計好、壞樣本分布差值絕對值的最大值。

KS與K統計量無關。

K統計量:是由R.A.Fisher於1928引進的一種對稱統計量,是累積量的無偏估計量。

常見3種KS:

K-S test(K-S檢驗)
模型KS(KS統計量)
特征KS(KS統計量)
其背后的原理都是一致的,其中第2點和第3點實際在做的事情是:比較預測y和真實y分布是否一致或計算差距的度量。

KS為好壞樣本累計分布差異的最大值,其計算邏輯描述如下:

1.計算每個區間或值對應的好壞樣本數。

2.計算累計好樣本數占總好樣本數比率(good%)和累計壞樣本數占總壞樣本數比率(bad%)。

3.計算兩者的差值:累計good%-累計bad%;

4.取絕對值中的最大值記為KS值。

下面給出一個實現參考,CalKS;

當然,KS應用多年,自然有相關的開源實現:

Sklearn中的實現:

import numpy  as np
from sklearn import metrics

Scipy中的實現:

from scipy.stats import ks_2samp

Best-KS離散化

Best-KS分箱邏輯和基於熵的分箱邏輯基本一致。

Best-KS分箱算法屬於二分遞歸分割的技術,遞歸將產生一棵二叉樹
每次遞歸時獨立計算特征序列的KS,與上一步無關。
特征分箱后特征的KS值必將小於等於原特征KS值。
主要算法邏輯可參考上述基於熵的分箱方法的實現,而算法第3點可以由上述的CalKS實現,得到KS最大值對應的點,考慮相關的實現細節后,最終代碼如下:def bestks_cut (df,...) 參考6.3.2.3。

6 卡方分箱原理與實現

卡方分箱算法最早見於Randy Kerber的卡方分箱論文 ,后續關於卡方分箱的內容基本都是基於該版本的優化或改進。

卡方分箱基於卡方檢驗,而卡方檢驗的背后則是卡方分布(Chi-Square Distribution或χ2-distribution)。

卡方基礎

卡方分布、卡方檢驗:一般指的是Pearson卡方檢驗(Pearson's chi-squared test)(由Pearson提出並證明式(6-8)在一定條件下滿足卡方分布),屬於非參數假設檢驗的一種。

有兩種類型的卡方檢驗,但其本質都是頻數之間差異的度量。其原假設為:觀察頻數與期望頻數無差異或兩組變量相互獨立不相關。

卡方擬合優度檢驗(Chi-Square Goodness of Fit Test):用於檢驗樣本是否來自某一個分布,比如檢驗某樣本是否為正態分布
獨立性卡方檢驗(Chi-Square Test for Independence),查看兩組類別變量分布是否有差異或相關,以列聯表(contingency table,如2×2表格)的形式比較。以列連表形式的卡方檢驗中,卡方統計量由式(6-8)給出
其中,O表示觀察到的頻數(即實際的頻數),E表示期望的頻數。

很明顯,越小的卡方值,表明兩者相差越小,反之,越大的卡方值表明兩者差異越大。

示例如圖6-14所示,左側表示觀察到的兩組樣本分布,右側演示了期望頻數的計算過程。由觀察頻數和期望頻數帶入式(6-8)即可求得卡方值。

從計算方式來看,調換行列,卡方值不變,但一般約定是自變量放在列中,因變量放在行中。

科學計算庫scipy包含了卡方檢驗的實現,使用示例如下:

from scipy.stats import chi2_contingency
obs = np.array([[25,50],[30,15]])
# 注意,默認有校准
chi2, p, dof, ex = chi2_contingency(obs,correction=False)

該接口輸出解釋如下:

chi2:表示卡方值,此處值為12.587413;
p:p_value,此處值為0.000388;
dof:表示自由度,此處值為1;
ex:表示期望頻率,此處值如圖6-14所示。
另外,scipy庫中具有計算卡方值的chisquare()函數,但該函數需要手動計算期望值,而chi2_contingency()的易用性更好.

from scipy.stats import chi2
def chi2_table(freedom=10, alpha=None):
    '''使用示例
       1. chi2_table()
       2.chi2_table(alpha=[0.1])
    '''
    if alpha is None:
        alpha = [0.99, 0.95, 0.90, 0.5, 0.1, 0.05, 0.025, 0.01, 0.005]
    df = pd.DataFrame([chi2.isf(alpha, df=i) for i in range(1, freedom)])
    df.columns = alpha
    df.index = df.index + 1
    return df

執行chi2_table()將得到表6-1。

 

 換個角度描述卡方檢驗的物理含義:當兩個分箱/組中,好壞(正負)分布是一致時,卡方為0,相似時接近0,對應到卡方分箱算法中,應該合並這兩箱/組樣本。

7 卡方離散化

卡方分箱采用的是自底向上的算法邏輯,算法執行的過程是逐級合並的過程,正是文獻41使用的“ChiMerge”——卡方合並的原因。

該過程表示如圖6-15所示。圖中顯示了算法的實現過程:計算相鄰組的卡方值,卡方值滿足條件的相鄰組逐層向上合並。

卡方合並的原則是:組內差異小,組間差異大,也就是說卡方值小的(分布差異小)合並,直到和相鄰的分布差異大時停止

算法實現過程分為:

1) 計算卡方值

2) 粗分箱,此處簡單通過數值精度和等分12份實現粗分箱來便於演示

3) 生成列聯表

4) 計算各相鄰箱的卡方值

5) 設定卡方閾值,常使用0.90、0.95、0.99的置信度

6) 設置合並的條件:小於閾值時進行合並

7) 循環直到卡方值不滿足閾值條件

其中chi2_merge_core()實現的功能是合並最小卡方值的相鄰組(合並到左邊組):

def chi2_merge_core(cvs, freq, cutoffs, minidx):
    '''卡方合並邏輯'''
    print('最小卡方值索引:',minidx,';分割點:',cutoffs)
    # minidx后一箱合並到前一組
    tmp = freq[minidx] + freq[minidx + 1]
    freq[minidx] = tmp
    # 刪除minidx后一組
    freq = np.delete(freq, minidx + 1, 0)
    # 刪除對應的分隔點
    cutoffs = np.delete(cutoffs, minidx + 1, 0)
    cvs = np.delete(cvs, minidx, 0)

    # 更新前后兩個組的卡方值,其他部分卡方值未變化
    if minidx <= (len(cvs) - 1):
        cvs[minidx] = chi2_value(freq[minidx:minidx + 2])
    if minidx - 1 >= 0:
        cvs[minidx - 1] = chi2_value(freq[minidx - 1:minidx + 1])
    return cvs, freq, cutoffs
該循環輸出如下:
最小卡方值索引: 0 ;分割點: [ 7.  9. 10. 12. 14. 16. 18. 19. 21. 23. 24. 26. 28.]
最小卡方值索引: 5 ;分割點: [ 7. 10. 12. 14. 16. 18. 19. 21. 23. 24. 26. 28.]
最小卡方值索引: 5 ;分割點: [ 7. 10. 12. 14. 16. 18. 21. 23. 24. 26. 28.]
最小卡方值索引: 5 ;分割點: [ 7. 10. 12. 14. 16. 18. 23. 24. 26. 28.]
最小卡方值索引: 5 ;分割點: [ 7. 10. 12. 14. 16. 18. 24. 26. 28.]
最小卡方值索引: 5 ;分割點: [ 7. 10. 12. 14. 16. 18. 26. 28.]
最小卡方值索引: 4 ;分割點: [ 7. 10. 12. 14. 16. 18. 28.]]

本章小結

隨着開源算法包集成越來越強大,必然出現更多開箱即用的特征工程方法,部分開源包已經將特征工程作為內置功能的一部分,對用戶無感知。

例如CatBoost開源庫對類別變量的自動處理。這在簡化使用的同時,也帶來了副作用:底層的細節越來越不為大眾所知了。

本章提供了所有上述特征處理方法的Python代碼,這些代碼都源於筆者的工程實踐,值得讀者詳細參考學習。

節選自《機器學習:軟件工程方法與實現》第6章

 


免責聲明!

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



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