5.3.3 缺失值处理方式

在实际工作中,需要根据缺失值的类型和使用场景,对缺失值进行必要的处理。总体上来说,缺失值的处理方式分为删除、填充和不处理。对于主观数据,人将影响数据的真实性,存在缺失值的样本的其他属性值无法保证可靠性,那么依赖于这些属性值的填充也是不可靠的,所以对于主观数据一般不推荐插补的方法,例如客户问卷调查数据。插补主要是针对客观数据,它的可靠性有保证,例如客户年龄。

1.删除

Pandas提供的dropna接口可以方便地删除行列缺失数据,接口用法如下。


titanic_df.shape

输出结果如下:


(891,15)

#如果行数据中有空值,按行删除,按列删除配置axis=1
titanic_df_row  = titanic_df.dropna (axis=0) 
titanic_df_row.shape

输出结果如下:


(182,15)

以上代码只是对dropna接口的简单使用,在实际工作中除非数据特征和样本足够多,一般不会只要有缺失值就一定将样本删除,需要计算缺失值的比例以及该变量的区分能力。如果变量的缺失值比例高但是有一定区分能力,则需要结合实际情况考虑保留还是删除。下面的代码案例简单展示了删除缺失值比例大于0.5的列。


def drop_nan_stat(df, copy=False, axis=0, nan_threshold=0.9):
   '''按行、列的缺失值比例删除大于缺失值阈值的行、列'''
    assert isinstance(df, pd.DataFrame)
    return_df = df.copy() if copy else df
    n_rows, n_cols = return_df.shape

    if axis == 0:
        t = return_df.isnull().sum(axis=0)
        t = pd.DataFrame(t, columns=['NumOfNan'])
        t['PctOFNan'] = t['NumOfNan'] / n_rows
        return_df = return_df.drop(
            labels=t[t.PctOFNan > nan_threshold].index.tolist(), axis=1)
    elif axis == 1:
        t = return_df.isnull().sum(axis=1)
        t = pd.DataFrame(t, columns=['NumOfNan'])
        t['PctOFNan'] = t['NumOfNan'] / n_cols
        print(t)
        return_df = return_df.drop(
            labels=t[t.PctOFNan > nan_threshold].index.tolist(), axis=0)

    return return_df

删除缺失值比例大于0.5的列。


titanic_df_col = drop_nan_stat(df=titanic_df,
                            copy=True,
                            axis=0,
                            nan_threshold=0.5)
msno.bar(df=titanic_df_col, figsize=(8, 4), fontsize=18)

输出如图5-21所示。

图5-21 删除部分列的缺失值条形图

对比图5-20可以发现,缺失值比例大于0.5的列deck已经被删除。

2.填充

对缺失值的填充大体可分为3种:替换缺失值、拟合缺失值、虚拟变量。替换是通过数据中非缺失数据的统计指标或业务经验值填充,拟合是通过其他特征建模来填充,虚拟变量是用衍生的新变量代替缺失值。

(1)替换缺失值

替换缺失值较常见的是使用统计方法。如果缺失值是定距型的,那么就以该变量的非缺失值的平均值、中位数等统计值来插补缺失的值;如果缺失值是非定距型的,那么就根据统计学中的众数,用该变量的众数来补齐缺失的值。另外,针对一些专业领域,数据工作者基于他们对行业的理解,对缺失值进行人工填充,往往会得到比统计填充更好的效果。下面主要介绍统计方法替换缺失值。

数值变量age使用均值填充示例:


#由于后续的案例也会使用titanic_df,因而保持titanic_df数据不变,复制一份新数据进行填充操作
titanic_df_fill=titanic_df.copy()
titanic_df_fill.info()

titanic_df_fill['age'].fillna(titanic_df_fill['age'].median(), inplace=True)
#判断age填充后是否还有缺失值
titanic_df_fill['age'].isnull().any()

输出结果如下:


False

