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 都可以在管道中用作构建支持插补的复合估计器的方法。请参阅在构建估计器之前插补缺失值

7.4.3.1. IterativeImputer 的灵活性#

R 数据科学生态系统中有许多成熟的插补包:Amelia、mi、mice、missForest 等。missForest 很流行,它恰好是不同顺序插补算法的一个特定实例,所有这些算法都可以通过向 IterativeImputer 传递用于预测缺失特征值的不同回归器来实现。在 missForest 的情况下,这个回归器是一个随机森林。请参阅使用 IterativeImputer 变体插补缺失值

7.4.3.2. 多重插补与单次插补#

在统计学界,通常的做法是执行多重插补,例如,为单个特征矩阵生成 m 个单独的插补。然后将这 m 个插补中的每一个都通过后续的分析管道(例如,特征工程、聚类、回归、分类)。这 m 个最终分析结果(例如,保留的验证错误)允许数据科学家了解分析结果如何因缺失值引起的固有不确定性而有所不同。上述做法称为多重插补。

我们对 IterativeImputer 的实现受到 R MICE 包(Multivariate Imputation by Chained Equations)[1] 的启发,但与之不同的是,它返回单次插补而不是多重插补。但是,当 sample_posterior=True 时,IterativeImputer 也可以通过对相同数据集重复应用不同的随机种子用于多重插补。有关多重插补与单次插补的更多讨论,请参阅参考资料 [2] 第 4 章。

当用户对测量缺失值引起的不确定性不感兴趣时,单次插补与多重插补在预测和分类上下文中的有用程度仍然是一个悬而未决的问题。

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

References

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. ]])

有关用法的另一个示例,请参阅在构建估计器之前插补缺失值

References

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 值。下面是这些估计器的列表,按类型(聚类、回归器、分类器、转换)分类