
使用這些基本 REST API 最佳實踐構建出色的 API
上圖為大家熟悉的機器學習建模流程圖(擴展閱讀:一文全覽機器學習建模流程(Python代碼)),整個建模流程非常重要的一步,是對于數據的預處理和特征工程,它很大程度決定了最后建模效果的好壞。
首先我們來了解一下『特征工程』。事實上大家在ShowMeAI的實戰系列文章?Python機器學習綜合項目-電商銷量預估[2]?和?Python機器學習綜合項目-電商銷量預估(進階)[3]?中已經看到了我們做了特征工程的處理。
如果我們對特征工程(feature engineering)做一個定義,那它指的是:利用領域知識和現有數據,創造出新的特征,用于機器學習算法;可以手動或自動。
在業界有一個很流行的說法:據與特征工程決定了模型的上限,改進算法只不過是逼近這個上限而已。
這是因為,在數據建模上,『理想狀態』和『真實場景』是有差別的,很多時候原始數據并不是規矩干凈含義明確充分的形態:
而特征工程處理,相當于對數據做一個梳理,結合業務提取有意義的信息,以干凈整齊地形態進行組織:特征工程有著非常重要的意義:
本篇內容,ShowMeAI帶大家一起來系統學習一下特征工程,包括『1.特征類型』『2.數據清洗』『3.特征構建』『4.特征變換』『5.特征選擇』等板塊內容。
我們這里用最簡單和常用的Titanic數據集給大家講解。
Titanic 數據集是非常適合數據科學和機器學習新手入門練習的數據集,數據集為1912年泰坦尼克號沉船事件中一些船員的個人信息以及存活狀況。我們可以根據數據集訓練出合適的模型并預測新數據(測試集)中的存活狀況。
Titanic 數據集可以通過 Seaborn 工具庫直接加載,如下代碼所示:
import pandas as pd
import numpy as np
import seaborn as sns
df_titanic = sns.load_dataset('titanic')
其中數據集的數據字段描述如下圖所示:
在具體演示 Titanic 的數據預處理與特征工程之前,ShowMeAI再給大家構建一些關于數據的基礎知識。
數據可以分為『結構化數據』和『非結構化數據』,比如在互聯網領域,大部分存儲在數據庫內的表格態業務數據,都是結構化數據;而文本、語音、圖像視頻等就屬于非結構化數據。
對于我們記錄到的數據,我們通常又可以以『定量數據』和『定性數據』對齊進行區分,其中:
實際數據挖掘或者建模之前,我們會有『數據預處理』環節,對原始態的數據進行數據清洗等操作處理。
因為現實世界中數據大體上都是不完整、不一致的『臟數據』,無法直接進行數據挖掘,或者挖掘結果差強人意。
『臟數據』產生的主要成因包括:篡改數據、數據不完整、數據不一致、數據重復、異常數據等。
數據清洗過程包括『2.1 數據對齊』、『2.2 缺失值處理』、『2.3 異常值處理』『2.4 數據轉化等』數據處理方法。我們對這些處理方法做詳細講解。
采集到的原始數據,格式形態不一,我們會對時間、字段以及相關量綱等進行數據對齊處理,數據對齊和規整化之后的數據整齊一致,更加適合建模。如下圖為一些處理示例:
① 時間
2022-02-20
、20220220
、2022/02/20
、20/02/2022
〗。② 字段
③ 量綱
數據缺失是真實數據中常見的問題,因為種種原因我們采集到的數據并不一定是完整的,我們有一些缺失值的常見處理方式。具體的處理方式可以展開成圖:
下面回到我們的Titanic數據集,我們演示一下各種方法。我們先對數據集的缺失值情況做一個了解(匯總分布):
df_titanic.isnull().sum()
survived 0
pclass 0
sex 0
age 177
sibsp 0
parch 0
fare 0
embarked 2
class 0
who 0
adult_male 0
deck 688
embark_town 2
alive 0
alone 0
最直接粗暴的處理是剔除缺失值,即將存在遺漏信息屬性值的對象 (字段,樣本/記錄) 刪除,從而得到一個完備的信息表。優缺點如下:
在我們當前Titanic的案例中,embark_town
字段有 2 個空值,考慮刪除缺失處理下。
df_titanic[df_titanic["embark_town"].isnull()]
df_titanic.dropna(axis=0,how='any',subset=['embark_town'],inplace=True)
(2) 數據填充 · 缺失值處理
第2大類是我們可以通過一些方法去填充缺失值。比如基于統計方法、模型方法、結合業務的方法等進行填充。
① 手動填充。根據業務知識來進行人工手動填充。
② 特殊值填充。將空值作為一種特殊的屬性值來處理,它不同于其他的任何屬性值。如所有的空值都用unknown
填充。一般作為臨時填充或中間過程。
df_titanic['embark_town'].fillna('unknown', inplace=True)
③ 統計量填充。若缺失率較低,可以根據數據分布的情況進行填充。常用填充統計量如下:
中位數填充——fare:缺失值較多,使用中位數填充。
df_titanic['fare'].fillna(df_titanic['fare'].median(), inplace=True)
眾數填充——embarked:只有兩個缺失值,使用眾數填充。
df_titanic['embarked'].isnull().sum()
#執行結果:2
df_titanic['embarked'].fillna(df_titanic['embarked'].mode(), inplace=True)
df_titanic['embarked'].value_counts()
#執行結果:
#S 64
同類均值填充。
age:根據 sex、pclass 和 who 分組,如果落在相同的組別里,就用這個組別的均值或中位數填充。
df_titanic.groupby(['sex', 'pclass', 'who'])['age'].mean()
age_group_mean = df_titanic.groupby(['sex', 'pclass', 'who'])['age'].mean().reset_index()
def select_group_age_median(row):
condition = ((row['sex'] == age_group_mean['sex']) &
(row['pclass'] == age_group_mean['pclass']) &
(row['who'] == age_group_mean['who']))
return age_group_mean[condition]['age'].values[0]
df_titanic['age'] =df_titanic.apply(lambda x: select_group_age_median(x) if np.isnan(x['age']) else x['age'],axis=1)
④ 模型預測填充。如果其他無缺失字段豐富,我們也可以借助于模型進行建模預測填充,將待填充字段作為Label,沒有缺失的數據作為訓練數據,建立分類/回歸模型,對待填充的缺失字段進行預測并進行填充。
我們以 Titanic 案例中的 age 字段為例,講解一下:
age 缺失量較大,這里我們用 sex、pclass、who、fare、parch、sibsp 六個特征構建隨機森林模型,填充年齡缺失值。
df_titanic_age = df_titanic[['age', 'pclass', 'sex', 'who','fare', 'parch', 'sibsp']]
df_titanic_age = pd.get_dummies(df_titanic_age)
df_titanic_age.head()
# 乘客分成已知年齡和未知年齡兩部分
known_age = df_titanic_age[df_titanic_age.age.notnull()]
unknown_age = df_titanic_age[df_titanic_age.age.isnull()]
# y 即目標年齡
y_for_age = known_age['age']
# X 即特征屬性值
X_train_for_age = known_age.drop(['age'], axis=1)
X_test_for_age = unknown_age.drop(['age'], axis=1)
from sklearn.ensemble import RandomForestRegressor
rfr = RandomForestRegressor(random_state=0, n_estimators=2000, n_jobs=-1)
rfr.fit(X_train_for_age, y_for_age)
# 用得到的模型進行未知年齡結果預測
y_pred_age = rfr.predict(X_test_for_age)
# 用得到的預測結果填補原缺失數據
df_titanic.loc[df_titanic.age.isnull(), 'age'] = y_pred_age
sns.distplot(df_titanic.age)
⑤ 插值法填充。還可以用插值法對數據填充,細分一下包括線性插值、多重插補、熱平臺插補、拉格朗日插值、牛頓插值等。
線性插值法
使用插值法可以計算缺失值的估計值,所謂的插值法就是通過兩點 , 估計中間點的值。假設 是一條直線,通過已知的兩點來計算函數 ,然后只要知道 就能求出 ,以此方法來估計缺失值。
.interpolate(method = 'linear', axis)
方法將通過linear
插值使用沿著給定axis
的值替換 NaN 值,這個差值也就是前后或者上下的中間值
df_titanic['fare'].interpolate(method = 'linear', axis = 0)
同時,也可用行值插入
df_titanic['fare'].interpolate(method = 'linear', axis = 1)
多重插補(Multiple Imputation)
多值插補的思想來源于貝葉斯估計,認為待插補的值是隨機的,它的值來自于已觀測到的值。具體實踐上通常是估計出待插補的值,然后再加上不同的噪聲,形成多組可選插補值。根據某種選擇依據,選取最合適的插補值。 多重插補方法分為三個步驟:
⑥ 啞變量填充。有另外一種非常有意思的填充方式,叫做『啞變量填充』,在變量為離散型,且不同值較少的情況下可以采用。以 Titanic 數據為例:
IS_SEX_MALE
、IS_SEX_FEMALE
、IS_SEX_NA
。other
,降低維度。此做法可最大化保留變量的信息。以下為參考代碼示例:
sex_list = ['MALE', 'FEMALE', np.NaN, 'FEMALE', 'FEMALE', np.NaN, 'MALE']
df = pd.DataFrame({'SEX': sex_list})
display(df)
df.fillna('NA', inplace=True)
df = pd.get_dummies(df['SEX'],prefix='IS_SEX')
display(df)
# 原始數據
SEX
0 MALE
1 FEMALE
2 NaN
3 FEMALE
4 FEMALE
5 NaN
6 MALE
# 填充后
IS_SEX_FEMALE IS_SEX_MALE IS_SEX_NA
0 0 1 0
1 1 0 0
2 0 0 1
3 1 0 0
4 1 0 0
5 0 0 1
6 0 1
當特征值缺失超過??以上,建議刪除〖或加入『是』『否』標記位信息〗,容易影響模型效果
df_titanic.drop(["deck"],axis=1)
數據質量也會很大程度影響機器學習應用效果,數據的錯誤值或異常值可能會造成測量誤差或異常系統條件的結果,給模型學習帶來很大的問題。實際我們很多時候會有異常值檢測與處理環節,下面給大家做一個梳理。
① 基于統計分析。通常用戶用某個統計分布對數據點進行建模,再以假定的模型,根據點的分布來確定是否異常。如通過分析統計數據的散度情況,即數據變異指標,對數據的分布情況有所了解,進而通過數據變異指標來發現數據中的異常點數據。
常用的數據變異指標有極差、四分位數間距、均差、標準差、變異系數等等,如變異指標的值大表示變異大、散布廣;值小表示離差小,較密集。
比如,最大最小值可以用來判斷這個變量的取值是否超過了合理的范圍,如客戶的年齡為 歲或 歲,為異常值。
② 3σ原則。如果數據近似正態分布,在??原則下,異常值為一組測定值中與平均值的偏差超過??倍標準差的值。
如果數據服從正態分布,距離平均值 之外的值出現的概率為 ,屬于極個別的小概率事件。 如果數據不服從正態分布,也可以用遠離平均值的多少倍標準差來描述。
③ 箱線圖分析。大家還記得在數據分析部分有一個很有效的工具叫做箱線圖[6]。箱型圖判斷異常值的方法以四分位數和四分位距為基礎,四分位數具有魯棒性,因此箱型圖識別異常值比較客觀,在識別異常值時有一定的優越性。
箱線圖提供了識別異常值的一個標準:如果一個值小于 或大于 的值,則被稱為異常值。
sns.catplot(y="fare",x="survived", kind="box", data=df_titanic,palette="Set2")
④ 基于模型檢測。我們也可以基于模型對異常值檢測。基本思路是先建立一個數據模型,那些同模型不能完美擬合的對象就視作異常。如果模型是簇的集合,則異常是不顯著屬于任何簇的對象。在使用回歸模型時,異常是相對遠離預測值的對象。
⑤ 基于距離。我們還有基于距離的方法可以用于異常檢測。這類方法基于下面這個假設:如果一個數據對象和大多數點距離都很遠,那這個對象就是異常。通過定義對象之間的臨近性度量,根據距離判斷異常對象是否遠離其他對象,主要使用的距離度量方法有絕對距離(曼哈頓距離)、歐氏距離和馬氏距離等方法。
⑥ 基于密度。一個很直接的異常檢測思路是基于分布密度來做。具體為:考察當前點周圍密度,局部異常點/離群點的局部密度顯著低于大部分近鄰點。這類方法適用于非均勻的數據集。
⑦ 基于聚類。我們可以基于聚類的方法進行異常檢測,遠離 cluster 的樣本更可能是異常值。
不過該方法會受到聚類 cluster 個數 的影響,一種策略是對于不同的簇個數重復該分析;另一種方法是找出大量小簇,其想法是較小的簇傾向于更加凝聚。如果存在大量小簇時一個對象是異常點,則它多半是一個真正的異常點。不利的一面是一組異常點可能形成小簇而逃避檢測。
⑧ 基于鄰近度的異常點檢測。同樣的,我們也有基于近鄰度的思路來做異常檢測,我們認為異常點遠離大部分的點。這種方法比統計學方法更一般、更容易使用,因為確定數據集的有意義的鄰近性度量比確定它的統計分布更容易。
一個對象的異常點得分由到它的 最近鄰的距離給定,所以異常點得分對 的取值高度敏感:
在數據處理階段將離群點作為影響數據質量的異常點考慮,而不是作為通常所說的異常檢測目標點,一般采用較為簡單直觀的方法,結合箱線圖和 MAD 的統計方法判斷變量的離群點。如下為繪制散點圖根據分布直接判斷。
sns.scatterplot(x="fare", y="age", hue="survived",data=df_titanic,palette="Set1")
對異常值處理,需要具體情況具體分析,異常值處理方法常用的有以下幾種:
前序的數據預處理過程能保證我們拿到干凈整齊準確的數據,但這些數據未必對于建模是最有效的,下一步我們通常會進行特征構建,結合業務場景產生衍生變量來提升數據表達能力和模型建模效果。
統計特征是一類非常有效的特征,尤其在時序問題場景中,以下為統計特征構建的一些思考維度和方法:
回到Titanic數據集,我們來看看結合業務理解,我們可以做哪些新特征:
① 年齡處理。我們對年齡 age 字段進行進一步處理,考慮到不同的年齡段對應的人群可能獲救概率不同,我們根據年齡值分成不同區間段,對應到 child、young、midlife、old 等
def age_bin(x):
if x <= 18:
return 'child'
elif x <= 30:
return 'young'
elif x <= 55:
return 'midlife'
else:
return 'old'
df_titanic['age_bin'] = df_titanic['age'].map(age_bin)
df_titanic['age_bin'].unique()
執行結果:
array(['young', 'midlife', 'child', 'old'], dtype=object)
② 抽取『稱呼』特征。 我們在 name 字段里,可以看到各種不同的稱呼,如『Mr』『Master』『Dr』等,這些稱呼體現了乘客的身份等信息,我們可以對其做抽取構建新的特征。
# 提取稱呼
df_titanic['title'] = df_titanic['name'].map(lambda x: x.split(',')[1].split('.')[0].strip())
df_titanic['title'].value_counts()
執行結果如下:
Mr 757
Miss 260
Mrs 197
Master 61
Rev 8
Dr 8
Col 4
Ms 2
Major 2
Mlle 2
Dona 1
Sir 1
Capt 1
Don 1
Lady 1
Mme 1
the Countess 1
Jonkheer 1
我們做一個簡單的『稱呼』統計
# 對稱呼細分,是官員,還是皇室,還是女士、先生、小姐
df_titanic['title'].unique()
執行結果:
array(['Mr', 'Mrs', 'Miss', 'Master', 'Don', 'Rev', 'Dr', 'Mme', 'Ms',
'Major', 'Lady', 'Sir', 'Mlle', 'Col', 'Capt', 'the Countess',
'Jonkheer', 'Dona'], dtype=object)
下面我們對這些『稱呼』『稱謂』做一個規范化統一。
title_dictionary = {
"Mr": "Mr",
"Mrs": "Mrs",
"Miss": "Miss",
"Master": "Master",
"Don": "Royalty",
"Rev": "Officer",
"Dr": "Officer",
"Mme": "Mrs",
"Ms": "Mrs",
"Major": "Officer",
"Lady": "Royalty",
"Sir": "Royalty",
"Mlle": "Miss",
"Col": "Officer",
"Capt": "Officer",
"the Countess": "Royalty",
"Jonkheer": "Royalty",
"Dona": 'Mrs'
}
df_titanic['title'] = df_titanic['title'].map(title_dictionary)
df_titanic['title'].value_counts()
執行結果如下:
Mr 757
Miss 262
Mrs 201
Master 61
Officer 23
Royalty 5
③ 抽取家庭規模。在 Titanic 上,有的成員之間有親屬關系,考慮到家族大小對于最終是否獲救也有影響,我們可以構建一個?family_size
?的特征,用于表征家庭規模。
df_titanic['family_size'] = df_titanic['sibsp'] + df_titanic['parch'] + 1
df_titanic['family_size'].head()
執行結果如下:
0 2
1 2
2 1
3 2
4 1
在電商等場景下,數據有一定的周期規律,我們可以提取一些周期值作為有效信息。時序周期的一些考慮維度如下:
數據分桶,是對連續值屬性處理的一種常用方法,它指的是我們把連續數值切段,并把連續值歸屬到對應的段中。數據分桶也叫做數據分箱或離散化。
① 等頻、等距分桶
(a) 自定義分箱。指根據業務經驗或者常識等自行設定劃分的區間,然后將原始數據歸類到各個區間中。
(b) 等距分箱。按照相同寬度將數據分成幾等份。
從最小值到最大值之間,均分為 等份。如果 、 為最小最大值,則每個區間的長度為 ,區間邊界值為 、、、。
等距分箱只考慮邊界,每個等份里面的實例數量可能不等。等距分桶的缺點是受到異常值的影響比較大。
(c) 等頻分箱。將數據分成幾等份,每等份數據里面的個數是一樣的。
在等頻分箱中,區間的邊界值要經過計算獲得,最終每個區間包含大致相等的實例數量。比如說 ,每個區間應該包含大約 的實例。
數值變量分箱。我們先對船票價格做一個等頻切分(大家如果對船票價格進行分布繪圖,會發現是很長尾的分布,并不適合等距切分),看看分開的區間段。
# qcut 等頻率分箱
df_titanic['fare_bin'], bins = pd.qcut(df_titanic['fare'], 5, retbins=True)
df_titanic['fare_bin'].value_counts()
結果如下:
(7.854, 10.5] 184
(21.679, 39.688] 180
(-0.001, 7.854] 179
(39.688, 512.329] 176
(10.5, 21.679] 172
bins #array([ 0. , 7.8542, 10.5 , 21.6792, 39.6875, 512.3292])
下面根據區間段對其進行等頻切分
# 對船票fare進行分段分桶
def fare_cut(fare):
if fare <= 7.8958:
return 0
if fare <= 10.5:
return 1
if fare <= 21.6792:
return 2
if fare <= 39.6875:
return 3
return 4
df_titanic['fare_bin'] = df_titanic['fare'].map(fare_cut)
相比船票價格,年齡 age 字段的分布更加集中,且區間大小比較明確,我們采用等距切分,代碼如下:
# cut 等距離分箱
bins = [0, 12, 18, 65, 100]
pd.cut(df_titanic['age'], bins).value_counts
② Best-KS分桶。
實現步驟:
- 將特征值值進行從小到大的排序。
- 計算出 最大的那個值,即為切點,記為 。然后把數據切分成兩部分。
- 重復步驟2,進行遞歸, 左右的數據進一步切割。直到 的箱體數達到我們的預設閾值即可。
- 連續型變量:分箱后的 值 分箱前的 值
- 分箱過程中,決定分箱后的 值是某一個切點,而不是多個切點的共同作用。這個切點的位置是原始 值最大的位置。
③ 卡方分桶。自底向上的(即基于合并的)數據離散化方法,依賴于卡方檢驗:具有最小卡方值的相鄰區間合并在一起,直到滿足確定的停止準則。
基本思想:如果兩個相鄰的區間具有非常類似的類分布,則這兩個區間可以合并;否則,它們應當保持分開。而低卡方值表明它們具有相似的類分布。
實現步驟:
- 預先定義一個卡方的閾值
- 初始化;根據要離散的屬性對實例進行排序,每個實例屬于一個區間
- 合并區間:計算每一對相鄰區間的卡方值,將卡方值最小的一對區間合并
代碼實現:https://github.com/Lantianzz/Scorecard-Bundle
④ 最小熵法分箱。還有最小熵分箱法,需要使總熵值達到最小,也就是使分箱能夠最大限度地區分因變量的各類別。
熵是信息論中數據無序程度的度量標準,提出信息熵的基本目的是找出某種符號系統的信息量和冗余度之間的關系,以便能用最小的成本和消耗來實現最高效率的數據存儲、管理和傳遞。
數據集的熵越低,說明數據之間的差異越小,最小熵劃分就是為了使每箱中的數據具有最好的相似性。給定箱的個數,如果考慮所有可能的分箱情況,最小熵方法得到的箱應該是具有最小熵的分箱。
我們在有些場景下會考慮特征組合構建強特征,如下為常用的特征組合構建方式:(擴展閱讀:一文歸納Python特征生成方法(全))
多項式特征。針對連續值特征,我們對幾個特征構建多項式特征,以達到特征組合與高階增強的作用。
在Titanic的例子中,如下為數值型特征:
df_titanic_numerical = df_titanic[['age','sibsp','parch','fare','family_size']]
df_titanic_numerical.head()
我們可以參考下述代碼構建多項式特征
# 擴展數值特征
from sklearn.preprocessing import PolynomialFeatures
poly = PolynomialFeatures(degree=2, include_bias=False, interaction_only=False)
df_titanic_numerical_poly = poly.fit_transform(df_titanic_numerical)
pd.DataFrame(df_titanic_numerical_poly, columns=poly.get_feature_names()).head()
在構建完成特征后,我們查看下衍生新特征變量的相關性情況,下面的熱力圖heatmap里顏色越深相關性越大:
sns.heatmap(pd.DataFrame(df_titanic_numerical_poly, columns=poly.get_feature_names()).corr())
我們對于構建完的特征,會做一些『特征變換』的操作,以適應不同的模型,更好地完成建模。
標準化操作也稱作 Z-score 變換,它使數值特征列的算數平均為 ,方差(以及標準差)為 ,如圖所示。
注意:如果數值特征列中存在數值極大或極小的outlier(通過EDA發現),應該使用更穩健(robust)的統計數據:用中位數而不是算術平均數,用分位數(quantile)而不是方差。這種標準化方法有一個重要的參數:(分位數下限,分位數上限),最好通過EDA的數據可視化確定。免疫outlier。
from sklearn.preprocessing import StandardScale
#標準化模型訓練
Stan_scaler = StandardScaler()
Stan_scaler.fit(x)
x_zscore = Stan_scaler.transform(x)
x_test_zscore = Stan_scaler.transform(x_test)
joblib.dump(Stan_scaler,'zscore.m') #寫入文件
歸一化操作會基于向量模長調整數據幅度大小,但并不會改變原始數據的順序。如圖所示。
幅度縮放是為了讓不同特征的取值在大體一致的數量級和數據區間內,比較常用的方法是最大最小值縮放,如圖所示。
from sklearn import preprocessing
min_max_scaler = preprocessing.MinMaxScaler()
min_max_scaler.fit_transform(x)
x_minmax = min_max_scaler.transform(x)
x_test_minmax = min_max_scaler.transform(x_test)
joblib.dump(min_max_scaler,'min_max_scaler.m') #寫入文件
歸一化和標準化是兩個非常常見的特征變換操作,下面我們來對比一下標準化和歸一化:
它們分別的適用場景可以歸納總結如下:
參考ShowMeAI教程 圖解機器學習算法:從入門到精通系列教程[7]
具體模型參考ShowMeAI教程?圖解機器學習算法:從入門到精通系列教程[8]
我們在有些場景下,還會對數值字段進行分布調整或者校正,利用統計或數學變換來減輕數據分布傾斜的影響。使原本密集的區間的值盡可能的分散,原本分散的區間的值盡量的聚合。 大部分變換函數都屬于冪變換函數簇,主要作用是穩定方差,保持分布接近于正態分布并使得數據與分布的平均值無關。我們來看看一些典型的非線性統計變換。
① log變換。log 變換通常用來創建單調的數據變換。主要作用為穩定方差,始終保持分布接近于正態分布并使得數據與分布的平均值無關。
log 變換屬于冪變換函數簇,數學表達式為
下面我們對 Titanic 數據集中的船票價格字段進行 log1p 變換,示例代碼如下:
sns.distplot(df_titanic.fare,kde=False)
df_titanic['fare_log'] = np.log((1+df_titanic['fare']))
sns.distplot(df_titanic.fare_log,kde=False)
② box-cox變換。box-cox 變換是 box 和 cox 在1964年提出的一種廣義冪變換方法,是統計建模中常用的一種數據變換,用于連續的響應變量不滿足正態分布的情況。box-cox 變換之后,可以一定程度上減小不可觀測的誤差和預測變量的相關性。
box-cox 變換的主要特點是引入一個參數,通過數據本身估計該參數進而確定應采取的數據變換形式,box-cox 變換可以明顯地改善數據的正態性、對稱性和方差相等性,對許多實際數據都是行之有效的。
box-cox 變換函數數學表達式如下:
生成的變換后的輸出 ,是輸入 和變換參數的函數;當 時,該變換就是自然對數 log 變換,前面我們已經提到過了。 的最佳取值通常由最大似然或最大對數似然確定。
下面我們對Titanic數據集中的船票價格字段進行 box-cox 變換,示例代碼如下:
# 從數據分布中移除非零值
fare_positive_value = df_titanic[(~df_titanic['fare'].isnull()) & (df_titanic['fare']>0)]['fare']
import scipy.stats as spstats
# 計算最佳λ值
l, opt_lambda = spstats.boxcox(fare_positive_value)
print('Optimal lambda value:', opt_lambda) # -0.5239075895755266
# 進行 Box-Cox 變換
fare_boxcox_lambda_opt = spstats.boxcox(df_titanic[df_titanic['fare']>0]['fare'],lmbda=opt_lambda)
sns.distplot(fare_boxcox_lambda_opt,kde=Fal
對于類別型的字段特征(比如顏色、類型、好壞程度),有很多模型并不能直接處理,我們對其進行編碼后能更好地呈現信息和支撐模型學習。有以下常見的類別型變量編碼方式:
① 標簽編碼(label encoding)。標簽編碼(label encoding)是最常見的類別型數據編碼方式之一,編碼值介于??和 n_classes-1 之間的標簽。例如:比如有?我們把其轉換為?。
from sklearn.preprocessing import LabelEncoder
le = LabelEncoder()
le.fit(["超一線", "一線", "二線", "三線"])
print('特征:{}'.format(list(le.classes_)))
# 輸出 特征:['一線', '三線', '二線', '超一線']
print('轉換標簽值:{}'.format(le.transform(["超一線", "一線", "二線"])))
# 輸出 轉換標簽值:array([3 0 2]...)
print('特征標簽值反轉:{}'.format(list(le.inverse_transform([2, 2, 1]))))
# 輸出 特征標簽值反轉:['二線', '二線', '三線
② 獨熱向量編碼(one hot encoding)。獨熱編碼通常用于處理類別間不具有大小關系的特征。例如:特征:血型,一共有四種類別?,采用獨熱編碼后,會把血型變成有一個4維的稀疏向量(最終生成的稀疏向量的維度,和類別數相同):
如果借助于pandas工具庫(查看ShowMeAI的?數據分析系列教程[9]?和?數據科學工具速查 | Pandas使用指南[10]?進行詳細了解),獨熱向量編碼的 Python 代碼參考示例如下:
sex_list = ['MALE', 'FEMALE', np.NaN, 'FEMALE', 'FEMALE', np.NaN, 'MALE']
df = pd.DataFrame({'SEX': sex_list})
display(df)
df.fillna('NA', inplace=True)
df = pd.get_dummies(df['SEX'],prefix='IS_SEX')
display(df)
最終變換前后的結果如下:
# 原始數據
SEX
0 MALE
1 FEMALE
2 NaN
3 FEMALE
4 FEMALE
5 NaN
6 MALE
# 獨熱向量編碼后
IS_SEX_FEMALE IS_SEX_MALE IS_SEX_NA
0 0 1 0
1 1 0 0
2 0 0 1
3 1 0 0
4 1 0 0
5 0 0 1
下面我們對’sex’, ‘class’, ‘pclass’, ’embarked’, ‘who’, ‘family_size’, ‘age_bin’這些字段都進行獨熱向量編碼。
pd.get_dummies(df_titanic, columns=['sex', 'class', 'pclass', 'embarked', 'who', 'family_size', 'age_bin'],drop_first=True)
當然,我們也可以借助SKLearn(查看ShowMeAI教程?SKLearn最全應用指南?和?AI建模工具速查 | Scikit-learn使用指南?詳細學習),進行獨熱向量編碼實現:
import numpy as np
from sklearn.preprocessing import OneHotEncoder
# 非負整數表示的標簽列表
labels = [0,1,0,2]
# 行向量轉列向量
labels = np.array(labels).reshape(len(labels), -1)
# 獨熱向量編碼
enc = OneHotEncoder()
enc.fit(labels)
targets = enc.transform(labels).toarray()
# 如果不加 toarray() 的話,輸出的是稀疏的存儲格式,即索引加值的形式,也可以通過參數指定 sparse = False 來達到同樣的效果
輸出結果如下:
array([[ 1., 0., 0.],
[ 0., 1., 0.],
[ 1., 0., 0.],
[ 0., 0., 1.]])
③ 標簽二值化(LabelBinarizer)。功能與 OneHotEncoder 一樣,但是 OneHotEncoder 只能對數值型變量二值化,無法直接對字符串型的類別變量編碼,而 LabelBinarizer 可以直接對字符型變量二值化。
from sklearn.preprocessing import LabelBinarizer
lb=LabelBinarizer()
labelList=['yes', 'no', 'no', 'yes','no2']
# 將標簽矩陣二值化
dummY=lb.fit_transform(labelList)
print("dummY:",dummY)
# 逆過程
yesORno=lb.inverse_transform(dummY)
print("yesOrno:",yesORno)
輸出如下:
dummY: [[0 0 1]
[1 0 0]
[1 0 0]
[0 0 1]
[0 1 0]]
yesOrno: ['yes' 'no' 'no' 'yes' 'no2']
在實際的機器學習項目中,我們可能還會做降維[11]處理,主要因為數據存在以下幾個問題:
通過特征降維希望達到的目的:減少特征屬性的個數,確保特征屬性之間是相互獨立的。
常用的降維方法有:PCA、SVD、LDA、T-sne等非線性降維。
這里降維的講解,我們給大家基于 iris 數據集講解:
from sklearn import datasets
iris_data = datasets.load_iris()
X = iris_data.data
y = iris_data.target
def draw_result(X, y):
plt.figure()
# 提取 Iris-setosa
setosa = X[y == 0]
# 繪制點:參數 1 x 向量,y 向量
plt.scatter(setosa[:, 0], setosa[:, 1], color="red", label="Iris-setosa")
versicolor = X[y == 1]
plt.scatter(versicolor[:, 0], versicolor[:, 1], color="orange", label="Iris-versicolor")
virginica = X[y == 2]
plt.scatter(virginica[:, 0], virginica[:, 1], color="blue", label="Iris-virginica")
plt.legend()
plt.show()
draw_result(X, y)
① PCA(Principal Component Analysis)。關于PCA主成分分析降維算法,大家可以查閱ShowMeAI文章?圖解機器學習 | 降維算法詳解[12]?進行詳細學習。
② SVD(Singular Value Decomposition)
SVD方法的主要步驟如下:
所以??是??特征值分解的特征向量按列組成的正交矩陣,??是??特征值組成的對角矩陣,也可以看出??的奇異值??是??特征值??的平方根。
假如 的特征向量為 , 中對應的 則可以由下式求出:
也即奇異值分解的關鍵在于對??進行特征值分解。
from sklearn.decomposition import TruncatedSVD
iris_2d = TruncatedSVD(2).fit_transform(X)
draw_result(iris_2d, y)
PCA VS SVD
PCA求解關鍵在于求解協方差矩陣 的特征值分解。
SVD關鍵在于 的特征值分解。
很明顯二者所解決的問題非常相似,都是對一個實對稱矩陣進行特征值分解,如果?。?/p>
則有:
此時SVD與PCA等價,所以PCA問題可以轉化為SVD問題求解。
③ ?LDA(Linear Discriminant Analysis)。是有監督的降維,通過最小化類內離散度與最大化類間離散度來獲得最優特征子集。
上圖解讀:LD1通過線性判定,可以很好的將呈正態分布的兩個類分開。LD2的線性判定保持了數據集的較大方差,但LD2無法提供關于類別的信息,因此LD2不是一個好的線性判定。
from sklearn.discriminant_analysis import LinearDiscriminantAnalysis as LDA
lda = LDA(n_components=2)
iris_2d = lda.fit_transform(X, y)
draw_result(iris_2d, y)
LDA VS PCA
PCA 試圖尋找到方差最大的正交的主成分分量軸 LDA 發現可以最優化分類的特征子空間 LDA 和 PCA 都是可用于降低數據集維度的線性轉換技巧 PCA 是無監督算法 LDA 是監督算法 LDA 是一種更優越的用于分類的特征提取技術
④ ?T-SNE。T-SNE(t-distributed stochastic neighbor embedding)是一種非線性降維方法:
from sklearn.manifold import TSNE
tsne = TSNE(n_components=2)
iris_2d = tsne.fit_transform(X)
draw_result(iris_2d, y)
特征選擇是在建模過程中經常會用到的一個處理,也有重要意義:
總體來說,進行特征選擇有2個主要考慮方向:
對特征選擇的方法進行歸類,又大體可以歸納為下述3種:
feature_selection
?庫來進行特征選擇。① 方差過濾。這是通過特征本身的方差來篩選特征的類。比如一個特征本身的方差很小,就表示樣本在這個特征上基本沒有差異,可能特征中的大多數值都一樣,甚至整個特征的取值都相同,那這個特征對于樣本區分沒有什么作用。
我們會剔除掉方差非常小的字段特征,參考代碼實現如下:
from sklearn.feature_selection import VarianceThreshold
variancethreshold = VarianceThreshold() #實例化,默認方差為 0.方差<=0 的過濾掉
df_titanic_numerical = df_titanic[['age','sibsp','parch','fare','family_size']]
X_var = variancethreshold.fit_transform(df_titanic_numerical) #獲取刪除不合格特征后的新特征矩陣
del_list = df_titanic_numerical.columns[variancethreshold.get_support()==0].to_list() #獲得刪除
② 卡方過濾??ǚ綑z驗,專用于分類算法,捕捉相關性,追求p小于顯著性水平的特征??ǚ竭^濾是專門針對離散型標簽(即分類問題)的相關性過濾。
p值和取到這一個統計量的概率取值其實是正相關的:?值越大,取到這個統計量的概率就越大,即越合理;?值越小,取到這個統計量的概率就越小,即越不合理,此時應該拒絕原假設,接收備擇假設。
df_titanic_categorical = df_titanic[['sex', 'class', 'embarked', 'who', 'age_bin','adult_male','alone','fare_bin']]
df_titanic_numerical = df_titanic[['age','sibsp','parch','fare','family_size','pclass']]
df_titanic_categorical_one_hot = pd.get_dummies(df_titanic_categorical, columns=['sex', 'class', 'embarked', 'who', 'age_bin','adult_male','alone','fare_bin'], drop_first=True)
df_titanic_combined = pd.concat([df_titanic_numerical,df_titanic_categorical_one_hot],axis=1)
y = df_titanic['survived']
X = df_titanic_combined.iloc[:,1:]
from sklearn.feature_selection import chi2
from sklearn.feature_selection import SelectKBest
chi_value, p_value = chi2(X,y)
#根據 p 值,得出 k 值
k = chi_value.shape[0] - (p_value > 0.05).sum() #要保留的特征的數量 14
#根據卡方值,選擇前幾特征,篩選后特征
X_chi = SelectKBest(chi2, k=14).fit_transform(X, y)
③ F檢驗。?檢驗捕捉線性相關性,要求數據服從正態分布,追求??值小于顯著性水平特征。
from sklearn.feature_selection import f_classif
f_value, p_value = f_classif(X,y)
#根據 p 值,得出 k 值
k = f_value.shape[0] - (p_value > 0.05).sum()
#篩選后特征
X_classif = SelectKBest(f_classif, k=14).fit_transform(X, y)
④ 互信息法?;バ畔⒎ㄊ怯脕聿蹲矫總€特征與標簽之間的任意關系(包括線性和非線性關系)的過濾方法。
from sklearn.feature_selection import mutual_info_classif as MIC
#互信息法
mic_result = MIC(X,y) #互信息量估計
k = mic_result.shape[0] - sum(mic_result <= 0) #16
X_mic = SelectKBest(MIC, k=16).fit_transform(X, y)
① 歸特征刪除法。遞歸消除刪除法使用一個基模型來進行多輪訓練,每輪訓練后,消除若干權值系數的特征,再基于新的特征集進行下一輪訓練。使用feature_selection
?庫的 RFE 類來選擇特征的代碼如下:
from sklearn.feature_selection import RFE
from sklearn.linear_model import LogisticRegression
#遞歸特征消除法,返回特征選擇后的數據
#參數 estimator 為基模型
#參數 n_features_to_select 為選擇的特征個數
X_ref = RFE(estimator=LogisticRegression(), n_features_to_select=10).fit_transform(X, y)
② 特征重要性評估。我們基于一些模型(如各類樹模型)可以得到特征重要度,進而進行篩選
from sklearn.ensemble import ExtraTreesClassifier
# 建模與獲取特征重要度
model = ExtraTreesClassifier()
model.fit(X, y)
print(model.feature_importances_)
# 特征重要度排序
feature=list(zip(X.columns,model.feature_importances_))
feature=pd.DataFrame(feature,columns=['feature','importances'])
feature.sort_values(by='importances',ascending=False).head(20)
③ 排列重要性評估。我們還有一類方法可以評估特征重要度,進而進行篩選,叫作排列重要度。
原理:在訓練機器學習模型之后計算置換重要性。這種方法在向模型提出假設,如果在保留目標和所有其他列的同時隨機打亂一列驗證集特征數據,對預測機器學習模型的準確性的影響程度。對于一個具有高度重要性的特征,random-reshuffle 會對機器學習模型預測的準確性造成更大的損害。
優點:快速計算;易于使用和理解;特征重要性度量的屬性;追求特征穩定性。
參考代碼實現如下:
import numpy as np
import pandas as pd
from sklearn.model_selection import train_test_split
from sklearn.ensemble import RandomForestClassifier
import eli5
from eli5.sklearn import PermutationImportance
my_model = RandomForestClassifier(random_state=0).fit(train_X, train_y)
perm = PermutationImportance(my_model, random_state=1).fit(val_X, val_y)
eli5.show_weights(perm, feature_names = val_X.columns.tolist())
① 基于懲罰項的特征選擇法。使用帶懲罰項的基模型,除了篩選出特征外,同時也進行了降維。
使用feature_selection
庫的SelectFromModel
類結合帶 L1 懲罰項的邏輯回歸模型,來選擇特征的代碼如下:
from sklearn.feature_selection import SelectFromModel
from sklearn.linear_model import LogisticRegression
#帶 L1 和 L2 懲罰項的邏輯回歸作為基模型的特征選擇,這個設置帶 L1 懲罰項的邏輯回歸作為基模型的特征選擇
lr = LogisticRegression(solver='liblinear',penalty="l1", C=0.1)
X_sfm = SelectFromModel(lr).fit_transform(X, y)
X_sfm.shape
(891, 7
使用 feature_selection 庫的 SelectFromModel 類結合 SVM 模型,來選擇特征的代碼如下:
from sklearn.feature_selection import SelectFromModel
from sklearn.svm import LinearSVC
lsvc = LinearSVC(C=0.01,penalty='l1',dual=False).fit(X, y)
model = SelectFromModel(lsvc,prefit=True)
X_sfm_svm = model.transform(X)
X_sfm_svm.shape
(891, 7
② 基于樹模型。樹模型中 GBDT 也可用來作為基模型進行特征選擇,使用 feature_selection 庫的 SelectFromModel 類結合 GBDT 模型,來選擇特征的代碼如下:
from sklearn.feature_selection import SelectFromModel
from sklearn.ensemble import GradientBoostingClassifier
#GBDT 作為基模型的特征選擇
gbdt = GradientBoostingClassifier()
X_sfm_gbdt = SelectFromModel(gbdt).fit_transform(X, y)
關于特征選擇,做一個經驗總結,如下:
最后,ShowMeAI結合實際工業應用經驗,總結一些特征工程要點:
構建特征的有效性,和業務及數據分布強相關,因此建議在此步驟之前做EDA探索性數據分析來充分理解數據。
可以參考ShowMeAI文章?Python機器學習綜合項目-電商銷量預估[13]?和?Python機器學習綜合項目-電商銷量預估<進階>[14]?了解EDA的基本過程和方法。
我們可能會做的一些數據預處理與特征處理如下:
① 連續特征離散化
② 數值截斷
.clip(low,upper)
方法結合業務場景和數據分布,進行合理的缺失值、異常值處理。
建議不要上來就做PCA或LDA降維,最好先構建特征并對特征做篩選。
① 線性組合(linear combination)
median(N1)_by(C1) 中位數
mean(N1)_by(C1) 算術平均數
mode(N1)_by(C1) 眾數
min(N1)_by(C1) 最小值
max(N1)_by(C1) 最大值
std(N1)_by(C1) 標準差
var(N1)_by(C1) 方差
freq(C2)_by(C1) 頻數
③ 統計特征+線性組合
N1 - median(N1)_by(C1)
N1 - mean(N1)_by(C1)
④ 基于樹模型創造新特征
在 Scikit-Learn 和 XGBoost 里,可以基于
apply()
以及decision_path()
等方法實現。
我們在不同類型的模型里,也會考慮不同的特征工程方法:
① 樹模型
② 依賴樣本距離的模型
文章轉自微信公眾號@算法進階