数据预处理

预处理
归一化 列:preprocessing.MinMaxScaler
preprocessing.StandardScaler
行:preprocessing.Normalizer
分类与编码 preprocessing.OrdinalEncoder()
preprocessing.OneHotEncoder()
数据离散化
多项式化 PolynomialFeatures(degree=2,include_bias=False)
缺失值处理

归一化

1
2
3
4
5
6
7
8
9
10
11
12
13
from sklearn.preprocessing import MinMaxScaler,StandardScaler,Normalizer

# MinMaxScaler() 列特征
iris_train=MinMaxScaler().fit_transform(iris_train)
iris_test=MinMaxScaler().fit_transform(iris_test)

# StandardScaler() 列特征
iris_train=StandardScaler().fit_transform(iris_train)
iris_test=StandardScaler().fit_transform(iris_test)

# Normalizer() 行特征
iris_train=Normalizer().fit_transform(iris_train)
iris_test=Normalizer().fit_transform(iris_test)

列特征

主要在线性模型中进行

名称 公式 说明
StandardScaler 标准化 $x’=\frac{x-\bar{x}}{\sigma}$ $\bar{x}$为每一列特征的均值
$\sigma$为每一列特征的标准差
MinMaxScaler 归一化 $x’=\frac{x-Min}{Max-Min}$ $Min$:每一列特征的最小值
$Max$:每一列特征的最大值

为什么树形模型不需要进行归一化、标准化

因为树形模型的损失函数与特征尺度无关,所以树节点进行分裂的时候以及计算叶节点得分的时候同样与特征尺度无关。

分裂点 叶节点得分
ID3-分类 遍历特征,使$g(D,A)=H(D)-H(D A)$最大
损失函数只与样本量有关,与特征的尺度无关
即分类结果A
C4.5-分类 遍历特征,使$g_{R}(D,A)= \frac {g(D,A)}{H_A(D)}$最大
损失函数只与样本量有关,与特征的尺度无关
即分类结果
CART-分类 遍历特征,使$Gini(D)-Gini(D,A)$最大
损失函数只与样本量有关,与特征的尺度无关
即分类结果
CART-回归 使用CART回归树拟合残差
分裂点使用CART规则,与特征尺度无关
使$L=(y_i-T(x))^2$最小
选取均值,与特征尺度无关
GDBT-分类 使用CART回归树拟合残差
分裂点使用CART规则,与特征尺度无关
使$- \sum_{k=1}^K y_klog \; p_k(x)$最小
与特征尺度无关
GBDT-回归 $L=(y_i-f(x_i))^2$
分裂点使得损失函数最小,与特征尺度无关
使$L=(y_i-f(x_i))^2$最小
选取残差的均值,与特征尺度无关
xgboost $Gain=\frac 1 2 [\frac {G_L^2}{H_L + \lambda} + \frac {G_R^2}{H_R + \lambda} - \frac {(G_L + G_R)^2}{H_L + H_R + \lambda}] - \gamma$
使增益最大,与特征尺度无关
$w_j^* = - \frac {G_j} {H_j + \lambda}$与特征尺度无关

决策树与随机森林

