DataFrameMapper做特徵工程
前言
在資料探勘流程中,特徵工程是極其重要的環節,我們經常要結合實際資料,對某些型別的資料做特定變換,甚至多次變換,除了一些常見的基本變換(參考我之前寫的『資料探勘比賽通用框架』
)外,還有很多非主流的奇技淫巧。所以,儘管有sklearn.pipeline
這樣的流水線模式,但依然滿足不了一顆愛折騰資料的心。好在,我找到了一個小眾但好用的庫——sklearn_pandas,能相對簡潔地進行特徵工程,使其變得優雅而高效。
目前這個專案還在維護,大家有什麼想法可以到 sklearn_pandas 的 github 主頁提問題,以及獲取最新的版本。
本文的pdf版本和資料集可通過關注『資料探勘機養成記』公眾號並回復我還要
1. 關於DataFrameMapper
sklearn_pandas 起初是為了解決這樣一個問題:在 sklearn 的舊版本中,很多常見模組(特徵變換器、分類器等)對pandas 的DataFrame
型別不支援,必須先用DataFrame
自帶的.values
、.as_matrix
之類的方法,將DataFrame
型別轉換成numpy
的ndarray
型別,再輸入到sklearn
的模組中,這個過程略麻煩。因此 sklearn_pandas提供了一個方便的轉換介面,省去自己轉換資料的過程。
但當我花了幾天時間探索了 sklearn_pandas 的庫及其跟 pandas、sklearn 相應模組的聯絡後,我發現sklearn 0.16.0 向後的版本對 DataFrame的相容性越來越好,經我實際測試,現在最新的 0.17.1 版本中,model、preprocessing等模組的大部分函式已完全支援 DataFrame 型別的輸入,所以我認為:
sklearn_pandas 的重點不再是資料型別轉換,而是通過其自創的
DataFrameMapper
類,更簡潔地、把 sklearn 的transformer
靈活地運用在DataFrame
當中,甚至可以發揮你的聰明才智,將幾乎大部分特徵變換在幾行程式碼內完成,而且一目瞭然。
sklearn_pandas 官方文件提供的例子比較少,我看了下它的原始碼,有以下重要發現
DataFrameMapper
繼承自 sklearn 的BaseEstimator
和TransformerMixin
,所以DataFrameMapper
可以看做 sklearn 的TransformerMixin
類,跟 sklearn 中的其他Transformer
一樣,比如可以作為Pipeline
的輸入引數
DataFrameMapper
內部機制是先將指定的DataFrame
的列轉換成ndarray
型別,再輸入到 sklearn 的相應transformer
中
DataFrameMapper
接受的變換型別是 sklearn 的transformer
類,因而除了 sklearn 中常見的變換 (標準化、正規化、二值化等等)還可以用sklearn 的FunctionTransformer
來進行自定義操作
本文先介紹下如何用DataFrameMapper
型別進行特徵工程,再將skleanr_pandas、sklearn、pandas
這三個庫結合,應用到一個具體的資料探勘案例中。
2. 用DataFrameMapper
做特徵工程
[注意]在正式進入本節前,建議先閱讀本人之前寫的『[scikit-learn]特徵二值化編碼函式的一些坑』,瞭解sklearn 和 pandas 常見的二值化編碼函式的特性和一些注意點。
若輸入資料的一行是一個樣本,一列是一個特徵,那簡單的理解,『特徵工程』就是列變換。本節將講解如何用DataFrameMapper
結合sklearn
的Transformer
類,來進行列變換
首先import
本文將會用到的所有類(預設已裝好scikit-learn,
pandas, sklearn_pandas 等庫)
import random import sklearn import pandas as pd import numpy as np import matplotlib.pyplot as plt
# frameworks for ML from sklearn_pandas import DataFrameMapper from sklearn.pipeline import make_pipeline from sklearn.cross_validation import cross_val_score from sklearn.grid_search import GridSearchCV
# transformers for category variables from sklearn.preprocessing import LabelBinarizer from sklearn.preprocessing import MultiLabelBinarizer from sklearn.preprocessing import LabelEncoder from sklearn.preprocessing import OneHotEncoder
# transformers for numerical variables from sklearn.preprocessing import MinMaxScaler from sklearn.preprocessing import StandardScaler from sklearn.preprocessing import Normalizer
# transformers for combined variables from sklearn.decomposition import PCA from sklearn.preprocessing import PolynomialFeatures
# user-defined transformers from sklearn.preprocessing import FunctionTransformer
# classification models from sklearn.ensemble import RandomForestClassifier from sklearn.linear_model import LogisticRegression
# evaluation from sklearn.metrics import scorer
我們以如下的資料為例
testdata = pd.DataFrame({'pet': ['cat', 'dog', 'dog', 'fish', 'cat', 'dog', 'cat', 'fish'], 'age': [4., 6, 3, 3, 2, 3, 5, 4], 'salary': [90, 24, 44, 27, 32, 59, 36, 27]})
2.2. 單列變換
『單列』可以是 1-D array,也可以是 2-D array,為了迎合不同的 transformer,但最終輸出都是 2-Darray,具體我們看以下例子
mapper = DataFrameMapper([ ('pet', LabelBinarizer()), ('age', MinMaxScaler()), (['age'], OneHotEncoder()) ]) mapper.fit_transform(testdata)
我們分別對這三列做了二值化編碼、最大最小值歸一化等,但要注意,OneHotEncoder
接受的是2-D
array的輸入,其他是 1-Darray,具體請參考我之前寫的『[scikit-learn]特徵二值化編碼函式的一些坑』。上面程式碼的執行結果如下
array([[ 1. , 0. , 0. , 0.5 , 0. , 0. , 1. , 0. , 0. ], [ 0. , 1. , 0. , 1. , 0. , 0. , 0. , 0. , 1. ], [ 0. , 1. , 0. , 0.25, 0. , 1. , 0. , 0. , 0. ], [ 0. , 0. , 1. , 0.25, 0. , 1. , 0. , 0. , 0. ], [ 1. , 0. , 0. , 0. , 1. , 0. , 0. , 0. , 0. ], [ 0. , 1. , 0. , 0.25, 0. , 1. , 0. , 0. , 0. ], [ 1. , 0. , 0. , 0.75, 0. , 0. , 0. , 1. , 0. ], [ 0. , 0. , 1. , 0.5 , 0. , 0. , 1. , 0. , 0. ]])
分別對應三種變換,前三列和後五列是pet
和age
的二值化編碼,第四列是age
的最大最小值歸一化。
同樣,我們也可以將這些變換『級聯』起來(類似 sklearn 裡的pipeline
):
mapper = DataFrameMapper([ (['age'],[ MinMaxScaler(), StandardScaler()]), ]) mapper.fit_transform(testdata)
將age
列先最大最小值歸一化,再標準化,輸出結果:
array([[0.20851441],
[ 1.87662973],
[-0.62554324],
[-0.62554324],
[-1.4596009 ],
[-0.62554324],
[ 1.04257207],
[ 0.20851441]])
2.3. 多列變換
除了上面的單列變換,DataFrameMapper
也能處理多列
2.3.1. 多列各自用同樣的變換
有時候我們要對很多列做同樣操作,比如二值化編碼、標準化歸一化等,也可以藉助於DataFrameMapper
,使得執行更高效、程式碼更簡潔。
mapper = DataFrameMapper([ (['salary','age'], MinMaxScaler()) ]) mapper.fit_transform(testdata)
這裡同時對age
和salary
進行歸一化,結果如下
array([[ 1. , 0.5 ], [ 0. , 1. ], [ 0.3030303 , 0.25 ], [ 0.04545455, 0.25 ], [ 0.12121212, 0. ], [ 0.53030303, 0.25 ], [ 0.18181818, 0.75 ], [ 0.04545455, 0.5 ]])
同樣,這些變換也可以級聯
mapper = DataFrameMapper([ (['salary','age'], [MinMaxScaler(),StandardScaler()]) ]) mapper.fit_transform(testdata) array([[ 2.27500192, 0.20851441], [-0.87775665, 1.87662973], [ 0.07762474, -0.62554324], [-0.73444944, -0.62554324], [-0.49560409, -1.4596009 ], [ 0.79416078, -0.62554324], [-0.30452782, 1.04257207], [-0.73444944, 0.20851441]])
2.3.2. 多列整體變換
多列變換時,除了分別對每列變換,我們有時還需要對某些列進行整體變換,比如 降維(PCA, LDA) 和特徵交叉等,也可以很便捷地藉助DataFrameMapper
實現
mapper = DataFrameMapper([ (['salary','age'], [MinMaxScaler(), PCA(2)]), (['salary','age'],[MinMaxScaler(), PolynomialFeatures(2)]) ]) mapper.fit_transform(testdata) array([[-0.57202956, -0.4442768 , 1. , 1. , 0.5 , 1. , 0.5 , 0.25 ], [ 0.53920967, -0.32120213, 1. , 0. , 1. , 0. , 0. , 1. ], [-0.12248009, 0.14408706, 1. , 0.3030303 , 0.25 , 0.09182736, 0.07575758, 0.0625 ], [ 0.09382212, 0.28393922, 1. , 0.04545455, 0.25 , 0.00206612, 0.01136364, 0.0625 ], [-0.10553503, 0.45274661, 1. , 0.12121212, 0. , 0.01469238, 0. , 0. ], [-0.31333498, 0.0206881 , 1. , 0.53030303, 0.25 , 0.2812213 , 0.13257576, 0.0625 ], [ 0.2507869 , -0.20998092, 1. , 0.18181818, 0.75 , 0.03305785, 0.13636364, 0.5625 ], [ 0.22956098, 0.07399884, 1. , 0.04545455, 0.5 , 0.00206612, 0.02272727, 0.25 ]])
以上我們對age
和salary
列分別進行了PCA
和生成二次項特徵
2.4. 對付稀疏變數
(寫完此文後發現該功能並不是很work)
sklearn 中OneHotEncoder
類和某些處理文字變數的類(比如CountVectorizer
)的預設輸出是sparse
型別,而其他很多函式輸出是普通的
ndarray,這就導致資料拼接時可能出錯。為了統一輸出,DataFrameMapper
提供sparse
引數來設定輸出稀疏與否,預設是False
。
2.5. 保留指定列
(穩定版 1.1.0 中沒有此功能,development 版本中有 )
從上面的實驗中我們可以看到,對於我們指定的列,DataFrameMapper
將忠誠地執行變換,對於未指定的列,則被拋棄。
而真實場景中,對於未指定的列,我們可能也需要做相應處理,所以DataFrameMapper
提供default
引數用於處理這類列:False
:
全部丟棄(預設)None
:
原封不動地保留other
transformer
: 將 transformer 作用到所有剩餘列上
2.6. 自定義列變換
不難發現,上面我們利用DataFrameMapper
所做的列變換,大多是呼叫sklearn
中現有的模組(OneHotEncoder
,MinMaxEncoder
,PCA
等),那如果遇到一些需要自己定義的變換,該怎麼做呢?比如常見的對長尾特徵做log(x+1)
之類的變換?
對 sklearn 熟悉的同學開動一下腦筋,答案馬上就有了——那就是FunctionTransformer
,該函式的具體引數細節可參考sklearn
的官方文件,這裡簡單給個例子
mapper = DataFrameMapper([ (['salary','age'], FunctionTransformer(np.log1p)) ]) mapper.fit_transform(testdata) Out[32]: array([[ 4.51085951, 1.60943791], [ 3.21887582, 1.94591015], [ 3.80666249, 1.38629436], [ 3.33220451, 1.38629436], [3.49650756, 1.09861229], [ 4.09434456, 1.38629436], [ 3.61091791, 1.79175947], [ 3.33220451, 1.60943791]])
以上我們將 numpy 中的函式log1p
(作用等同於log(x+1)
)通過FunctionTransformer
包裹成一個sklearn
的transformer
類,就能直接作用在不同列上啦。
動手能力強的同學還可以自己定義函式,提示一下,用 numpy 的ufunc
,這裡就不贅述了,留給大家探索吧。
2.7. 小小的總結
基於以上內容,以及我對 sklearn、pandas 相關函式的瞭解,我總結了以下對比表格:
DataFrameMapper | sklearn 、pandas |
---|---|
對列的變換比較靈活,可篩選出一個或多個列,並用一個或多個 sklearn 的 transformer 作用,組合起來極其強大;同時通過繼承機制,它本身也可以看做是 sklearn 的 transformer 類,輸入 sklearn 的相關類(如Pipeline, FeatureUnion) | pandas.get_dummies只能簡單地對一列或多列進行二值化變換,去掉某些列時得用 drop;sklearn.pipeline裡的featureUnion只能對整個 DataFrame 做不同變換並簡單拼接 |
返回的是ndarray型別,每列feature沒有名字(不過,可以通過自己新增 column 的名字生成新的 DataFrame) | pandas.get_dummies可以給新生成的變數取名;對於dict型別的樣本,sklearn的DictVectorizer有get_feature_name方法獲取變換後的變數名 |
至此,DataFrameMapper
的精髓已悉數傳授,想必大家已摩拳擦掌躍躍欲試了吧。OK,接下來進入實戰!
鑑於不少網站私自爬取我的原創文章,我決定在文中插入二維碼以維護來源,希望不會打擾到各位閱讀
3. 實戰
在進入實戰前,先結合本人前作——『新手資料探勘的幾個常見誤區』,簡單梳理一下資料探勘的流程:
資料集被分成訓練集、驗證集、測試集,其中訓練集驗證集進行交叉驗證,用來確定最佳超引數。在最優引數下,用整個訓練集+驗證集上進行模型訓練,最終在測試集看預測結果
我們這裡結合一個實際的業務資料集(獲取方式:關注公眾號『資料探勘機養成記』回覆我還要
即可),來進行流程講解。首先載入資料集
df = pd.read_csv("toy_data_sample.csv", dtype = {'Month': object,'Day':object, 'Saler':object}) df.head()
資料集欄位如下
這是一個常見的時間序列資料集,所以我們按照時間上的不同,將其劃分為訓練集(1~5月)和測試集(6月)
Train = df[df.Month<<span class="" style="margin: 0px; padding: 0px; max-width: 100%; box-sizing: border-box !important; word-wrap: break-word !important; color: rgb(221, 17, 68);">'06'
][df.columns.drop('Month')] Test = df.ix[df.index.difference(Train.index), df.columns.drop(['Month'])] Trainy = Train.ix[:, -1]; Testy = Test.ix[:, -1]
3.1. 資料探查
3.1.1. 缺失值處理
常見的缺失值處理手段有
填充
丟棄
看做新類別
我們先簡單統計一下每個欄位的空值率
Train.count().apply(lambda x: float(Train.shape[0]-x)/Train.shape[0]) Out[5]: Day 0.000000
Cost 0.000000
Continent 0.000000
Country 0.000000
TreeID 0.000000
Industry 0.000000
Saler 0.329412
Label 0.000000
dtype: float64
這組資料比較理想,只有Saler
欄位是缺失的,所以我們只需要看下Saler
和目標變數之間的關係
tmp = pd.DataFrame({'null': Train.Label[Train.Saler.isnull()].value_counts(), 'not_null': Train.Label[Train.Saler.notnull()].value_counts()}) tmp = tmp.apply(lambda x: x/sum(x)) tmp.T.plot.bar(stacked = True)
結果如下
以上結果表明空值對預測結果似乎有些影響,所以我們暫且將空值看做一類新的類別:
Train['Saler'] = Train.Saler.apply(lambda x: "NaN" if pd.isnull(x) else x) Test['Saler'] = Test.Saler.apply(lambda x: "NaN" if pd.isnull(x) else x)
3.1.2. 長尾特徵
長尾分佈也是一種很常見的分佈形態,常見於數值型別的變數,最簡單的方法是用log(x+1)
處理。在我們的資料集當中,Cost
這個欄位便是數值型別,我們看下它的分佈:
plt.figure(1) Train.Cost.apply(lambda x: x/10).hist() plt.figure(2) Train.Cost.apply(lambda x: np.log(x+1).round()).hist()
log 變化的效果還是不錯的,變數的分佈相對均衡了。
3.2. 特徵工程
通過上面簡單的資料探查,我們基本確定了缺失值和長尾特徵的處理方法,其他類別變數我們可以做簡單的 One-hot編碼,整個策略如下
欄位 | 變換 |
---|---|
‘Cost’ |