【特征工程】17种将离散特征转化为数字特征的方法
作者 | Samuele Mazzanti?
編譯 | VK?
來源 | Towards Data Science
“你知道哪種梯度提升算法?”
“Xgboost,LightGBM,Catboost,HistGradient。”
“你知道哪些離散變量的編碼?”
“one-hot”
在一次數據科學面試中聽到這樣的對話我不會感到驚訝。不過,這將是相當驚人的,「因為只有一小部分數據科學項目涉及機器學習,而實際上所有這些項目都涉及一些離散數據」。
?離散變量的編碼是將一個離散列轉換為一個(或多個)數字列的過程。
?這是必要的,因為計算機處理數字比處理字符串更容易。為什么?因為用數字很容易找到關系(比如“大”、“小”、“雙”、“半”)。然而,當給定字符串時,計算機只能說出它們是“相等”還是“不同”。
然而,盡管離散變量的編碼有影響,但它很容易被數據科學從業者忽視。
?離散變量的編碼是一個令人驚訝的被低估的話題。
?這就是為什么我決定深化編碼算法的知識。我從一個名為“category_encoders”的Python庫開始(這是Github鏈接:https://github.com/scikit-learn-contrib/category_encoders)。使用它非常簡單:
!pip?install?category_encodersimport?category_encoders?as?cece.OrdinalEncoder().fit_transform(x)這篇文章是對庫中包含的17種編碼算法的演練。對于每種算法,我用幾行代碼提供了簡短的解釋和Python實現。其目的不是要重新發明輪子,而是要認識到算法是如何工作的。畢竟,
?“除非你能寫代碼,否則你不懂”。
?并非所有編碼都是相同的
我根據17種編碼算法的一些特點對它們進行了分類。類似決策樹:
分割點為:
「監督/無監督」:當編碼完全基于離散列時,它是無監督的。如果編碼是基于原始列和第二列(數字)的某個函數,則它是監督的。
「輸出維度」:分類列的編碼可能產生一個數值列(輸出維度=1)或多個數值列(輸出維度>1)。
「映射」:如果每個等級都有相同的輸出-無論是標量(例如OrdinalEncoder)還是數組(例如onehotcoder),那么映射是唯一的。相反,如果允許同一等級具有不同的可能輸出,則映射不是唯一的。
17種離散編碼算法
1.「OrdinalEncoder」
每個等級都映射到一個整數,從1到L(其中L是等級數)。在這種情況下,我們使用了字母順序,但任何其他自定義順序都是可以接受的。
sorted_x?=?sorted(set(x)) ordinal_encoding?=?x.replace(dict(zip(sorted_x,?range(1,?len(sorted_x)?+?1))))你可能認為該編碼是沒有意義的,尤其是當等級沒有內在順序的時候。你是對的!實際上,它只是一種方便的表示,通常用于節省內存,或作為其他類型編碼的中間步驟。
2.CountEncoder
每個等級都映射到該級別的觀察數。
count_encoding?=?x.replace(x.value_counts().to_dict())這種編碼可以作為每個級別的“可信度”的指標。例如,一個機器學習算法可能會自動決定只考慮其計數高于某個閾值的級別所帶來的信息。
3.OneHotEncoder
編碼算法中最常用的。每個級別映射到一個偽列(即0/1的列),指示該行是否攜帶屬于該級別。
one_hot_encoding?=?ordinal_encoding.apply(lambda?oe:?pd.Series(np.diag(np.ones(len(set(x))))[oe?-?1].astype(int)))這意味著,雖然你的輸入是一個單獨的列,但是你的輸出由L列組成(原始列的每個級別對應一個列)。這就是為什么OneHot編碼應該小心處理:你最終得到的數據幀可能比原來的大得多。
一旦數據是OneHot編碼,它就可以用于任何預測算法。為了使事情一目了然,讓我們對每一個等級進行一次觀察。
假設我們觀察到一個目標變量,叫做y,包含每個人的收入(以千美元計)。讓我們用線性回歸(OLS)來擬合數據。
為了使結果易于閱讀,我在表的側面附加了OLS系數。
在OneHot編碼的情況下,截距沒有特定的意義。在這種情況下,由于我們每層只有一個觀測值,通過加上截距和乘上系數,我們得到y的精確值(沒有誤差)。
4.SumEncoder
下面的代碼一開始可能有點晦澀難懂。但是不要擔心:在這種情況下,理解如何獲得編碼并不重要,而是如何使用它。
sum_encoding?=?one_hot_encoding.iloc[:,?:-1].apply(lambda?row:?row?if?row.sum()?==?1?else?row.replace(0,?-1),?axis?=?1)SumEncoder屬于一個名為“對比度編碼”的類。這些編碼被設計成在回歸問題中使用時具有特定的行為。換句話說,如果你想讓回歸系數有一些特定的屬性,你可以使用其中的一種編碼。
特別是,當你希望回歸系數加起來為0時,使用SumEncoder。如果我們采用之前的相同數據并擬合OLS,我們得到的結果是:
這一次,截距對應于y的平均值。此外,通過取最后一級的y并從截距(68-50)中減去它,我們得到18,這與剩余系數之和(-15-5+2=-18)正好相反。這正是我前面提到的SumEncoder的屬性。
5.BackwardDifferenceEncoder
另一種對比度編碼。
這個編碼器對序數變量很有用,也就是說,可以用有意義的方式對其等級進行排序的變量。BackwardDifferenceEncoder設計用于比較相鄰的等級。
backward_difference_encoding?=?ordinal_encoding.apply(lambda?oe:?pd.Series([i?/?len(set(x))?for?i?in?range(1,?oe)]?+?[-?i?/?len(set(x))?for?i?in?range(len(set(x))?-?oe,?0,?-1)]))假設你有一個有序變量(例如教育水平),你想知道它與一個數字變量(例如收入)之間的關系。比較每一個連續的水平(例如學士與高中,碩士與學士)與目標變量的關系可能很有趣。這就是BackwardDifferenceEncoder的設計目的。讓我們看一個例子,使用相同的數據。
截距與y的平均值一致。學士的系數為10,因為學士的y比高中高10,碩士的系數等于7,因為碩士的y比單身漢高7,依此類推。
6.HelmertEncoder
HelmertEncoder與BackwardDifferenceEncoder非常相似,但不是只與前一個進行比較,而是將每個級別與之前的所有級別進行比較。
helmert_encoding?=?ordinal_encoding.apply(lambda?oe:?pd.Series([0]?*?(oe?-?2)?+?([oe?-?1]?if?oe?>?1?else?[])?+?[-1]?*?(len(set(x))?-?oe)) ).div(pd.Series(range(2,len(set(x))?+?1)))]
讓我們看看OLS模型能給我們帶來什么:
PhD的系數是24,因為PhD比之前水平的平均值高24-((35+45+52)/3)=24。同樣的道理適用于所有的等級。
7.PolynomialEncoder
另一種對比編碼。
顧名思義,PolynomialEncoder被設計用來量化目標變量相對于離散變量的線性、二次和三次行為。
def?do_polynomial_encoding(order):#?代碼來自https://github.com/pydata/patsy/blob/master/patsy/contrasts.pyn?=?len(set(x))scores?=?np.arange(n)scores?=?np.asarray(scores,?dtype=float)scores?-=?scores.mean()raw_poly?=?scores.reshape((-1,?1))?**?np.arange(n).reshape((1,?-1))q,?r?=?np.linalg.qr(raw_poly)q?*=?np.sign(np.diag(r))q?/=?np.sqrt(np.sum(q?**?2,?axis=1))q?=?q[:,?1:]return?q[order?-?1]polynomial_encoding?=?ordinal_encoding.apply(lambda?oe:?pd.Series(do_polynomial_encoding(oe)))我知道你在想什么。一個數值變量如何與一個非數值變量有線性(或二次或三次)關系?這是基于這樣一個假設,即潛在的離散變量不僅具有順序性,而且具有等間距。
基于這個原因,我建議謹慎使用它,只有當你確信這個假設是合理的。
8.BinaryEncoder
BinaryEncoder 與OrdinalEncoder基本相同,唯一的區別是將整數轉換成二進制數,然后每個位置數字都是one-hot編碼。
binary_base?=?ordinal_encoding.apply(lambda?oe:?str(bin(oe))[2:].zfill(len(bin(len(set(x))))?-?2)) binary_encoding?=?binary_base.apply(lambda?bb:?pd.Series(list(bb))).astype(int)輸出由偽列組成,就像OneHotEncoder的情況一樣,但是它會導致相對于one-hot的維數降低。
老實說,我不知道這種編碼有什么實際應用。
9.BaseNEncoder
BaseNEncoder只是BinaryEncoder的一個推廣。實際上,在BinaryEncoder中,數字以2為基數,而在BaseNEncoder中,數字以n為底,n大于1。
def?int2base(n,?base):out?=?''while?n:out?+=?str(int(n?%?base))n?//=?basereturn?out[::-1]base_n?=?ordinal_encoding.apply(lambda?oe:?int2base(n?=?oe,?base?=?base)) base_n_encoding?=?base_n.apply(lambda?bn:?pd.Series(list(bn.zfill(base_n.apply(len).max())))).astype(int)讓我們看一個base=3的例子。
老實說,我不知道這種編碼有什么實際應用。
10.HashingEncoder
在HashingEncoder中,每個原始級別都使用一些哈希算法(如SHA-256)進行哈希處理。然后,將結果轉換為整數,并取該整數相對于某個(大)除數的模。通過這樣做,我們將每個原始字符串映射到一個某個范圍的整數。最后,這個過程得到的整數是one-hot編碼的。
def?do_hash(string,?output_dimension):hasher?=?hashlib.new('sha256')hasher.update(bytes(string,?'utf-8'))string_hashed?=?hasher.hexdigest()string_hashed_int?=?int(string_hashed,?16)string_hashed_int_remainder?=?string_hashed_int?%?output_dimensionreturn?string_hashed,?string_hashed_int,?string_hashed_int_remainderhashing?=?x.apply(lambda?string:?pd.Series(do_hash(string,?output_dimension),?index?=?['x_hashed',?'x_hashed_int',?'x_hashed_int_remainder'])) hashing_encoding?=?hashing['x_hashed_int_remainder'].apply(lambda?rem:?pd.Series(np.diag(np.ones(output_dimension))[rem])).astype(int)讓我們看一個輸出維數為10的示例。
散列的基本特性是得到的整數是均勻分布的。所以,如果除數足夠大,兩個不同的字符串不太可能映射到同一個整數。那為什么有用呢?實際上,這有一個非常實際的應用叫做“哈希技巧”。
假設你希望使用邏輯回歸來生成電子郵件垃圾郵件分類器。你可以通過對數據集中包含的所有單詞進行ONE-HOT編碼來實現這一點。主要的缺點是你需要將映射存儲在單獨的字典中,并且你的模型維度將在新字符串出現時發生更改。
使用散列技巧可以很容易地克服這些問題,因為通過散列輸入,你不再需要字典,并且輸出維是固定的(它只取決于你最初選擇的除數)。此外,對于散列的屬性,你可以認為新字符串可能具有與現有字符串不同的編碼。
11.TargetEncoder
假設有兩個變量:一個是離散變量(x),一個是數值變量(y)。假設你想把x轉換成一個數值變量。你可能需要使用y“攜帶”的信息。一個明顯的想法是取x的每個級別的y的平均值。在公式中:
這是合理的,但是這種方法有一個很大的問題:有些群體可能太小或太不穩定而不可靠。許多有監督編碼通過在組平均值和y的全局平均值之間選擇一種中間方法來克服這個問題:
其中$w_i$在0和1之間,取決于組的“可信”程度。
接下來的三種算法(TargetEncoder、MEstimateEncoder和JamesSteinEncoder)根據它們定義$w_i$的方式而有所不同。
在TargetEncoder中,權重取決于組的數量和一個稱為“平滑”的參數。當“平滑”為0時,我們僅依賴組平均值。然后,隨著平滑度的增加,全局平均權值越來越多,導致正則化更強。
y_mean?=?y.mean() y_level_mean?=?x.replace(y.groupby(x).mean()) weight?=?1?/?(1?+?np.exp(-(count_encoding?-?1)?/?smoothing)) target_encoding?=?y_level_mean?*?weight?+?y_mean?*?(1?-?weight)讓我們看看結果如何隨著一些不同的平滑值而變化。
12.MEstimateEncoder
MEstimateEncoder類似于TargetEncoder,但$w_i$取決于一個名為“m”的參數,該參數設置全局平均值的絕對權重。m很容易理解,因為它可以被視為若干個觀測值:如果等級正好有m個觀測值,那么等級平均值和總體平均權重是相同的。
y_mean?=?y.mean() y_level_mean?=?x.replace(y.groupby(x).mean()) weight?=?count_encoding?/?(count_encoding?+?m) m_estimate_encoding?=??y_level_mean?*?weight?+?y_grand_mean?*?(1?-?weight)讓我們看看不同m值的結果是如何變化的:
13.「JamesSteinEncoder」
TargetEncoder和MEstimateEncoder既取決于組的數量,也取決于用戶設置的參數值(分別是smoothing和m)。這不方便,因為設置這些權重是一項手動任務。
一個自然的問題是:有沒有一種方法可以在不需要任何人為干預的情況下,設定一個最佳的工作環境?JamesSteinEncoder試圖以一種基于統計數據的方式來做到這一點。
y_mean?=?y.mean() y_var?=?y.var() y_level_mean?=?x.replace(y.groupby(x).mean()) y_level_var?=?x.replace(y.groupby(x).var())weight?=?1?-?(y_level_var?/?(y_var?+?y_level_var)?*?(len(set(x))?-?3)?/?(len(set(x))?-?1)) james_stein_encoding?=?y_level_mean?*?weight?+?y_mean?*?(1?-?weight)直覺是,一個高方差的群體的平均值應該不那么可信。因此,群體方差越高,權重就越低(如果你想知道更多關于公式的知識,我建議克里斯?賽義德的這篇文章)。
讓我們看一個數值示例:
JamesSteinEncoder有兩個顯著的優點:它提供比最大似然估計更好的估計,并且不需要任何參數設置。
14.GLMMEncoder
GLMMEncoder采用一種完全不同的方法。
基本上,它擬合y上的線性混合效應模型。這種方法利用了一個事實,即線性混合效應模型是為處理同質觀察組而精心設計的。因此,我們的想法是擬合一個沒有回歸變量(只有截距)的模型,并使用層次作為組。
然后,輸出就是截距和隨機效應的總和。
model?=?smf.mixedlm(formula?=?'y?~?1',?data?=?y.to_frame(),?groups?=?x).fit() intercept?=?model.params['Intercept'] random_effect?=?x.replace({k:?float(v)?for?k,?v?in?model.random_effects.items()}) glmm_encoding?=?intercept?+?random_effect15.WOEEncoder
WOEEncoder(代表“證據權重 Weight of Evidence”編碼器)只能用于二元變量,即級別為0/1的目標變量。
證據權重背后的想法是你有兩種分布:
1的分布(每組1的個數/y中1的個數)
0的分布(每組0的個數/y中0的個數)
該算法的核心是將1的分布除以0的分布(對于每個組)。當然,這個值越高,我們就越有信心認為這個基團“偏向”1,反之亦然。然后,取該值的對數。
y_level_ones?=?x.replace(y.groupby(x).apply(lambda?l:?(l?==?1).sum())) y_level_zeros?=?x.replace(y.groupby(x).apply(lambda?l:?(l?==?0).sum())) y_ones?=?(y?==?1).sum() y_zeros?=?(y?==?0).sum() nominator?=?y_level_ones?/?y_ones denominator?=?y_level_zeros?/?y_zeros woe_encoder?=?np.log(nominator?/?denominator)如你所見,由于公式中存在對數,因此無法直接解釋輸出。然而,它作為機器學習的一個預處理步驟工作得很好。
16.LeaveOneOutEncoder
到目前為止,所有的15個編碼器都有一個唯一的映射。
但是,如果你計劃使用編碼作為預測模型的輸入(例如GB),這可能是一個問題。實際上,假設你使用TargetEncoder。這意味著你在X_train中引入了關于y_train的信息,這可能會導致嚴重的過擬合風險。
關鍵是:如何在限制過擬合的風險的同時保持有監督的編碼?LeaveOneOutEncoder提供了一個出色的解決方案。它執行普通的目標編碼,但是對于每一行,它不考慮該行觀察到的y值。這樣,就避免了行方向的泄漏。
y_level_except_self?=?x.to_frame().apply(lambda?row:?y[x?==?row['x']].drop(row.name).to_list(),?axis?=?1) leave_one_out_encoding?=?y_level_except_self.apply(np.mean)17.CatBoostEncoder
CatBoost是一種梯度提升算法(如XGBoost或LightGBM),它在許多問題中都表現得非常好。
CatboostEncoder的工作原理基本上類似于LeaveOneOutEncoder,但是是一個在線方法。
但是如何模擬在線行為?想象一下你有一張桌子。然后,在桌子中間的某個地方劃一排。CatBoost所做的是假裝當前行上方的行已經被及時觀察到,而下面的行還沒有被觀察到(即將來會觀察到)。然后,該算法執行leave one out編碼,但僅基于已觀察到的行。
y_mean?=?y.mean() y_level_before_self?=?x.to_frame().apply(lambda?row:?y[(x?==?row['x'])?&?(y.index?<?row.name)].to_list(),?axis?=?1) catboost_encoding?=?y_level_before_self.apply(lambda?ylbs:?(sum(ylbs)?+?y_mean?*?a)?/?(len(ylbs)?+?a))這似乎有些荒謬。為什么要拋棄一些可能有用的信息呢?你可以將其簡單地視為對輸出進行隨機化的更極端嘗試(例如,減少過擬合)。
謝謝你的閱讀!我希望你覺得這篇文章有用。
往期精彩回顧適合初學者入門人工智能的路線及資料下載機器學習及深度學習筆記等資料打印機器學習在線手冊深度學習筆記專輯《統計學習方法》的代碼復現專輯 AI基礎下載機器學習的數學基礎專輯 獲取本站知識星球優惠券,復制鏈接直接打開: https://t.zsxq.com/qFiUFMV 本站qq群704220115。加入微信群請掃碼:總結
以上是生活随笔為你收集整理的【特征工程】17种将离散特征转化为数字特征的方法的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 【Python基础】一文理解Python
- 下一篇: 【爬虫、算法】基于Dijkstra算法的