6.4. 缺失值插补#

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

6.4.1. 单变量插补与多变量插补#

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

6.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 编码的缺失值必须与密集输入一起使用。

SimpleImputer 类还支持使用 'most_frequent''constant' 策略表示为字符串值或 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']]

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

6.4.3. 多元特征估算#

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

注意

此估计器目前仍处于实验阶段:默认参数或行为细节可能会在没有任何弃用周期的情况下发生变化。解决以下问题将有助于稳定 IterativeImputer:收敛标准 (#14338)、默认估计器 (#13286) 以及随机状态的使用 (#15611)。要使用它,您需要显式导入 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 都可以在管道中用作构建支持估算的复合估计器的一种方式。请参见 在构建估计器之前估算缺失值

6.4.3.1. IterativeImputer 的灵活性#

R 数据科学生态系统中存在许多成熟的估算包:Amelia、mi、mice、missForest 等。missForest 很受欢迎,事实证明它是不同顺序估算算法的特定实例,所有这些算法都可以通过将不同的回归器传递到 IterativeImputer 中来实现,这些回归器用于预测缺失的特征值。在 missForest 的情况下,此回归器是随机森林。请参见 使用 IterativeImputer 的变体估算缺失值

6.4.3.2. 多重估算与单一估算#

在统计学界,执行多重估算是一种常见的做法,例如,为单个特征矩阵生成 m 个独立的估算。然后,将这 m 个估算中的每一个都放入随后的分析管道(例如,特征工程、聚类、回归、分类)。这 m 个最终分析结果(例如,保留的验证错误)使数据科学家能够了解分析结果如何因缺失值造成的固有随机性而有所不同。上述做法称为多重估算。

我们对 IterativeImputer 的实现受到 R MICE 包(链式方程多元估算)[1] 的启发,但与之不同的是,它返回单个估算而不是多个估算。但是,当 sample_posterior=True 时,IterativeImputer 也可用于多重估算,方法是使用不同的随机种子对同一个数据集重复应用它。请参见 [2],第 4 章详细讨论了多重估算与单一估算。

当用户对测量因缺失值造成的随机性不感兴趣时,单一估算与多重估算在预测和分类方面到底有多有用,这仍然是一个悬而未决的问题。

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

6.4.3.3. 参考文献#

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

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

参考文献

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

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

MissingIndicator 中使用 Pipeline 时,请确保使用 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)

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

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

6.4.7. 处理 NaN 值的估计器#

一些估计器被设计为在没有预处理的情况下处理 NaN 值。以下是这些估计器的列表,按类型(聚类、回归器、分类器、变换)分类