7.4. 缺失值填充#

出于各种原因,许多真实世界数据集都包含缺失值,通常编码为空白、NaN 或其他占位符。然而,此类数据集与 scikit-learn 估计器不兼容,因为后者假定数组中的所有值均为数值且均具有意义。使用不完整数据集的一种基本策略是丢弃包含缺失值的整行和/或整列。然而,这样做是以丢失可能有价值(即使不完整)的数据为代价的。一种更好的策略是填充缺失值,即从数据的已知部分推断出它们。请参阅填充的术语表条目。

7.4.1. 单变量填充与多变量填充#

一种填充算法是单变量填充,它仅使用该特征维度中的非缺失值来填充第 i 个特征维度中的值(例如 SimpleImputer)。相比之下,多变量填充算法使用所有可用的特征维度来估计缺失值(例如 IterativeImputer)。

7.4.2. 单变量特征填充#

SimpleImputer 类提供了填充缺失值的基本策略。缺失值可以用提供的常数值填充,或者使用缺失值所在每列的统计量(均值、中位数或众数)填充。该类还支持不同的缺失值编码方式。

以下代码片段演示了如何使用包含缺失值的列(轴 0)的均值来替换编码为 np.nan 的缺失值

>>> import numpy as np
>>> from sklearn.impute import SimpleImputer
>>> imp = SimpleImputer(missing_values=np.nan, strategy='mean')
>>> imp.fit([[1, 2], [np.nan, 3], [7, 6]])
SimpleImputer()
>>> X = [[np.nan, 2], [6, np.nan], [7, 6]]
>>> print(imp.transform(X))
[[4.          2.        ]
 [6.          3.666]
 [7.          6.        ]]

SimpleImputer 类还支持稀疏矩阵

>>> import scipy.sparse as sp
>>> X = sp.csc_matrix([[1, 2], [0, -1], [8, 4]])
>>> imp = SimpleImputer(missing_values=-1, strategy='mean')
>>> imp.fit(X)
SimpleImputer(missing_values=-1)
>>> X_test = sp.csc_matrix([[-1, 2], [6, -1], [7, 6]])
>>> print(imp.transform(X_test).toarray())
[[3. 2.]
 [6. 3.]
 [7. 6.]]

请注意,此格式并非旨在隐式存储矩阵中的缺失值,因为它会在转换时使其稠密化。编码为 0 的缺失值必须与稠密输入一起使用。

当使用 'most_frequent''constant' 策略时,SimpleImputer 类还支持表示为字符串值或 pandas 分类值的类别数据

>>> import pandas as pd
>>> df = pd.DataFrame([["a", "x"],
...                    [np.nan, "y"],
...                    ["a", np.nan],
...                    ["b", "y"]], dtype="category")
...
>>> imp = SimpleImputer(strategy="most_frequent")
>>> print(imp.fit_transform(df))
[['a' 'x']
 ['a' 'y']
 ['a' 'y']
 ['b' 'y']]

有关其他用法示例,请参阅在构建估计器之前填充缺失值

7.4.3. 多变量特征填充#

一种更复杂的方法是使用 IterativeImputer 类,它将每个包含缺失值的特征建模为其他特征的函数,并使用该估计值进行填充。它以迭代循环的方式进行:在每个步骤中,一个特征列被指定为输出 y,其他特征列被视为输入 X。回归器在已知 y(X, y) 上进行拟合。然后,回归器用于预测 y 的缺失值。这以迭代方式对每个特征进行,然后重复 max_iter 次填充轮。返回最终填充轮的结果。

注意

