單變數過濾方法主要是基於特徵變數和目標變數之間的相關性、互信息等計算出來的,總結如下:

1、最簡單的方差選擇法

from sklearn.feature_selection import VarianceThreshold
result=VarianceThreshold(threshold=0.5).fit_transform(data.data)
#然後根據各個特徵的方差的結果來刪除方差太小的特徵
#如果嫌人工看麻煩可以使用selectkbest或者selectprecentile來自動選擇

這種方法雖然簡單,但是最大的問題是將特徵的重要性完全歸結於統計學上的方差,然而問題在於在實際的業務場景中,可能方差很小的特徵攜帶了非常重要的信息。舉個例子,比如正負樣本非常不均衡的二分類問題中,正樣本有10個,負樣本有10000個,我們的建模目標是盡量用模型將這10個正樣本分辨出來,假設我們存在某個特徵恰好正樣本在該特徵上取值為1,負樣本在該特徵上取值為0,則這個特徵的方差會很小,但是確具有重要的意義,另外,方差的計算還會受到異常值的影響所以使用前可能還需要事先對異常值進行相應的處理。所以,個人一般傾向於使用這種方法來篩選方差為0或者極其接近於0的特徵。

2、覆蓋率

from《美團機器學習實戰》,主要是針對類別型特徵來計算的一個衡量指標,假設樣本個數一共有10000個,某個類別特徵f1一共有「A」,「B」,「C」三種,並且分別有8000個「A」,1950個「B」以及50個「C」,則「A」,「B」,「C」的覆蓋率分別為:8000/10000,1950/10000,50/10000。類似的,覆蓋率小的特徵更容易被剔除。

#假設f1為類別型特徵則:
from collections import Counter
Counter(f1) #即可計算出不同的類別特徵的數量,然後分別除以總樣本數量即可

這個其實簡單說就是根據類別特徵中各個子類別的數量來進行處理,比如100000個樣本某些類別出現的次數很小例如就5次或者10次,這這些出現次數很少的類別可以合併為「other」類,這樣進行onehot展開的時候一方面能降低高基數類別特徵的onehot展開之後維度太高的問題,一方面能夠降低過擬合的風險。是高基類類別特徵的一種比較常規常見而且簡單好理解的處理方式,問題在於到底出現多少次算是「少數類」要合併到「other」中去?這個目前沒有什麼明確的標準,個人經驗是根據分佈圖的情況來人工嘗試劃分幾次然後比較最終的模型評價的結果。

3、互信息

簡單回歸一下資訊理論的知識:

信息奠基人香農(Shannon)認為「信息是用來消除隨機不確定性的東西」。也就是說衡量信息量大小就看這個信息消除不確定性的程度。(習慣上對數的底數我們取2或者是e,下面是2的情況)

1、信息量

很好理解,「太陽每天從東方升起」這個事件的概率為1,則信息量I=0,因為這是一件確定性的事件,所以不確定性為0,則信息量為0。

2、信息熵