随机森林是从训练集中,不停的抽取子样本集,利用子样本集训练决策树的集成学习。所以归一化是否对随机森林有影响,只需要考察决策树。决策树的分支方式如下

  • 信息增益 (与左右节点中样本的数量有关)

    设训练数据集为$D,|D|$表示其样本容量,即样本个数。设有$K$个类$C_k,k=1,2,…,K$,$|C_k|$为属于类$C_k$的样本个数,。设特征$A$有$n$个不同的取值${a_1,a_2,…,a_n}$,根据特征$A$的取值将$D$划分为$n$个子集$D_1,D_2,…,D_n,|D_i|$为$D_i$的样本个数,$\sum_{i=1}^{n}|D_i|=|D|$。记子集$D_i$中属于类$C_k$的样本的集合为$D_{ik}$,即$D_{ik}=D_i⋂C_k$,$|D_{ik}|$为$D_{ik}$的样本个数。

    • $H(D)=-\sum_{k=1}^{K}\frac{|C_k|}{|D|}log_2\frac{|C_k|}{|D|}$
    • $H(D|A)=-\sum_{i=1}^{n}\frac{|D_i|}{|D|}H(D_i)=-\sum_{i=1}^{n}\frac{|D_i|}{|D|}\sum_{k=1}^{K}\frac{|D_{ik}|}{|D_i|}log_2\frac{|D_{ik}|}{|D_i|}$
      发现信息增益只与不同结点的样本量有关 ,$H(D)$与根结点的总样本量以及不同类别的样本量有关;$H(D|A)$与子结点的总样本量以及不同类别的样本量有关。
  • 信息增益比 (与左右节点中样本的数量有关)

    设特征$A$有$n$个不同的取值${a_1,a_2,…,a_n}$,根据特征$A$的取值将$D$划分为$n$个子集$D_1,D_2,…,D_n,|D_i|$为$D_i$的样本个数。则

    发现信息增益只与不同结点的样本量有关 ,$G(D|A)$与结点的总样本量以及不同类别的样本量有关;$H_A(D)$与父结点的样本量与子结点的样本量有关。

  • CART-基尼指数 (与左右节点中样本的数量有关)

    • $Gini(D)$

      假设有 $K$ 个类,样本点属于第$k$类的概率为$p_k$,则概率分布的基尼指数定义为
      $$
      \begin{align*}

    Gini(p)&=\sum_{k=1}^{K}p_k(1-p_k)=1-\sum_{k=1}^{K}p_k^2\\

    Gini(D)&=1-\sum_{k=1}^{K}\left ( \frac{|c_k|}{D} \right )^2

    \end{align*}

    Gini(D,A)=\frac{|D_1|}{D}Gini(D_1)+\frac{|D_2|}{D}Gini(D_2)
    $$

      **只与每一个类别中样本的数量有关**
    
  • CART-回归

    1. 选择分裂点使所有两个树节点中的损失函数$L$最小

    2. 选择得分使得损失函数$L$最小,得分是$y$的均值

      全程与特征的尺度无关

GBDT

  • 结点分裂。计算残差就是负梯度$r_{m,i}$,根据残差拟合一颗回归树$CART$(回归树的损失函数为平方差损失函数)得到树的结构,由此可知分裂点的选择就是CART回归树的分裂方法,与特征尺度全程无关。
  • 得分。计算得分使损失函数最小,与特征尺度无关

xgboost

$j$为叶子结点的序号,$T$ 为叶子结点的总数 ;$i$ 为样本的序号,$n$ 为样本的总数;$w_q(x_i)$是求取$x_i$权值的对应函数;$\sum_{i \in I_j} g_i$ 为同一叶子结点的 $g_i$ 的和;;$\sum_{=1}^T \sum_{i \in I_j} = \sum_{i=1}^n$。其中

  • 结点分裂:取增益的最大值,发现增益与特征尺度无关。$G_R,G_L$都与特征尺度无关,所以结点的分裂与特征尺度无关

  • 结点的权重:结点的得分与特征尺度无关

