1. 程式人生 > >DataFrameMapper做特徵工程

DataFrameMapper做特徵工程

前言

在資料探勘流程中,特徵工程是極其重要的環節,我們經常要結合實際資料,對某些型別的資料做特定變換,甚至多次變換,除了一些常見的基本變換(參考我之前寫的『資料探勘比賽通用框架』)外,還有很多非主流的奇技淫巧。所以,儘管有sklearn.pipeline這樣的流水線模式,但依然滿足不了一顆愛折騰資料的心。好在,我找到了一個小眾但好用的庫——sklearn_pandas,能相對簡潔地進行特徵工程,使其變得優雅而高效。

目前這個專案還在維護,大家有什麼想法可以到 sklearn_pandas 的 github 主頁提問題,以及獲取最新的版本。

本文的pdf版本和資料集可通過關注『資料探勘機養成記』公眾號並回復我還要

獲取,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 官方文件提供的例子比較少,我看了下它的原始碼,有以下重要發現

  1. DataFrameMapper 繼承自 sklearn 的 BaseEstimator TransformerMixin ,所以 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 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.6332354],                         'salary':  [9024442732593627]})

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

分別對應三種變換,前三列和後五列是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) 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,接下來進入實戰!

鑑於不少網站私自爬取我的原創文章,我決定在文中插入二維碼以維護來源,希望不會打擾到各位閱讀

優雅高效地資料探勘——基於Python的sklearn_pandas庫

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)

結果如下

優雅高效地資料探勘——基於Python的sklearn_pandas庫

以上結果表明空值對預測結果似乎有些影響,所以我們暫且將空值看做一類新的類別:

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

優雅高效地資料探勘——基於Python的sklearn_pandas庫

log 變化的效果還是不錯的,變數的分佈相對均衡了。

3.2. 特徵工程

通過上面簡單的資料探查,我們基本確定了缺失值和長尾特徵的處理方法,其他類別變數我們可以做簡單的 One-hot編碼,整個策略如下

欄位 變換
‘Cost’