前言
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.2. 特徵工程
3.2. 交叉驗證
3.3. 預測
4. 思考
5. 參考資料
打賞
Bonus
前言在數據挖掘流程中,特徵工程是極其重要的環節,我們經常要結合實際數據,對某些類型的數據做特定變換,甚至多次變換,除了一些常見的基本變換(參考我之前寫的『數據挖掘比賽通用框架』)外,還有很多非主流的奇技淫巧。所以,儘管有sklearn.pipeline這樣的流水線模式,但依然滿足不了一顆愛折騰數據的心。好在,我找到了一個小眾但好用的庫——sklearn_pandas,能相對簡潔地進行特徵工程,使其變得優雅而高效。
目前這個項目還在維護,大家有什麼想法可以到 sklearn_pandas 的 github 主頁提問題,以及獲取最新的版本。
本文的pdf版本和數據集可通過關注『數據挖掘機養成記』公眾號並回復我還要獲取,pdf版本排版比公眾號給力
1. 關於DataFrameMappersklearn_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-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]])
除了上面的單列變換,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 作用到所有剩餘列上
不難發現,上面我們利用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 相關函數的了解,我總結了以下對比表格:
DataFrameMappersklearn 、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,接下來進入實戰!
鑑於不少網站私自爬取我的原創文章,我決定在文中插入二維碼以維護來源,希望不會打擾到各位閱讀
在進入實戰前,先結合本人前作——『新手數據挖掘的幾個常見誤區』,簡單梳理一下數據挖掘的流程:
數據集被分成訓練集、驗證集、測試集,其中訓練集驗證集進行交叉驗證,用來確定最佳超參數。在最優參數下,用整個訓練集+驗證集上進行模型訓練,最終在測試集看預測結果
我們這裡結合一個實際的業務數據集(獲取方式:關注公眾號『數據挖掘機養成記』回復我還要即可),來進行流程講解。首先加載數據集
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.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』Standardization『Cost』Log -> Binarization『TreeID』Cut-off -> Binarizationother categorial featureBinarization在確定好特徵工程的策略後,我們便可以上我們的大殺器——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_
Out:{'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. 思考行文至此,洋洋灑灑千言,但依然只是完成了數據挖掘中最基本的流程,所做的特徵變換和選用的模型也都非常簡單,所以還有很大的提升空間。
此處以下留兩個點,可以動手實踐,也歡迎在群裡探討(群二維碼見第6節『Bonus』)
當選用的 model 不是 sklearn 中的模塊時(比如 xgboost),特徵工程還可以用 sklearn_pandas 的 DataFrameMapper, 但 sklearn 中傻瓜模式的 pipeline 就無從作用了,必須自己搭建 cross validation 流程
bad case 也有分析的價值
從單模型到模型的 ensemble
5. 參考資料sklearn_pandas 官方文檔、源碼及 github 上的 issues
pandas、scikit-learn 官方文檔
寒小陽的博客(http://blog.csdn.net/han_xiaoyang/article/details/49797143)
打賞寫公眾號文章耗費大量腦細胞和時間
歡迎打賞一枚高貴的茶葉蛋,感謝~
一個人寫作難免有紕漏,所以建了個微信群,希望大家能多提批評建議,多提問題,深入交流,共同進步。這次我將把本文數據集和代碼放到群裡
本群宗旨
討論交流(但不限於)本公眾號文章所涉及的技術問題
分享你讀過之後有收穫的文章,或者你的優質原創
群二維碼(9月6日前有效):