归一化、标准化对线性模型的影响

  1. 归一化只是对输入空间进行缩放,并不影响模型的全局最优解以及局部最优解。

    假设输入特征分别为$x_1$、$x_2$,进行标准化之后,使用回归模型进行训练

    由上式可知任何特征经过缩放之后,都可以通过$w,b$变换成之前的模样。所以归一化只是对输入空间进行缩放,并不影响模型的全局最优解以及局部最优解。

  2. 在训练过程中,由于学习率以及初始点,不会根据输入空间的进行等比例缩放。因此很可能会导致模型最终的局部最优解不同。

    例如初始点都选择为$(1,1)$,那么归一化之后的初始点相对于原始空间将发生变化

  3. 在梯度下降过程中可以解决不恰当的学习率、以及初始点导致梯度爆炸等情况。

    因为在特征较多的时候,需要根据特征的不同尺度,选择不同的学习率与初始点,不然很容易发生梯度爆炸的问题。例如,学习率选择为0.01,对于有的特征来讲学习率太小,需要很长的迭代才会拟合;对于有的特征来讲,学习率太大了,会发生梯度爆炸。此时需要对不同的特征设置不同的学习率。进行归一化之后,特征的失度变得一致,梯度也将变得一致,不再需要对不同的特征设置不同的学习率。

    注意:模型的收敛速度与初始点、学习率的选取都有关。大多数情况下,归一化会加快模型的收敛速度;但是某些时候导致收敛速度变慢。

行特征

Normalizer

行特征进行归一化,使行特征的和为1。主要用于TF-IDF等特征。

norm参数 说明
l1 样本各个行特征值除以各个行特征值的绝对值之和
l2 样本各个行特征值除以各个行特征值的平方之和
max 样本各个行特征值除以样本中行特征值最大的值
1
2
3
4
5
6
7
8
9
10
11
>>> from sklearn.preprocessing import Normalizer
>>> X = [[4, 1, 2, 2],
... [1, 3, 9, 3],
... [5, 7, 5, 1]]
>>> transformer = Normalizer().fit(X) # fit does nothing.
>>> transformer
Normalizer(copy=True, norm='l2')
>>> transformer.transform(X)
array([[0.8, 0.2, 0.4, 0.4], #4^2/(4^2+1+2^2+2^2)=0.8
[0.1, 0.3, 0.9, 0.3],
[0.5, 0.7, 0.5, 0.1]])

分类&编码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# 序号编码
from sklearn import preprocessing
enc = preprocessing.OrdinalEncoder()
X = [['male', 'from US', 'uses Safari'], ['female', 'from Europe', 'uses Firefox']]
enc.fit(X) # fit来学习编码
temp=enc.transform([['female', 'from US', 'uses Safari']]) # 进行编码

# 独热编码
from sklearn import preprocessing
enc = preprocessing.OneHotEncoder()
X = [['male', 'from US', 'uses Safari'], ['female', 'from Europe', 'uses Firefox']]
enc.fit(X) # fit来学习编码
temp=enc.transform([['female', 'from US', 'uses Safari']]).toarray() # 进行编码

# 二进制编码

面对分类特征,需要对特征进行编码,不同的编码方式对模型有不同的影响。下面分别介绍几种同编码方式对线性模型与树形模型的影响。例如对于A、B、O型血,其编码方式如下所示:

序号编码 独热编码 二进制编码
编码 1 A
2 B
3 C
1 0 0 A
0 1 0 B
0 0 1 C
1 0 0 A
0 1 0 B
1 1 0 C
编码特点 共同使用一个变量,类别特征具有大小关系 不具有大小关系,每一个变量对应着一个类别 不同类别之间具有某种联系,使得类别无法通过单一变量进行区分
线性模型 不同类别产生了大小关系,使得特征对模型的贡献度呈单调关系 不同类别分配不同的权重,使得每一个类别都有不同的贡献度。是线性模型最好的编码方式 例如类别C为1 1 0,是类别A与类别B的和,对模型贡献度也是两者之和。但是在实际应用中,类别C与类别A、B没有任何关系
多分枝树模型 无影响。一次分裂,就能够将,不同类别放入不同的分支。是多分枝树最好的编码 方式 一次分支能够筛选出一类样本,如果想要筛选出所有样本,需要增加树的深度。本例中,树的深度起码为3。此时与二叉树没有区别。 一次分支能够筛选出部分样本,例如,对第一个分支进行分类,能够识别出A、C与B,其中类别A与类别C在一起;如果想要筛选出所有样本,需要增加树的深度。此时与二叉树没有区别。
二分支树模型 一次分支,无法识别出所有类别或一种类别,其中部分类别绑定在一起。如果想要识别所有类别需要增加树的深度 一次分支,只能识别出来一种类别。如果想要识别所有类别需要增加树的深度。 一次分支,无法识别出所有类别,例如,对第一个分支进行分类,能够识别出A、C与B,其中类别A与类别C在一起。如果想要识别所有类别需要增加树的深度。

