歡迎光臨
每天分享高質量文章

學習|機器學習特徵工程的基本技術

介紹

什麼是特徵,為什麼我們需要特徵工程?基本上,所有的機器學習演演算法都使用一些輸入資料來建立輸出結果。這個輸入資料包含一些特徵,這些特徵通常以結構化列的形式呈現。演演算法需要具有特定特性的特徵才能正常工作。因此,出現了對特徵工程的需求。我認為特徵工程的工作主要有兩個標的:

  • 準備合適的輸入資料集,符合機器學習演演算法的要求。
  • 提高機器學習模型的效能。

您所使用的特性對結果的影響比其他任何東西都大。據我所知,沒有一種演演算法能夠單獨補充正確的特徵工程所帶來的資訊增益。— Luca Massaron

需要內推資料工作,請加微信:luqin360

根據《福布斯》的一項調查,資料科學家把80%的時間花在資料準備上:

這個指標非常令人印象深刻,顯示了特徵工程在資料科學中的重要性。因此,我決定寫這篇文章,它總結了特徵工程的主要技術和簡短描述。我還為每種技術添加了一些基本的python指令碼。您需要匯入pandas和Numpy庫來執行它們。

1import pandas as pd
2import numpy as np

上面的一些技術可能在某些演演算法或資料集上工作得更好,而其中一些技術可能在所有情況下都是有益的。本文不打算在這方面做太深入的討論,因為每一種方法可能足以寫一篇文章了。我試圖保持解釋簡短和資訊豐富。我認為獲得特徵工程專業知識的最好方法是在不同的資料集上實踐不同的技術,並觀察它們對模型效能的影響。

技術串列

1 插值

缺失值是為機器學習準備資料時最常見的問題之一。缺失值的原因可能是人為錯誤、資料流中斷、隱私問題等等。不管是那種原因,缺失值都會影響機器學習模型的效能。

在模型訓練階段,一些機器學習平臺會自動地刪除含有缺失值的行(樣本)。由於減少了樣本數量,它會降低模型的效能。另一方面,很多機器學習演演算法不能夠接受含有缺失值資料集。

缺失值最簡單的解決方案是刪除含有缺失值的行或者列。基於可選擇的閾值刪除法,使用閾值70%為例,刪除缺失值不低於這個閾值的行或者列。

