特征工程之缺失值处理

缺失值处理

一般来说,未经处理的原始数据中通常会存在缺失值、离群值等,因此在建模训练之前需要处理好缺失值。
缺失值处理方法一般可分为:删除、统计值填充、统一值填充、前后向值填充、插值法填充、建模预测填充和具体分析7种方法。

直接删除

理论部分
缺失值最简单的处理方法是删除,所谓删除就是删除属性或者删除样本,删除一般可分为两种情况:

  1. 删除属性(特征)
    如果某一个特征中存在大量的缺失值(缺失量大于总数据量的40%~50%及以上),
    那么我们可以认为这个特征提供的信息量非常有限,这个时候可以选择删除掉这一维特征。

  2. 删除样本
    如果整个数据集中缺失值较少或者缺失值数量对于整个数据集来说可以忽略不计的情况下,
    那么可以直接删除含有缺失值的样本记录。

注意事项:
如果数据集本身数据量就很少的情况下,不建议直接删除缺失值。

代码实现
构造假数据做演示,就上面两种情况进行代码实现删除。

import numpy as np
import pandas as pd

# 构造数据
def dataset():
    col1 = [1, 2, 3, 4, 5, 6, 7, 8, 9,10]
    col2 = [3, 1, 7, np.nan, 4, 0, 5, 7, 12, np.nan]
    col3 = [3, np.nan, np.nan, np.nan, 9, np.nan, 10, np.nan, 4, np.nan]
    y = [10, 15, 8, 12, 17, 9, 7, 14, 16, 20]
    data = {'feature1':col1, 'feature2':col2, 'feature3':col3, 'label':y}
    df = pd.DataFrame(data)
    return df

data = dataset()
data
feature1feature2feature3label
013.03.010
121.0NaN15
237.0NaN8
34NaNNaN12
454.09.017
560.0NaN9
675.010.07
787.0NaN14
8912.04.016
910NaNNaN20
data.isnull().sum()
feature1    0
feature2    2
feature3    6
label       0
dtype: int64
# 删除属性
def delete_feature(df):
    N = df.shape[0]  # 样本数
    no_nan_count = df.count().to_frame().T  # 每一维特征非缺失值的数量
    del_feature, save_feature = [], []
    for col in no_nan_count.columns.tolist():
        loss_rate = (N - no_nan_count[col].values[0])/N  # 缺失率
        # print(loss_rate)
        if loss_rate > 0.5:  # 缺失率大于 50% 时,将这一维特征删除
            del_feature.append(col)
        else:
            save_feature.append(col)
    return del_feature, df[save_feature]

del_feature, df11 = delete_feature(data)
print(del_feature)
df11
['feature3']
feature1feature2label
013.010
121.015
237.08
34NaN12
454.017
560.09
675.07
787.014
8912.016
910NaN20

从上面可以看出,feature2 的缺失值较少,可以采取直接删除措施

# 删除样本
def delete_sample(df):
    df_ = df.dropna()
    return df_

delete_sample(df11)
feature1feature2label
013.010
121.015
237.08
454.017
560.09
675.07
787.014
8912.016

统计值填充

理论部分

  1. 对于特征的缺失值,可以根据缺失值所对应的那一维特征的统计值来进行填充。

  2. 统计值一般泛指平均值、中位数、众数、最大值、最小值等,具体使用哪一种统计值要根据具体问题具体分析。

  3. 注意事项:当特征之间存在很强的类别信息时,需要进行类内统计,效果比直接处理会更好。
    比如在填充身高时,需要先对男女进行分组聚合之后再进行统计值填充处理(男士的一般平均身高1.70,女士一般1.60)。

代码实现
使用上面数据帧 df11 作为演示数据集,分别实现使用各个统计值填充缺失值。

# 使用上面 df11 的数据帧作为演示数据
df11
feature1feature2label
013.010
121.015
237.08
34NaN12
454.017
560.09
675.07
787.014
8912.016
910NaN20
# 均值填充
print(df11.mean())
df11.fillna(df11.mean())
feature1     5.500
feature2     4.875
label       12.800
dtype: float64
feature1feature2label
013.00010
121.00015
237.0008
344.87512
454.00017
560.0009
675.0007
787.00014
8912.00016
9104.87520
# 中位数填充
print(df11.median())
df11.fillna(df11.median())
feature1     5.5
feature2     4.5
label       13.0
dtype: float64
feature1feature2label
013.010
121.015
237.08
344.512
454.017
560.09
675.07
787.014
8912.016
9104.520

