avatar


2.特征预处理

关于该部分,在《机器学习实战方法(Python):特征工程-1.特征预处理》,有更详细的讨论。

特征预处理在不同的数据下有不同的方法,常见有这么几种:

  1. 对于数值型数据:
    1. 归一化
    2. 标准化
    3. 缺失值的处理
  2. 对于时间型数据:
    1. 时间的切分
  3. 对于类别型数据:
    1. One-Hot编码

其中One-Hot编码在上一章已经讨论过了,我们这里主要讨论其他几种方法。

我们用scikit-learnPandas进行特征的预处理。

归一化

首先,我们讨论归一化

为什么要做归一化

假设存在如下的数据:

城市 面积(平方千米) 机场数量 火车站数量
上海 6340.5 2 4
南昌 7402 1 2
北京 16410.54 2 5

我们现在想用两点之间的距离公式,来衡量不同城市之间的相似度。显然,城市面积在这里占了非常大的比重,甚至我们只需要比较城市面积即可。
但,假如三个特征同等重要。这时候便需要进行归一化。
即:归一化的目的是避免某个特征对结果造成了更大的影响。
特别注意:不是所有算法都需要对数据进行归一化。概率模型(树形模型)不需要归一化,因为它们不关心变量的值,而是关心变量的分布和变量之间的条件概率,如决策树、随机森林。

我们会有专门的文章讨论决策树和随机森林:

归一化的计算

归一化的定义:通过数学方法把原始数据映射到一定的区间之间,默认[0,1]。
归一化的公式

x=xminmaxminx' = \frac{x - min}{max - min}

x=x(mxmi)+mix'' = x' * (mx - mi) + mi

其中

  • maxmax代表所在列的最大值
  • minmin代表所在列的最小值
  • mxmx代表指定区间的上限,默认为1
  • mimi代表指定区间的下限,默认为0

以上述的数据为例。
上海面积在进行归一化后

x=6340.56340.516410.546340.5=0x' = \frac{6340.5 - 6340.5}{16410.54 - 6340.5} = 0

x=0(10)+0=0x'' = 0 * (1 - 0) + 0 = 0

MinMaxScaler

我们用scikit-learn工具进行进行归一化。

1
from sklearn.preprocessing import MinMaxScaler

示例代码:

1
2
3
4
from sklearn.preprocessing import MinMaxScaler
mm = MinMaxScaler()
data = [[6340.5,2,4],[7402,1,2],[16410.54,2,5]]
print(mm.fit_transform(data))

运行结果:

1
2
3
[[0.         1.         0.66666667]
[0.1054117 0. 0. ]
[1. 1. 1. ]]

我们也可以把

1
mm = MinMaxScaler()

修改为

1
mm = MinMaxScaler(feature_range=[0,10])

以此来指定区间。
运行结果:

1
2
3
[[ 0.         10.          6.66666667]
[ 1.05411696 0. 0. ]
[10. 10. 10. ]]

归一化的缺点

假设数据中存在异常点,例如:

城市 面积(平方千米) 机场数量 火车站数量
上海 6340.5 2 4
南昌 7402 1 2
北京 16410.54 2 5
异常 100000000 8 -10000

我们在对数据进行归一化之后,结果如下

1
2
3
4
[[0.00000000e+00 1.42857143e+00 9.99900050e+00]
[1.06156731e-04 0.00000000e+00 9.99700150e+00]
[1.00706785e-03 1.42857143e+00 1.00000000e+01]
[1.00000000e+01 1.00000000e+01 0.00000000e+00]]

这时候,我们看到因为存在一个面积为100000000异常,导致其他城市的面积都非常接近0,显然,面积在这里的作用微乎其微。
同理,因为存在一个火车站数量为-10000异常,导致其他城市的火车站数量也都非常接近1,显然,火车站数量在这里的几乎没有作用。
即,我们得出结论如下:
因为最大值与最小值非常容易受异常点影响,所以归一化的健壮性较差。通常,归一化只适合精确小数据场景。

标准化

为避免归一化缺点所造成的影响,我们引入标准化。

标准化的计算

标准化的定义:通过数学方法把原始数据映射到均值为0,方差为1的范围内。
标准化的公式

