特征工程系列:特征预处理
- 1. 什么是特征工程
- 2. 数值型特征无量纲化
-
- 2.1 标准化 z-score StandardScaler
- 2.2 归一化
-
- 2.2.1 MinMax归一化
- 2.2.2 MaxAbs归一化
- 2.3 正态分布化 Normalization
- 2.4 标准化和归一化对比
- 2.5 归一化和标准化使用的场景
- 3. 数据型特征特征分箱(数据离散化)
-
- 3.1 无监督分箱法
-
- 3.1.1 自定义分箱
- 3.1.2 等距分箱 pd.cut()
- 3.1.3 等频分箱 pd.qcut()
- 3.1.4 聚类分箱 \*
- 3.1.5 二值法 Binarizer
- 3.2 有监督分箱法
-
- 3.2.1 卡方分箱法
- 3.2.2 最小熵法分箱
- 4. 统计变换
-
- 4.1 Log变换
- 4.2 Box-Cox变换
- 5. 类别特征编码
-
- 5.1 标签编码 LabelEncoder
- 5.2 独热编码 OneHotEncode
- 5.3 标签二值化 LabelBinarizer
- 5.3 多标签二值化 MultiLabelBinarizer
- 5.4 平均数编码 Mean Encoding
1. 什么是特征工程
-
特征工程是利用数据领域的相关知识来创建能够使机器学习算法达到最佳性能的特征的过程
-
特征工程又包含了Data PreProcessing(数据预处理)、Feature Selection(特征选择)、Feature Extraction**(特征提取**)和Feature construction(特征构造)等子问题
-
获取尽可能小的特征子集,不显著降低分类精度、不影响分类分布以及特征子集应具有稳定、适应性强等特点。
2. 数值型特征无量纲化
- 数据标准化原因:
- 某些算法要求样本具有零均值和单位方差
- 需要消除样本不同属性具有不同量级时的影响
- 归一化 可能提高精度 (数量级的差异将导致量级较大的属性占据主导地位,从而与实际情况相悖)
- 数量级的差异导致迭代收敛速度减慢 (走之子型路线)
- 依赖于样本距离的算法 对数据的数量级敏感
2.1 标准化 z-score StandardScaler
-
标准化的前提是特征值服从正态分布,针对于某个属性,标准化后,其转换成标准正态分布
-
基于原始数据的均值和标准差进行数据的标准化,(元数据-均值)/标准差
-
将A的原始值x使用z-score标准化到x’
-
优点:
- 简单,容易计算,不受数据量级的影响
-
缺点:
- 需要总体的平均值与方差,真实的分析比较难得到,通常用样本均值和标准差替代
- 对于数据分布有一定要求,正态分布最好
- Z-Score消除了数据具有的实际意义,A的Z-Score与B的Z-Score与他们各自的分数不再有关系,只能用于比较数据间的结果,数据的真实意义还需要还原原值
- 存在异常值时无法保证平衡的特征尺度
-
1
2from sklearn.preprocessing import StandardScaler
standardScaler = StandardScaler().fit_transform(X_train)
2.2 归一化
2.2.1 MinMax归一化
-
利用边界值信息,把属性缩放到[0,1]
-
(元数据-最小值)/(最大值-最小值)
-
缺点:
- 当有新数据加入时,导致min、max变化,要重新定义
- 对异常值的存在非常敏感
-
1
2from sklearn.preprocessing import MinMaxScaler
minMaxScaler = MinMaxScaler().fit_transform(X_train)
2.2.2 MaxAbs归一化
-
单独的缩放和转化每个特征,将属性缩放到[-1,1]
-
元数据/最大值
-
不会移动/居中数据,不会破坏稀疏性
-
缺点:
- 当有新数据加入时,导致min、max变化,要重新定义
- 对异常值的存在非常敏感
-
1
2from sklearn.preprocessing import MaxAbsScaler
maxAbsScaler = MaxAbsScaler().fit_transform(X_train)
2.3 正态分布化 Normalization
-
正则化的过程是将每个样本缩放到单位范数(每个样本的范数为1)
-
如果要使用如二次型(点积)或者其它核方法计算两个样本之间的相似性这个方法会很有用。
-
文本分类和聚类分析中经常使用的向量空间模型
-
Normalization主要思想是对每个样本计算其p-范数(2范式的拓展),然后对该样本中每个元素除以该范数
-
这样处理的结果是使得每个处理后样本的p-范数(l1-norm,l2-norm)等于1
-
1
2from sklearn.preprocessing import Normalizer
normalizer = Normalizer(norm='l2').fit_transform(X_train)
2.4 标准化和归一化对比
-
相同点:
- 都能取消由于量纲不同引起的误差
- 都是一种线性变换,都是对向量X按照比例压缩再进行平移
-
不同点:
- 目的不同,归一化是为了消除纲量压缩到[0,1]区间,标准化只是调整特征整体的分布
- 归一化与最大,最小值有关;标准化与均值,标准差有关
2.5 归一化和标准化使用的场景
- 归一化
- 如果对输出结果范围有要求,如图像处理中,将RGB图像转换为灰度图像后值限定在[0 255]的范围
- 如果数据较为稳定,不存在极端的最大最小值
- 不涉及距离度量、协方差计算、数据不符合正太分布的时候
- 基于树的方法不需要进行特征的归一化,例如随机森林,bagging与boosting等方法。
- 标准化
- 如果数据存在异常值和较多噪音
- 在分类、聚类算法中,需要使用距离来度量相似性的时候(如SVM、KNN)、或者使用PCA技术进行降维的时候
- 一般来说,优先使用标准化,如果我们对于数据的分布有假设的话,更加有效的方法是使用相对应的概率密度函数来转换
3. 数据型特征特征分箱(数据离散化)
-
数值型数据转化成类别型数据。连续值的取值空间可能是无穷的,为了方便在模型中处理,需要对连续值特征进行离散化处理
-
优点:
-
离散特征的增加和减少都很容易,易于模型的快速迭代
-
稀疏向量内积乘法运算速度快,计算结果方便存储,容易扩展
-
离散化后的特征对异常数据有很强的鲁棒性,异常数据也属于划分的区间内
-
模型会更稳定,年龄增加不会变成完全不同的人
-
对于线性模型,表达能力受限,单变量离散化为N个后,每个变量有单独的权重,相当于模型引入了非线性,能够提升模型表达能力,加大拟合
-
可以进行特征交叉,由M+N个变量变为M*N个变量,进一步引入非线性,提升表达能力
-
降低了模型过拟合的风险
-
可以将缺失作为独立的一类带入模型
-
将所有变量变换到相似的尺度上
-
3.1 无监督分箱法
3.1.1 自定义分箱
- 指根据业务经验或者常识等自行设定划分的区间,然后将原始数据归类到各个区间中
3.1.2 等距分箱 pd.cut()
-
按照相同宽度将数据分成几等份
-
受异常值的影响较大
-
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16import pandas as pd
df = pd.DataFrame([[22,1],[13,1],[33,1],[52,0],[16,0],[42,1],[53,1],[39,1],[26,0],[66,0]],columns=['age','Y'])
df['age_bin_1'] = pd.cut(df['age'],3) #新增一列存储等距划分的分箱特征
display(df)
# 输出
age Y age_bin_1
0 22 1 (12.947, 30.667]
1 13 1 (12.947, 30.667]
2 33 1 (30.667, 48.333]
3 52 0 (48.333, 66.0]
4 16 0 (12.947, 30.667]
5 42 1 (30.667, 48.333]
6 53 1 (48.333, 66.0]
7 39 1 (30.667, 48.333]
8 26 0 (12.947, 30.667]
9 66 0 (48.333, 66.0]
3.1.3 等频分箱 pd.qcut()
-
将数据分成几等份,每等份数据里面的个数是一样的
-
区间的边界值要经过选择,使得每个区间包含大致相等的实例数量
-
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16import pandas as pd
df = pd.DataFrame([[22,1],[13,1],[33,1],[52,0],[16,0],[42,1],[53,1],[39,1],[26,0],[66,0]],columns=['age','Y'])
df['age_bin_1'] = pd.qcut(df['age'],3) #新增一列存储等频划分的分箱特征
display(df)
# 输出
age Y age_bin_2
0 22 1 (12.999, 26.0]
1 13 1 (12.999, 26.0]
2 33 1 (26.0, 42.0]
3 52 0 (42.0, 66.0]
4 16 0 (12.999, 26.0]
5 42 1 (26.0, 42.0]
6 53 1 (42.0, 66.0]
7 39 1 (26.0, 42.0]
8 26 0 (12.999, 26.0]
9 66 0 (42.0, 66.0]
3.1.4 聚类分箱 *
-
基于k均值聚类的分箱:k均值聚类法将观测值聚为k类
-
分箱的有序性:第一个分箱中所有观测值都要小于第二个分箱中的观测值,第二个分箱中所有观测值都要小于第三个分箱中的观测值,等等。
-
实现步骤
- 进行归一化处理
- 应用k-means聚类算法,采用等距法设定k-means聚类算法的初始中心,得到聚类中心
- 在得到聚类中心后将相邻的聚类中心的中点作为分类的划分点,将各个对象加入到距离最近的类中,从而将数据划分为多个区间
- 重新计算每个聚类中心,然后重新划分数据,直到每个聚类中心不再变化,得到最终的聚类结果
-
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29import pandas as pd
from sklearn.cluster import KMeans
data = pd.DataFrame([[22,1],[13,1],[33,1],[52,0],[16,0],[42,1],[53,1],[39,1],[26,0],[66,0]],columns=['age','Y'])
feature = 'age'
k = 3
kmodel = KMeans(n_clusters=k) #k为聚成几类
kmodel.fit(data.values.reshape(len(data),-1)) #训练模型
c = pd.DataFrame(kmodel.cluster_centers_,columns=data.columns) #求聚类中心
c = c.sort_values(by=feature) #排序
x = c.rolling(2).mean().iloc[1:,] #滑动窗口 计算均值 第一个是NAN
w = [data[feature].min()-1] + list(x.loc[:,feature]) + [data[feature].max()] #划分依据 注意(a,b] 包右不包左
data['new_'+feature] = pd.cut(data[feature],w,) #新增一列存储等频划分的分箱特征
data['new_'+feature+'_label'] = pd.cut(data[feature],w,labels=range(k)) #新增一列存储等频划分的分箱特征标签
display(data)
age Y new_age new_age_label
0 22 1 (12.0, 28.625] 0
1 13 1 (12.0, 28.625] 0
2 33 1 (28.625, 47.5] 1
3 52 0 (47.5, 66.0] 2
4 16 0 (12.0, 28.625] 0
5 42 1 (28.625, 47.5] 1
6 53 1 (47.5, 66.0] 2
7 39 1 (28.625, 47.5] 1
8 26 0 (12.0, 28.625] 0
9 66 0 (47.5, 66.0] 2
3.1.5 二值法 Binarizer
-
二值化可以将数值型(numerical)的feature进行阀值化得到boolean型数据
-
这对于下游的概率估计来说可能很有用(比如:数据分布为Bernoulli分布时)
-
1
2
3from sklearn.preprocessing import Binarizer
# Binarizer函数也可以设定一个阈值,结果数据值大于阈值的为1,小于阈值的为0
binarizer = Binarizer(threshold=0.0).fit_transform(X_train)
3.2 有监督分箱法
3.2.1 卡方分箱法
-
自底向上的(即基于合并的)数据离散化方法
-
它依赖于卡方检验:具有最小卡方值的相邻区间合并在一起,直到满足确定的停止准则
-
相对类频率在一个区间内应当完全一致,低卡方值表明它们具有相似的类分布
-
实现步骤
- 定义卡方阈值
- 初始化,要离散的属性对实例进行排序,每个实例属于一个区间
- 合并区间,计算每一对相邻区间的卡方值,卡方值最小的一对区间合并
-
注意
- 类别和属性独立时,有90%的可能性,计算得到的卡方值会小于4.6
- 大于阈值4.6的卡方值就说明属性和类不是相互独立的,不能合并
- ChiMerge算法推荐使用0.90、0.95、0.99置信度,最大区间数取10到15之间
- 对于类别型变量,需要分箱时需要按照某种方式进行排序
-
代码
3.2.2 最小熵法分箱
- 需要使总熵值达到最小,也就是使分箱能够最大限度地区分因变量的各类别
- 数据集的熵越低,说明数据之间的差异越小,最小熵划分就是为了使每箱中的数据具有最好的相似性
- 熵是信息论中数据无序程度的度量标准,提出信息熵的基本目的是找出某种符号系统的信息量和冗余度之间的关系,以便能用最小的成本和消耗来实现最高效率的数据存储、管理和传递。
4. 统计变换
- 数据分布的倾斜有很多负面的影响,可以使用特征工程技巧,利用变换函数来减轻这种影响
- 变换函数都属于幂变化函数簇
- 主要作用是帮助稳定方差,始终保持分布接近正态分布,数据与分布的平均值无关
4.1 Log变换
-
y
=
l
o
g
b
(
x
)
y = log_b(x)
y=logb?(x)
-
当应用于倾斜分布时 Log 变换是很有用的
-
前提:数值型值必须为正数
-
作用:
- Log变换倾向于拉伸那些落在较低的幅度范围内自变量值的范围,倾向于压缩或减少更高幅度范围内的自变量值的范围
- 针对一些数值连续特征的方差不稳定,特征值重尾分布我们需要采用Log化来调整整个数据分布的方差,属于方差稳定型数据转换
-
适用场景:
- 分本分析领域
- 时间序列分析领域
-
1data['income_log'] = np.log((1+data['income']))
4.2 Box-Cox变换
-
$ y = f(x,\lambda) = log_e(\lambda) … when \lambda=0 $
-
$ y = f(x,\lambda) = (x^\lambda -1) /\lambda …when \lambda >0$
-
有一个前提:数值型值必须先变换为正数(与 log 变换所要求的一样) 可以使用一个常数对数值进行偏移
-
作用:
- 用于连续的响应变量不满足正态分布的情况
- 可以一定程度上减小不可观测的误差和预测变量的相关性
- 是引入一个参数,通过数据本身估计该参数进而确定应采取的数据变换形式
-
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27from scipy import stats
data = pd.DataFrame([[221,1],[1223,1],[3333,1],[5112,0],[2,0],[4132,1],[512433,1],[3923,1],[216,0],[66,0]],columns=['income','Y'])
# 从数据分布中移除非零值
income = np.array(data['income'])
income_clean = income[~np.isnan(income)]
# 计算最佳λ值
l, opt_lambda = stats.boxcox(income_clean)
print('Optimal lambda value:', opt_lambda)
# 进行Box-Cox变换
data['income_boxcox_lambda_opt'] = stats.boxcox(data['income'],lmbda=opt_lambda)
# data['income_boxcox_lambda_opt'] = l 两个一样
display(data)
'''
income Y income_boxcox_lambda_opt
0 221 1 5.470031
1 1223 1 7.234056
2 3333 1 8.274630
3 5112 0 8.720118
4 2 0 0.694323
5 4132 1 8.498330
6 512433 1 13.578663
7 3923 1 8.444275
8 216 0 5.446536
9 66 0 4.232861
'''
5. 类别特征编码
5.1 标签编码 LabelEncoder
-
对不连续的数字或者文本进行编号,编码值介于0与n_classes-1之间
-
相对于OneHot,LabelEncoder编码占用空间少,并且支持文本特征编码
-
缺点是:它隐含了一个假设:不同的类别之间,存在一种顺序关系,例如:比如有[dog,cat,dog,mouse,cat],我们把其转换为[1,2,1,3,2]。这里就产生了一个奇怪的现象:dog和mouse的平均值是cat。
-
1
2
3
4
5
6
7
8
9
10
11
12from sklearn.preprocessing import LabelEncoder
le = LabelEncoder()
le.fit(["paris", "paris", "tokyo", "amsterdam"])
print('特征:{}'.format(list(le.classes_)))
# 输出 特征:['amsterdam', 'paris', 'tokyo']
print('转换标签值:{}'.format(le.transform(["tokyo", "tokyo", "paris"])))
# 输出 转换标签值:array([2, 2, 1]...)
print('特征标签值反转:{}'.format(list(le.inverse_transform([2, 2, 1]))))
# 输出 特征标签值反转:['tokyo', 'tokyo', 'paris']
5.2 独热编码 OneHotEncode
-
OneHotEncode只能对数值型变量二值化,无法直接对字符串型的类别变量编码
-
独热编码解决了分类器不好处理属性数据的问题,有扩充特征的作用。它的值只有0和1,不同的类型存储在垂直的空间。
-
类别的数量很多时,特征空间会变得很大,可以使用PCA减少维度
-
为什么用独热编码?
- 因为大部分算法是基于向量空间中的度量来进行计算的
- 为了使非偏序关系的变量取值不具有偏序性,并且到圆点是等距的
- 将离散特征的取值扩展到了欧式空间
- 将离散型特征使用one-hot编码,会让特征之间的距离计算更加合理
-
为什么特征向量要映射到欧式空间?
- 在回归、分类、聚类等机器学习算法中,特征之间距离的计算或相似度的计算是非常重要的
- 常用的距离或相似度的计算都是在欧式空间的相似度计算。
-
例子
- 假如有三种颜色特征:红、黄、蓝。在利用机器学习的算法时一般需要进行向量化或者数字化。
- 那么你可能想令 红=1,黄=2,蓝=3。那么这样其实实现了标签编码,即给不同类别以标签。然而这意味着机器可能会学习到“红<黄<蓝”,但这并不是我们的让机器学习的本意,只是想让机器区分它们,并无大小比较之意。
- 所以这时标签编码是不够的,需要进一步转换。因为有三种颜色状态,所以就有3个比特。即红色:1 0 0 ,黄色: 0 1 0,蓝色:0 0 1 。如此一来每两个向量之间的距离都是根号2,在向量空间距离都相等,所以这样不会出现偏序性,基本不会影响基于向量空间度量算法的效果
-
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35'''
当特征是字符串类型时,需要先用 LabelEncoder() 转换成连续的数值型变量,
再用 OneHotEncoder() 二值化 。
'''
from sklearn.preprocessing import OneHotEncoder
enc = OneHotEncoder()
enc.fit([[0, 0, 3], [1, 1, 0], [0, 2, 1], [1, 0, 2]]) # fit来学习编码
enc.transform([[0, 1, 3]]).toarray() # 进行编码
# 输出:array([[ 1., 0., 0., 1., 0., 0., 0., 0., 1.]])
# 第一个特征有两个值 0 和 1 分别对应 [0.,1.] 和 [1.,0.,]
# 第二个特征有三个值 1 对应 [0,1,0]
# 第三个特征有四个值 3 对应 [0,0,0,1]
import pandas as pd
import numpy as np
sex_list = ['MALE', 'FEMALE', np.NaN, 'FEMALE', ]
df = pd.DataFrame({<!-- -->'SEX': sex_list})
display(df)
# 输出
SEX
0 MALE
1 FEMALE
2 NaN
3 FEMALE
df = pd.get_dummies(df['SEX'],prefix='IS_SEX')
display(df)
# 输出
IS_SEX_FEMALE IS_SEX_MALE
0 0 1
1 1 0
2 0 0
3 1 0
5.3 标签二值化 LabelBinarizer
-
针对OneHotEncode只能对数值型变量二值化,无法对字符串类型的变量编码
-
LabelBinarizer可以直接对字符型变量二值化
-
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16from sklearn.preprocessing import LabelBinarizer
lb = LabelBinarizer()
lb.fit([1, 2, 6, 4, 2])
print(lb.classes_)
# 输出 array([1, 2, 4, 6])
print(lb.transform([1, 6]))
# 输出 array([[1, 0, 0, 0],
[0, 0, 0, 1]])
print(lb.fit_transform(['yes', 'no', 'no', 'yes']))
# 输出 array([[1],
[0],
[0],
[1]])
5.3 多标签二值化 MultiLabelBinarizer
-
用于label encoding,生成一个(n_examples * n_classes)大小的0~1矩阵,每个样本可能对应多个label
-
使用场景
- 每个特征中有多个文本单词,用户兴趣特征(如特征值:”健身 电影 音乐”)适合使用多标签二值化,因为每个用户可以同时存在多种兴趣爱好。
- 多分类类别值编码的情况。电影分类标签中(如:[action, horror]和[romance, commedy])需要先进行多标签二值化,然后使用二值化后的值作为训练数据的标签值。
-
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15from sklearn.preprocessing import MultiLabelBinarizer
mlb = MultiLabelBinarizer()
print(mlb.fit_transform([(1, 2), (3,)]))
# 输出:array([[1, 1, 0],
[0, 0, 1]])
print(mlb.classes_)
# 输出:array([1, 2, 3])
print(mlb.fit_transform([{<!-- -->'sci-fi', 'thriller'}, {<!-- -->'comedy'}]))
# 输出:array([[0, 1, 1],
[1, 0, 0]])
print(list(mlb.classes_))
# 输出:['comedy', 'sci-fi', 'thriller']
5.4 平均数编码 Mean Encoding
-
针对高基数类别特征的有监督编码。当一个类别特征列包括了极多不同类别时(如家庭地址,动辄上万)时,可以采用
-
在贝叶斯的架构下,利用所要预测的应变量(target variable),有监督地确定最适合这个定性特征的编码方式
-
为什么要用平均数编码
- 如果某一个特征是定性的(categorical),而这个特征的可能值非常多(高基数),那么平均数编码(mean encoding)是一种高效的编码方式。在实际应用中,这类特征工程能极大提升模型的性能。
- 定性特征表示某个数据属于一个特定的类别,值通常是从0到n的离散整数
- 这个定性特征所有可能的不同值的数量。在高基数(high cardinality)的定性特征面前,这些数据预处理的方法往往得不到令人满意的结果。
- 和独热编码相比,节省内存和计算时间,有效增强模型表现
-
1
2
3
4
5# 参考 https://zhuanlan.zhihu.com/p/26308272
MeanEnocodeFeature = ['item_city_id','item_brand_id'] #声明需要平均数编码的特征
ME = MeanEncoder(MeanEnocodeFeature) #声明平均数编码的类
trans_train = ME.fit_transform(X,y)#对训练数据集的X和y进行拟合
test_trans = ME.transform(X_test)#对测试集进行编码