前言
关于特征工程,已经对空值、数值型和文本数据的处理做了大致方法的说明,这篇对数据类型中的另一大重要部分——分类变量,作处理方法总结。
声明:关于编程语法相关问题不会展开论述,本文只针对方法路线
分类变量的编码
先说明什么样的数据被称为分类变量:分类变量是用来表示类别或标记的,例如西瓜书(周志华的,应该都知道的吧)中西瓜数据集里的色泽(青绿,乌黑,浅白)、纹理(清晰,稍糊)等。更加不好处理的情况是,这些分类变量与数值变量掺杂在一起作为整个数据集,如下图(仅截取一部分):
但是,在实际的数据集中,类别的数量总是有限的,为了参与数学算法运算(特别是回归),一定要将分类变量转成数值型,可以用数字表示,但与数值型变量不同,分类变量不能被排序(尽最大可能要避免有顺序)。
1、one-hot 编码
one-hot使用一组比特位,每个比特位表示一种可能的类别。每一个比特位表示一个特征,因此,一个可能有k个类别的分类器就可以编码为一个长度为K的特征向量。
**举例:**使用原数据集中(自己建立)的一个分类变量的列作为例子,该列原始数据如下:
df = pd.DataFrame({"City":["SF","SF","SF","NYC","NYC","NYC","Seattle","Seattle","Seattle"],
"Rent":[3999,300,3333,2222,4444,888,88888,44443,3222]})
City Rent
0 SF 3999
1 SF 300
2 SF 3333
3 NYC 2222
4 NYC 4444
5 NYC 888
6 Seattle 88888
7 Seattle 44443
8 Seattle 3222
可以看到,该数据集有两个属性,一个为分类变量,一个为数值型变量。使用pandas中的get_dummies(pd,col)一键one-hot。
代码如下:
ont_hot_df = pd.get_dummies(df,["City"])
一键one-hot后,原数据集就变成了:
Rent City_NYC City_SF City_Seattle
0 3999 0 1 0
1 300 0 1 0
2 3333 0 1 0
3 2222 1 0 0
4 4444 1 0 0
5 888 1 0 0
6 88888 0 0 1
7 44443 0 0 1
8 3222 0 0 1
仅在该属性有值的位置取1,其余为0。
one-hot缺点:
- 分类较多时,会造成冗余,使得同一个问题有多个有效模型;
- 分类较多时,不可避免地会造成维数灾难;
- List item很明显,对于one-hot编码,各行的和一定等于1,即存在线性关系,线性相关的特征会使训练出的模型不唯一。特征的不同线性组合可以做出同样的预测,所以需要做些额外的努力才能理解某个特征对预测结果的作用。
2、虚拟编码
如果一个样本中有k个分类,one-hot编码会产生k个自由度,但是变量本身只需要k-1个自由度,虚拟编码在进行表示时只使用k-1 个特征,除去了额外的自由度。
这么说有点绕,不好理解,下面举个例子:
e1 e2
San Francisco 1 0
New York 0 1
Seattle 0 0
上面有三个分类,使用one-hot编码应该有3(k)列(自由度),但是虚拟编码将其中的一类通过一个全零向量表示,它被称为参照类(上例中的“Seattle”)。
代码:
dummy_df = pd.get_dummies(df, prefix=['city'], drop_first=True)
结果(如果看不明白虚拟编码和one-hot编码的话,可以用下表与上面one-hot后的表作下对比):
Rent city_SF city_Seattle
0 3999 1.0 0.0
1 4000 1.0 0.0
2 4001 1.0 0.0
3 3499 0.0 0.0
4 3500 0.0 0.0
5 3501 0.0 0.0
6 2499 0.0 1.0
7 2500 0.0 1.0
8 2501 0.0 1.0
使用虚拟编码的好处: 使用虚拟编码的模型结果比使用one-hot 编码的模型结果更具解释性**,没有冗余。(具体实际效果只能使用交叉验证来选择则编码方式)
缺点: 不直观,全零向量不太容易处理缺失数据。
3、效果编码
效果编码与虚拟编码非常相似,简单地说,区别就在于参照类由全零向量改成了全-1向量,如下:
e1 e2
San Francisco 1 0
New York 0 1
Seattle -1 -1
效果编码需要在虚拟编码的基础上,将参照类的全零向量改成全-1向量:
代码:
ffect_df = dummy_df.copy() #dummy_df为上面虚拟编码后的数据集
effect_df.ix[3:5, ['city_SF', 'city_Seattle']] = -1.0
效果如下(注意对比):
Rent city_SF city_Seattle
0 3999 1.0 0.0
1 4000 1.0 0.0
2 4001 1.0 0.0
3 3499 -1.0 -1.0
4 3500 -1.0 -1.0
5 3501 -1.0 -1.0
6 2499 0.0 1.0
7 2500 0.0 1.0
8 2501 0.0 1.0
效果编码使用另外一种编码表示参照类,从而避免了虚拟编码存在的问题,但是全由-1 组成的向量是个密集向量,计算和存储的成本都比较高。正是因为这个原因,像Pandas 和scikit-learn这样的常用机器学习软件包更喜欢使用虚拟编码或one-hot 编码,而不是效果编码。
处理大型分类变量
当类别的数量变得非常大时,上面介绍的3种编码方式都会出现问题,所以需要另外的策略来处理超大型分类变量。
处理大型分类变量的目标:既要内存高效,又能生成精确的、训练速度很快的模型。
现有的解决方案可以分类如下。
(1) 不在编码问题上搞什么花样,使用一个简单、容易训练的模型,在很多机器上使用one-hot 编码训练线性模型(逻辑回归或线性支持向量机)。
(2) 压缩特征,有两种方式。
a) 特征散列化,通常用于线性模型。
b) 分箱计数,常用于线性模型和树模型。
one-hot编码上面已经说完了,下面谁基于特征的处理方法。
1、特征散列化
先放一张图:
图中的散列函数 (有点类似于哈希化)是一种确定性函数,他可以将一个可能无界的整数映射到一个有限的整数范围[1,m]中。
举例: 图中的每一个单词可以看成一个分类,散列函数经过算法,将每一分类映射成一个散列值 。
因为输入域可能大于输出范围,所以可能有多个值被映射为同样的输出,这称为碰撞。均匀散列函数可以确保将大致相同数量的数值映射到m 个分箱中。
代码:
from sklearn.feature_extraction import FeatureHasher
h = FeatureHasher(n_features=m, input_type='string')
举例:
from sklearn.feature_extraction import FeatureHasher
h = FeatureHasher(n_features = 3,input_type='string')
f = h.transform(df["City"])
print(f.toarray())
结果:
[[-1. 0. -1.]
[-1. 0. -1.]
[-1. 0. -1.]
[-2. 0. -1.]
[-2. 0. -1.]
[-2. 0. -1.]
[-3. 2. 2.]
[-3. 2. 2.]
[-3. 2. 2.]]
优点:特征散列化对计算能力大有裨益
缺点:牺牲了直观的用户可解释性
2、分箱计数
思想:分箱计数不使用分类变量的值作为特征,而是使用目标变量取这个值的概率。换个说法,不对分类变量的值进行编码,而是计算分类变量值与要预测的目标变量之间的相关统计量。
假设有10000个分类变量,one-hot 编码会生成一个长度为10 000 的稀疏向量,只在对应当前数据点的列上有一个1。分箱计数会将所有10 000 个二值列编码为一个单独的特征,是0和1 之间的一个实数值。简而言之,分箱计数将一个分类变量转换为与其值相关的统计量,它可以将一个大型的、稀疏的、二值的分类变量表示(如one-hot 编码生成的结果)转换为一个小巧的、密集的、实数型的数值表示。
举例:
import pandas as pd
# 使用这个超过6GB的数据集的前10 000行作为训练集
df = pd.read_csv('data/train_subset.csv')
# 看看训练集中有多少个唯一的特征
len(df['device_id'].unique())
7201
# 对每个类别,我们要计算:
# Theta = [counts, p(click), p(no click), p(click)/p(no click)]
def click_counting(x, bin_column):
clicks = pd.Series(x[x['click'] > 0][bin_column].value_counts(),name='clicks')
no_clicks = pd.Series(x[x['click'] < 1][bin_column].value_counts(),name='no_clicks')
counts = pd.DataFrame([clicks,no_clicks]).T.fillna('0')
counts['total_clicks'] = counts['clicks'].astype('int64') +counts['no_clicks'].astype('int64')
return counts
def bin_counting(counts):
counts['N+'] = counts['clicks'].astype('int64').divide(counts['total_clicks'].astype('int64'))
counts['N-'] = counts['no_clicks'].astype('int64').divide(counts['total_clicks'].astype('int64'))
counts['log_N+'] = counts['N+'].divide(counts['N-'])
# 如果只想返回分箱属性就进行过滤
bin_counts = counts.filter(items= ['N+', 'N-', 'log_N+'])
return counts, bin_counts
# 分箱计数示例:device_id
bin_column = 'device_id'
device_clicks = click_counting(df.filter(items=[bin_column, 'click']),bin_column)
device_all, device_bin_counts = bin_counting(device_clicks)
# 检查一下,确定我们处理了所有设备
print(len(device_bin_counts))
7201
print(device_all.sort_values(by = 'total_clicks', ascending=False).head(4))
#结果
clicks no_clicks total N+ N- log_N+
a99f214a 15729 71206 86935 0.180928 0.819072 0.220894
c357dbff 33 134 167 0.197605 0.802395 0.246269
31da1bd0 0 62 62 0.000000 1.000000 0.000000
936e92fb 5 54 59 0.084746 0.915254 0.092593
后面还有关于如何处理稀有类、防止数据泄露、无界计数等情况的处理方法,以后如果遇到了再详细的说吧!
总结
对分类变量说了这么方法,下面总结下各自的优缺点:
one-hot编码
|优点: 容易实现、可能是最精确的、可用于在线学习
|缺点: 计算效率不高、不能适应可增长的类别、只适用于线性模型、对于大数据集,需要大规模的分布式优化
特征散列化
|优点: • 容易实现
• 模型训练成本更低
• 容易适应新类别
• 容易处理稀有类
• 可用于在线学习
|缺点 :• 只适合线性模型或核方法
• 散列后的特征无法解释
• 精确度难以保证
分箱计数
优点: • 训练阶段的计算负担最小
• 可用于基于树的模型
• 比较容易适应新类别
• 可使用 back-off 方法或最小计数图处理稀有类
• 可解释
缺点: • 需要历史数据
• 需要延迟更新,不完全