注意:多分支树包括ID3、C4.5;二分支树包括CART、GBDT、XGBoost。但是目前sklearn里面的所有树模型都是二分支树模型

离散化

连续特征离散化的优势:

  1. 离散特征的增加和减少都很容易,易于模型的快速迭代;
  2. 稀疏向量内积乘法运算速度快,计算结果方便存储,容易扩展;
  3. 逻辑回归属于广义线性模型,表达能力受限;单变量离散化为N个后,每个变量有单独的权重,相当于为模型引入了非线性,能够提升模型表达能力,加大拟合;例如使用独热编码,此时函数为非线性函数。
  4. 离散化后可以进行特征交叉(多项式化),由M+N个变量变为M*N个变量,进一步引入非线性,提升表达能力;
  5. 离散化后的特征对异常数据有很强的鲁棒性:比如一个特征是年龄>30是1,否则0。如果特征没有离散化,一个异常数据“年龄300岁”会给模型造成很大的干扰。
  6. 特征离散化后,模型会更稳定,比如如果对用户年龄离散化,20-30作为一个区间,不会因为一个用户年龄长了一岁就变成一个完全不同的人。当然处于区间相邻处的样本会刚好相反,所以怎么划分区间是门学问。

李沐曾经说过:模型是使用离散特征还是连续特征,其实是一个“海量离散特征+简单模型” 同 “少量连续特征+复杂模型”的权衡。既可以离散化用线性模型,也可以用连续特征加深度学习。就看是喜欢折腾特征还是折腾模型了。通常来说,前者容易,而且可以n个人一起并行做,有成功经验;后者目前看很赞,能走多远还须拭目以待。

多项式化&高维组合特征

特征多项式化,能够有效的将不同特征组合起来,弥补了线性模型的不足;对于树形模型来说,模型会使用组合特征,当树的深度足够时,可以表达特征多项化的效果。4个特征$(x_1,x_2,x_3,x_4)$,度为2的多项式转换公式如下:

1
2
3
4
5
6
7
8
9
10
11
'''
多项式变换
'''
from sklearn.preprocessing import PolynomialFeatures
import pandas as pd

data = pd.DataFrame({'Integer':[0,1,2,1],'Categorical':[4,5,6,7]})
#多项式转换
#参数degree为度,默认值为2.include_bias常数项
poly = PolynomialFeatures(degree=2,include_bias=False)
temp = poly.fit_transform(data)
  • 对于树形模型来说,模型会使用组合特征,当树的深度足够时,可以表达特征多项化的效果。例如$feature1\times feature2>=500$是$class2$,$feaute1\times feature2<500$是$class1$树模型额表达方式如下所示

      graph TD
    A[feature1]-->|<50|b[feature2] b--="">|<10|c[class1] b--="">|>=10|D[feture1 
    ...] A-->|>=50|E[feature2] E-->|<10|f[feature1 ...] E-->|>=10|G[class2]

缺失值处理

删除数据

1
2
3
4
5
# 删除数据表中含有空值的行
df.dropna(how='any',axis=0)