由于众数可能会存在多个,因此返回的是序列而不是一个值所以在填充众数的时候,我们可以 df11[‘feature’].mode()[0],可以取第一个众数作为填充值

# 众数填充
print(df11.mode())

def mode_fill(df):
    for col in df.columns.tolist():
        if df[col].isnull().sum() > 0:  # 有缺失值就进行众数填充
            df_ = df.fillna(df11[col].mode()[0])
            
    return df_

mode_fill(df11)   
   feature1  feature2  label
0         1       7.0      7
1         2       NaN      8
2         3       NaN      9
3         4       NaN     10
4         5       NaN     12
5         6       NaN     14
6         7       NaN     15
7         8       NaN     16
8         9       NaN     17
9        10       NaN     20
feature1feature2label
013.010
121.015
237.08
347.012
454.017
560.09
675.07
787.014
8912.016
9107.020
# 最大值/最小值填充
df11.fillna(df11.max())
df11.fillna(df11.min())
feature1feature2label
013.010
121.015
237.08
340.012
454.017
560.09
675.07
787.014
8912.016
9100.020

统一值填充

理论部分

  1. 对于缺失值,把所有缺失值都使用统一值作为填充词,所谓统一值是指自定义指定的某一个常数
  2. 常用的统一值有:空值、0、正无穷、负无穷或者自定义的其他值
  3. 注意事项:当特征之间存在很强的类别信息时,需要进行类内统计,效果比直接处理会更好。
    比如在填充身高时,需要先对男女进行分组聚合之后再进行统一值填充处理
    (男士的身高缺失值使用统一填充值就自定为常数1.70,女士自定义常数1.60)。

代码实现
任然使用数据帧 df11 进行演示,实现统一值填充缺失值的应用。

df11
feature1feature2label
013.010
121.015
237.08
34NaN12
454.017
560.09
675.07
787.014
8912.016
910NaN20
# 统一值填充
# 自定义统一值常数为 10
df11.fillna(value=10)
feature1feature2label
013.010
121.015
237.08
3410.012
454.017
560.09
675.07
787.014
8912.016
91010.020

前后向值填充

理论部分
前后向值填充是指使用缺失值的前一个或者后一个的值作为填充值进行填充。

代码实现
任然使用数据帧 df11 作为演示的数据集,实现前后向值填充。

df11
feature1feature2label
013.010
121.015
237.08
34NaN12
454.017
560.09
675.07
787.014
8912.016
910NaN20
df11.fillna(method='ffill')  # 前向填充
feature1feature2label
013.010
121.015
237.08
347.012
454.017
560.09
675.07
787.014
8912.016
91012.020

从上面的后向填充我们发现明显的 Bug:
如果最后一个是缺失值,那么后向填充无法处理最后一个的缺失值;
如果第一个是缺失值,那么前向填充无法处理第一个的缺失值。

因此在进行前后向值填充时,要根据具体情况来进行填充,
一般同时进行前向填充+后向填充就可以解决上面的问题。

插值法填充

工作原理
所谓的插值法,就是在X范围区间中挑选一个或者自定义一个数值,
然后代进去插值模型公式当中,求出数值作为缺失值的数据。

** 1. 多项式插值**
理论公式及推导
已知n+1个互异的点 P 1 : ( x 1 , y 1 ) , P 2 : ( x 2 , y 2 ) , . . . , P n + 1 : ( x n + 1 , y n + 1 ) P1:(x1,y1),P2:(x2,y2),...,P_{n+1}:(x_{n+1},y_{n+1}) P1:(x1,y1)P2:(x2,y2)...Pn+1:(xn+1,yn+1)
可以求得经过这n+1个点,最高次不超过n的多项式 Y = a 0 + a 1 X + a 2 X 2 + . . . + a n X n Y=a_0+a_1X+a_2X^2+...+a_nX^n Y=a0+a1X+a2X2+...+anXn
其中计算系数A的公式如下:
A = [ a 0 , a 1 , . . . , a n ] T = X − 1 Y , 其 中 X − 1 是 X 的 逆 矩 阵 A=[a_0,a_1,...,a_n]^T=X^{-1}Y,其中X^{-1}是X的逆矩阵 A=[a0,a1,...,an]T=X1YX1X
(1)其中X,Y形式如下,求待定系数A:
X = [ 1 x 1 x 1 2 . . . x 1 n 1 x 2 x 2 2 . . . x 2 n . . . . . 1 x n + 1 x n + 1 2 . . . x n + 1 n ] , Y = [ y 1 y 2 . y n + 1 ] X=\begin{bmatrix} 1&x_1&x_1^2&...&x_1^n\\ 1&x_2&x_2^2&...&x_2^n\\ .&.&.&.&.\\ 1&x_{n+1}&x_{n+1}^2&...&x_{n+1}^n \end{bmatrix},Y=\begin{bmatrix} y_1&\\ y_2&\\ .&\\ y_{n+1}& \end{bmatrix} X=11.1x1x2.xn+1x12x22.xn+12..........x1nx2n.xn+1n,Y=y1y2.yn+1
(2)进行插值的公式, Y = A X Y=AX Y=AX

