6.4. Восстановление пропущенных значений

По разным причинам многие наборы данных реального мира содержат пропущенные значения, часто закодированные как пробелы, NaNs или другие символы. Однако такие наборы данных несовместимы с моделями scikit-learn, которые предполагают, что все значения в массиве являются числовыми, и все они имеют и сохраняют значение. Основная стратегия использования неполных наборов данных заключается в отбрасывании целых строк и/или столбцов, содержащих пропущенные значения. Однако за это приходится платить потерей данных, которые могут быть ценными (даже несмотря на их неполноту). Лучшей стратегией является восстановление недостающих значений, т. е. вывод их из известной части данных. См. запись в глоссарии о imputation.

6.4.1. Одномерное и многомерное восстановление

Одним из типов алгоритмов восстановления является одномерный, который восстановливает значения в i-м измерении признака, используя только не пропущенные значения в этом измерении признака (например, SimpleImputer). В отличие от этого, алгоритмы многомерной интерполяции используют весь набор доступных измерений признаков для оценки пропущенных значений (например, IterativeImputer).

6.4.2. Одномерное восстановление признаков

Класс SimpleImputer предоставляет базовые стратегии для восстановления пропущенных значений. Недостающие значения можно восстановить с помощью предоставленного постоянного значения или используя статистику (среднее, медиана или наиболее частое) каждого столбца, в котором находятся недостающие значения. Этот класс также позволяет использовать различные кодировки отсутствующих значений.

Следующий фрагмент демонстрирует, как заменить пропущенные значения, закодированные как np.nan, используя среднее значение столбцов (ось 0), которые содержат пропущенные значения:

>>> 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 также поддерживает категориальные данные, представленные в виде строковых значений или категориальных данных pandas при использовании стратегии 'most_frequent' или 'constant':

>>> 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']]

Другой пример использования см. в Imputing missing values before building an estimator.

6.4.3. Многомерное восстановление признаков

Более сложный подход заключается в использовании класса IterativeImputer, который моделирует каждый признак с пропущенными значениями как функцию других признаков и использует эту оценку для восстановления. Это происходит по кругу: на каждом шаге столбец признака обозначается как выход y, а остальные столбцы признака рассматриваются как входы X. Регрессор настраивается на (X, y) для известного 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.]]

И SimpleImputer, и IterativeImputer могут использоваться в конвейере как способ построения составной модели, поддерживающей восстановление. См. Imputing missing values before building an estimator.

6.4.3.1. Гибкость IterativeImputer

В экосистеме науки о данных R существует множество хорошо зарекомендовавших себя пакетов для интерполяции: Amelia, mi, mice, missForest и т. д. Популярным является missForest, который оказывается частным случаем различных алгоритмов последовательной интерполяции, которые могут быть реализованы с помощью IterativeImputer путем передачи различных регрессоров для предсказания пропущенных значений признаков. В случае missForest таким регрессором является Random Forest. См. Imputing missing values with variants of IterativeImputer.

6.4.3.2. Множественная и одиночная восстановление

В статистическом сообществе принято выполнять множественные восстановления, создавая, например, m отдельных восстановлений для одной матрицы признаков. Каждая из этих m восстановлений затем проходит через последующий конвейер анализа (например, инженерия признаков, кластеризация, регрессия, классификация). Конечные результаты анализа m (например, удержанные ошибки валидации) позволяют специалисту по исследованию данных понять, как результаты анализа могут отличаться из-за неопределенности, присущей отсутствующим значениям. Описанная выше практика называется множественной восстановление.

Наша реализация IterativeImputer была вдохновлена пакетом R MICE (Multivariate Imputation by Chained Equations) [1], но отличается от него тем, что возвращает одно восстановление вместо нескольких восстановлений. Однако IterativeImputer можно использовать и для множественных восстановлений, применяя его несколько раз к одному и тому же набору данных с разными случайными семенами при sample_posterior=True. Подробнее о множественных и единичных восстановлениях см. в [2], глава 4.

По-прежнему остается открытой проблема, насколько полезны одиночные и множественные восстановления в контексте предсказания и классификации, когда пользователь не заинтересован в измерении неопределенности из-за пропущенных значений.

Обратите внимание, что вызов метода transform класса IterativeImputer` не позволяет изменять количество выборок. Поэтому множественные восстановления не могут быть достигнуты одним вызовом transform.

6.4.3.3. References

6.4.4. Восстановление ближайших соседей

Класс KNNImputer обеспечивает восстановление недостающих значений с использованием подхода k-Nearest Neighbors. По умолчанию для поиска ближайших соседей используется метрика евклидова расстояния, поддерживающая недостающие значения, nan_euclidean_distances. Каждый отсутствующий признак восстанавливается с использованием значений от n_neighbors ближайших соседей, у которых есть значение для этого признака. Значения признаков соседей усредняются равномерно или взвешиваются по расстоянию до каждого соседа. Если в выборке отсутствует более одного признака, то соседи для этой выборки могут быть разными в зависимости от конкретного восстановливаемого признака. Если количество доступных соседей меньше, чем n_neighbors, и нет определенных расстояний до обучающего набора, при восстановлении используется среднее значение обучающего набора для данного признака. Если есть хотя бы один сосед с определенным расстоянием, при вычислении будет использовано взвешенное или невзвешенное среднее значение оставшихся соседей. Если признак постоянно отсутствует в обучении, он удаляется при трансформировании. Более подробную информацию о методологии см. в ссылке. [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. ]])

Другой пример использования см. в Imputing missing values before building an estimator.

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 полезен для преобразования набора данных в соответствующую двоичную матрицу, указывающую на наличие пропущенных значений в наборе данных. Это преобразование полезно в сочетании с восстановлением. При использовании восстановления сохранение информации о том, какие значения были пропущены, может быть полезным. Обратите внимание, что и SimpleImputer, и IterativeImputer имеют булевский параметр add_indicator (по умолчанию False), который при установке в True предоставляет удобный способ сложить вывод трансформатора MissingIndicator` с выводом восстановленных значений.

NaN обычно используется в качестве заполнителя для пропущенных значений. Однако он заставляет тип данных быть float. Параметр missing_values позволяет указать другой заполнитель, например integer. В следующем примере мы будем использовать -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, обязательно используйте FeatureUnion или ColumnTransformer для добавления индикаторных признаков к обычным признакам. Сначала мы получим набор данных 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-значений без предварительной обработки. Ниже приведен список таких моделей, классифицированных по типу (кластер, регрессор, классификатор, преобразование):