機器學習之數據預處理

機器學習之數據預處理

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. DataFrameMapper 繼承自 sklearn 的BaseEstimatorTransformerMixin ,所以 DataFrameMapper 可以看做 sklearn 的TransformerMixin 類,跟 sklearn 中的其他 Transformer 一樣,比如可以作為Pipeline 的輸入參數;

2. 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. ]])

分別對應三種變換,前三列和後五列是petage的二值化編碼,第四列是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)

這裡同時對agesalary進行歸一化,結果如下:

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 ]])

以上我們對agesalary列分別進行了 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. 實戰

在進入實戰前,先結合本人前作——『新手數據挖掘的幾個常見誤區』,簡單梳理一下數據挖掘的流程:

數據集被分成訓練集、驗證集、測試集,其中訓練集驗證集進行交叉驗證,用來確定最佳超參數。在最優參數下,用整個訓練集+驗證集上進行模型訓練,最終在測試集看預測結果。

我們這裡結合一個實際的業務數據集(鏈接: pan.baidu.com/s/1jHOTM5 密碼: 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 中的KFoldtrain_test_splitmetric.score等來進行交叉驗證,這裡簡化起見,我們直接用 GridSearchCV,但要注意的是,GridSearchCVFunctionTransformer類的支持不好,尤其有 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. 參考資料

  1. sklearn_pandas 官方文檔、源碼及 github 上的 issues
  2. pandas、scikit-learn 官方文檔
  3. 4.3. Preprocessing data
  4. 寒小陽的博客

補充: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 | 數據挖掘 | 機器學習 |