工作原理
(1)在事先已知的n+1个P点,可以通过A=X^(-1) Y求解得到待定系数A。
(2)假设有一空值,已知X(test_x)值,但Y值(缺失值的填充词)不知道,
由步骤1求解到的待定系数根据公式Y=AX可以求解出缺失值的数值。

import numpy as np

def Polynomial(x, y, test_x):
    '''
    test_x 的值一般是在缺失值的前几个或者后几个值当中,挑出一个作为参考值,
    将其值代入到插值模型之中,学习出一个值作为缺失值的填充值
    '''
    # 求待定系数
    array_x = np.array(x)  # 向量化
    array_y = np.array(y)
    n, X = len(x), []
    for i in range(n):  # 形成 X 矩阵
        l = array_x ** i
        X.append(l)
    X = np.array(X).T
    A = np.dot(np.linalg.inv(X), array_y)  # 根据公式求待定系数 A
    
    # 缺失值插值
    xx = []
    for j in range(n):
        k = test_x ** j
        xx.append(k)
    xx=np.array(xx)
    return np.dot(xx, A)
    
x, y, test_x = [1, 2, 3, 4], [1, 5, 2, 6], 3.5
Polynomial(x, y, test_x)
2.2499999999999716

2. lagrange插值
工作原理
可以证明,经过n+1个互异的点的次数不超过n的多项式是唯一存在的。
也就是说,无论是否是使用何种基底,只要基底能张成所需要的空间,都不会影响最终结果。 。

理论公式及推导
已知n+1个互异的点 P 1 : ( x 1 , y 1 ) , P 2 : ( x 2 , y 2 ) , . . . , P n + 1 : ( x n + 1 , y n + 1 ) P1:(x1,y1),P2:(x2,y2),...,P_{n+1}:(x_{n+1},y_{n+1}) P1:(x1,y1)P2:(x2,y2)...Pn+1:(xn+1,yn+1),令
l i ( x ) = ∏ ( j ≠ i ) ( j = 1 ) n + 1 x − x j x i − x j , 公 式 ( 1 ) l_i(x)=\prod_{(j\ne i)(j=1)}^{n+1}\frac{x-x_j}{x_i-x_j},公式(1) li(x)=(j=i)(j=1)n+1xixjxxj(1)
作为插值基底,则Lagrange值 L i ( x ) = ∑ i = 1 n + 1 l i ( x ) y i , 公 式 ( 2 ) L_i(x)=\sum \limits_{i=1}^{n+1}l_i(x)y_i,公式(2) Li(x)=i=1n+1li(x)yi(2)

工作原理
(1)先求出插值基底值
(2)再求Lagrange拉格朗日值

def Lagrange(x, y, test_x):
    '''
    所谓的插值法,就是在X范围区间中挑选一个或者自定义一个数值,
    然后代进去插值公式当中,求出数值作为缺失值的数据。
    '''
    n = len(x)
    L = 0
    for i in range(n):
        # 计算公式 1
        li = 1
        for j in range(n):
            if j != i:
                li *= (test_x-x[j])/(x[i]-x[j])
        # 计算公式 2
        L += li * y[i]
    return L
        
Lagrange(x, y, test_x) 
2.25

Pandas也自带差值方法

df11['feature2'].interpolate(method="linear")
0     3.0
1     1.0
2     7.0
3     5.5
4     4.0
5     0.0
6     5.0
7     7.0
8    12.0
9    12.0
Name: feature2, dtype: float64
df11['feature2'].interpolate(method="polynomial",order=2)
# order代表多项式的项数
0     3.000000
1     1.000000
2     7.000000
3     7.856922
4     4.000000
5     0.000000
6     5.000000
7     7.000000
8    12.000000
9          NaN
Name: feature2, dtype: float64

