最近又碰到了高基数类别特征的处理问题,正好也要把相关的解决方案添加到现有的线上机器学习系统里,这里总结一下以后免得又忘记了。
在特征工程里,特征编码是占比很重的一块,在kaggle的结构化数据比赛中,最终帮助选手胜利的关键因素之一往往是高级特征的构造和特征编码(很多时候特征编码也是在构造高级的特征),下面就来总结一下吧。
1、labelencoder 标签编码
如果是无序的非数值离散特征,一般直接用onehot独热编码了,有序的非数值离散特征才会用到标签编码,因为大部分算法是没有内置自动识别类别特征的功能的,所以需要做这么一步简单的转换,原理很easy了不用废话了,为了文章看起来完整才写的,使用labelencoder或者自己用字典来做映射即可。
2、onehotencoder 独热编码
针对类别特征,例如【男人,女人】,【晴天,雨天,阴天】,类别型特征,无序,最简单快捷的方式是通过独热编码转化为【0,1】或者【0,0,1】这样的形式,模型才能识别,同时也起到了扩充特征的作用(例如逻辑在特征进行onehot展开之后表达能力一般能够得到较好的提高)。sklearn的onehot,pandas的get_dummies或者自己用字典映射均可。
- 优点:独热编码解决了分类器不好处理属性数据的问题,在一定程度上也起到了扩充特征的作用。它的值只有0和1,不同的类型存储在垂直的空间。
- 缺点:1、当类别的数量很多时,特征空间会变得非常大。2、对于特定任务,例如词向量化,直接使用onehot的方式是无法考虑到词之间的交互关系的,onehot之后损失了部分信息。推而广之,如果特征之间是非独立的(比如上下文的词之间是存在交互关系,时间序列数据之间存在某些内在关系),就不能简单的使用onehot功能
3、label_binarize 二值化编码
举个例子就知道是干嘛用的了,比如特征为【晴天,雨天,阴天,雷暴】则特征转化为【是否晴天,是否雨天,是否阴天,是否雷暴】,用数字来表示【雷暴】就是[0,0,0,1],和onthot看起来很类似,很多时候不那么严格界定,其实等同于onehot,一般来说独热编码的结果是多个0和1个1组成的比如类别特征的处理,但是也存在处理之后出现多个1和多个0的情况,比如文本问题,whatever,不做严格区分,因为很多文章都不划分那么细,反正自己心里有数就行了,实现使用sklearn的label_binarize或者自己用字典来实现。
4、直方图编码与计数编码(count)
直方图编码,主要针对类别型特征与类别型标签的一种编码方式,还是举个例子来说明什么是直方图编码吧,最好理解了:
假设类别特征f1=【A,A,B,B,B,C,C】,对应的二分类标签为【0,1,0,1,1,0,0】,则我们是这样来计算类别特征f1中对应的类别的编码值的:
以A为例,类别特征f1的值为A的样本有两个,这两个样本的标签分别为【0,1】,则A被直方图编码为【1/2,1/2】=【0.5,0.5】(A的样本一共有2个所以分母为2,其中一个样本标签为1,一个样本标签为0),实际上就是计算取值为A的样本中,不同类别样本的比例,然后用这个比例来替换原始的类别标签,这里需要强调的是,无论是直方图编码还是我们后面要介绍的target encoding,本质上都是用类别特征的统计量来代替原来的类别值的,没什么神秘的地方,很好理解。
如法炮制,我们来对B进行类别编码,f1值为B的一共3个样本,其中一个样本标签为0,两个样本标签为1,所以B被编码为【1/3,2/3】,很好理解了。同样对于C,一共两个样本,并且两个样本标签均为0,则编码为【2/2,0】。
直方图编码实际上存在着比较多的问题,我们目前针对高基类特征的常用的目标编码或者均值编码实际上可以看作是在直方图编码之上的问题改进。
直方图编码存在以下问题:
1、没有考虑到类别特征中不同类别的数量的影响,举个例子,假设样本的某个类别特征为【A,A,A,A,A,A,B】,对应的标签为【0,0,0,1,1,1,0】,则根据直方图编码的公式得到的结果为A:【1/2,1/2】,B:【1,0】,然而这实际上对于A来说是很不公平的,因为B的样本数量太少,计算出来的结果根本不能算是明显的统计特征,而很可能是一种噪音,这实际上是一种非常“过拟合”的计算方式,因为一旦测试集中的样本有多个B之后,B的直方图编码的结果很可能发生非常大的变化;
2、假设没有1中出现的情况,所有的类别A,B的数量都比较均匀,直方图编码还是存在着一个潜在的隐患,直方图编码的计算非常依赖于训练集中的样本标签的分布情况,以f1特征的那个例子为例,实际上直方图这么计算的隐含的假设是潜在的所有的数据的在类别f1上的每一个类别计算出来的结果可以用训练集的结果来近似代替,简单说比如我在训练集中算出来A的直方图编码为【1/2,1/2】,即类别为A的样本中有一半标签0的样本,一半标签1的样本,那么一旦测试集的分布情况发生改变,或者是训练集本身的采样过程就是有偏的,则直方图编码的结果就是完全错误的,(比如全样本中,类别为A的样本其实只有10%是标签为0的,90%标签为1的,则这个时候A的直方图编码为【1/10,9/10】,训练集的产生可能是有偏的);
所以在可用的资料和kaggle比赛中很少有人会用到直方图编码,更多的使用target encoding和mean encoding。下面是简单的直方图编码的实现,因为不怎么用就懒得优化了。
def histogram_encoding(X,y):
category=list(set(X))
labels=list(set(y))
data=pd.concat([X,pd.DataFrame(y)],axis=1)
data.columns=['data','labels']
dictionary={}
for item in category:
temp=data[data['data']==item]
tp=temp['labels'].value_counts()
if tp.shape[0]<len(labels):
for label in labels:
if label not in tp.index:
tp[label]=0
nums=tp.tolist()
sums=sum(nums)
nums=[items*1.0/sums for items in nums] ### 这里sums如果-1就是one leave out的分类问题形式
##其实问题差别不是很大,数据量一般都是至少几十万的级别的这么一个数据点的删除与否没什么大影响
dictionary[item]=nums
hs_enc=X.copy()
hs_enc=hs_enc.values.tolist()
for i in range(len(hs_enc)):
hs_enc[i]=dictionary[hs_enc[i]]
return hs_enc,dictionary
而计数编码就更加简单了,以二分类问题为例,就是根据每一个类别特征的类别对二分类标签进行sum求和得到每个类别中样本标签为1的总数,一行搞定,不知道为什么这么简单的编码方式在比赛中效果这么好。。。:
df.groupby(['category'])['target'].transform(sum)
5、WOE编码
实际上这里细心一点就可以发现,woe编码仅仅针对于二分类问题,woe编码如下:
原理很简单就是根据woe的公式来计算即可。实际上woe编码的方法很容易就可以扩展到多类,后面会写。
单纯从woe的公式就可以看出woe编码存在的问题:
1、分母可能为0的问题;
2、类似于直方图编码,没有考虑到不同类别数量的大小,例如类别特征为【A,A,A,A,A,A,B】而标签为【0,0,0,1,1,1,1】这样的情况计算出来的woe明显对A这个类别不公平
3、应用局限性太大了,只能针对二分类问题,并且特征也必须为离散特征。
4、训练集计算的woe编码结果可能和测试集计算的woe编码结果存在较大差异(所有基于统计特征的编码方式的通病)
首先我们调个包,使用到的是注明scikit-learn contrib分支中的category_encoders:
from category_encoders import *
import pandas as