类别型数据的预处理方法

时间:2024-10-05 10:56:39

在机器学习过程中,我们往往需要对数据进行预处理。根据数据的取值情况,我们可以把数据总体分为两大类:类别型数据和数值型数据。

对于数值性变量,通常不需要对其进行编码处理。而如何处理类别型数据是一个难题。处理的方法有很多种,各有优缺并且各有适用的范围。

本文在收集相关资料后,试图对目前较为常见的处理方式做一个归纳总结。

 

1. 为什么需要编码

机器学习相关算法对各数据进行衡量时,依赖的大多是其数值关系,如决策树ID3算法依赖于香农熵的计算,线性回归模型依赖于欧式距离的计算。

数值型变量自然是可以直接被算法处理,而算法往往无法直接对分类型变量进行处理,因此在把数据应用于算法训练前,需要对其进行调整,把分类型变量转换成为数值。

 

2. 主要编码方式

2.1 Replace 直接替换

有些时候,在我们要进行特征处理的变量中,并不是所有的取值都需要进行特征处理,只需要对其中部分不符合要求的取值进行替换。

举例如下:

假设 test_df 包含的是8名同学在某次测验中的数学成绩,成绩记录的标准为:

  • 成绩低于60分,记为F

  • 成绩在[60, 70)之间,记为C

  • 成绩在[70, 80)之间,记为B

  • 成绩在[80, 100]之间,记为A

在记录的过程中,由于疏忽,一名同学的成绩被错记为其真实的数值成绩。

所以,在本数据集中需要对其进行特征编码,将该数值根据成绩记录的标准进行替换,以使得最终 math 变量的取值标准统一。

在本案例中,则需要将 93 替换为 A。可以考虑直接使用 Replace() 方法进行处理。

代码如下:

  1. test_dict = {'id': [1, 2, 3, 4, 5, 6, 7, 8], # 构建测试数据``
  2. 'math':['F', 'B', 'B', 'F', 93, 'A', 'C', 'C']}
  3. test_df = (test_dict)
  4. # 将math一列中的93替换成A
  5. test_df.replace({'math': {93: 'A'}}, inplace=True)
  6. test_df

显示结果如下:

2.2 map() 替换

除了使用 replace() 方法,也可以应用内置的 map() 方法进行直接替换处理。

map() 可以对 Dataframe 中某列内的元素进行操作,在此情景下的作用是:根据映射关系,为指定列匹配结果并进行替换。其处理原理与使用 replace() 进行直接替换的方式一致,需要自行指定各类别取值应该如何转换为数值。

但其特别之处在于,map() 方法作用于整列变量的元素,因此需要对该列变量中所有可能出现的取值指定映射关系,否则没有指定映射关系的取值将会被转换成NaN。因此,不适用于取值类别特别多的时候。

例一:没有指定全部映射关系,出现 NaN

借用讲述 replace() 部分的案例,我们要做的工作是将93转换成为A。

如果使用 map() 方法,并只对93进行转换,而没有对所有取值指定映射关系。上例案例可以修改为:

  1. test_df['math'] = test_df['math'].map({93: 'A'})
  2. test_df

处理后的数据对比:

可以看出,这里的 map() 只对93进行了转换,而 math 变量中其余取值都变成了 NaN。

例二:将 art 变量的取值全部进行映射处理

假设art变量共5个取值:[a1, a2, a3, b1, b2]。现在需要将[a1, a2, a3]转换为a,[b1, b2]转换为b。

代码如下:

  1. import pandas as pd​
  2. test_df2 = ({'id': [1, 2, 3, 4, 5],
  3. 'art': ['a1', 'a2', 'b1', 'a3', 'b2']})
  4. # 对art变量的取值一一构建映射关系,并进行替换
  5. test_df2['art'] = test_df2['art'].map({'a1': 'a', 'a2': 'a', 'a3': 'a',
  6. 'b1': 'b', 'b2': 'b'})
  7. test_df2

显示结果如下:

2.3 LabelEncoding 标签编码

直接替换方法适用于原始数据集中只存在少量数据需要人工进行调整的情况。如果需要调整的数据量非常大且数据格式不统一,直接替换的方法也可以实现我们的目的,但是这种方法需要的工作量会非常大。因此, 我们需要能够快速对整列变量的所有取值进行编码的方法。