# 删除缺失值列
df.dropna(how='any',axis=1)
总览 1. 牺牲了大量的数据,通过减少历史数据换取完整的信息,这样可能丢失了很多隐藏的重要信息;
2. 当缺失数据比例较大时,特别是缺失数据非随机分布时,直接删除可能会导致数据发生偏离,比如原本的正态分布变为非正太;
删除样本 删除样本以后,可能造成模型性能下降。当数据量过大时,只能通过实验评估删除数据对模型的影响,难以从理论上进行评估,原因有以下两个:
1. 删除之后样本,发生偏移,正负样本比例发生变化
2. 特征空间发生变化,删除样本之后,导致样本的某些特征分布发生变化
删除特征 删除样本以后,可能造成模型性能下降。当特征过多时,只能通过实验评估删除特征对模型的影响,难以从理论上进行评估,原因如下:
1. 通过计算正负样本在特征中的显著性变化或者IV值变化,可以判断单一特征对模型的影响。所以线性模型可以使用此种方法进行特征选择
2. 但是如果当特征与其他特征组合起来,共同影响模型,此时便难以判断特征的重要性

判断组合特征对于模型的影响。如下所示,性别中存在部分缺失样本,但是可以通过已知性别与年龄,共同判断类别:

graph TD
A[年龄 
范围0到100]-->|<=50|b[性别] b--="">|男
1岁1个,共50个|C[class1] B-->|女
1岁1个,共50个|D[class2] B-->|不详|H[男] A-->|>50|E[性别] E-->|男
1岁1个,共50个|F[class2] E-->|女
1岁1个,共50个|G[class1] E-->|不详|J[女]

如果只有一项特征是无法进行判断的。例如年龄或者只有性别,此时样本呈现无偏分布。所以删除特征难以进行判断,因此根据特征值删除特征需要谨慎。

缺失值填充

操作方法 说明
填充均值、中位数或众数 效果一般,因为等于人为增加了噪声
用其他变量做预测模型来算出缺失变量 效果比方法1略好。有一个根本缺陷:
如果其他变量和缺失变量无关,则预测的结果无意义;如果预测结果相当准确,则又说明这个变量是没必要加入建模的
1
2
3
4
5
6
7
8
9
10
11
12
# 使用sklearn填充缺失值
from sklearn.preprocessing import Imputer
imp=Imputer(missing_values='NaN',strategy='mean',axis=0) #median中位数 most_frequent众数
imp.fit(x)
imp.transform(x)

# 使用pandas填充
data_train.fillna(0) #填充0
data_train.fillna(data_train.mean()) # 将所有行用各自的均值填充
data_train.fillna(data_train.mean()['browse_his':'card_num']) # 也可以指定某些行进行填充
data_train.fillna(method='pad') # 用前一个数据代替NaN:method='pad'
data_train.fillna(method='bfill') # 与pad相反,bfill表示用后一个数据代替NaN
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
# 定义工资改变特征缺失值处理函数,将有变化设为Yes,缺失设为No 
def set_salary_change(df): df.loc[(df.salary_change.notnull()),'salary_change']="Yes" df.loc[(df.salary_change.isnull()),'salary_change']="No" return dfdata_train=set_salary_change(data_train)

# 定义browse_his缺失值预测填充函数

defset_missing_browse_his(df):