x=xmeanσx' = \frac{x - mean}{\sigma}

其中

  • meanmean代表平均值
  • σ\sigma代表标准差

方差=(x1mean)2+(x2mean)2+样本总数\text{方差} = \frac{(x_1 - mean)^2 + (x_2 - mean)^2 + ···}{\text{样本总数}}

标准差=方差\text{标准差} = \sqrt{\text{方差}}

这里和正态分布标准化公式进行比较。都是减去均值,除以标准差。
若随机变量XX的概率密度函数为

p(x)=12πσe(xμ)22σ2,<x<p(x) = \frac{1}{\sqrt{2\pi}\sigma}e^{\frac{(x-\mu)^2}{2\sigma^2}}, -\infty < x < \infty

则称随机变量XX服从均值为μ\mu,标准差为σ\sigma的正态分布,计作XN(μ,σ2)X \sim N(\mu,\sigma^2)
μ=0\mu=0σ=1\sigma=1时的正态分布N(0,1)N(0,1)被称为标准正态分布。
正态分布的标准化公式为
XN(μ,σ)X \sim N(\mu,\sigma)

U=XμσU=\frac{X - \mu}{\sigma}

则,UN(0,1)U \sim N(0,1)

同样,我们以这个数据为例

城市 面积(平方千米) 机场数量 火车站数量
上海 6340.5 2 4
南昌 7402 1 2
北京 16410.54 2 5

上海的飞机场的数量=253(253)2+(153)2+(253)23=220.7071\text{上海的飞机场的数量} = \frac{2-\frac{5}{3}}{\sqrt{\frac{(2-\frac{5}{3})^2 + (1-\frac{5}{3})^2 + (2-\frac{5}{3})^2}{3}}} = \frac{\sqrt{2}}{2} \approx 0.7071

StandardScaler

我们可以用scikit-learn工具进行标准化。

1
from  sklearn.preprocessing import StandardScaler

示例代码:

1
2
3
4
from  sklearn.preprocessing import StandardScaler
ss = StandardScaler()
data = [[6340.5,2,4],[7402,1,2],[16410.54,2,5]]
print(ss.fit_transform(data))

运行结果:

1
2
3
[[-0.8213285   0.70710678  0.26726124]
[-0.58636365 -1.41421356 -1.33630621]
[ 1.40769214 0.70710678 1.06904497]]

示例代码:

1
2
3
4
5
from sklearn.preprocessing import StandardScaler

ss = StandardScaler()
data = [[6340.5, 2, 4], [7402, 1, 2], [16410.54, 2, 5], [100000000, 8, -10000]]
print(ss.fit_transform(data))

运行结果:

1
2
3
4
[[-0.57743597 -0.45083482  0.5774272 ]
[-0.57741145 -0.81150267 0.57696549]
[-0.57720339 -0.45083482 0.57765806]
[ 1.7320508 1.71317231 -1.73205075]]

这看起来,也没有解决问题啊。
因为!要求在大样本。

归一化和标准化的比较

  • 对于归一化,如果出现了异常点,影响了最大值和最小值,会对结果产生显著影响。
  • 对于标准化,在大样本的情况下,少量的异常点不会对平均值产生显著影响,也不会对方差产生显著影响,所以也不会对结果产生的显著影响。
    • 特别注意,是指在大样本,少量异常点的情况。

缺失值的处理

缺失值的处理方法

  1. 删除,当某行或某列的缺失值到达一定比例的时候,考虑删除整行或整列。
  2. 插补,用中位数、平均数或众数来填补缺失值。

实际上,对于缺失值的处理,还很有很多种方法。比如通过kNN进行缺失值的填补。

缺失值的处理工具

我们有两种工具可以处理缺失值。

  1. scikit-learn
  2. pandas

scikit-learn

网上会充斥这大量的这种方法。

1
from sklearn.preprocessing import Imputer

但是,实际上,在scikit-learn 0.22.2中,已经没有sklearn.preprocessing.Imputer了。
取而代之的是

1
from sklearn.impute import SimpleImputer

示例代码:

1
2
3
4
5
from sklearn.impute import SimpleImputer
import numpy as np
im = SimpleImputer(missing_values=np.nan,strategy='mean')
data = [[6340.5,2,4],[7402,np.nan,2],[16410.54,2,5]]
print(im.fit_transform(data))