LabelEncoding,即标签编码,作用是为变量的 n 个唯一取值分配一个[0, n-1]之间的编码,将该变量转换成连续的数值型变量。

假设 test_df 的 art 变量共5个取值:[a1, a2, a3, b1, b2],现在对其做 LabelEncoding 处理。art 变量共有5个唯一取值,算法将自动为 art 变量的这5个取值自动分配一个处于[0, 4]之间的数字。

代码如下:

  1. import pandas as pd
  2. test_df3 = ({'id': [1, 2, 3, 4, 5],
  3. 'art': ['a1', 'a2', 'b1', 'a3', 'b2']})
  4. from import LabelEncoder
  5. le = LabelEncoder() # 建立一个 LabelEncoding 编码器
  6. test_df3['art'] = le.fit_transform(test_df3['art']) # 对 art 变量进行 LabelEncoding
  7. test_df3

 我们可以看到,转换之后,art 变量的所有取值都变成了[0, 4]之间的数值。从而,机器学习算法可以在 art 这一变量上处理数值关系。

并且,在本例中art 变量的取值其实是应该有优先级的,即a1>a2>a3>b1>b2。

LabelEncoding 根据原各唯一取值的先后顺序进行排序后为其转换为对应次序的数值,转换后的结果也在数值上体现了这一优先级。

同时,应用 LabelEncoding 也可能产生一个奇怪的现象:

假设 animal 变量的取值为:['cat', 'dog', 'lion', 'tiger'],对其进行 LabelEncoding 处理后的结果为[0, 1, 2, 3]。

很显然,animal 变量包含的是几种动物的名称。LabelEncoding 算法为其进行字符排序后,得到[0, 1, 2, 3]的先后顺序,但我们在实际生活中,如果没有其余先决条件,直接对各动物的名称排序是没有根据的。

其次,从数值角度上来说,根据 LabelEncoding 处理后的结果,cat 和 dog 的平均值是 dog(0和2的平均值是1),3单位的 dog 等于1单位的 tiger(3*1=3)。这些数值关系是难以被解释的。

 

小结:

  • 相较于 Replace 等直接替换的方法,LabelEncoding 可以快速简便地将各取值转换成数值

  • LabelEncoding 处理后的数值为取值范围[0, n-1]的整数数值,在计算空间距离等算法中存在优先级关系。因此 LabelEncoding 适合有序数据的处理

  • LabelEncoding 处理后的取值,是没有具体含义的,数值不同只做区分作用,而不同于连续性数值一般具有现实意义(如在房屋售价回归模型中,房屋面积的增量可能导致售价的增量)

  • 对于无序数据,如动物的种类,LabelEncoding 处理后的结果会使得无序数据存在有序性,这会扰乱原数据间的关系。因此 LabelEncoding 不适用于无序数据的处理

2.4 OneHot Encoding

无序数据的各取值之间并不存在先后优先级关系,LabelEncoding 这一方法并不适用。接下来介绍两种针对无序数据常用的编码方案,这些方案能够在将原变量转换为数值变量的同时,避免数值大小带来优先级的影响。

OneHotEncoding,也称独热编码,是对没有优先级的类别型数据进行处理的一种方法。对于原变量中存在的 n 个唯一的类别取值,OneHotEncoding 会依赖于原取值生成新的 n 个0-1变量,新生成的每个变量都只有0和1两个取值。新生成的 n 个变量进行组合,就可以表示原变量的 n 个唯一取值。可以理解为对有 n 个取值的变量,经过独热编码处理后,转为 n 个二元变量,每次只有一个激活。

处理后的新数据,既能解决数值化的问题,又能够避免数值大小带来的优先级影响,但因为根据一个原有变量会生成 n 个新变量,数据集将会变得稀疏。

百度百科的解释如下:

独热编码即 One-Hot 编码,又称一位有效编码,其方法是使用N位状态寄存器来对N个状态进行编码,每个状态都有它独立的寄存器位,并且在任意时候,其中只有一位有效。

例如对六个状态进行编码:

自然顺序码为 000,001,010,011,100,101

