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,接下来进入实战!


鉴于不少网站私自爬取我的原创文章,我决定在文中插入二维码以维护来源,希望不会打扰到各位阅读

DataFrameMapper做特征工程_第1张图片


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

结果如下

DataFrameMapper做特征工程_第2张图片

以上结果表明空值对预测结果似乎有些影响,所以我们暂且将空值看做一类新的类别:

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

DataFrameMapper做特征工程_第3张图片

log 变化的效果还是不错的,变量的分布相对均衡了。

3.2. 特征工程

通过上面简单的数据探查,我们基本确定了缺失值和长尾特征的处理方法,其他类别变量我们可以做简单的 One-hot编码,整个策略如下

字段 变换
‘Cost’ Standardization
‘Cost’ Log -> Binarization
‘TreeID’ Cut-off -> Binarization
other categorial feature Binarization

在确定好特征工程的策略后,我们便可以上我们的大杀器——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_

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

  1. 当选用的 model 不是 sklearn 中的模块时(比如 xgboost),特征工程还可以用 sklearn_pandas 的DataFrameMapper, 但 sklearn 中傻瓜模式的 pipeline 就无从作用了,必须自己搭建 crossvalidation 流程

  2. bad case 也有分析的价值

  3. 从单模型到模型的 ensemble

5. 参考资料

  1. sklearn_pandas 官方文档、源码及 github 上的 issues

  2. pandas、scikit-learn 官方文档

  3. 寒小阳的博客(http://blog.csdn.net/han_xiaoyang/article/details/49797143)

你可能感兴趣的:(机器学习)