# 把已有的数值型特征取出来输入到RandomForestRegressor中
process_df=df[[browse_his' , 'gender', 'job', 'edu', 'marriage', 'family_type']]

# 乘客分成已知该特征和未知该特征两部分
known=process_df[process_df.browse_his.notnull()].as_matrix()
unknown=process_df[process_df.browse_his.isnull()].as_matrix()

# X为特征属性值
X=known[:,1:]
# y为结果标签值
y=known[:,0]

# fit到RandomForestRegressor之中
rfr=RandomForestRegressor(random_state=0,n_estimators=2000,n_jobs=-1)
rfr.fit(X,y)

# 用得到的模型进行未知特征值预测
predicted=rfr.predict(unknown[:,1::])
# 用得到的预测结果填补原缺失数据
df.loc[(df.browse_his.isnull()),'browse_his']=predicted

return df,rfr

data_train,rfr=set_missing_brows

数据填补

对缺失值的插补大体可分为两种:替换缺失值,拟合缺失值,虚拟变量。替换是通过数据中非缺失数据的相似性来填补,其核心思想是发现相同群体的共同特征,拟合是通过其他特征建模来填补,虚拟变量是衍生的新变量代替缺失值。

替换缺失值

  • 均值插补:

对于定类数据:使用 众数(mode)填补,比如一个学校的男生和女生的数量,男生500人,女生50人,那么对于其余的缺失值我们会用人数较多的男生来填补。

对于定量(定比)数据:使用平均数(mean)或中位数(median)填补,比如一个班级学生的身高特征,对于一些同学缺失的身高值就可以使用全班同学身高的平均值或中位数来填补。一般如果特征分布为正太分布时,使用平均值效果比较好,而当分布由于异常值存在而不是正太分布的情况下,使用中位数效果比较好。

注:此方法虽然简单,但是不够精准,可能会引入噪声,或者会改变特征原有的分布。 下图左为填补前的特征分布,图右为填补后的分布,明显发生了畸变。因此,如果缺失值是随机性的,那么用平均值比较适合保证无偏,否则会改变原分布。

1
2
3
4
Python中的使用:
#使用price均值对NA进行填充
df['price'].fillna(df['price'].mean())
df['price'].fillna(df['price'].median())

存在缺失值时的算法选择

主流的机器学习模型千千万,很难一概而论。但有一些经验法则(rule of thumb)供参考:

  • 树模型对于缺失值的敏感度较低,大部分时候可以在数据有缺失时使用。
  • 涉及到距离度量(distance measurement)时,如计算两个点之间的距离,缺失数据就变得比较重要。因为涉及到“距离”这个概念,那么缺失值处理不当就会导致效果很差,如K近邻算法(KNN)和支持向量机(SVM)。
  • 线性模型的代价函数(loss function)往往涉及到距离(distance)的计算,计算预测值和真实值之间的差别,这容易导致对缺失值敏感。
  • 神经网络的鲁棒性强,对于缺失数据不是非常敏感,但一般没有那么多数据可供使用。
  • 贝叶斯模型对于缺失数据也比较稳定,数据量很小的时候首推贝叶斯模型。

总结来看,对于有缺失值的数据在经过缺失值处理后:

  • 数据量很小,用朴素贝叶斯
  • 数据量适中或者较大,用树模型,优先 xgboost
  • 数据量较大,也可以用神经网络
  • 避免使用距离度量相关的模型,如KNN和SVM

三、处理缺失值的算法

(一)决策树模型怎么处理异常值?

随机森林(Random Forests)的提出者Leo Breiman提出两种解决缺失值的方法:

  • 方法1(快速简单但效果差):把数值型变量(numerical variables)中的缺失值用其所对应的类别中(class)的中位数(median)替换。把类别型变量(categorical variables)缺失的部分用所对应类别中出现最多的数值替代(most frequent non-missing value)
  • 方法2(耗时费力但效果好):虽然依然是使用中位数和出现次数最多的数来进行替换,方法2引入了权重。即对需要替换的数据先和其他数据做相似度测量(proximity measurement),在补全缺失点是相似的点的数据会有更高的权重W。

(二)xgboost怎么处理缺失值?

xgboost模型能够处理缺失值,也就是说模型允许缺失值存在。

论文中关于缺失值的处理: 将其看与稀疏矩阵的处理看作一样。在寻找split point的时候,不会对该特征为missing的样本进行遍历统计,只对该列特征值为non-missing的样本上对应的特征值进行遍历,通过这个技巧来减少了为稀疏离散特征寻找split point的时间开销。

在逻辑实现上,为了保证完备性,会分别处理将missing该特征值的样本分配到左叶子结点和右叶子结点的两种情形,计算增益后选择增益大的方向进行分裂即可。可以为缺失值或者指定的值指定分支的默认方向,这能大大提升算法的效率。如果在训练中没有缺失值而在预测中出现缺失,那么会自动将缺失值的划分方向放到右子树。