独热编码则是 000001,000010,000100,001000,010000,100000

假设 test_df 的 animal 变量取值共4个唯一取值:['cat', 'dog', 'lion', 'tiger'],因为各取值在数值上并不存在大小优先级关系,对其做 LabelEncoding 处理并不适合。现在使用 OneHot Encoding 对其处理。

代码如下:

  1. '''
  2. 方法一:sklearn 包的 OneHotEncoder
  3. 需要注意的是,sklearn库中的 OneHotEncoder() 方法只能处理数值型变量。如果是字符型数据,需要先对其使用 LabelEncoder() 转换为数值数据,再使用 OneHotEncoder() 进行独热编码处理,并且需要自行在原数据集中删去进行独热编码处理的原变量
  4. '''
  5. import pandas as pd
  6. test_df4 = ({'animal': ['cat', 'dog', 'lion', 'tiger', 'dog', 'cat']})
  7. from import LabelEncoder, OneHotEncoder
  8. le = LabelEncoder() # 建立一个 LabelEncoding 编码器
  9. test_df4['animal'] = le.fit_transform(test_df4['animal']) # 对 animal 变量进行LabelEncoding
  10. ohe = OneHotEncoder() # 建立一个 OneHot Encoding 编码器
  11. X = ohe.fit_transform(test_df4).toarray() # 编码后的数据为稀疏矩阵形式
  12. # 将编码后的数据填充至原数据集,以供展示
  13. test_df4 = ([test_df4, (X, columns=['a0', 'a1','a2', 'a3'])],
  14. axis=1)
  15. test_df4.drop('animal', axis=1, inplace=True) # 独热编码之后,原变量已经不需要,从数据集中剔除
  16. '''
  17. 方法二:pandas 包的 get_dummies() 方法
  18. get_dummies() 方法可以对数值数据和字符数据进行处理,直接在原数据集上应用该方法即可。该方法产生一个新的Dataframe,列名由原变量延伸而成。
  19. get_dummies() 方法产生的是一个新的Dataframe,将其合并入原数据集时,需要自行在原数据集中删去进行虚拟变量处理的原变量。
  20. '''
  21. import pandas as pd
  22. test_df4 = ({'animal': ['cat', 'dog', 'lion', 'tiger', 'dog', 'cat']})
  23. pd.get_dummies(test_df4,
  24. drop_first=False) # drop_first默认为False,产生 n 个新变量,即独热编码

展示结果如下:

使用 OneHotEncoder() 方法:

使用 get_dummies() 方法:

 

小结:

  • 独热编码将类别型数据转换成 n 个0-1变量,从而在数值上避免了这部分数据不应有的优先级问题。

  • 优点:独热编码解决了分类器不好处理属性数据的问题,在一定程度上也起到了扩充特征的作用。它的值只有0和1,不同的类型存储在垂直的空间。

  • 缺点:当类别的数量很多时,特征空间会变得非常大,即庞大的稀疏矩阵。在这种情况下,一般可以用PCA来减少维度。而且独热编码+PCA这种组合在实际中也非常有用。

  • 独热编码同下文提及的虚拟变量,在应用时认为是同一处理方法的不同表达。

2.5 Dummy variables

Dummy Variables,也称哑变量、虚拟变量、0-1变量、二值变量(binary variable)等。哑变量的处理思路和应用原则同 OneHot Encoding 几乎一致,都是为了处理类别型数据进行转换时带来的优先级问题。在网络上查看的相关介绍中,哑变量和独热编码通常是混为一谈,少有对其作区分的解释。这里我参考《计量经济学》对其进行解释,以帮助理解。

 


虚拟变量是回归分析中常用的工具之一。对于部分定性变量,如性别为男或女,只需要1和0两个数值即可进行描述。哑变量实质上就是一个将数据区分为相互排斥类别(如男/女)的工具。

假设在线性回归模型中,自变量 city 有三个取值,分别为BJ、SH和SZ,因变量表示各地区教师的薪水。这时候线性回归的模型可以表示为:

对其进行独热编码处理,将会新生成三列新变量(C1、C2和C3),如下图所示:

  C1 C2 C3 C1+C2+C3