预测填充

理论部分
预测填充思路如下:
(1)把需要填充缺失值的某一列特征(Feature_A)作为新的标签(Label_A)
(2)然后找出与 Label_A 相关性较强的特征作为它的模型特征
(3)把 Label_A 非缺失值部分作为训练集数据,而缺失值部分则作为测试集数据
(4)若 Label_A 的值属于连续型数值,则进行回归拟合;若是类别(离散)型数值,则进行分类学习
(5)将训练学习到评分和泛化能力较好的模型去预测测试集,从而填充好缺失值

代码实现部分
使用 seaborn 模块中内置 IRIS 数据集进行演示,实现使用算法模型进行预测填充。

import seaborn as sns
import numpy as np
import warnings
import matplotlib.pyplot as plt
%matplotlib inline
warnings.filterwarnings('ignore')

dataset = sns.load_dataset('iris')
print(dataset.shape)
print(dataset.isnull().sum())
dataset.head()
(150, 5)
sepal_length    0
sepal_width     0
petal_length    0
petal_width     0
species         0
dtype: int64
sepal_lengthsepal_widthpetal_lengthpetal_widthspecies
05.13.51.40.2setosa
14.93.01.40.2setosa
24.73.21.30.2setosa
34.63.11.50.2setosa
45.03.61.40.2setosa
  1. 把需要填充缺失值的某一列特征(petal_width)作为新的标签(Label_petal_width)
# 将特征 petal_width 处理成含有 30 个缺失值的特征
dataset['Label_petal_length'] = dataset['petal_length']
for i in range(0, 150, 5):
    dataset.loc[i, 'Label_petal_length'] = np.nan
print(dataset.isnull().sum())
dataset.head()
sepal_length           0
sepal_width            0
petal_length           0
petal_width            0
species                0
Label_petal_length    30
dtype: int64
sepal_lengthsepal_widthpetal_lengthpetal_widthspeciesLabel_petal_length
05.13.51.40.2setosaNaN
14.93.01.40.2setosa1.4
24.73.21.30.2setosa1.3
34.63.11.50.2setosa1.5
45.03.61.40.2setosa1.4
  1. 然后找出与 Label_petal_length 相关性较强的特征作为它的模型特征
dataset.corr()
sepal_lengthsepal_widthpetal_lengthpetal_widthLabel_petal_length
sepal_length1.000000-0.1175700.8717540.8179410.875744
sepal_width-0.1175701.000000-0.428440-0.366126-0.449716
petal_length0.871754-0.4284401.0000000.9628651.000000
petal_width0.817941-0.3661260.9628651.0000000.963768
Label_petal_length0.875744-0.4497161.0000000.9637681.000000

可以发现特征 sepal_length、petal_width 与 Label_petal_width 有着强关联,
因此 sepal_length、petal_width 作为 Label_petal_length 的模型特征

  1. 把 Label_petal_length 非缺失值部分作为训练集数据,而缺失值部分则作为测试集数据
data = dataset[['sepal_length', 'petal_width', 'Label_petal_length']].copy()
train = data[data['Label_petal_length'].notnull()]
test  = data[data['Label_petal_length'].isnull()]
print(train.shape)
print(test.shape)
(120, 3)
(30, 3)
  1. 由于 Label_petal_length 的值属于连续型数值,则进行回归拟合
from sklearn.model_selection import train_test_split
from sklearn.linear_model import LinearRegression
from sklearn.metrics import r2_score

# 将训练集进行切分,方便验证模型训练的泛化能力
x_train, x_valid, y_train, y_valid = train_test_split(train.iloc[:, :2], 
                                                      train.iloc[:, 2], 
                                                      test_size=0.3
                                                     )
print(x_train.shape, x_valid.shape)
print(y_train.shape, y_valid.shape)

# 使用简单的线性回归进行训练
lr = LinearRegression()
lr.fit(x_train, y_train)
y_train_pred = lr.predict(x_train)
print('在训练集中的表现:', r2_score(y_train_pred, y_train))
y_valid_pred = lr.predict(x_valid)
print('在验证集中的表现:', r2_score(y_valid_pred, y_valid))
(84, 2) (36, 2)
(84,) (36,)
>>>在训练集中的表现: 0.9513919768204693
>>>在验证集中的表现: 0.9375742227297561
  1. 将训练学习到评分和泛化能力较好的模型去预测测试集,从而填充好缺失值
    由上面来看,模型在训练集以及验证集上的表现相差不大并且效果挺不错的,
    这说明模型的泛化能力不错,可以用于投放使用来预测测试集