运行结果:

1
2
3
[[6.340500e+03 2.000000e+00 4.000000e+00]
[7.402000e+03 2.000000e+00 2.000000e+00]
[1.641054e+04 2.000000e+00 5.000000e+00]]

pandas

我们基于pandas.DataFrame进行缺失值的操作,实际上更多的时候,我们也是用pandas进行缺失值处理,而不是scikit-learn

关于pandas的更多讨论,我们可以参考《用Python分析数据的方法和技巧:3.pandas》

在这里,我们主要讨论这些方法

  1. DataFrame
  2. isnull
  3. notnull
  4. 用布尔索引进行处理
  5. 用dropna进行处理
  6. 用fillna进行处理
  7. 特殊字符的处理

DataFrame

示例代码:

1
2
3
4
5
6
7
import pandas as pd
import numpy as np
df = pd.DataFrame({'city': ['上海', '南昌', '北京'],
'area': [6340.5,7402,16410.54],
'airport': [2, np.NaN, 2],
'station': [4,2, 5]})
print(df)

运行结果:

1
2
3
4
  city      area  airport  station
0 上海 6340.50 2.0 4
1 南昌 7402.00 NaN 2
2 北京 16410.54 2.0 5

isnull

示例代码:

1
print(pd.isnull(df))

运行结果:

1
2
3
4
    city   area  airport  station
0 False False False False
1 False False True False
2 False False False False

notnull

示例代码:

1
print(pd.notnull(df))

运行结果:

1
2
3
4
   city  area  airport  station
0 True True True True
1 True True False True
2 True True True True

用布尔索引进行处理

示例代码:

1
print(df[pd.notnull(df['airport'])])

运行结果:

1
2
3
  city      area  airport  station
0 上海 6340.50 2.0 4
2 北京 16410.54 2.0 5

用dropna进行处理

处理DataFrame

示例代码:

1
2
3
print(df.dropna(axis=0))
print(df.dropna(axis=0,how='any'))
print(df.dropna(axis=0,how='all'))

运行结果:

1
2
3
4
5
6
7
8
9
10
  city      area  airport  station
0 上海 6340.50 2.0 4
2 北京 16410.54 2.0 5
city area airport station
0 上海 6340.50 2.0 4
2 北京 16410.54 2.0 5
city area airport station
0 上海 6340.50 2.0 4
1 南昌 7402.00 NaN 2
2 北京 16410.54 2.0 5

处理Series对象

示例代码:

1
2
3
se=pd.Series([4,None,8,None,5])
print(se)
se.dropna()

运行结果:

1
2
3
4
5
6
7
8
9
10
11
0    4.0
1 NaN
2 8.0
3 NaN
4 5.0
dtype: float64

0 4.0
2 8.0
4 5.0
dtype: float64

默认how='any'

默认how='any',去除任意列为None的行。

how='any',示例代码:

1
2
3
4
df=pd.DataFrame([[1,2,3],[None,None,2],[None,None,None],[8,8,None]])
print(df)
df = df.dropna(how='any')
print(df)

运行结果:

1
2
3
4
5
6
7
     0    1    2
0 1.0 2.0 3.0
1 NaN NaN 2.0
2 NaN NaN NaN
3 8.0 8.0 NaN
0 1 2
0 1.0 2.0 3.0

默认how='any',示例代码:

1
2
3
4
df=pd.DataFrame([[1,2,3],[None,None,2],[None,None,None],[8,8,None]])
print(df)
df = df.dropna()
print(df)

运行结果:

1
2
3
4
5
6
7
     0    1    2
0 1.0 2.0 3.0
1 NaN NaN 2.0
2 NaN NaN NaN
3 8.0 8.0 NaN
0 1 2
0 1.0 2.0 3.0

how=‘all’

how='all',过滤全为NaN的行。

示例代码:

1
2
3
4
df=pd.DataFrame([[1,2,3],[None,None,2],[None,None,None],[8,8,None]])
print(df)
df = df.dropna(how='all')
print(df)

运行结果:

1
2
3
4
5
6
7
8
9
     0    1    2