BJ 1 0 0 1
SH 0 1 0 1
SZ 0 0 1 1

1)如果对这三列新变量进行水平相加,会得到一列取值全部为1的数据;由于截距对每个观测都(隐含地)为1,又会得到一列取值完全为1的数据。因此,这三列新变量值的和再次生成了截距项,因此导致了完全共线性,在这种情况下,要对数据进行模型估计是不能实现的。

2)虚拟变量陷阱(dummy variable trap)即完全共线性或完全多重共线性(若变量间存在不止一个精确的关系)的情形,在这种情形下变量之间存在完全线性关系。要避免虚拟变量陷阱,虚拟变量的处理逻辑修改为:对于当前定性变量,n 个唯一的类别取值只生成 n-1 个新的0-1变量。对每个定型回归元而言,所引入的虚拟变量个数必须比该变量的类别数少一个。

在上例中,city 有3个类别,只需要引入3-1=2个0-1变量,即从C1、C2和C3中剔除一个。至于剔除哪一个新生成的0-1变量,不做指定,因为这三个变量中,任一变量都可由另两变量计算得出。

修改后的线性回归模型(剔除C3)可以表示为:

3)如果在模型中不使用截距项,为 city 变量生成3个新的0-1变量也能够避免虚拟变量陷阱。

这时候的线性回归模型(剔除截距项)可以表示为:

模型各系数分别表示不同群体的均值,解释如下:

  • ​β1:BJ教师薪水的均值

  • ​β2:SH教师薪水的均值

  • ​β3:SZ教师薪水的均值

4)选择 n-1 个0-1变量还是剔除截距项

通常认为,含有截距项的方程能够更好的处理相关问题,不带有截距项的方程可以用于辅助检验模型效果。

因此在机器学习应用时,一般不考虑剔除截距项的处理方式,默认处理方式为选择 n-1 个0-1变量。


 

对虚拟变量的解释如上,但在实际应用过程中一般只需要注意一点:独热编码新生成 n 个0-1变量,而虚拟变量处理新生成 n-1 个0-1变量。这也是在应用时哑变量不同于独热编码的最显著的区别。

同上例,假设 test_df 的 animal 变量取值共4个唯一取值:['cat', 'dog', 'lion', 'tiger'],因为各取值在数值上并不存在大小优先级关系,对其进行虚拟变量处理。

实现代码如下:

  1. import pandas as pd
  2. test_df4 = ({'animal': ['cat', 'dog', 'lion', 'tiger', 'dog', 'cat']})
  3. pd.get_dummies(test_df4,
  4. drop_first=True) # drop_first设置为True,产生 n-1 个新变量,即虚拟变量

展示结果如下:

 

小结:

  • 虚拟变量源于线性回归问题,它将类别型数据转换成 n-1 个0-1变量,从而在数值上避免了这部分数据不应有的优先级和虚拟变量陷阱问题。

  • 在应用时,虚拟变量处理和独热编码一般认为是同一处理方法。一般情况下使用独热编码(n个0-1变量),如果涉及线性回归问题则需要转换为虚拟变量处理(n-1个0-1变量)

3. 特殊情况 - 高势集

高势集,或称高数量类别特征(high-cardinality categorical attributes),是指不同值的数量非常多的类别型特征。反正,则称之为“低数量类别特征”,也就是我们一般考虑时的情况。

一般来说,应用算法需要预先将分类特征转化为数值特征,如常见的在“低数量类别属性”上进行虚拟变量或独热编码处理。出于这两个算法的内在特征,一个有 n 个唯一取值的变量处理后会产生 n(或n-1)个新的0-1变量。如果一个类别特征唯一取值的数量太多,即“高数量类别特征”,对其进行独热编码等操作会导致数据集的列数变得非常庞大,可能会因此产生变量数为数十万、数百万的稀疏矩阵。

如:假设test_df 数据集大小为(100000, 5),存在5个分类型变量,每个变量的唯一取值数量为[2000, 3000, 4000, 5000, 60000],进行独热编码转换后的数据集是大小为(100000, 200000)的稀疏矩阵。

因此,对高势集直接应用独热编码处理,显然效果非常差。这里介绍几种针对高势集的常用解决方法。