y_test_pred = lr.predict(test.iloc[:, :2])
test.loc[:, 'Label_petal_length'] = y_test_pred
df_no_nan = pd.concat([train, test], axis=0)
print(df_no_nan.isnull().sum())
df_no_nan.head()
sepal_length          0
petal_width           0
Label_petal_length    0
dtype: int64
sepal_lengthpetal_widthLabel_petal_length
14.90.21.4
24.70.21.3
34.60.21.5
45.00.21.4
64.60.31.4

上面就是预测填充的代码示例以及详细讲解。

KNN填充

利用knn算法填充,其实是把目标列当做目标标量,利用非缺失的数据进行knn算法拟合,最后对目标列缺失进行预测。(对于连续特征一般是加权平均,对于离散特征一般是加权投票)

df11
feature1feature2label
013.010
121.015
237.08
34NaN12
454.017
560.09
675.07
787.014
8912.016
910NaN20
from fancyimpute import KNN
fill_knn = KNN(k=3).fit_transform(df11)
data_2 = pd.DataFrame(fill_knn)
data_2
Imputing row 1/10 with 0 missing, elapsed time: 0.001
012
01.03.00000010.0
12.01.00000015.0
23.07.0000008.0
34.01.33333312.0
45.04.00000017.0
56.00.0000009.0
67.05.0000007.0
78.07.00000014.0
89.012.00000016.0
910.08.81818220.0

具体分析

上面两次提到具体问题具体分析,为什么要具体问题具体分析呢?因为属性缺失有时并不意味着数据缺失,
缺失本身是包含信息的,所以需要根据不同应用场景下缺失值可能包含的信息进行合理填充。
下面通过一些例子来说明如何具体问题具体分析,仁者见仁智者见智,仅供参考:

  1. “年收入”:商品推荐场景下填充平均值,借贷额度场景下填充最小值;
  2. “行为时间点”:填充众数;
  3. “价格”:商品推荐场景下填充最小值,商品匹配场景下填充平均值;
  4. “人体寿命”:保险费用估计场景下填充最大值,人口估计场景下填充平均值;
  5. “驾龄”:没有填写这一项的用户可能是没有车,为它填充为0较为合理;
  6. ”本科毕业时间”:没有填写这一项的用户可能是没有上大学,为它填充正无穷比较合理;
  7. “婚姻状态”:没有填写这一项的用户可能对自己的隐私比较敏感,应单独设为一个分类,如已婚1、未婚0、未填-1。

缺失数据可视化

手工查看每个变量的缺失值是非常麻烦的一件事情,
missingno提供了一个灵活且易于使用的缺失数据可视化和实用程序的小工具集,可以快速直观地总结数据集的完整性。

import missingno as msno
data
feature1feature2feature3label
013.03.010
121.0NaN15
237.0NaN8
34NaNNaN12
454.09.017
560.0NaN9
675.010.07
787.0NaN14
8912.04.016
910NaNNaN20
  • 无效矩阵的数据密集显示

如果data太大,需要data.sample(250)重新随机抽样

msno.matrix(data,labels=True)
<matplotlib.axes._subplots.AxesSubplot at 0x18e33f710f0>

我们可以一目了然的看到每个变量的缺失情况,
变量feature1,label数据是完整的,feature2变量中间段和最后部分有缺失,feature3确实较多。

  • msno.bar 是列缺失值的简单可视化:
msno.bar(data,labels=True)
<matplotlib.axes._subplots.AxesSubplot at 0x18e2f767668>

  • missingno相关性热图:一个变量的存在或不存在如何强烈影响的另一个的存在:
msno.heatmap(data,figsize=(16, 7))
<matplotlib.axes._subplots.AxesSubplot at 0x18e2f895550>

  • missingno树形图使用层次聚类算法通过它们的无效性相关性(根据二进制距离测量)将变量彼此相加。在树的每个步骤,基于哪个组合最小化剩余簇的距离来分割变量。变量集越单调,它们的总距离越接近零,并且它们的平均距离(y轴)越接近零。
msno.dendrogram(data)
<matplotlib.axes._subplots.AxesSubplot at 0x18e33a560b8>

微信公众号:邯郸路220号子彬院 获取更多内容

  • 5
    点赞
  • 26
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值