0 1.0 2.0 3.0
1 NaN NaN 2.0
2 NaN NaN NaN
3 8.0 8.0 NaN
0 1 2
0 1.0 2.0 3.0
1 NaN NaN 2.0
3 8.0 8.0 NaN

axis=1

axis=1,滤除列。

axis=1,示例代码:

1
2
3
4
df=pd.DataFrame([[1,2,3],[None,None,2],[None,None,None],[8,8,None]])
print(df)
df = df.dropna(axis=1)
print(df)

运行结果:

1
2
3
4
5
6
7
8
     0    1    2
0 1.0 2.0 3.0
1 NaN NaN 2.0
2 NaN NaN NaN
3 8.0 8.0 NaN
Empty DataFrame
Columns: []
Index: [0, 1, 2, 3]

df.dropna(axis=1,how="all"),示例代码:

1
2
3
4
df=pd.DataFrame([[1,2,3,None],[None,None,2,None],[None,None,None,None],[8,8,None,None]])
print(df)
df = df.dropna(axis=1,how="all")
print(df)

运行结果:

1
2
3
4
5
6
7
8
9
10
     0    1    2     3
0 1.0 2.0 3.0 None
1 NaN NaN 2.0 None
2 NaN NaN NaN None
3 8.0 8.0 NaN None
0 1 2
0 1.0 2.0 3.0
1 NaN NaN 2.0
2 NaN NaN NaN
3 8.0 8.0 NaN

subset

subset,指定列。

示例代码:

1
2
3
4
df=pd.DataFrame([[1,2,3,None],[None,None,2,None],[None,None,None,None],[8,8,None,None]])
print(df)
df = df.dropna(subset=[2],how="any")
print(df)

运行结果:

1
2
3
4
5
6
7
8
     0    1    2     3
0 1.0 2.0 3.0 None
1 NaN NaN 2.0 None
2 NaN NaN NaN None
3 8.0 8.0 NaN None
0 1 2 3
0 1.0 2.0 3.0 None
1 NaN NaN 2.0 None

inplace=True

inplace=True,原地修改。

示例代码:

1
2
3
4
df=pd.DataFrame([[1,2,3,None],[None,None,2,None],[None,None,None,None],[8,8,None,None]])
print(df)
df.dropna(subset=[2],how="any",inplace=True)
print(df)

运行结果:

1
2
3
4
5
6
7
8
     0    1    2     3
0 1.0 2.0 3.0 None
1 NaN NaN 2.0 None
2 NaN NaN NaN None
3 8.0 8.0 NaN None
0 1 2 3
0 1.0 2.0 3.0 None
1 NaN NaN 2.0 None

注意,这时候一定不能用参数去接收。示例代码:

1
2
3
4
df=pd.DataFrame([[1,2,3,None],[None,None,2,None],[None,None,None,None],[8,8,None,None]])
print(df)
df=df.dropna(subset=[2],how="any",inplace=True)
print(df)

运行结果:

1
2
3
4
5
6
     0    1    2     3
0 1.0 2.0 3.0 None
1 NaN NaN 2.0 None
2 NaN NaN NaN None
3 8.0 8.0 NaN None
None

用fillna进行处理

快速开始

示例代码:

1
2
3
4
5
print(df.fillna(0))
print(df.fillna(df.mean()))
# 只操作某一列
df['airport'] = df['airport'].fillna(df['airport'].mean())
print(df)

运行结果:

1
2
3
4
5
6
7
8
9
10
11
12
  city      area  airport  station
0 上海 6340.50 2.0 4
1 南昌 7402.00 0.0 2
2 北京 16410.54 2.0 5
city area airport station
0 上海 6340.50 2.0 4
1 南昌 7402.00 2.0 2
2 北京 16410.54 2.0 5
city area airport station
0 上海 6340.50 2.0 4
1 南昌 7402.00 2.0 2
2 北京 16410.54 2.0 5

参数详解