3.1 主观合并类别

高势集中的某些变量具有非常多的唯一取值,直接对其进行独热编码毫无疑问会使得数据集变得极度稀疏。但我们不能够忽视一个现实,即数据集中的变量应当是具有一定含义的,其种类繁多的取值之间可能也具有一定联系。

我们可以根据相关业务背景、专业知识,对这些取值进行划分合并,手动将原始的 n 种取值降为 k 种取值(k远小于n)。这样就可以把高数量类别特征转化成为低数量类别特征。随后,再对转化后的低数量类别特征应用OneHot编码等处理,即可避免产生过于庞大的稀疏矩阵。

举例如下:

test_df 数据集中的 position 变量包含着某地区上班族的工作岗位信息,该变量的构成规律为:供职公司+供职岗位+当前工作年限,如"microsoft_PM_5"。假设position 变量共包含200个公司、100个职位和10个工作年限取值,即 position 变量的唯一取值数量为200000,如['apple_PM_1', 'amazon_RD_3', 'microsoft_HR_5']。

 

直接对 position 变量进行独热编码显然不适合,我们可以根据 position 的构成规律对其进行划分。若将 position 取值转化为员工对应的供职公司,则 position 变量的类别数可以降至200;若将取值转化为员工对应的供职岗位,则 position 变量的类别数可以降至100。 

3.2 Clustering

目前有一个常见的方法处理这个问题叫做 Clustering,即将原始的 1-to-n 的 mapping 问题变成 1-to-k 的 mapping 问题(k远小于n)。为了达到这个目标,这个高数量类别属性首先将依据 target 的值 grouping 成k个类(clusters),然后再依据这个 grouping 的结果进行 one-hot 编码。

同方法3.1基于相关业务逻辑和专业知识进行主观转换相比,Clustering 方法侧重于使用相关算法及度量指标进行转换,这种方法最大程度的保留了原始数据的信息。

grouping 的方法有一些:其中一个就是Hierarchical clustering Algorithm,它使用一个基于target的的统计距离来 grouping,grouping 的标准也可以度量每合并两个 clusters 带来的信息增量的影响,如gain ratio。两个距离最短的 clusters 将会被合并成一个。这个过程一直被迭代,直到迭代过程没有明显的改善。

更多相关解释可见《统计学习方法》5.2.2。

3.3 重编码

3.3.1 哈希编码

这里直接摘取《特征哈希编码及哈希算法》一文对其进行介绍:

哈希算法并不是一个特定的算法而是一类算法的统称。哈希算法也叫散列算法,一般来说满足这样的关系:f(data)=key,输入任意长度的data数据,经过哈希算法处理后输出一个定长的数据key。同时这个过程是不可逆的,无法由key逆推出data。

如果是一个data数据集,经过哈希算法处理后得到key的数据集,然后将keys与原始数据进行一一映射就得到了一个哈希表。一般来说哈希表M符合M[key]=data这种形式。哈希表的好处是当原始数据较大时,我们可以用哈希算法处理得到定长的哈希值key,那么这个key相对原始数据要小得多。我们就可以用这个较小的数据集来做索引,达到快速查找的目的。

因此在面对高基数类别变量时,就可以用特征哈希法编码的方式将原始的高维特征向量压缩成较低维特征向量,且尽量不损失原始特征的表达能力。

但是哈斯算法有一个问题,就是哈希值是一个有限集合,而输入数据则可以是无穷多个。那么建立一对一关系明显是不现实的。所以"碰撞"(不同的输入数据对应了相同的哈希值)是必然会发生的,所以一个成熟的哈希算法会有较好的抗冲突性。同时在实现哈希表的结构时也要考虑到哈希冲突的问题。

但是有一份博客做过这个实验(博客原地址:/2014/11/20/),发现特征哈希法可以降低特征数量,从而加速算法训练与预测过程,以及降低内存消耗;但代价是通过哈希转换后学习的模型变得很难检验,我们很难对训练出的模型参数做出合理解释。特征哈希法的另一个问题是它会把多个原始特征哈希到相同的位置上,出现哈希里的collision现象。但实际实验表明这种collision对算法的精度影响很小。

哈希编码的具体原理可以参考:hash trick在机器学习中的使用