1threshold = 0.7
2#Dropping columns with missing value rate higher than threshold
3data = data[data.columns[data.isnull().mean() 4
5#Dropping rows with missing value rate higher than threshold
6data = data.loc[data.isnull().mean(axis=1

數值插補

缺失值插補是一個很好的選擇,與缺失值刪除法比較起來,它可以保持資料的大小不變。但是,當你進行缺失值插補的時候,要做一個重要的考慮。我建議你剛開始你要考慮列中缺失的預設值。例如,你有一列僅有1和NA,行中的NA可能就是關聯到0,另一個例子,你有一個串列示“上個月客戶訪問的次數”,缺失值可能使用了0替換,只要你覺得它是合理的解決方案。

缺失值的另一個原因是連線不同大小的表時,這種情況下,填補0也可能是個合理的做法。

除了缺失值的一些預設值做法外,我認為最有效的填補法是使用列的中位數填補。因為列的均值對異常值敏感,這個時候中位數顯得更為穩健。

1#Filling all missing values with 0
2data = data.fillna(0)
3#Filling missing values with medians of the columns
4data = data.fillna(data.median())

類別填補

類別中的缺失值採用列中出現次數最多的值來替換是一種好的選擇。若是你發現列中的值是均勻分佈和沒有一個主導值,那採用像“Other”來填充可能是合理的。因為在這種情形下,你的插入值可能要為一種隨機的選擇。

1#Max fill function for categorical columns
2data['column_name'].fillna(data['column_name'].value_counts()
3.idxmax(), inplace=True)
2 處理異常值

在提及如何處理異常值之前,我想說下檢測異常值的最佳方法是展示資料的視覺化。所有其他統計方法都易出錯,而視覺化異常值提供了一個可以做出高精度決策的機會。無論如何,我計劃在另一篇文章中深入討論視覺化,讓我們繼續討論統計方法。

統計方法沒有我提到的那麼精確,但另一方面,他的優勢是速度快。這裡我將列出兩種處理異常值的不同方法。它們是使用標準差和百分位數來檢測異常值。

標準差的異常值檢測

如果一個值距離比均值還高x倍標準差,它可以被認定為是缺失值,那麼這個x怎麼定呢?

根據經驗法則,x在2和4之間取值會比較合理。

1#Dropping the outlier rows with standard deviation
2factor = 3
3upper_lim = data['column'].mean () + data['column'].std () * factor
4lower_lim = data['column'].mean () - data['column'].std () * factor
5
6data = data[(data['column'] 'column'] > lower_lim)]

另外,z-score可用於替換上面的式子。z分數(或標準分數)使用標準差標準化值與平均值之間的距離。

百分位數的異常值檢測

另外一種檢測異常值的數學方法是使用百分位數。你可以假設從頂端或者底部的一個值的特定分位數為異常值。關鍵點是如何設定這個分位數,這依賴於前面所提到的資料的分佈。

另外,一個常見的錯誤是根據資料的方位使用百分位數。換句話說,你的資料範圍是0到100,頂端的5%是值不落在96到100,頂端5%意味著這些值在資料的95%百分位數外。

1#Dropping the outlier rows with Percentiles
2upper_lim = data['column'].quantile(.95)
3lower_lim = data['column'].quantile(.05)
4
5data = data[(data['column'] 'column'] > lower_lim)]

異常值的困境:放棄還是使用

異常值處理的另外一個方法是上限替換刪除,這樣做可以保持資料集的大小,它可能讓你最終的模型效能更好。

另一方面,異常值替換會影響到資料的分佈,因此,最好不要誇大它。

1#Capping the outlier rows with Percentiles
2upper_lim = data['column'].quantile(.95)
3lower_lim = data['column'].quantile(.05)
4data.loc[(df[column] > upper_lim),column] = upper_lim
5data.loc[(df[column] 
3 分箱

分箱可以應用到連續變數和離散變數。

 1#Numerical Binning Example
2Value      Bin       
30-30   ->  Low       
431-70  ->  Mid       
571-100 ->  High
6
7#Categorical Binning Example
8Value      Bin       
9Spain  ->  Europe      
10Italy  ->  Europe       
11Chile  ->  South America
12Brazil ->  South America

分箱的主要好處就是使得模型更加穩健和防止過擬合,因而,它是對效能有代價的。每次你分箱一些東西,你就犧牲了一些資訊和讓你的資料更正則化。

效能和過擬合的折中與平衡是分箱處理的關鍵點。依我看,對於數值列,除了一些明顯的過擬合情況外,分箱對於某些演演算法來說可能是多餘的,因為它會影響模型的效能。

然而,對於類別變數,哪些具有低頻的標簽值可能會影響統計模型的穩健性,因此,分配一個通用的類別給那些出現低頻的值能夠保持模型的穩健性。舉個例子來說,假設你的資料大小有100000行,對於哪些類別標簽值少於100的賦予一個新的類別“Other”可能是合理的選擇。

 1#Numerical Binning Example
2data['bin'] = pd.cut(data['value'], bins=[0,30,70,100], labels=["Low""Mid""High"])
3   value   bin
40      2   Low
51     45   Mid
62      7   Low
73     85  High
84     28   Low
9
10#Categorical Binning Example
11     Country
120      Spain
131      Chile
142  Australia
153      Italy
164     Brazil
17conditions = [
18    data['Country'].str.contains('Spain'),
19    data['Country'].str.contains('Italy'),
20    data['Country'].str.contains('Chile'),
21    data['Country'].str.contains('Brazil')]
22
23choices = ['Europe''Europe''South America''South America']
24
25data['Continent'] = np.select(conditions, choices, default='Other')
26     Country      Continent
270      Spain         Europe
281      Chile  South America
292  Australia          Other
303      Italy         Europe
314     Brazil  South America
4 Log變換

對數變換(或者自然對數變換)是特徵工程裡面一種常用的數學變換,對數變換的好處:

  • 透過變換可以幫助處理有偏資料,其分佈可能變得更趨於正態化。
  • 在大多數情況下,資料的大小順序在資料的範圍內發生變化。例如,15歲和20歲之間的差距並不等於65歲和70歲之間的差距。就年齡而言,是的,它們是相同的,但就其他所有方面而言,年輕時5年的差異意味著更大的差異。這種型別的資料來自乘法過程,對數變換將大小差異標準化。
  • 由於對幅度差異進行歸一化處理,使得模型的魯棒性增強,從而降低了異常值的影響。

關鍵提示:應用log轉換的資料必須只有正值,否則將收到錯誤。此外,您還可以在轉換資料之前將1新增到資料中。因此,您可以確保轉換的輸出為正。

Log(x+1)

 1#Log Transform Example
2data = pd.DataFrame({'value':[2,45, -23, 85, 28, 2, 35, -12]})
3data['log+1'] = (data['value']+1).transform(np.log)
4#Negative Values Handling
5#Note that the values are different
6data['log'] = (data['value']-data['value'].min()+1) .transform(np.log)
7   value  log(x+1)  log(x-min(x)+1)
80      2   1.09861          3.25810
91     45   3.82864          4.23411
102    -23       nan          0.00000
113     85   4.45435          4.69135
124     28   3.36730          3.95124
135      2   1.09861          3.25810
146     35   3.58352          4.07754
157    -12       nan          2.48491
5 One-Hot 編碼

One-hot編碼是機器學習中最常見的編碼方法之一。此方法將列中的值擴充套件到多個標誌列,併為它們分配0或1。這些二進位制值表示分組列和編碼列之間的關係。

這種方法將分類資料(演演算法很難理解)更改為數字格式,並使您能夠在不丟失任何資訊的情況下對分類資料進行分組。(詳情請參閱最後一部分類別列分組)

為什麼要One-hot?如果列中有N個不同的值,將它們對映到N-1二進位制列就足夠了,因為缺失的值可以從其他列中推出。如果我們手上的所有列都等於0,缺失的值必須等於1。這就是它為什麼稱為One-hot編碼的原因。然而,我將給出一個使用pandas的get_dummies函式的例子。這個函式將列中的所有值對映到多個列。

1encoded_columns = pd.get_dummies(data['column'])
2data = data.join(encoded_columns).drop('column', axis=1)
6 分組操作

在大多數機器學習演演算法中,每個實體都由訓練資料集中的一行表示,其中每一列顯示實體的不同特徵。這種資料叫做”Tidy”。

整潔的資料集易於操作、建模和視覺化,並且具有特定的結構:每個變數是一列,每個觀察是一行,每種型別的觀察單元是一個表。— Hadley Wickham

由於實體的多行性,諸如事務之類的資料集很少符合上面整齊資料的定義。在這種情況下,我們根據實體對資料進行分組,然後每個實體僅用一行表示。

分組操作的關鍵是確定特徵的聚合函式。對於數值特徵,平均函式和和函式通常是比較合理的選擇,而對於分類特徵則比較複雜。

類別列分組

對類別列聚合我建議三種不同的方法。

方法一是選擇頻率最高的標簽。換句話說,對類別列進行max操作,但是,普通的max函式通常不會傳回值,你要使用lambda函式達成這個標的。

1data.groupby('id').agg(lambda x: x.value_counts().index[0])

方法二是用透視表,這種方法類似於前面步驟中的編碼方法,但有一點不同。
可以將其定義為分組列和編碼列之間的值的聚合函式,而不是二進製表示法。 如果您的標的是超越二進位制標誌列並將多個特徵合併到聚合特徵中,這將是一個不錯的選擇,這些功能更具資訊性。

1#Pivot table Pandas Example
2data.pivot_table(index='column_to_group', columns='column_to_encode'values='aggregation_column', aggfunc=np.sum, fill_value = 0)

方法三選項是在應用One-hot編碼後按function應用分組。 此方法保留所有資料 – 在第一個選項中丟失一些 – 此外,您同時將編碼列從分類轉換為數字。 您可以檢視下一節,瞭解數值列分組的說明。

數值列分組

在大多數情況下,數值列使用求和和均值函式進行分組。根據特徵的含義,兩者都是可取的。例如,如果希望獲得比率列,可以使用二進位制列的平均值。在同一個例子中,sum函式也可以用來獲得總計數。

1#sum_cols: List of columns to sum
2#mean_cols: List of columns to average
3grouped = data.groupby('column_to_group')
4
5sums = grouped[sum_cols].sum().add_suffix('_sum')
6avgs = grouped[mean_cols].mean().add_suffix('_avg')
7
8new_df = pd.concat([sums, avgs], axis=1)
7 特徵拆分

特徵拆分是使它們在機器學習方面有用的好方法。 大多數情況下,資料集包含違反整潔資料原則的字串列。 透過將列的可利用部分提取到新特徵中:

  • 我們使機器學習演演算法能夠理解它們。
  • 可以對它們進行分箱和分組。
  • 透過發現潛在資訊來提高模型效能。

拆分特徵是一個不錯的選擇,但是,沒有一種通用方法可以拆分特徵。 它取決於列的特性以決定如何拆分它。 我們用兩個例子來介紹它。 首先,普通名稱列的簡單拆分特徵:

 1data.name
20  Luther NGonzalez
31    Charles MYoung
42        Terry Lawson
53       Kristen White
64      Thomas Logsdon
7#Extracting first names
8data.name.str.split(" ").map(lambda xx[0])
90     Luther
101    Charles
112      Terry
123    Kristen
134     Thomas
14#Extracting last names
15data.name.str.split(" ").map(lambda xx[-1])
160    Gonzalez
171       Young
182      Lawson
193       White
204     Logsdon

上面的例子透過只使用第一個和最後一個元素來處理超過兩個單詞的名稱,這使得函式對於corner情況非常健壯,在處理這樣的字串時應該考慮這一點。

split函式的另一種情況是提取兩個字元之間的字串部分。下麵的示例顯示了透過在一行中使用兩個拆分函式來實現這種情況。

 1#String extraction example
2data.title.head()
30                      Toy Story (1995)
41                        Jumanji (1995)
52               Grumpier Old Men (1995)
63              Waiting to Exhale (1995)
74    Father of the Bride Part II (1995)
8data.title.str.split("(", n=1, expand=True)[1].str.split(")", n=1, expand=True)[0]
90    1995
101    1995
112    1995
123    1995
134    1995
8 縮放

在大多數情況下,資料集的數字特徵沒有一定的範圍,它們彼此不同。 在現實生活中,期望年齡和收入列具有相同的範圍是無稽之談。 但從機器學習的角度來看,這兩列如何進行比較呢?

縮放解決了這個問題。 在縮放過程之後,連續特徵在範圍方面變得相同。 對於許多演演算法來說,這個過程並不是強制性的,但是應用它可能仍然很好。 然而,基於距離計算的演演算法(例如k-NN或k-Means)需要將連續特徵縮放為模型輸入。

基本上,有兩種常用的縮放方法:

歸一化

歸一化(或最小 – 最大歸一化)把所有值縮放在0和1之間的固定範圍內。該變換不改變特徵的分佈,並且由於標準偏差減小,異常值的影響增加。 因此,在規範化之前,建議處理異常值。

 1data = pd.DataFrame({'value':[2,45-238528235-12]})
2
3data['normalized'] = (data['value'] - data['value'].min()) / (data['value'].max() - data['value'].min())
4   value  normalized
50      2        0.23
61     45        0.63
72    -23        0.00
83     85        1.00
94     28        0.47
105      2        0.23
116     35        0.54
127    -12        0.10

標準化

標準化(或z分數歸一化)在考慮標準偏差的同時對值進行縮放。 如果特徵的標準偏差不同,它們的範圍也會彼此不同。 這減少了特徵中異常值的影響。

在下麵的標準化公式中,平均值顯示為μ,標準偏差顯示為σ。

 1data = pd.DataFrame({'value':[2,45-238528235-12]})
2
3data['standardized'] = (data['value'] - data['value'].mean()) / data['value'].std()
4   value  standardized
50      2         -0.52
61     45          0.70
72    -23         -1.23
83     85          1.84
94     28          0.22
105      2         -0.52
116     35          0.42
127    -12         -0.92
9 抽取資料

雖然日期列通常提供有關模型標的的有價值資訊,但它們被忽略為輸入。 可能是這樣的原因,日期可以以多種格式呈現,這使得演演算法難以理解,甚至它們被簡化為“01-01-2017”之類的格式。

如果在沒有操作的情況下保留日期列,則在機器學習演演算法中建立值之間的序數關係是非常具有挑戰性的。 在這裡,我建議對日期進行三種型別的預處理:

  • 將日期的各個部分提取到不同的列:年,月,日等。
  • 以年,月,日等方式提取當前日期和列之間的時間段。
  • 從日期中提取一些特定特徵:工作日的名稱,週末與否,假日與否等。

如果將日期列轉換為上面提取的列,則會公開它們的資訊,並且機器學習演演算法可以輕鬆理解它們。

 1from datetime import date
2
3data = pd.DataFrame({'date':
4['01-01-2017',
5'04-12-2008',
6'23-06-1988',
7'25-08-1999',
8'20-02-1993',
9]})
10
11#Transform string to date
12data['date'] = pd.to_datetime(data.date, format="%d-%m-%Y")
13
14#Extracting Year
15data['year'] = data['date'].dt.year
16
17#Extracting Month
18data['month'] = data['date'].dt.month
19
20#Extracting passed years since the date
21data['passed_years'] = date.today().year - data['date'].dt.year
22
23#Extracting passed months since the date
24data['passed_months'] = (date.today().year - data['date'].dt.year) * 12 + date.today().month - data['date'].dt.month
25
26#Extracting the weekday name of the date
27data['day_name'] = data['date'].dt.day_name()
28        date  year  month  passed_years  passed_months   day_name
290 2017-01-01  2017      1             2             26     Sunday
301 2008-12-04  2008     12            11            123   Thursday
312 1988-06-23  1988      6            31            369   Thursday
323 1999-08-25  1999      8            20            235  Wednesday
334 1993-02-20  1993      2            26            313   Saturday

總結

我試圖解釋在特徵工程過程中有益的基本方法。 在本文之後,繼續進行其他主題的資料準備,如特徵選擇,訓練/測試分割和取樣可能是一個不錯的選擇。

最後,我想以一個提醒來結束這篇文章。這些技術並不是神奇的工具。如果您的資料很小、很臟且無用,那麼特性工程可能仍然無能為力。不要忘記“垃圾進,垃圾出!”

如果這篇文章對你有用,請考慮給這篇文章點贊,並與朋友和/或同事分享。
如有任何問題或其他討論,請隨時發表留言。

已同步到看一看
贊(0)

分享創造快樂