1
fillna(value=None, method=None, axis=None, inplace=False, limit=None)
  • value: 表示填充的值,可以是一个指定值,也可以是字典, Series或DataFrame。
  • method: 填充的方式,默认为None,有ffillpadbfillbfill四种填充方式可以使用。
    • ffillpad表示用缺失值的前一个值填充,如果axis=0,则用空值上一行的值填充,如果axis=1,则用空值左边的值填充。假如空值在第一行或第一列,以及空值前面的值全都是空值,则无法获取到可用的填充值,填充后依然保持空值。
    • bfillbackfill表示用缺失值的后一个值填充,axis的用法以及找不到填充值的情况同ffillpad
    • 注意!当指定填充方式method时,不能同时指定填充值value,否则报错。
  • axis: 通常配合method参数使用,axis=0表示按行,axis=1表示按列。
  • limit: 表示填充执行的次数。如果是按行填充,则填充一行表示执行一次,按列同理。

填充计算值

填充列的平均值,示例代码:

1
2
3
4
5
6
7
8
df = pd.DataFrame([[np.nan, 2, np.nan, 0],
[3, 4, np.nan, 1],
[np.nan, np.nan, np.nan, 5],
[np.nan, 3, np.nan, 4]],
columns=list('ABCD'))
print(df)
df = df.fillna(df.mean())
print(df)

运行结果:

1
2
3
4
5
6
7
8
9
10
     A    B   C  D
0 NaN 2.0 NaN 0
1 3.0 4.0 NaN 1
2 NaN NaN NaN 5
3 NaN 3.0 NaN 4
A B C D
0 3.0 2.0 NaN 0
1 3.0 4.0 NaN 1
2 3.0 3.0 NaN 5
3 3.0 3.0 NaN 4

和groupby配合使用

假设存在数据如下:

类别 col1 col2 col3
A NaN NaN e
A d NaN NaN
A NaN NaN NaN
A NaN c NaN
B NaN Y NaN
B NaN NaN NaN
B X NaN Z

需要根据类别排序,对缺失值进行填充,如下:

类别 col1 col2 col3
A d c e
A d c e
A d c e
A d c e
B X Y Z
B X Y Z
B X Y Z

分析,有些在前面的行,有些在后面的行。
思路,用groupby进行分组,分组后填充数据用method='bfill'先向后填充,再用method='ffill'向前填充,以此完成所有缺失值位置的填充。

示例代码:

1
2
3
4
5
6
7
8
9
10
11
12
df = pd.DataFrame([['A', None, None, 'e'],
['A', 'd', None, None],
['A', None, None, None],
['A', None, 'c', None],
['B', None, 'Y', None],
['B', None, None, None],
['B', 'X', None, 'Z']],
columns=['类别', 'col1', 'col2', 'col3'])
print(df)
df = df.groupby('类别', group_keys=False).apply(lambda x: x.fillna(method='bfill'))
df = df.groupby('类别', group_keys=False).apply(lambda x: x.fillna(method='ffill'))
print(df)

运行结果:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
  类别  col1  col2  col3
0 A None None e
1 A d None None
2 A None None None
3 A None c None
4 B None Y None
5 B None None None
6 B X None Z
类别 col1 col2 col3
0 A d c e
1 A d c e
2 A d c e
3 A d c e
4 B X Y Z
5 B X Y Z
6 B X Y Z

还可以简写,示例代码:

1
2
3
4
5
6
7
8
9
10
11
df = pd.DataFrame([['A', None, None, 'e'],
['A', 'd', None, None],
['A', None, None, None],
['A', None, 'c', None],
['B', None, 'Y', None],
['B', None, None, None],
['B', 'X', None, 'Z']],
columns=['类别', 'col1', 'col2', 'col3'])
print(df)
df = df.groupby('类别', group_keys=False).apply(lambda x: x.fillna(method='bfill').fillna(method='ffill'))
print(df)

运行结果:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
  类别  col1  col2  col3
0 A None None e
1 A d None None
2 A None None None
3 A None c None
4 B None Y None
5 B None None None
6 B X None Z
类别 col1 col2 col3
0 A d c e
1 A d c e
2 A d c e
3 A d c e
4 B X Y Z
5 B X Y Z
6 B X Y Z

指定列

示例代码:

1
2
3
4
5
6
7
8
9
10
11
12
df_dic = {
'g': [1, 1, 2, 2, 1],
'a': [1, 2, 11, None, None],
'b': [3, None, 12, None, None],
'c': [31, None, 32, None, None]
}