看公式,以扔硬幣為例子,假設硬幣均勻正反改為均為0.5,則扔均勻硬幣根據公式可得信息熵就是-(1/2log2(1/2)+1/2*log2(1/2),其實就是加權平均信息量。這個和id3決策樹裡面用的信息熵是一個東西。信息熵用來衡量事物不確定性。信息熵越大,事物越具不確定性,事物越複雜,具有的總的信息量越大。

3、聯合熵與條件熵

上面是聯合熵的公式,其實如果把(x,y)當做一個整體z,其形式和信息熵的公式是差不多的。

上面是條件熵,含義很直觀,就是已知Y的情況下,X的信息熵的大小,舉一個極端的例子,如果X和Y相互獨立,則以Y為先驗的X的聯合熵在數值上等於X的信息熵。聯合熵和信息熵的關係為:

4、互信息(信息增益)

從公式來看,互信息就是衡量引入X之後,Y的信息熵的變化量,如果信息熵變化很大,說明X的引入讓Y的不確定性提高了,那麼就說明這個特徵X相對於target Y是很重要的。

這個其實就是ID3用到的信息增益,對,兩個是一個東西。

下面詳細展開:

douban.com/note/6215885 發現了一篇關於互信息的討論很深刻的文章,作者真是非常細心和專業!下面的代碼部分就是直接根據這個網址裏的內容寫的

正式地,兩個離散隨機變數 X 和 Y 的互信息可以定義為:

其中 p(x,y) 是 X 和 Y 的聯合概率分佈函數,而p(x)和p(y)分別是 X 和 Y 的邊緣概率分佈函數。

在連續隨機變數的情形下,求和被替換成了二重定積分:

其中 p(x,y) 當前是 X 和 Y 的聯合概率密度函數,而p(x)和p(y)分別是 X 和 Y 的邊緣概率密度函數。但是尷尬的是,「 X 和 Y 的聯合概率密度函數」以及「 X 和 Y 的邊緣概率密度函數」我們事先是不知道的,它不像兩個變數都是離散變數的情況那樣可以直接通過計數的方式進行計算,所以更常見的是對離散的特徵和目標值來計算互信息的值(當然可以對連續值進行分箱離散化之後計算互信息,決策樹之所以能對連續特徵進行互信息-信息增益的計算,實際上是一開始做了二分類,也就是把原來的連續特徵離散化成兩塊稱為離散型特徵,所以本質上也是計算兩個離散的列之間(離散化特徵與分類標籤)的互信息)。

from sklearn.metrics import mutual_info_score
signals=[1,1,0,0,0]
label_1=[a,a,s,s,s]
mutual_info_score(signals,label_1)

根據這個公式手動計算一下

為了驗證這個函數沒寫錯還是自己手動算一下有沒有問題:

0.4*math.log(2.5,np.e)+0.6*math.log(5/3.0,np.e)=0.6730116670092565 和調包的結果一樣,nice。看來是沒問題了。

然而接下來出現了這麼一個問題

label_2=[a,b,c,d,e]
mutual_info_score(signals,label_1)

計算得到的結果居然和上一段代碼的結果完全一樣?

從文氏圖的角度出發:

首先互信息的另一個公式:

這個公式的形式直接暗示著集合的文氏圖,如下:

則根據互信息我們可以寫出如下代碼:

from collections import Counter
signals=[1,1,0,0,0]
labels_1=[a,a,s,s,s]
sig_label_1=[1a,1a,0s,0s,0s]
def entropy(labels):
prob_dict=Counter(labels)
s=sum(prob_dict.values())
probs=np.array([i/s for i in prob_dict.values])
return -prob.dot(np.log(probs))

信息熵計算公式

結果和上面的是完全相同的。

問題在於,更複雜的 labels_2 的熵確實變大了。但聯合熵也同步變大,求差之後對消,於是交集部分的互信息毫無差異。用圖來看會更明確:

那麼為瞭解決這個問題,就引入了標準互信息化NMI,正好sklearn中也已經做了實現。

NMI的公式如下:

式(1)使用幾何平均描述 H(A)和H(B)的規模,式(2)使用算術平均。新版的sklearn中默認使用的是算是平均的版本也就是(1)式。(標準化的計算方式有min,geometric,arithmetic和max四種,不設置則用默認的幾何平均,不過官方文檔的警告提示「The behavior of NMI will change in version 0.22. To match the behavior of v_measure_score, NMI will use average_method=arithmetic by default.」)

然後我們重新計算一下:

from sklearn.metrics import normalized_mutual_info_score
signals=[1,1,0,0,0]
label_1=[a,a,s,s,s]
print(normalized_mutual_info_score(signals,label_1))
label_2=[a,b,c,d,e]
print(normalized_mutual_info_score(signals,label_2))

計算結果如上圖

很好,總算是working了。通過標準化互信息的方式就能比較好的對不同的特徵進行比較,避免了未標準化信息在上文中提到的bug。當然,即使是標準化互信息計算出來的結果也並不是放之四海而皆準的,和所有的評價指標一樣,它只是從某個角度反應了特徵的重要性得分而已。


4、卡方檢驗

(from 百度百科)卡方檢驗就是統計樣本的實際觀測值與理論推斷值之間的偏離程度,實際觀測值與理論推斷值之間的偏離程度就決定卡方值的大小,如果卡方值越大,二者偏差程度越大;反之,二者偏差越小;若兩個值完全相等時,卡方值就為0,表明理論值完全符合。

注意:卡方檢驗針對分類變數。

sklearn中的chi2計算的結果是卡方統計量。

源代碼可查:

X = check_array(X, accept_sparse=csr)
if np.any((X.data if issparse(X) else X) < 0):
raise ValueError("Input X must be non-negative.")

Y = LabelBinarizer().fit_transform(y)
if Y.shape[1] == 1:
Y = np.append(1 - Y, Y, axis=1)

observed = safe_sparse_dot(Y.T, X) # n_classes * n_features

feature_count = X.sum(axis=0).reshape(1, -1)
class_prob = Y.mean(axis=0).reshape(1, -1)
expected = np.dot(class_prob.T, feature_count)

blog.csdn.net/ludan_xia 找到了一篇解釋的比較清楚的關於卡方統計量計算的文章:

為了驗證sklearn的chi2的計算公式沒問題(懶得去折騰源代碼了直接算一算比較一下就行),使用一些簡單數據手動計算結果然後再用sklearn的chi2計算比較:

import numpy as np

drink_milk=np.ones(139) #139個喝牛奶的
target1=drink_milk.copy()
target1[43:139]=0 #96個人沒感冒,43個人感冒
not_drink_milk=np.zeros(112)
target2=not_drink_milk.copy()
target2[0:28]=1

x=np.concatenate([drink_milk,not_drink_milk])
y=np.concatenate([target1,target2])

from sklearn.feature_selection import chi2
chi2(x,y)

結果發現。。。怎麼計算的結果不一樣???


def _chisquare(f_obs, f_exp):
"""Fast replacement for scipy.stats.chisquare.
Version from https://github.com/scipy/scipy/pull/2525 with additional
optimizations.
"""
f_obs = np.asarray(f_obs, dtype=np.float64)

k = len(f_obs)
# Reuse f_obs for chi-squared statistics
chisq = f_obs
chisq -= f_exp
chisq **= 2
with np.errstate(invalid="ignore"):
chisq /= f_exp
chisq = chisq.sum(axis=0)
return chisq, special.chdtrc(k - 1, chisq)

def chi2(X, y):
"""Compute chi-squared stats between each non-negative feature and class.
This score can be used to select the n_features features with the
highest values for the test chi-squared statistic from X, which must
contain only non-negative features such as booleans or frequencies
(e.g., term counts in document classification), relative to the classes.
Recall that the chi-square test measures dependence between stochastic
variables, so using this function "weeds out" the features that are the
most likely to be independent of class and therefore irrelevant for
classification.
Read more in the :ref:`User Guide <univariate_feature_selection>`.
Parameters
----------
X : {array-like, sparse matrix}, shape = (n_samples, n_features_in)
Sample vectors.
y : array-like, shape = (n_samples,)
Target vector (class labels).
Returns
-------
chi2 : array, shape = (n_features,)
chi2 statistics of each feature.
pval : array, shape = (n_features,)
p-values of each feature.
Notes
-----
Complexity of this algorithm is O(n_classes * n_features).
See also
--------
f_classif: ANOVA F-value between label/feature for classification tasks.
f_regression: F-value between label/feature for regression tasks.
"""

# XXX: we might want to do some of the following in logspace instead for
# numerical stability.
X = check_array(X, accept_sparse=csr)
if np.any((X.data if issparse(X) else X) < 0):
raise ValueError("Input X must be non-negative.")

Y = LabelBinarizer().fit_transform(y)
if Y.shape[1] == 1:
Y = np.append(1 - Y, Y, axis=1)

observed = safe_sparse_dot(Y.T, X) # n_classes * n_features

feature_count = X.sum(axis=0).reshape(1, -1)
class_prob = Y.mean(axis=0).reshape(1, -1)
expected = np.dot(class_prob.T, feature_count)

return _chisquare(observed, expected)

測了半天終於找到問題了。。。真是服了,chi2僅僅計算y=1的時候的卡方值,對照原文:

按照公式來看的話應該是:(43-39.3231)**2/39.3231+(96-99.6769)**2/99.6769+(28-31.6848)**2/31.6848+(84-80.3125)**2/80.3125=1.077和手算的結果是一樣的,但是根據chi2的源代碼顯示,它只計算了(43-39.3231)**2/39.3231+(96-99.6769)**2/99.6769=0.4806。。。。。。也就是僅僅計算了喝牛奶組的情況(如上圖),不喝牛奶組的計算結果沒有計入在內。。。。wadu heck

好吧,只能說算是計算了特徵相對於目標值「1」的卡方值。

折騰死了、、、、、、

5、Fisher得分

其中Uij和Pij分別為特徵i在類別j中的均值和方差,Ui為特徵i的均值,nj為類別j中的樣本數量。所以顯然,fisher scoring針對的是連續型的feature與離散型的target。feature在不同的類別target之間的差異越大,在同一個類別中的差異越小,則特徵越重要。

非常nice,居然找到了skfeature這種東西。。。

github.com/jundongl/sci git下載下來然後python setup.py install,pip 沒法下載,應該是都沒放到pypy裏

#源代碼:
def fisher_score(X, y):
"""
This function implements the fisher score feature selection, steps are as follows:
1. Construct the affinity matrix W in fisher score way
2. For the r-th feature, we define fr = X(:,r), D = diag(W*ones), ones = [1,...,1], L = D - W
3. Let fr_hat = fr - (fr*D*ones)*ones/(ones*D*ones)
4. Fisher score for the r-th feature is score = (fr_hat*D*fr_hat)/(fr_hat*L*fr_hat)-1
Input
-----
X: {numpy array}, shape (n_samples, n_features)
input data
y: {numpy array}, shape (n_samples,)
input class labels
Output
------
score: {numpy array}, shape (n_features,)
fisher score for each feature
Reference
---------
He, Xiaofei et al. "Laplacian Score for Feature Selection." NIPS 2005.
Duda, Richard et al. "Pattern classification." John Wiley & Sons, 2012.
"""

# Construct weight matrix W in a fisherScore way
kwargs = {"neighbor_mode": "supervised", "fisher_score": True, y: y}
W = construct_W(X, **kwargs)

# build the diagonal D matrix from affinity matrix W
D = np.array(W.sum(axis=1))
L = W
tmp = np.dot(np.transpose(D), X)
D = diags(np.transpose(D), [0])
Xt = np.transpose(X)
t1 = np.transpose(np.dot(Xt, D.todense()))
t2 = np.transpose(np.dot(Xt, L.todense()))
# compute the numerator of Lr
D_prime = np.sum(np.multiply(t1, X), 0) - np.multiply(tmp, tmp)/D.sum()
# compute the denominator of Lr
L_prime = np.sum(np.multiply(t2, X), 0) - np.multiply(tmp, tmp)/D.sum()
# avoid the denominator of Lr to be 0
D_prime[D_prime < 1e-12] = 10000
lap_score = 1 - np.array(np.multiply(L_prime, 1/D_prime))[0, :]

# compute fisher score from laplacian score, where fisher_score = 1/lap_score - 1
score = 1.0/lap_score - 1
return np.transpose(score)

驚喜的發現fisher_score 實現是純函數式的而且用的numpy,這樣用後期如果需要提高運行速度可以比較方便的用numba或者cython來做加速

from skfeature.function.similarity_based import fisher_score
from sklearn.datasets import load_iris
X=load_iris().data
y=load_iris().target
print(fisher_score.fisher_score(X,y))

6、woe編碼和IV值

懶得手打,下文來自於(cnblogs.com/WoLykos/p/9

woe編碼和IV值的計算主要針對的是離散型特徵和二分類問題所提出的一種特徵重要性的衡量方法,在信貸、風控模型中很常見,iv是在woe的基礎上計算的,所以我們先簡單瞭解一下woe的概念。

woe全稱是「Weight of Evidence」,即證據權重,是對原始自變數的一種編碼形式。

進行WOE編碼前,需要先把這個變數進行分組處理(離散化)

  其中,pyi是這個組中響應客戶(即模型中預測變數取值為「是」或1的個體,也叫壞樣本)佔所有樣本中所有響應客戶的比例,pni是這個組中未響應客戶(也叫好樣本)佔樣本中所有未響應客戶的比例;

  #yi是這個組中響應客戶的數量,#ni是這個組中未響應客戶的數量,#yT是樣本中所有響應客戶的數量,#nT是樣本中所有未響應客戶的數量。

  從這個公式中我們可以體會到,WOE表示的實際上是「當前分組中響應客戶佔所有響應客戶的比例」和「當前分組中沒有響應的客戶佔所有沒有響應的客戶的比例」的差異。

  為了更簡單明瞭一點,我們來做個簡單變換,得:

  不難看出,woe表示的是當前這個組中響應客戶和未響應客戶的比值,和所有樣本中這個比值的差異。這個差異是用這兩個比值的比值,再取對數來表示的。例子如下:

WOE越大,這種差異越大,這個分組裡的樣本響應的可能性就越大,WOE越小,差異越小,這個分組裡的樣本響應的可能性就越小。woe反映的是在自變數每個分組下違約用戶對正常用戶佔比和總體中違約用戶對正常用戶佔比之間的差異;從而可以直觀的認為woe蘊含了自變數取值對於目標變數(違約概率)的影響。再加上woe計算形式與logistic回歸中目標變數的logistic轉換(logist_p=ln(p/1-p))如此相似,因而可以將自變數woe值替代原先的自變數值。(注意,所謂woe編碼就是將某個類別特徵的各個類別分別用它的woe值進行替換,這樣就解決了類別變數展開之後的稀疏問題。)

woe的手動編寫就不用我寫了吧,網上一大堆自己找一個就行了,計算思路也很簡單,下面推薦的是一個很nice的scikit_learncontrib——categorical_encoders,最新版本的還支持catboost對分類變數的編碼方式,cool,代碼本身邏輯也比較easy,後期如果需要自主修改也比較方便。(根據源代碼的提示,對於woe編碼中可能存在的除0問題,woeencoder默認是使用1來代替除數,這個參數的大小可以自己設置,同時對於訓練集中存在的類別如果測試集中出現了新的則使用handle_unknown來處理,具體參看官網的api介紹)

from category_encoders.woe import WOEEncoder
woe=WOEEncoder(cols=cols,handle_unknown=return_nan)
woe_result=woe.fit_transform(data[cols],target)

Category Encoders?

contrib.scikit-learn.org

具體用法參考官方文檔就行了。

對於一個分組後的變數,第i 組的WOE前面已經介紹過,同樣,對於分組i,也會有一個對應的IV值,計算公式如下:

有了一個變數各分組的IV值,我們就可以計算整個變數的IV值,方法很簡單,就是把各分組的IV相加:

這樣我們就最終得到了每一個特徵對應的IV值。至於為什麼我們不使用woe的絕對值相加的方式而不通過IV這種類似加權的方式來計算結果的方式的原因在於,如果某一類出現的比例佔比很小可能會導致其woe的值很大從而影響判斷,具體的原因可以參看下面的鏈接,不贅述。

https://blog.csdn.net/iModel/article/details/79420437?

blog.csdn.net

從woe的計算公式可以看出,WOE其實描述了變數當前這個分組,對判斷個體是否會響應(或者說屬於哪個類)所起到影響方向和大小,當WOE為正時,變數當前取值對判斷個體是否會響應起到的正向的影響,當WOE為負時,起到了負向影響。而WOE值的大小,則是這個影響的大小的體現。為了規避不同類別的比例大小對woe計算的影響從而引入了IV值的計算,所以IV值越大則表示這個特徵對於正負樣本的區分的貢獻度越大。

7、相關係數

嚴格來說,相關係數有三種,pearson、spearman、kendall,我們平常最常說最常用的是pearson皮爾森相關係數,公式如下:

需要注意的是如果兩個變數中有一個為完全相同的特徵,即特徵的取值都是同一個值,那麼相關係數是計算不出來的(不過這種特徵一般都會刪掉吧。。。)

通過公式可以看出,計算相關係數的時候是不需要進行標準化的:

相關係數的計算過程中本身就包含了一個標準化的過程從而消除了不同量綱的影響。

簡單的相關係數的分類

  • 0.8-1.0 極強相關
  • 0.6-0.8 強相關
  • 0.4-0.6 中等程度相關
  • 0.2-0.4 弱相關
  • 0.0-0.2 極弱相關或無相關

但是pearson相關係數有一個比較麻煩的問題:

1、 它對於線性關係敏感對於非線性關係不敏感,非線性關係即使是一一對應的關係使用pearson相關係數計算出來的結果也會比較差。比如x1=【-5,-3,-1,1,3,5】而x2=x1**2=【25,9,1,1,9,25】,二者之間是一一對應的關係,但是pearson相關係數為-0.5多

2、對異常值很敏感。這個看公式就知道了,自己手動算一下也可以。

一般來說,使用線性回歸和邏輯回歸的時候會使用相關係數作為特徵重要性的評價指標之一,而如果是樹模型則更傾向於使用下面的這個相關係數。

spearman 秩相關係數

懶得寫了直接上網址:

https://blog.csdn.net/qq_30138291/article/details/79801777?

blog.csdn.net

斯皮爾曼相關性係數,通常也叫斯皮爾曼秩相關係數。「秩」,可以理解成就是一種順序或者排序,那麼它就是根據原始數據的排序位置進行求解,這種表徵形式就沒有了求皮爾森相關性係數時那些限制。下面來看一下它的計算公式:

計算過程就是:首先對兩個變數(X, Y)的數據進行排序,然後記下排序以後的位置(X』, Y』),(X』, Y』)的值就稱為秩次,秩次的差值就是上面公式中的di,n就是變數中數據的個數,最後帶入公式就可求解結果。

很顯然,spearm對於異常值一點也不敏感。並且它表示的是兩列數據之間的排序的相關性。如果使用樹模型,spearman進行特徵選擇會更合理一些。

8、sklearn中還存在著這幾種過濾式評價指標

簡單看了一下理論推導還是比較複雜的,鑒於使用的並不多,所以,心情好的時候再吧。。。

至於mRMR,emmm,其實和相關性的選擇方法存在一樣的缺陷,就是無法考慮到原始特徵較弱而組合特徵較強的情況(比如目標變數由特徵變數進行異或得到的情況),所以我就不贅述了,至於《美團機器學習實踐》中提到的CFS實際上不能算是嚴格意義上的過濾式特徵選擇方法,他的原理應該劃入包裹式特徵消除方法的範疇中,所以後續寫到這裡的時候再詳細展開吧。


推薦閱讀:
相關文章