0、前言
数据和特征决定了机器学习的上限,而模型和算法只是逼近这个上限而已。
由此可见,特征工程在机器学习中占有相当重要的地位。在实际应用当中,可以说特征工程是机器学习成功的关键。
那特征工程是什么?
特征工程是利用数据领域的相关知识来创建能够使机器学习算法达到最佳性能的特征的过程。
虽然我们也有自动的机器学习框架,如 AutoML(但该框架也强调了它需要好的特征才能跑出好的效果!)。特征工程永不过时,即使对于自动化方法,其中也有一部分经常需要根据数据类型、领域和要解决的问题而设计特殊的特征。
特征工程又包含了Data PreProcessing(数据预处理)、Feature Extraction(特征提取)、Feature Selection(特征选择)和Feature construction(特征构造)等子问题,而数据预处理又包括了数据清洗和特征预处理等子问题。详情可参考下图:
本篇文章我们讨论特征预处理中的无量纲化、特征分桶、统计变换和特征编码。
1、数据无量纲化
1.1、 数据无量纲化的介绍
Tree-based models doesn’t depend on scaling
Non-tree-based models hugely depend on scaling
在机器学习算法实践中,我们往往有着将不同规格的数据转换到同一规格,或不同分布的数据转换到某个特定分布的需求,这种需求统称为将数据“无量纲化”。譬如梯度和矩阵为核心的算法中,譬如逻辑回归,支持向量机,神经网络,无量纲化可以加快求解速度;而在距离类模型,譬如K近邻,K-Means聚类中,无量纲化可以帮我们提升模型精度,避免某一个取值范围特别大的特征对距离计算造成影响。(一个特例是决策树和树的集成算法们,对决策树我们不需要无量纲化,决策树可以把任意数据都处理得很好。)
数据的无量纲化可以是线性的,也可以是非线性的。线性的无量纲化包括中心化(Zero-centered或者Mean-subtraction)处理和缩放处理(Scale)。中心化的本质是让所有记录减去一个固定值,即让数据样本数据平移到某个位置。缩放的本质是通过除以一个固定值,将数据固定在某个范围之中,取对数也算是一种缩放处理。常见的无量纲化方法有标准化和归一化。
数据标准化的原因:
- 某些算法要求样本具有零均值和单位方差;
-
需要消除样本不同属性具有不同量级时的影响。
归一化有可能提高精度;
数量级的差异将导致量级较大的属性占据主导地位,从而与实际情况相悖(比如这时实际情况是值域范围小的特征更重要);归一化有可能提升收敛速度;
数量级的差异将导致迭代收敛速度减慢,当使用梯度下降法寻求最优解时,很有可能走“之字型”路线(垂直等高线走),从而导致需要迭代很多次才能收敛;-
依赖于样本距离的算法对于数据的数量级非常敏感。
比较这两个图,前者是没有经过归一化的,在梯度下降的过程中,走的路径更加的曲折,而第二个图明显路径更加平缓,收敛速度更快。
1.2、数据标准化(Standardization)
1)定义
标准化数据通过减去均值然后除以方差(或标准差),这种数据标准化方法经过处理后数据符合标准正态分布,即均值为0,标准差为1,转化函数为:
适用于:如果数据的分布本身就服从正态分布,就可以用这个方法。
通常这种方法基本可用于有outlier的情况,但是,在计算方差和均值的时候outliers仍然会影响计算。所以,在出现outliers的情况下可能会出现转换后的数的不同feature分布完全不同的情况。
2)效果
如下图,经过StandardScaler之后,横坐标与纵坐标的分布出现了很大的差异,这可能是outliers造成的。
3)优缺点
优点:
Z-Score最大的优点就是简单,容易计算,Z-Score能够应用于数值型的数据,并且不受数据量级的影响,因为它本身的作用就是消除量级给分析带来的不便。
缺点:
- 估算Z-Score需要总体的平均值与方差,但是这一值在真实的分析与挖掘中很难得到,大多数情况下是用样本的均值与标准差替代;
- Z-Score对于数据的分布有一定的要求,正态分布是最有利于Z-Score计算的;
- Z-Score消除了数据具有的实际意义,A的Z-Score与B的Z-Score与他们各自的分数不再有关系,因此Z-Score的结果只能用于比较数据间的结果,数据的真实意义还需要还原原值;
- 在存在异常值时无法保证平衡的特征尺度。
4)实现代码-sklearn中的StandardScaler:
from sklearn.preprocessing import StandardScaler
import numpy as np
np.random.seed(1)
data = np.random.randint(0,100,10).reshape(-1, 2)
print(data)
# data = [[-1, 3], [-0.5, 96], [0, 10], [1, 18]]
scaler = StandardScaler() #实例化
scaler.fit(data) #fit,本质是生成均值和方差
print(scaler.mean_) #查看均值的属性mean_
print(scaler.var_) #查看方差的属性var_
x_std = scaler.transform(data) #通过接口导出结果
print(x_std)
# print(x_std.mean()) #导出的结果是一个数组,用mean()查看均值
# print(x_std.std()) #用std()查看方差
scaler.fit_transform(data) #使用fit_transform(data)一步达成结果
# scaler.inverse_transform(x_std) #使用inverse_transform逆转标准化
对于StandardScaler和MinMaxScaler来说,空值NaN会被当做是缺失值,在fit的时候忽略,在transform的时候保持缺失NaN的状态显示。并且,尽管去量纲化过程不是具体的算法,但在fit接口中,依然只允许导入至少二维数组,一维数组导入会报错。通常来说,我们输入的X会是我们的特征矩阵,现实案例中特征矩阵不太可能是一维所以不会存在这个问题。
1.3、数据归一化
1.3.1、MinMax归一化
将特征缩放至特定区间,将特征缩放到给定的最小值和最大值之间,或者也可以将每个特征的最大绝对值转换至单位大小。这种方法是对原始数据的线性变换,将数据归一到[0,1]中间。转换函数为:
这种方法有个缺陷就是当有新数据加入时,可能导致max和min的变化,需要重新定义。
敲黑板,这种方法对于outlier非常敏感,因为outlier影响了max或min值,所以这种方法只适用于数据在一个范围内分布的情况
from sklearn.preprocessing import MinMaxScaler
import pandas as pd
data = [[-1, 2], [-0.5, 6], [0, 10], [1, 18]]
pd.DataFrame(data)
#实现归一化
scaler = MinMaxScaler() #实例化
scaler = scaler.fit(data) #fit,在这里本质是生成min(x)和max(x)
result = scaler.transform(data) #通过接口导出结果
result
array([[0. , 0. ],
[0.25, 0.25],
[0.5 , 0.5 ],
[1. , 1. ]])
result_ = scaler.fit_transform(data) #训练和导出结果一步达成
scaler.inverse_transform(result_) #将归一化后的结果逆转
array([[-1. , 2. ],
[-0.5, 6. ],
[ 0. , 10. ],
[ 1. , 18. ]])
#使用MinMaxScaler的参数feature_range实现将数据归一化到[0,1]以外的范围中
data = [[-1, 2], [-0.5, 6], [0, 10], [1, 18]]
scaler = MinMaxScaler(feature_range=[5,10]) #依然实例化
result = scaler.fit_transform(data) #fit_transform一步导出结果
result
#当X中的特征数量非常多的时候,fit会报错并表示,数据量太大了我计算不了
#此时使用partial_fit作为训练接口
#scaler = scaler.partial_fit(data)
array([[ 5. , 5. ],
[ 6.25, 6.25],
[ 7.5 , 7.5 ],
[10. , 10. ]])
使用numpy来实现归一化
import numpy as np
X = np.array([[-1, 2], [-0.5, 6], [0, 10], [1, 18]])
#归一化
X_nor = (X - X.min(axis=0)) / (X.max(axis=0) - X.min(axis=0))
X_nor
#逆转归一化
X_returned = X_nor * (X.max(axis=0) - X.min(axis=0)) + X.min(axis=0)
X_returned
array([[-1. , 2. ],
[-0.5, 6. ],
[ 0. , 10. ],
[ 1. , 18. ]])
1.3.2、MaxAbs归一化归一化
单独地缩放和转换每个特征,使得训练集中的每个特征的最大绝对值将为1.0,将属性缩放到[-1,1]。它不会移动/居中数据,因此不会破坏任何稀疏性。
缺点:
这种方法有一个缺陷就是当有新数据加入时,可能导致max和min的变化,需要重新定义;
MaxAbsScaler与先前的缩放器不同,绝对值映射在[0,1]范围内。
在仅有正数据时,该缩放器的行为MinMaxScaler与此类似,因此也存在大的异常值。
from sklearn.preprocessing import MinMaxScaler
#区间缩放,返回值为缩放到[0, 1]区间的数据
minMaxScaler = MinMaxScaler().fit(data)
minMaxScaler.transform(data)
array([[0. , 0. ],
[0.25, 0.25],
[0.5 , 0.5 ],
[1. , 1. ]])
1.3.1、RobustScaler归一化
如果你的数据包含许多异常值,使用均值和方差缩放可能并不是一个很好的选择。这种情况下,你可以使用 robust_scale 以及 RobustScaler 作为替代品。它们对你的数据的中心和范围使用更有鲁棒性的估计。
1.4、标准化与归一化的对比
1.4.1、标准化与归一化的异同
相同点:
它们的相同点在于都能取消由于量纲不同引起的误差;都是一种线性变换,都是对向量X按照比例压缩再进行平移。
不同点:
- 目的不同,归一化是为了消除纲量压缩到[0,1]区间;标准化只是调整特征整体的分布;
- 归一化与最大,最小值有关;标准化与均值,标准差有关;
- 归一化输出在[0,1]之间;标准化无限制。
1.4.2、什么时候用归一化?什么时候用标准化?
- 如果对输出结果范围有要求,用归一化;
- 如果数据较为稳定,不存在极端的最大最小值,用归一化;
- 如果数据存在异常值和较多噪音,用标准化,可以间接通过中心化避免异常值和极端值的影响。
1.4.3、归一化与标准化的应用场景
- 在分类、聚类算法中,需要使用距离来度量相似性的时候(如SVM、KNN)、或者使用PCA技术进行降维的时候,标准化(Z-score standardization)表现更好;
- 在不涉及距离度量、协方差计算、数据不符合正太分布的时候,可以使用第一种方法或其他归一化方法。比如图像处理中,将RGB图像转换为灰度图像后将其值限定在[0 255]的范围;
- 基于树的方法不需要进行特征的归一化。例如随机森林,bagging与boosting等方法如果是基于参数的模型或者基于距离的模型,因为需要对参数或者距离进行计算,都需要进行归一化。
1.5、sklearn中的数据预处理和特征工程
- 模块
preprocessing
:几乎包含数据预处理的所有内容
- 模块
Impute
:填补缺失值专用
- 模块
feature_selection
:包含特征选择的各种方法的实践
- 模块
decomposition
:包含降维算法
2、缺失值处理
机器学习和数据挖掘中所使用的数据,永远不可能是完美的。很多特征,对于分析和建模来说意义非凡,但对于实际收集数据的人却不是如此,因此数据挖掘之中,常常会有重要的字段缺失值很多,但又不能舍弃字段的情况。因此,数据预处理中非常重要的一项就是处理缺失值。
2.1、 使用Pandas和Numpy处理缺失值
import pandas as pd
import numpy as np
arr = pd.DataFrame(np.array([[1,np.nan],[np.nan,2],[100,np.nan],[np.nan,2],[1,np.nan],[np.nan,2]]))
# arr.drop_duplicates()
arr.iloc[:,0].fillna(axis=0,value=arr.iloc[:,0].mean(), inplace=True)
arr.dropna(axis=0,inplace=True)
#.dropna(axis=0)删除所有有缺失值的行,
# dropna(axis=1)删除所有有缺失值的列
#参数inplace,为True表示在原数据集上进行修改,为False表示生成一个复制对象,不修改原数据,默认False
arr.iloc[:,0].values
array([34., 34., 34.])
arr = np.array([[1,np.nan],[np.nan,2]
,[100,np.nan],[np.nan,2]
,[200,np.nan],[50,2]
,[1,np.nan],[70,2]
,[1,np.nan],[70,2]
,[1,np.nan],[np.nan,2]])
arr[:,0]
array([ 1., nan, 100., nan, 200., 50., 1., 70., 1., 70., 1.,
nan])
2.2、 使用sklearn中的Imputer处理缺失值
arr = np.array([[1,np.nan],[np.nan,2]
,[100,np.nan],[np.nan,2]
,[200,np.nan],[50,2]
,[1,np.nan],[70,2]
,[1,np.nan],[70,2]
,[1,np.nan],[np.nan,2]])
from sklearn.preprocessing import Imputer
# from sklearn.impute import SimpleImputer
imp_mean = Imputer(missing_values='NaN', strategy='mean', axis=0,verbose=0)
imp_median = Imputer(missing_values='NaN', strategy='median', axis=0)
imp_mode = Imputer(missing_values='NaN', strategy='most_frequent', axis=0)
# imp_median = SimpleImputer(strategy="median") #用中位数填补
# imp_0 = Imputer(missing_values='NaN', strategy="constant",fill_value=0) #用0填补
imp_mean = imp_mean.fit_transform(arr[:,0].reshape(-1,1)) #fit_transform一步完成调取结果
imp_median = imp_median.fit_transform(arr[:,0].reshape(-1,1))
imp_mode = imp_mode.fit_transform(arr[:,0].reshape(-1,1))
# print(arr[:,0].reshape(-1,1))
# print("*****")
print(imp_mean)
# print("*****")
# print(imp_median)
# print("*****")
# print(imp_mode)
[[ 1. ]
[ 54.88888889]
[100. ]
[ 54.88888889]
[200. ]
[ 50. ]
[ 1. ]
[ 70. ]
[ 1. ]
[ 70. ]
[ 1. ]
[ 54.88888889]]
3、处理分类型特征:编码与哑变量
在机器学习中,大多数算法,譬如逻辑回归,支持向量机SVM,k近邻算法等都只能够处理数值型数据,不能处理文字,在sklearn当中,除了专用来处理文字的算法,其他算法在fit的时候全部要求输入数组或矩阵,也不能够导入文字型数据(其实手写决策树和普斯贝叶斯可以处理文字,但是sklearn中规定必须导入数值型)。然而在现实中,许多标签和特征在数据收集完毕的时候,都不是以数字来表现的。比如说,学历的取值可以是["小学",“初中”,“高中”,"大学"],付费方式可能包含["支付宝",“现金”,“微信”]等等。在这种情况下,为了让数据适应算法和库,我们必须将数据进行编码,即是说,将文字型数据转换为数值型。
preprocessing.LabelEncoder:标签专用,能够将分类转换为分类数值
from sklearn.preprocessing import LabelEncoder
y = ["支付宝", "现金", "微信"] #要输入的是标签,不是特征矩阵,所以允许一维
le = LabelEncoder() #实例化
le = le.fit(y) #导入数据
label = le.transform(y) #transform接口调取结果
le.classes_ #属性.classes_查看标签中究竟有多少类别
label #查看获取的结果label
# le.fit_transform(y) #也可以直接fit_transform一步到位
# le.inverse_transform(label) #使用inverse_transform可以逆转
# data.iloc[:,-1] = label #让标签等于我们运行出来的结果
# data.head()
# #如果不需要展示的话我会这么写:
# from sklearn.preprocessing import LabelEncoder
# data.iloc[:,-1] = LabelEncoder().fit_transform(data.iloc[:,-1])
array([1, 2, 0], dtype=int64)
preprocessing.OneHotEncoder:独热编码,创建哑变量
在泰坦尼克号生存预测数据集舱门Embarked这一列中,舱门分别为S、C、G,我们可以用LabelEncoder
把分类变量都转换成数字对应的类别。假设我们使用[0,1,2]代表了三个不同的舱门,然而这种转换是正确的吗?
我们来思考三种不同性质的分类数据:
1) 舱门(S,C,Q)
三种取值S,C,Q是相互独立的,彼此之间完全没有联系,表达的是S≠C≠Q的概念。这是名义变量。
2) 学历(小学,初中,高中)
三种取值不是完全独立的,我们可以明显看出,在性质上可以有高中>初中>小学这样的联系,学历有高低,但是学历取值之间却不是可以计算的,我们不能说小学 + 某个取值 = 初中。这是有序变量。
3) 体重(>45kg,>90kg,>135kg)
各个取值之间有联系,且是可以互相计算的,比如120kg - 45kg = 90kg,分类之间可以通过数学计算互相转换。这是有距变量。
然而在对特征进行编码的时候,这三种分类数据都会被我们转换为[0,1,2],这三个数字在算法看来,是连续且可以计算的,这三个数字相互不等,有大小,并且有着可以相加相乘的联系。所以算法会把舱门,学历这样的分类特征,都误会成是体重这样的分类特征。这是说,我们把分类转换成数字的时候,忽略了数字中自带的数学性质,所以给算法传达了一些不准确的信息,而这会影响我们的建模。
对于名义变量,我们只有使用哑变量的方式来处理,才能够尽量向算法传达最准确的信息,下面使用sklearn中的方法和pandas中的方法分别对字段进行哑变量处理
from sklearn.preprocessing import OneHotEncoder
from sklearn.preprocessing import LabelEncoder
data_np = np.array([[1, '语文'], [2, '英语']
, [3, '语文'], [4, '英语']
, [3, '语文'], [4, '数学']])
data = pd.DataFrame({'A' : 1,
'B' : pd.Timestamp('20130102'),
'C' : pd.Series(1,index=list(range(4)),dtype='float32'),
'D' : np.array([3] * 4,dtype='int32'),
'E' : pd.Categorical(["test","train","test","train"]),
'F' : ['foo1', 'foo1', 'foo2', 'foo2'],
'G' : pd.Series([1, 2, 1, 3], dtype='object')})
from sklearn.preprocessing import OneHotEncoder
from sklearn.preprocessing import LabelEncoder
data_np[:,-1] = le.fit_transform(data_np[:,-1])
# print(data_np)
enc = OneHotEncoder(n_values='auto').fit(data_np[:,-1].reshape(-1, 1))
result = enc.transform(data_np[:,-1].reshape(-1,1))
data_np = np.concatenate((data_np, result.toarray()), axis=1)
data_np
#依然可以直接一步到位,但为了给大家展示模型属性,所以还是写成了三步
# OneHotEncoder(n_values='auto').fit_transform(data_np[:,-1].reshape(-1,1)).toarray()
# #依然可以还原
# enc.inverse_transform(result)
array([['1', '2', '0.0', '0.0', '1', '0.0', '1.0'],
['2', '1', '0.0', '1.0', '0', '1.0', '0.0'],
['3', '2', '0.0', '0.0', '1', '0.0', '1.0'],
['4', '1', '0.0', '1.0', '0', '1.0', '0.0'],
['3', '2', '0.0', '0.0', '1', '0.0', '1.0'],
['4', '0', '1.0', '0.0', '0', '1.0', '0.0']], dtype='
data.F = le.fit_transform(data.F)
print(data)
res = enc.fit_transform(data.F.values.reshape(-1,1))
# res
# pd.DataFrame(res.toarray())
newdata = pd.concat([data,pd.DataFrame(res.toarray())],axis=1)
newdata
A B C D E F G
0 1 2013-01-02 1.0 3 test 0 1
1 1 2013-01-02 1.0 3 train 0 2
2 1 2013-01-02 1.0 3 test 1 1
3 1 2013-01-02 1.0 3 train 1 3
4、处理连续型特征--数据离散化
离散化是数值型特征非常重要的一个处理,其实就是要将数值型数据转化成类别型数据。连续值的取值空间可能是无穷的,为了便于表示和在模型中处理,需要对连续值特征进行离散化处理。
数据离散化的优点:
- 离散特征的增加和减少都很容易,易于模型的快速迭代;
- 稀疏向量内积乘法运算速度快,计算结果方便存储,容易扩展;
- 离散化后的特征对异常数据有很强的鲁棒性;比如一个特征是年龄>30是1,否则0。如果特征没有离散化,一个异常数据“年龄300岁”会给模型造成很大的干扰;
- 对于线性模型,表达能力受限;单变量离散化为N个后,每个变量有单独的权重,相当于模型引入了非线性,能够提升模型表达能力,加大拟合;离散化后可以进行特征交叉,由M+N个变量变为M*N个变量,进一步引入非线性,提升表达能力;
- 特征离散化后,模型会更稳定;比如如果对用户年龄离散化,20-30作为一个区间,不会因为一个用户年龄长了一岁就变成一个完全不同的人。当然处于区间相邻处的样本会刚好相反,所以怎么划分区间是门学问;特征离散化以后,起到了简化了逻辑回归模型的作用,降低了模型过拟合的风险;
- 可以将缺失作为独立的一类带入模型;
- 将所有变量变换到相似的尺度上。
sklearn.preprocessing.Binarizer:二值化
根据阈值将数据二值化(将特征值设置为0或1),用于处理连续型变量。大于阈值的值映射为1,而小于或等于阈值的值映射为0。默认阈值为0时,特征中所有的正值都映射到1。二值化是对文本计数数据的常见操作,分析人员可以决定仅考虑某种现象的存在与否。它还可以用作考虑布尔随机变量的估计器的预处理步骤(例如,使用贝叶斯设置中的伯努利分布建模)。
data_2 = data.copy()
from sklearn.preprocessing import Binarizer
data_bin = np.random.randint(0,100,20)
print(data_bin)
X = data_bin.reshape(-1,1) #类为特征专用,所以不能使用一维数组
transformer = Binarizer(threshold=30).fit_transform(X)
# transformer
[44 15 30 91 14 19 26 86 7 99 53 47 60 34 32 19 67 24 83 94]
常见的分箱方法:
-
无监督分想法
自定义分箱:根据业务经验或者常识等自行设定划分的区间,然后将原始数据归类到各个区间中。
等距分箱:按照相同宽度将数据分成几等份。缺点是受到异常值的影响比较大
等频分箱:将数据分成几等份,每等份数据里面的个数是一样的。区间的边界值要经过选择,使得每个区间包含大致相等的实例数量。比如说 N=10 ,每个区间应该包含大约10%的实例。
聚类分箱:基于k均值聚类的分箱:k均值聚类法将观测值聚为k类,但在聚类过程中需要保证分箱的有序性:第一个分箱中所有观测值都要小于第二个分箱中的观测值,第二个分箱中所有观测值都要小于第三个分箱中的观测值,等等。
-
有监督分想法
卡方分箱法:自底向上的(即基于合并的)数据离散化方法。它依赖于卡方检验:具有最小卡方值的相邻区间合并在一起,直到满足确定的停止准则。对于精确的离散化,相对类频率在一个区间内应当完全一致。因此,如果两个相邻的区间具有非常类似的类分布,则这两个区间可以合并;否则,它们应当保持分开。而低卡方值表明它们具有相似的类分布。
最小熵法分箱:需要使总熵值达到最小,也就是使分箱能够最大限度地区分因变量的各类别。熵是信息论中数据无序程度的度量标准,提出信息熵的基本目的是找出某种符号系统的信息量和冗余度之间的关系,以便能用最小的成本和消耗来实现最高效率的数据存储、管理和传递。数据集的熵越低,说明数据之间的差异越小,最小熵划分就是为了使每箱中的数据具有最好的相似性。给定箱的个数,如果考虑所有可能的分箱情况,最小熵方法得到的箱应该是具有最小熵的分箱。
preprocessing.KBinsDiscretizer 分箱
这是将连续型变量划分为分类变量的类,能够将连续型变量排序后按顺序分箱后编码。总共包含三个重要参数:
参数 | 含义&输入 |
---|---|
n_bins | 每个特征中分箱的个数,默认5,一次会被运用到所有导入的特征 |
encode | 编码的方式,默认“onehot” "onehot":做哑变量,之后返回一个稀疏矩阵,每一列是一个特征中的一个类别,含有该 类别的样本表示为1,不含的表示为0 “ordinal”:每个特征的每个箱都被编码为一个整数,返回每一列是一个特征,每个特征下含 有不同整数编码的箱的矩阵 "onehot-dense":做哑变量,之后返回一个密集数组。 |
strategy | 用来定义箱宽的方式,默认"quantile" "uniform":表示等宽分箱,即每个特征中的每个箱的最大值之间的差为 (特征.max() - 特征.min())/(n_bins) "quantile":表示等位分箱,即每个特征中的每个箱内的样本数量都相同 "kmeans":表示按聚类分箱,每个箱中的值到最近的一维k均值聚类的簇心得距离都相同 |