此估计器目前仍处于实验阶段:默认参数或行为细节可能会在没有任何弃用周期的情况下发生变化。解决以下问题将有助于稳定 IterativeImputer:收敛准则(#14338)和默认估计器(#13286)。要使用它,您需要显式导入 enable_iterative_imputer

>>> import numpy as np
>>> from sklearn.experimental import enable_iterative_imputer
>>> from sklearn.impute import IterativeImputer
>>> imp = IterativeImputer(max_iter=10, random_state=0)
>>> imp.fit([[1, 2], [3, 6], [4, 8], [np.nan, 3], [7, np.nan]])
IterativeImputer(random_state=0)
>>> X_test = [[np.nan, 2], [6, np.nan], [np.nan, 6]]
>>> # the model learns that the second feature is double the first
>>> print(np.round(imp.transform(X_test)))
[[ 1.  2.]
 [ 6. 12.]
 [ 3.  6.]]

SimpleImputerIterativeImputer 都可以在 Pipeline 中使用,作为构建支持填充的复合估计器的一种方式。请参阅在构建估计器之前填充缺失值

7.4.3.1. IterativeImputer 的灵活性#

R 数据科学生态系统中有许多成熟的填充包:Amelia, mi, mice, missForest 等。missForest 很受欢迎,并且是不同序列填充算法的一个特定实例,所有这些算法都可以通过向 IterativeImputer 传入不同的回归器来预测缺失特征值来实现。对于 missForest,此回归器是随机森林。请参阅使用 IterativeImputer 的变体填充缺失值

7.4.3.2. 多重填充与单一填充#

在统计学界,通常的做法是执行多重填充,例如,为单个特征矩阵生成 m 个独立的填充。然后将这 m 个填充中的每一个都通过后续的分析流水线(例如特征工程、聚类、回归、分类)。这 m 个最终分析结果(例如留出验证误差)使数据科学家能够了解分析结果可能因缺失值引起的固有不确定性而有所不同。上述做法称为多重填充。

我们对 IterativeImputer 的实现灵感来源于 R MICE 包(通过链式方程进行多变量填充)[1],但不同之处在于它返回单一填充而不是多重填充。然而,当 sample_posterior=True 时,通过对相同数据集重复应用 IterativeImputer 并使用不同的随机种子,它也可以用于多重填充。有关多重填充与单一填充的更多讨论,请参阅[2],第 4 章。

当用户对测量由缺失值引起的不确定性不感兴趣时,单一填充与多重填充在预测和分类上下文中的有用性仍然是一个开放问题。

请注意,不允许调用 IterativeImputertransform 方法来改变样本数量。因此,无法通过一次调用 transform 实现多重填充。

参考文献

7.4.4. 最近邻填充#

KNNImputer 类提供了使用 k-最近邻方法填充缺失值的填充功能。默认情况下,使用支持缺失值的欧几里得距离度量 nan_euclidean_distances 来查找最近邻。每个缺失特征都使用其 n_neighbors 个最近邻的特征值进行填充,这些邻居对该特征有值。邻居的特征值按统一方式或按到每个邻居的距离加权平均。如果一个样本有多个特征缺失,那么该样本的邻居可能会因正在填充的特定特征而异。当可用邻居的数量小于 n_neighbors 且训练集没有定义距离时,填充期间使用该特征的训练集平均值。如果至少有一个邻居具有定义的距离,则填充期间将使用剩余邻居的加权或未加权平均值。如果某个特征在训练中始终缺失,则在 transform 期间将其移除。有关该方法的更多信息,请参阅参考文献 [OL2001]

以下代码片段演示了如何使用包含缺失值的样本的两个最近邻的平均特征值来替换编码为 np.nan 的缺失值

>>> import numpy as np
>>> from sklearn.impute import KNNImputer
>>> nan = np.nan
>>> X = [[1, 2, nan], [3, 4, 3], [nan, 6, 5], [8, 8, 7]]
>>> imputer = KNNImputer(n_neighbors=2, weights="uniform")
>>> imputer.fit_transform(X)
array([[1. , 2. , 4. ],
       [3. , 4. , 3. ],
       [5.5, 6. , 5. ],
       [8. , 8. , 7. ]])

有关其他用法示例,请参阅在构建估计器之前填充缺失值

参考文献

7.4.5. 保持特征数量不变#

默认情况下,scikit-learn 填充器会丢弃完全为空的特征,即仅包含缺失值的列。例如

>>> imputer = SimpleImputer()
>>> X = np.array([[np.nan, 1], [np.nan, 2], [np.nan, 3]])
>>> imputer.fit_transform(X)
array([[1.],
       [2.],
       [3.]])

填充后,X 中第一个只包含 np.nan 的特征被丢弃了。虽然这个特征在预测设置中没有帮助,但丢弃列会改变 X 的形状,这在更复杂的机器学习流水线中使用填充器时可能会出现问题。参数 keep_empty_features 提供了通过常数值填充来保留空特征的选项。在大多数情况下,这个常数值是零

>>> imputer.set_params(keep_empty_features=True)
SimpleImputer(keep_empty_features=True)
>>> imputer.fit_transform(X)
array([[0., 1.],
       [0., 2.],
       [0., 3.]])

7.4.6. 标记填充值#

MissingIndicator 转换器可用于将数据集转换为相应的二进制矩阵,指示数据集中缺失值的存在。此转换与填充结合使用时非常有用。在使用填充时,保留关于哪些值缺失的信息可能具有信息量。请注意,SimpleImputerIterativeImputer 都具有布尔参数 add_indicator(默认为 False),当设置为 True 时,提供了一种方便的方法,可以将 MissingIndicator 转换器的输出与填充器的输出堆叠起来。

NaN 通常用作缺失值的占位符。但是,它会强制数据类型为浮点数。参数 missing_values 允许指定其他占位符,例如整数。在以下示例中,我们将使用 -1 作为缺失值

>>> from sklearn.impute import MissingIndicator
>>> X = np.array([[-1, -1, 1, 3],
...               [4, -1, 0, -1],
...               [8, -1, 1, 0]])
>>> indicator = MissingIndicator(missing_values=-1)
>>> mask_missing_values_only = indicator.fit_transform(X)
>>> mask_missing_values_only
array([[ True,  True, False],
       [False,  True,  True],
       [False,  True, False]])

参数 features 用于选择构建掩码的特征。默认情况下,它是 'missing-only',它在 fit 时返回包含缺失值的特征的填充器掩码

>>> indicator.features_
array([0, 1, 3])

参数 features 可以设置为 'all',以返回所有特征,无论它们是否包含缺失值

>>> indicator = MissingIndicator(missing_values=-1, features="all")
>>> mask_all = indicator.fit_transform(X)
>>> mask_all
array([[ True,  True, False, False],
       [False,  True, False,  True],
       [False,  True, False, False]])
>>> indicator.features_
array([0, 1, 2, 3])

当在 Pipeline 中使用 MissingIndicator 时,请务必使用 FeatureUnionColumnTransformer 将指示器特征添加到常规特征中。首先我们获取 iris 数据集,并向其中添加一些缺失值。

>>> from sklearn.datasets import load_iris
>>> from sklearn.impute import SimpleImputer, MissingIndicator
>>> from sklearn.model_selection import train_test_split
>>> from sklearn.pipeline import FeatureUnion, make_pipeline
>>> from sklearn.tree import DecisionTreeClassifier
>>> X, y = load_iris(return_X_y=True)
>>> mask = np.random.randint(0, 2, size=X.shape).astype(bool)
>>> X[mask] = np.nan
>>> X_train, X_test, y_train, _ = train_test_split(X, y, test_size=100,
...                                                random_state=0)

现在我们创建一个 FeatureUnion。所有特征都将使用 SimpleImputer 进行填充,以便分类器能够处理这些数据。此外,它还会添加来自 MissingIndicator 的指示变量。

>>> transformer = FeatureUnion(
...     transformer_list=[
...         ('features', SimpleImputer(strategy='mean')),
...         ('indicators', MissingIndicator())])
>>> transformer = transformer.fit(X_train, y_train)
>>> results = transformer.transform(X_test)
>>> results.shape
(100, 8)

当然,我们不能使用转换器进行任何预测。我们应该将其封装在一个带分类器(例如,一个 DecisionTreeClassifier)的 Pipeline 中,以便能够进行预测。

>>> clf = make_pipeline(transformer, DecisionTreeClassifier())
>>> clf = clf.fit(X_train, y_train)
>>> results = clf.predict(X_test)
>>> results.shape
(100,)

7.4.7. 处理 NaN 值的估计器#

有些估计器设计用于直接处理 NaN 值,无需预处理。下面是这些估计器的列表,按类型(聚类、回归器、分类器、转换器)分类