特征工程的這些基本功你都會了嗎
1引言
特征是什么?為什么需要工程設計?
基本上,所有機器學習算法都是將一些輸入數(shù)據(jù)轉(zhuǎn)化為輸出。這些輸入數(shù)據(jù)包括若干特征,通常是以由列組成的表格形式出現(xiàn)。
而算法往往要求輸入具有某些特性的特征才能正常工作。因此,出現(xiàn)了對特征工程的需求。
特征工程至少有兩個目標,
構(gòu)建適合機器學習算法要求的輸入數(shù)據(jù)。 改善機器學習模型的性能。
根據(jù)《福布斯》的一項調(diào)查,數(shù)據(jù)科學家把 80% 左右的時間花在數(shù)據(jù)收集、清晰以及預處理等數(shù)據(jù)準備上。

這點顯示了特征工程在數(shù)據(jù)科學中的重要性。因此有必要整理一下特征工程的主要技術(shù)。本篇通過 Pandas 和 Numpy 等庫來實際操練。
import?pandas?as?pd
import?numpy?as?np
獲得特征工程專業(yè)知識的最佳方法是對各種數(shù)據(jù)集試驗不同的技術(shù),并觀察其對模型性能的影響。
本文主要介紹以下幾個方面,
1、數(shù)據(jù)插補 2、處理異常值 3、分箱操作 4、對數(shù)轉(zhuǎn)換 5、獨熱編碼 6、分組操作 7、特征拆分 8、縮放操作 9、日期處理
2數(shù)據(jù)插補
缺失值是為機器學習準備數(shù)據(jù)時可能遇到的最常見問題之一。缺少值的原因可能是人為錯誤、數(shù)據(jù)流中斷、隱私問題等。無論是什么原因,缺少值都會影響機器學習模型的性能。
一般來說,機器學習算法不接受包含缺失值的輸入,而有一些機器學習平臺會自動刪除包含缺失值的行,但這樣做往往會降低模型性能。
處理缺失值的最簡單方案是刪除行或整個列。沒有最佳的刪除閾值,但是可以使用 70% 作為閾值,并嘗試刪除缺失值高于此閾值的行和列。
threshold?=?0.7
#?Dropping?columns?with?missing?value?rate?higher?than?threshold
data?=?data[data.columns[data.isnull().mean()?
#?Dropping?rows?with?missing?value?rate?higher?than?threshold
data?=?data.loc[data.isnull().mean(axis=1)?.數(shù)值插補
缺失值插補法,與缺失值刪除法比較起來是一個更好的選擇,至少它可以保持數(shù)據(jù)的規(guī)模不變。但是,插補法需要考慮插補什么值。
首先,你可以考慮列中缺失的默認值。例如,你有一列僅有 1 和 nan,行中的 nan 可能就是 0。另一個例子,你有一個列表示上個月客戶訪問的次數(shù),缺失值可能也是 0。
產(chǎn)生缺失值的另一個原因是在連接大小不同的表時格引入的,此時插補 0 也可能是個合理的做法。
除了用默認值插補缺失值外,還有一個比較有效的做法就是使用列的中位數(shù)插補缺失值,而不是平均值,因為中位數(shù)比均值更為穩(wěn)健。
#?Filling?all?missing?values?with?0
data?=?data.fillna(0)
#?Filling?missing?values?with?medians?of?the?columns
data?=?data.fillna(data.median())
.類別插補
用列中出現(xiàn)次數(shù)最多的值替換缺失值是處理類別型數(shù)據(jù)時的一個不錯的選擇。但是,如果該列中的值是均勻分布的,則使用 Other 類別插補可能更加合理。
#?Max?fill?function?for?categorical?columns
data['column_name'].fillna(data['column_name'].value_counts().idxmax(),?inplace=True)
3處理異常值

在提到如何處理異常值之前,檢測異常值的最佳方法是直觀地展示數(shù)據(jù)。所有其他統(tǒng)計方法都容易犯錯誤,而將異常值可視化則有機會進行高精度的決策。
正如我所提到的,統(tǒng)計方法不夠精確,但另一方面,它們卻具有優(yōu)勢,而且速度很快。在這里,我將列出兩種處理異常值的不同方法。這些將使用標準差和百分位來檢測異常值。
.基于標準差的異常值檢測
如果某個值與平均值的距離大于
#?Dropping?the?outlier?rows?with?standard?deviation
factor?=?3
upper_lim?=?data['column'].mean?()?+?data['column'].std?()?*?factor
lower_lim?=?data['column'].mean?()?-?data['column'].std?()?*?factor
data?=?data[(data['column']?'column']?>?lower_lim)]
此外,可以使用
.基于百分位的異常值檢測
檢測異常值的另一種統(tǒng)計方法是使用百分位。你可以從頂部或底部劃分某些區(qū)間中的值作為異常值。這再次需要設置百分比這個閾值,這取決于數(shù)據(jù)分布。
此外,一個常見的錯誤是根據(jù)數(shù)據(jù)范圍使用百分位。換句話說,如果你的數(shù)據(jù)范圍是 0 到 100,則前 5% 的值不是 96 到 100 之間的值。這里的前 5% 表示值不在數(shù)據(jù)量的第 95 個百分點之內(nèi)。

#?Dropping?the?outlier?rows?with?Percentiles
upper_lim?=?data['column'].quantile(.95)
lower_lim?=?data['column'].quantile(.05)
data?=?data[(data['column']?'column']?>?lower_lim)]
.設限與丟棄
處理異常值的另一種方法是將其設置為上限,而不是丟棄。這樣做可以保留數(shù)據(jù)規(guī)模,并且對于最終模型性能來說可能會更好。
另一方面,設上限封頂可能會影響數(shù)據(jù)的分布,因此也不要過于吹捧它。
#?Capping?the?outlier?rows?with?Percentiles
upper_lim?=?data['column'].quantile(.95)
lower_lim?=?data['column'].quantile(.05)
data.loc[(df[column]?>?upper_lim),?column]?=?upper_lim
data.loc[(df[column]?4分箱

分箱可以應用于類別型數(shù)據(jù)和數(shù)值型數(shù)據(jù)。
#?Numerical?Binning?Example
Value??????Bin???????
0-30???->??Low???????
31-70??->??Mid???????
71-100?->??High
#?Categorical?Binning?Example
Value??????Bin???????
Spain??->??Europe??????
Italy??->??Europe???????
Chile??->??South?America
Brazil?->??South?America
分箱的主要動機是使模型更加健壯并防止過擬合,但同時也會降低性能。每次分箱不僅會犧牲信息,也會使得數(shù)據(jù)更加規(guī)范化。
性能與過擬合之間的權(quán)衡是分箱過程的關(guān)鍵。
對于數(shù)值型特征,除了一些明顯的過擬合的情況外,分箱對于某種算法可能是多余的,因為它對模型性能有影響。 然而,對于類別型特征,低頻標簽可能會對統(tǒng)計模型的魯棒性產(chǎn)生負面影響。因此,為這些不太頻繁的值分配一般類別有助于保持模型的魯棒性。例如,數(shù)據(jù)大小為 100,000 行,則將計數(shù)少于 100 的標簽合并到 Other之類的新類別可能是一個不錯的選擇。
#?Numerical?Binning?Example
data['bin']?=?pd.cut(data['value'],?bins=[0,30,70,100],?labels=["Low",?"Mid",?"High"])
???value???bin
0??????2???Low
1?????45???Mid
2??????7???Low
3?????85??High
4?????28???Low
#?Categorical?Binning?Example
?????Country
0??????Spain
1??????Chile
2??Australia
3??????Italy
4?????Brazil
conditions?=?[
????data['Country'].str.contains('Spain'),
????data['Country'].str.contains('Italy'),
????data['Country'].str.contains('Chile'),
????data['Country'].str.contains('Brazil')]
choices?=?['Europe',?'Europe',?'South?America',?'South?America']
data['Continent']?=?np.select(conditions,?choices,?default='Other')
?????Country??????Continent
0??????Spain?????????Europe
1??????Chile??South?America
2??Australia??????????Other
3??????Italy?????????Europe
4?????Brazil??South?America
5Log 對數(shù)變換
對數(shù)變換是特征工程中最常用的數(shù)學變換之一,它的好處有,
它有助于處理偏度不為 0 的數(shù)據(jù),并且在轉(zhuǎn)換后,分布變得更接近正態(tài)分布。 在大多數(shù)情況下,數(shù)據(jù)的數(shù)量級在不同范圍內(nèi)是不同的。例如,年齡 15 和 20 之間的數(shù)量差異并不等于年齡 65 和 70 之間的數(shù)量差異。就年份而言,是的,它們是相同的,但是對于其他方面,年輕年齡的 5 年差異意味著更高的數(shù)量差異。這種類型的數(shù)據(jù)來自乘性過程,對數(shù)變換將起到規(guī)范化(normalize)數(shù)量差異的作用。
由于數(shù)量差異的歸一化,模型變得更加健壯,因此它也減少了異常值的影響。
需要注意的是,你要應用對數(shù)變換的數(shù)據(jù)必須是正值,否則會出現(xiàn)錯誤。另外,可以在轉(zhuǎn)換數(shù)據(jù)之前將 1 加到數(shù)據(jù)中,用于確保變換后的輸出值也是正的。
Log(x+1)
#?Log?Transform?Example
data?=?pd.DataFrame({'value':[2,45,?-23,?85,?28,?2,?35,?-12]})
data['log+1']?=?(data['value']+1).transform(np.log)
#?Negative?Values?Handling
#?Note?that?the?values?are?different
data['log']?=?(data['value']-data['value'].min()+1)?.transform(np.log)
???value??log(x+1)??log(x-min(x)+1)
0??????2???1.09861??????????3.25810
1?????45???3.82864??????????4.23411
2????-23???????nan??????????0.00000
3?????85???4.45435??????????4.69135
4?????28???3.36730??????????3.95124
5??????2???1.09861??????????3.25810
6?????35???3.58352??????????4.07754
7????-12???????nan??????????2.48491
6獨熱編碼
獨熱編碼是機器學習中最常見的編碼方法之一。此方法將一列中的值分布到多個標記列,并為其分配 0 或 1。這些二進制值表示類別和編碼之間的關(guān)系。
該方法將算法難以正確理解的分類型數(shù)據(jù)更改為數(shù)值格式,并使你可以在不丟失任何信息的情況下對類別數(shù)據(jù)進行分組。

.Why 獨熱編碼?
如果該列中有 N 個不同的值,則將它們映射到 N-1 個二進制列就足夠了,因為可以從其他列中扣除該缺失值。如果我們手中的所有列都等于 0,則缺失值必須等于 1。這就是為什么將其稱為獨熱編碼的原因。但是,我將使用 Pandas 的 get_dummies 函數(shù)給出一個示例,此函數(shù)將一列映射到多個列。
encoded_columns?=?pd.get_dummies(data['column'])
data?=?data.join(encoded_columns).drop('column',?axis=1)
7分組操作
在大多數(shù)機器學習算法中,每個實例對應訓練數(shù)據(jù)集中的一行,而不同列對應不同特征。這種形式的數(shù)據(jù)稱為整齊(tidy)數(shù)據(jù)。
整齊數(shù)據(jù)集易于操作、建模和可視化,并具有特定的結(jié)構(gòu): 每個變量是一列,每個觀察值是一行,每種類型的觀察單位是表格。
諸如涉及事務處理之類的數(shù)據(jù)集由于一個實例對應多行數(shù)據(jù)而很少適合整齊數(shù)據(jù)的定義。在這種情況下,我們按實例對數(shù)據(jù)進行分組,然后每個實例僅由一行代表。
按操作分組的關(guān)鍵是確定特征的聚合函數(shù)。對于數(shù)值型特征,平均值和求和函數(shù)通常是不錯的選擇,而對于分類型特征,則較為復雜。
.分類特征分組
建議使用三種不同的方式來聚合分類特征:
第一種是選擇頻率最高的標簽。換句話說,這是分類特征的 max 操作,但是普通的 max 函數(shù)通常不返回此值,因此你需要自己定義,例如使用 lambda 函數(shù)。
data.groupby('id').agg(lambda?x:?x.value_counts().index[0])
第二種選擇是制作數(shù)據(jù)透視表(pivot table)。這種方法與上一步驟中的編碼方法類似,略有不同。代替二值符號,可以將其定義為分組列和編碼列之間的值的聚合函數(shù)。如果你打算超越二值標記列并將多重特征合并為更有用的聚合特征,那么這將是一個不錯的選擇。(該方法與 Pandas 中另一個函數(shù) groupby 作用類似,可以結(jié)合下圖例子來理解這一點。)

#?Pivot?table?Pandas?Example
data.pivot_table(index='column_to_group',?columns='column_to_encode',?values='aggregation_column',?aggfunc=np.sum,?fill_value?=?0)
最后一種分類特征分組方案是在應用獨熱編碼后應用分組函數(shù) group by。此方法將保留所有數(shù)據(jù)(在上面第一種方案中,會丟失一些數(shù)據(jù))。與此同時,還將編碼列從分類轉(zhuǎn)換為數(shù)值??梢蚤喿x下一部分以了解數(shù)值特征分組的說明。
.數(shù)值特征分組
在大多數(shù)情況下,數(shù)值特征使用求和以及均值函數(shù)分組。根據(jù)特征的含義,兩者都是可取的。例如,如果要獲取比率列,則可以取二值列的平均值。在同一示例中,sum 函數(shù)可用于獲得總數(shù)。
#?sum_cols:?List?of?columns?to?sum
#?mean_cols:?List?of?columns?to?average
grouped?=?data.groupby('column_to_group')
sums?=?grouped[sum_cols].sum().add_suffix('_sum')
avgs?=?grouped[mean_cols].mean().add_suffix('_avg')
new_df?=?pd.concat([sums,?avgs],?axis=1)
8特征拆分
拆分特征是使它們在機器學習中發(fā)揮作用的好辦法。很多時候,數(shù)據(jù)集包含一些字符串列,這就違反了整齊數(shù)據(jù)的原則。通過將列的可用部分提取成新特征,有利于
讓機器學習算法能夠理解它們。 可以將它們分箱和分組。
通過發(fā)掘潛在信息來提高模型性能。
split 函數(shù)是一個不錯的選擇,但是,沒有一種適用于拆分所有特征的通用方法。它取決于列的特性以及如何拆分它。讓我們通過兩個示例對其進行介紹。
首先,一個可用于拆分普通名字列的簡單 split 函數(shù),
data.name
0??Luther?N.?Gonzalez
1????Charles?M.?Young
2????????Terry?Lawson
3???????Kristen?White
4??????Thomas?Logsdon
#?Extracting?first?names
data.name.str.split("?").map(lambda?x:?x[0])
0?????Luther
1????Charles
2??????Terry
3????Kristen
4?????Thomas
#?Extracting?last?names
data.name.str.split("?").map(lambda?x:?x[-1])
0????Gonzalez
1???????Young
2??????Lawson
3???????White
4?????Logsdon
上面的示例通過僅使用第一個和最后一個詞來處理長度超過兩個單詞的名字,這使該函數(shù)在遇到極端情況時具有魯棒性,在處理此類字符串時應考慮到這一方法。
split 函數(shù)的另一個使用場景是提取兩個字符之間的字符串部分。以下示例顯示了通過在一行代碼中連續(xù)使用兩個 split 函數(shù)來實現(xiàn)此情況的方法。
#?String?extraction?example
data.title.head()
0??????????????????????Toy?Story?(1995)
1????????????????????????Jumanji?(1995)
2???????????????Grumpier?Old?Men?(1995)
3??????????????Waiting?to?Exhale?(1995)
4????Father?of?the?Bride?Part?II?(1995)
data.title.str.split("(",?n=1,?expand=True)[1].str.split(")",?n=1,?expand=True)[0]
0????1995
1????1995
2????1995
3????1995
4????1995
9縮放
在大多數(shù)情況下,數(shù)據(jù)集的數(shù)值特征沒有特定范圍,并且彼此不同。在實際中,如果要求年齡列和收入列具有相同的數(shù)值范圍肯定會讓人覺得沒道理。但是如果站在機器學習的角度來看的話,該如何比較這兩個數(shù)值特征呢?
縮放解決了這個問題。經(jīng)過縮放過程后,連續(xù)特征的范圍變得相同。對于許多算法來說,此過程不是強制性的,但應用起來效果可能很好。但是,基于距離計算的算法(例如 k-NN 或 k-Means)需要具有可縮放的連續(xù)特征作為模型輸入。
有兩種基本的數(shù)據(jù)縮放方式。
.歸一化
歸一化(或 min-max 歸一化)在 0 到 1 之間的固定范圍內(nèi)縮放所有值。
此變換不會更改特征的分布,并且由于標準差降低,異常值的影響會增加。因此,建議在該歸一化之前處理異常值。
data?=?pd.DataFrame({'value':[2,45,?-23,?85,?28,?2,?35,?-12]})
data['normalized']?=?(data['value']?-?data['value'].min())?/?(data['value'].max()?-?data['value'].min())
???value??normalized
0??????2????????0.23
1?????45????????0.63
2????-23????????0.00
3?????85????????1.00
4?????28????????0.47
5??????2????????0.23
6?????35????????0.54
7????-12????????0.10
.標準化
標準化(或 z-分數(shù)規(guī)范化)在考慮標準差的同時縮放特征值。如果特征的標準差不同,則它們的范圍也將彼此不同。這減少了特征中異常值的影響。
在以下標準化公式中,
data?=?pd.DataFrame({'value':[2,45,?-23,?85,?28,?2,?35,?-12]})
data['standardized']?=?(data['value']?-?data['value'].mean())?/?data['value'].std()
???value??standardized
0??????2?????????-0.52
1?????45??????????0.70
2????-23?????????-1.23
3?????85??????????1.84
4?????28??????????0.22
5??????2?????????-0.52
6?????35??????????0.42
7????-12?????????-0.92
10提取日期
盡管日期列通常給有關(guān)模型目標值提供了很多有用信息,但它們在機器學習學習中往往被忽略。日期可以以多種格式顯示,這使得算法很難理解,即使將日期簡化為 01-01-2017 之類的格式也是如此。
如果不處理日期列,那么在這些值之間建立序數(shù)關(guān)系對于機器學習算法來說是非常具有挑戰(zhàn)性的。在這里,建議對日期進行三種預處理,
將日期部分提取到不同的列中: 年、月、日等。 根據(jù)年、月、日等提取當前日期和這些列之間的時間差。
從日期中提取一些特定特征: 工作日的名稱,是否周末、是否休假等。
如果將日期列按上述方法提取出新的列,則它們的信息將會被更合理地表達出來,并且機器學習算法可以輕松地理解它們。
from?datetime?import?date
data?=?pd.DataFrame({'date':
['01-01-2017',
'04-12-2008',
'23-06-1988',
'25-08-1999',
'20-02-1993',
]})
#?Transform?string?to?date
data['date']?=?pd.to_datetime(data.date,?format="%d-%m-%Y")
#?Extracting?Year
data['year']?=?data['date'].dt.year
#?Extracting?Month
data['month']?=?data['date'].dt.month
#?Extracting?passed?years?since?the?date
data['passed_years']?=?date.today().year?-?data['date'].dt.year
#?Extracting?passed?months?since?the?date
data['passed_months']?=?(date.today().year?-?data['date'].dt.year)?*?12?+?date.today().month?-?data['date'].dt.month
#?Extracting?the?weekday?name?of?the?date
data['day_name']?=?data['date'].dt.day_name()
????????date??year??month??passed_years??passed_months???day_name
0?2017-01-01??2017??????1?????????????2?????????????26?????Sunday
1?2008-12-04??2008?????12????????????11????????????123???Thursday
2?1988-06-23??1988??????6????????????31????????????369???Thursday
3?1999-08-25??1999??????8????????????20????????????235??Wednesday
4?1993-02-20??1993??????2????????????26????????????313???Saturday

?英文鏈接?
Emre Ren?bero?lu: https://towardsdatascience.com/feature-engineering-for-machine-learning-3a5e293a5114