类似地,类别变量embarked使用众数填充的示例如下。


titanic_df_fill['embarked'].fillna(titanic_df_fill['embarked'].mode()[0],inplace=True)

(2)拟合缺失值

拟合是通过构建模型的方式对缺失值进行填充,连续变量的拟合使用回归模型,分类变量的拟合使用分类模型。

建模方法为:将原始数据按待填充的列分为两个数据集,一个数据集中该列未缺失,一个数据集中该列缺失。通过在未缺失数据集上建模,预测并填充缺失数据集中的列值。注意,待填充的列作为模型中的y值,实现建模和预测填充。下面以随机森林为例进行说明。


# 导入sklearn.ensemble.RandomForestRegressor
from sklearn.ensemble import RandomForestRegressor

# RandomForestRegressor只能处理数值、数据,获取缺失值年龄和数值类型变量
age_df = titanic_df[['age', 'fare', 'parch', 'sibsp', 'pclass']].copy()
print(age_df['age'].isnull().any())

输出结果如下:


True
# 按年龄是否缺失,可分为训练数据集和预测数据集
train_df = age_df[age_df.age.notnull()].as_matrix()
predict_df = age_df[age_df.age.isnull()].as_matrix()
# y即目标年龄
y = train_df[:, 0]
# X即特征属性值
X = train_df[:, 1:]

# 训练数据集使用RandomForestRegressor训练模型
rf_model = RandomForestRegressor(random_state=42, n_estimators=100)
rf_model.fit(X, y)
# 用训练好的模型预测数据集的年龄进行预测
predict_ages = rf_model.predict(predict_df[:, 1:])

# 预测结果填补原缺失数据
age_df.loc[(age_df.age.isnull()), 'age'] = predict_ages
print(age_df['age'].isnull().any())

输出结果如下:


False

(3)虚拟变量

虚拟变量是指通过判断变量值是否有缺失值来生成一个新的二分类变量。比如,列A中特征值缺失,那么生成的列B中的值为True;否则,列B中的值为False。


age_df['age'] = titanic_df['age'].copy()

#判断年龄是否缺失,衍生一个新变量age_nan
age_df.loc[(age_df.age.notnull()), 'age_nan'] = "False"
age_df.loc[(age_df.age.isnull()), 'age_nan'] = "True"

#统计新变量age_nan缺失和非缺失的数量,可以与之前的缺失值可视化进行缺失值数据核验
age_df['age_nan'].value_counts()

输出结果如下:


False    714
True     177

3.不处理

如果缺失包含了业务含义,那么完全有理由保留该变量,实际处理中可直接将缺失值填充为区别于正常值的默认值,比如-1,也可以采取第6章介绍的分箱方法,将缺失值单独分为一箱。但是,填充缺失值不一定完全符合客观事实,我们或多或少地改变了原始信息,而且不正确的填充往往会向数据中引入新的噪声,甚至产生错误。因此,在许多情况下,我们还是希望在尽量保持原始信息不发生变化的前提下对数据进行处理。

在实际应用中,一些模型无法应对具有缺失值的数据,因此要对缺失值进行处理。例如SVM和KNN,其模型原理中涉及了对样本距离的度量,如果缺失值处理不当,最终会导致模型预测效果很差。然而,一些模型本身就可以应对具有缺失值的数据,此时无须对数据进行处理,比如XGBoost、LightGBM等模型。

XGBoost算法允许特征存在缺失值。它对缺失值的处理方式如下:在特征k上寻找最佳分裂点时只对该列特征值为non-missing的样本进行遍历,从而减少时间开销。在逻辑实现上,为了保证完备性,会尝试将该列特征值为missing的样本分别分配到左叶子结点和右叶子结点,并计算和选择分裂后增益最大的那个方向,作为预测时特征值缺失样本的默认分支方向。如果在训练中没有缺失值而在预测中出现缺失值,那么会自动将缺失值的划分方向放到右叶子结点。