代码如下:

  1. import pandas as pd
  2. from category_encoders.hashing import HashingEncoder
  3. df_X = ([1,2,3,4,1,2,4,5,8,7,66,2,24,5,4,1,2,111,1,31,3,23,13,24],
  4. columns=list("A")) # 构建测试数据集
  5. he = HashingEncoder(cols=["A"], # 需要进行哈希编码的是 df_X 中的 A 列
  6. return_df=True)
  7. df_X = he.fit_transform(df_X) # 编码后的数据存为 df_X

 

小结:

  • 哈希编码可以作为一种降维方法,实现简单,所需计算量小且效果较好。

  • 哈希编码可以保持原有特征的稀疏性(preserve sparsity)

  • 缺点是哈希后学习到的模型很难检验,无法对模型参数做解释。

3.3.2 均值编码

简单来说,就是将原来独立的高数量类别特征的每个值映射到概率估计上。基本来讲,这个预处理方法将原始的值放置到实际的机器学习模型之前先通过一个简单的特征处理模型(如贝叶斯模型)。

具体的细节涉及很多细节知识,请参考此博文进行了解:高数量类别特征(high-cardinality categorical attributes)的预处理方法

 

4. 总结

4.1 定类类型的数据

对于定类类型的数据,建议使用one-hot encoding。定类类型就是纯分类,不排序,没有逻辑关系。比如性别分男和女,男女不存在任何逻辑关系,我们不能说男就比女好,或者相反。再者,中国各省市分类也可以用独热编码,同样各省不存在逻辑关系,这时候使用one-hot encoding会合适些。但注意,一般转换为 n-1 个变量,即做虚拟变量处理。

4.2 定序类型的数据

对于定序类型的数据,建议使用label encoding。定序类型也是分类,但有排序逻辑关系,等级上高于定类。比如,学历分小学,初中,高中,本科,研究生,各个类别之间存在一定的逻辑,显然研究生学历是最高的,小学最低。这时候使用Label encoding会显得更合适,因为自定义的数字顺序可以不破坏原有逻辑,并与这个逻辑相对应。

4.3 数值大小敏感的模型

对数值大小敏感的模型必须使用one-hot encoding或者Dummy。典型的例子就是LRSVM。二者的损失函数对数值大小是敏感的,并且变量间的数值大小是有比较意义的。而Label encoding的数字编码没有数值大小的含义,只是一种排序,因此对于这些模型都使用one-hot encoding。

4.4 数值大小不敏感的模型

对数值大小不敏感的模型(如树模型)不建议使用one-hot encoding。一般这类模型为树模型。将离散型特征进行one-hot编码的作用,是为了让距离计算更合理,但如果特征是离散的,并且不用one-hot编码就可以很合理的计算出距离,那么就没必要进行one-hot编码。 有些基于树的算法在处理变量时,并不是基于向量空间度量,数值只是个类别符号,即没有偏序关系,所以不用进行独热编码。 Tree Model不太需要one-hot编码: 对于决策树来说,one-hot的本质是增加树的深度。如果分类类别特别多,那么one-hot encoding会分裂出很多特征变量。这时候,如果我们限制了树模型的深度而不能向下分裂的话,一些特征变量可能就因为模型无法继续分裂而被舍弃损失掉了。因此,此种情况下可以考虑使用Label encoding。

 

5. 参考

  1. 百度百科,独热编码
  2. 独热编码(One-Hot Encoding)介绍及实现
  3. 特征工程——基于Sklearn的One-Hot编码,基于Pandas的Dummy Code(哑变量)
  4. 数据预处理:独热编码(One-Hot Encoding)和 LabelEncoder标签编码
  5. 高数量类别特征(high-cardinality categorical attributes)的预处理方法
  6. 特征哈希编码及哈希算法
  7. 高基数类别特征的编码处理
  8. Feature hashing(特征哈希)
  9. sklearn中的几种二值化编码函数:OneHotEncoder, LabelEncoder , LabelBinarizer
  10. 特征处理过程 中的 独热编码(onehot)与哑变量及python 代码实现
  11. 数据挖掘OneHotEncoder独热编码和LabelEncoder标签编码