df = pd.DataFrame(df_dic)
print(df)

df['b'] = df.groupby('g', group_keys=False)['b'].apply(lambda x: x.fillna(method='ffill'))
print(df)

运行结果:

1
2
3
4
5
6
7
8
9
10
11
12
   g     a     b     c
0 1 1.0 3.0 31.0
1 1 2.0 NaN NaN
2 2 11.0 12.0 32.0
3 2 NaN NaN NaN
4 1 NaN NaN NaN
g a b c
0 1 1.0 3.0 31.0
1 1 2.0 3.0 NaN
2 2 11.0 12.0 32.0
3 2 NaN 12.0 NaN
4 1 NaN 3.0 NaN

特殊字符的处理

这里我们以?为例。演示一个小技巧,即replace函数。
示例代码:

1
2
3
4
5
6
7
8
9
10
import pandas as pd
import numpy as np
df = pd.DataFrame({'city': ['上海', '南昌', '北京'],
'area': [6340.5,7402,16410.54],
'airport': [2, '?', 2],
'station': [4,2, 5]})
print(df)
print(pd.notnull(df))
df = df.replace('?',np.nan)
print(df.fillna(df.mean()))

运行结果:

1
2
3
4
5
6
7
8
9
10
11
12
  city      area airport  station
0 上海 6340.50 2 4
1 南昌 7402.00 ? 2
2 北京 16410.54 2 5
city area airport station
0 True True True True
1 True True True True
2 True True True True
city area airport station
0 上海 6340.50 2.0 4
1 南昌 7402.00 2.0 2
2 北京 16410.54 2.0 5

时间的切分

时间的切分同样基于pandas,举例如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import pandas as pd
df = pd.DataFrame({'姓名': ['甲', '乙', '丙'],'交易时间': [pd.Timestamp('2020-02-02 11:22:33'),pd.Timestamp('2019-03-03 12:23:34'),pd.Timestamp('2018-08-08 08:09:10')]})
print(df)
rnt['日期'] =df['交易时间'].dt.date
rnt['时间'] =df['交易时间'].dt.time
rnt['年'] = df['交易时间'].dt.year
rnt['季节'] = df['交易时间'].dt.quarter
rnt['月'] = df['交易时间'].dt.month
rnt['周']=df['交易时间'].dt.week
rnt['日'] = df['交易时间'].dt.day
rnt['小时'] =df['交易时间'].dt.hour
rnt['分钟'] =df['交易时间'].dt.minute
rnt['秒'] = df['交易时间'].dt.second
rnt['一年第几天'] =df['交易时间'].dt.dayofyear
rnt['一年第几周'] = df['交易时间'].dt.weekofyear
rnt['一周第几天'] = df['交易时间'].dt.dayofweek
rnt['一个月含有多少天'] = df['交易时间'].dt.days_in_month
rnt['星期名称'] =df['交易时间'].dt.weekday_name
print(rnt)

运行结果:
为方便阅读,运行结果以表格的形式展示。

姓名 交易时间 日期 时间 季节 小时 分钟 一年第几天 一年第几周 一周第几天 一个月含有多少天 星期名称
0 2020-02-02 11:22:33 2020-02-02 11:22:33 2020 1 2 5 2 11 22 33 33 5 6 29 Sunday
1 2019-03-03 12:23:34 2019-03-03 12:23:34 2019 1 3 9 3 12 23 34 62 9 6 31 Sunday
2 2018-08-08 08:09:10 2018-08-08 08:09:10 2018 3 8 32 8 8 9 10 220 32 2 31 Wednesday

特别注意:在部分新版本的pandas上可能会报错AttributeError: 'DatetimeProperties' object has no attribute 'weekday_name'
把倒数第二行的

1
rnt['星期名称'] =df['交易时间'].dt.weekday_name
修改为
1
rnt['星期名称'] =df['交易时间'].dt.day_name()
即可。
文章作者: Kaka Wan Yifan
文章链接: https://kakawanyifan.com/10202
版权声明: 本博客所有文章版权为文章作者所有,未经书面许可,任何机构和个人不得以任何形式转载、摘编或复制。

评论区