機器學習之數據預處理
5 人贊了文章
目錄
- 前言
- 1. 關於DataFrameMapper
- 2. 用DataFrameMapper做特徵工程
- 2.2. 單列變換
- 2.3. 多列變換
- 2.3.1. 多列各自用同樣的變換
- 2.3.2. 多列整體變換
- 2.4. 對付稀疏變數
- 2.5. 保留指定列
- 2.6. 自定義列變換
- 2.7. 小小的總結
- 3. 實戰
- 3.1. 數據探查
- 3.1.1. 缺失值處理
- 3.1.2. 長尾特徵
- 3.2. 特徵工程
- 3.2. 交叉驗證
- 3.3. 預測
- 4. 思考
- 5. 參考資料
前言
在數據挖掘流程中,特徵工程是極其重要的環節,我們經常要結合實際數據,對某些類型的數據做特定變換,甚至多次變換,除了一些常見的基本變換(參考我之前寫的『數據挖掘比賽通用框架』
)外,還有很多非主流的奇技淫巧。所以,儘管有sklearn.pipeline
這樣的流水線模式,但依然滿足不了一顆愛折騰數據的心。好在,我找到了一個小眾但好用的庫——sklearn_pandas,能相對簡潔地進行特徵工程,使其變得優雅而高效。
目前這個項目還在維護,大家有什麼想法可以到 sklearn_pandas 的 github 主頁提問題,以及獲取最新的版本。
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 官方文檔提供的例子比較少,我看了下它的源碼,有以下重要發現:
1.
2.DataFrameMapper
繼承自 sklearn 的BaseEstimator
和TransformerMixin
,所以DataFrameMapper
可以看做 sklearn 的TransformerMixin
類,跟 sklearn 中的其他Transformer
一樣,比如可以作為Pipeline
的輸入參數;DataFrameMapper
內部機制是先將指定的DataFrame
的列轉換成ndarray
類型,再輸入到 sklearn 的相應transformer
中;3.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 randomimport sklearnimport pandas as pdimport numpy as npimport matplotlib.pyplot as plt# frameworks for MLfrom sklearn_pandas import DataFrameMapperfrom sklearn.pipeline import make_pipelinefrom sklearn.cross_validation import cross_val_scorefrom sklearn.grid_search import GridSearchCV# transformers for category variablesfrom sklearn.preprocessing import LabelBinarizerfrom sklearn.preprocessing import MultiLabelBinarizerfrom sklearn.preprocessing import LabelEncoderfrom sklearn.preprocessing import OneHotEncoder# transformers for numerical variablesfrom sklearn.preprocessing import MinMaxScalerfrom sklearn.preprocessing import StandardScalerfrom sklearn.preprocessing import Normalizer# transformers for combined variablesfrom sklearn.decomposition import PCAfrom sklearn.preprocessing import PolynomialFeatures# user-defined transformersfrom sklearn.preprocessing import FunctionTransformer# classification modelsfrom sklearn.ensemble import RandomForestClassifierfrom sklearn.linear_model import LogisticRegression# evaluationfrom 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-D array,具體我們看以下例子:mapper = DataFrameMapper([ (pet, LabelBinarizer()), (age, MinMaxScaler()), ([age], OneHotEncoder()) ])mapper.fit_transform(testdata)
我們分別對這三列做了二值化編碼、最大最小值歸一化等,但要注意,OneHotEncoder
接受的是 2-D array的輸入,其他是 1-D array,具體請參考我之前寫的『[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)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
的精髓已悉數傳授,想必大家已摩拳擦掌躍躍欲試了吧。OK,接下來進入實戰!
3. 實戰
在進入實戰前,先結合本人前作——『新手數據挖掘的幾個常見誤區』,簡單梳理一下數據挖掘的流程:
數據集被分成訓練集、驗證集、測試集,其中訓練集驗證集進行交叉驗證,用來確定最佳超參數。在最優參數下,用整個訓練集+驗證集上進行模型訓練,最終在測試集看預測結果。
我們這裡結合一個實際的業務數據集(鏈接: https://pan.baidu.com/s/1jHOTM54 密碼: dx8t),來進行流程講解。
首先載入數據集:
df = pd.read_csv("toy_data_sample.csv", dtype = {Month: object,Day:object, Saler:object}) df.head()
數據集欄位如下:
這是一個常見的時間序列數據集,所以我們按照時間上的不同,將其劃分為訓練集(1~5月)和測試集(6月)Train = df[df.Month<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.000000Cost 0.000000Continent 0.000000Country 0.000000TreeID 0.000000Industry 0.000000Saler 0.329412Label 0.000000dtype: 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 編碼,整個策略如下:
在確定好特徵工程的策略後,我們便可以上我們的大殺器——DataFrameMapper
了,把所有的變換集成到一起。
feature_mapper = DataFrameMapper([ ([Cost], [FunctionTransformer(np.log1p), FunctionTransformer(np.round), LabelBinarizer()]), ([Cost],[Normalizer(),StandardScaler()]), ([Day],OneHotEncoder()), ([Day], FunctionTransformer(lambda x: x%7)), (Continent, LabelBinarizer()), (Country, LabelBinarizer()), (Industry, LabelBinarizer()), (Saler, [LabelBinarizer()]), (TreeID, [FunctionTransformer(lambda x: string_cut(x,0,2), validate=False), LabelBinarizer()]), (TreeID, [FunctionTransformer(lambda x: string_cut(x,2,4), validate=False), LabelBinarizer()]), ])
3.2. 交叉驗證
特徵工程完畢後,便是交叉驗證。交叉驗證最重要的目的是為了尋找最優的超參數(詳見本人前作『新手數據挖掘的幾個常見誤區』),通常我們會藉助 sklearn 中的KFold
,train_test_split
, metric.score
等來進行交叉驗證,這裡簡化起見,我們直接用 GridSearchCV
,但要注意的是,GridSearchCV
對FunctionTransformer
類的支持不好,尤其有 lambda 函數時。所以為簡化起見,我們注釋掉上面使用了 lambda 函數的FunctionTransformer
類(有興趣的同學可以嘗試拋棄GridSearchCV
,手動進行交叉驗證)。
這裡我們選用最常見的LogisticRegression
,並調整它的超參數——正則係數C
和正則方式penalty
(對此不熟悉的同學趕緊補下『邏輯回歸』的基礎知識)。同時如前面所講,我們用pipeline
把特徵工程和模型訓練都流程化,輸入到GridSearchCV
中:
pipe = make_pipeline(feature_mapper,LogisticRegression())pipe.set_params(logisticregression__C=1,logisticregression__penalty=l1 )grid = GridSearchCV(pipe, cv=3,param_grid={logisticregression__C:np.arange(0.1,2,0.3),logisticregression__penalty: [l1,l2]}, n_jobs = 4, scoring =accuracy)grid.fit(Train, Trainy)
我們定義了三折交叉驗證(cv = 3),並選用準確率(scoring = 『accuracy』)作為評估指標,運行結果如下:
print grid.best_params_ , grid.best_score_{logisticregression__penalty: l2, logisticregression__C: 0.10000000000000001} 0.752941176471
最佳超參數是取 L2 正則,並且正則係數為 0.1。
3.3. 預測
在得到模型的最優超參數後,我們還需要在訓練集+驗證集上進行特徵變換,並在最優超參數下訓練模型,然後將相應特徵變換和模型施加到測試集上,最後評估測試集結果。
而現在,這一系列流程被GridSearchCV
大大簡化,只需兩行代碼即可搞定:
predy = grid.predict(Test)scorer.accuracy_score(predy, Testy)
最後結果為0.6166666666666667
,即測試集上的分類準確率。
4. 思考
行文至此,洋洋洒洒千言,但依然只是完成了數據挖掘中最基本的流程,所做的特徵變換和選用的模型也都非常簡單,所以還有很大的提升空間。
1. 當選用的 model 不是 sklearn 中的模塊時(比如 xgboost),特徵工程還可以用 sklearn_pandas 的 DataFrameMapper, 但 sklearn 中傻瓜模式的 pipeline 就無從作用了,必須自己搭建 cross validation 流程;
2. bad case 也有分析的價值;3. 從單模型到模型的 ensemble;
5. 參考資料
- sklearn_pandas 官方文檔、源碼及 github 上的 issues
- pandas、scikit-learn 官方文檔
- 4.3. Preprocessing data
- 寒小陽的博客
補充:pandas 中 one-hot 方式
離散特徵的編碼分為兩種情況:
1. 離散特徵的取值之間沒有大小的意義 color: [red, blue],那麼就使用one-hot編碼;
2. 離散特徵的取值有大小的意義 size: [X, XL, XXL],可使用數值的映射{X:1, XL:2, XXL:3};
使用pandas可以很方便的對離散型特徵進行one-hot編碼。
import pandas as pddf = pd.DataFrame([ [green, M, 10.1, class1], [red, L, 13.5, class2], [blue, XL, 15.3, class1]]) df.columns = [color, size, prize, classLabel] size_mapping = { XL: 3, L: 2, M: 1}df[size] = df[size].map(size_mapping) class_mapping = {label:idx for idx,label in enumerate(set(df[classLabel]))}df[classLabel] = df[classLabel].map(class_mapping)
pd.get_dummies(df)
cateOneHot = pd.get_dummies(df[[classLabel]], columns=[classLabel])
推薦閱讀:
※(機器學習篇)大數據帶你精準預測消費者感興趣的商家活動
※【應用】運用Re-Encryption技術對你的IPFS網路數據進行多重保護
※2017 CCF ADL會議總結
※0基礎大數據學習的4個步驟
※數據挖掘過程中:數據預處理
TAG:深度學習DeepLearning | 數據挖掘 | 機器學習 |