scikit-learnMiddleCoding

Как обрабатывать пропущенные значения в scikit-learn с помощью SimpleImputer и IterativeImputer?

SimpleImputer заполняет NaN одним статистическим значением (mean/median/most_frequent/constant), вычисленным при fit; IterativeImputer моделирует каждый признак по остальным итеративно — точнее при MCAR/MAR, но медленнее и чувствительнее к порядку признаков.

SimpleImputer: быстрое заполнение одним значением

SimpleImputer вычисляет одну статистику (mean, median, most_frequent, constant) на обучающей выборке и заменяет все NaN этим значением. Это stateless после fit: при transform используются только сохранённые statistics_.

import numpy as np
import pandas as pd
from sklearn.impute import SimpleImputer, IterativeImputer
from sklearn.pipeline import Pipeline
from sklearn.compose import ColumnTransformer
from sklearn.preprocessing import StandardScaler, OneHotEncoder
from sklearn.ensemble import HistGradientBoostingClassifier
from sklearn.model_selection import train_test_split
from sklearn.metrics import classification_report

# Синтетический датасет с пропусками
np.random.seed(42)
n = 1000
df = pd.DataFrame({
    "age":     np.random.randint(18, 65, n).astype(float),
    "income":  np.random.exponential(50000, n),
    "tenure":  np.random.randint(0, 20, n).astype(float),
    "country": np.random.choice(["US", "DE", "BR", "IN"], n),
    "plan":    np.random.choice(["free", "pro", "enterprise"], n),
})
y = (df["income"] > 60000).astype(int)

# Вносим пропуски
for col in ["age", "income", "tenure"]:
    mask = np.random.rand(n) < 0.15  # 15% NaN
    df.loc[mask, col] = np.nan

X_train, X_test, y_train, y_test = train_test_split(
    df, y, test_size=0.2, stratify=y, random_state=42
)

# SimpleImputer в Pipeline
num_cols = ["age", "income", "tenure"]
cat_cols = ["country", "plan"]

preprocessor = ColumnTransformer([
    ("num", Pipeline([
        ("impute", SimpleImputer(strategy="median")),  # median устойчив к выбросам
        ("scale",  StandardScaler()),
    ]), num_cols),
    ("cat", Pipeline([
        ("impute", SimpleImputer(strategy="most_frequent")),
        ("encode", OneHotEncoder(handle_unknown="ignore", sparse_output=False)),
    ]), cat_cols),
])

pipeline = Pipeline([
    ("prep", preprocessor),
    ("clf",  HistGradientBoostingClassifier(random_state=42)),
])
pipeline.fit(X_train, y_train)

# Проверяем сохранённые статистики
simple_imp = pipeline.named_steps["prep"].named_transformers_["num"].named_steps["impute"]
print("statistics_ (median):", simple_imp.statistics_)  # array с медианами по каждому признаку

y_pred = pipeline.predict(X_test)
print(classification_report(y_test, y_pred))

IterativeImputer: мультивариантное восстановление

IterativeImputer моделирует каждый признак как функцию остальных признаков. На каждой итерации для каждого столбца с NaN обучается регрессор (по умолчанию BayesianRidge) на строках без пропусков и предсказывает пропущенные значения. Процесс повторяется max_iter раз до сходимости.

from sklearn.impute import IterativeImputer
from sklearn.linear_model import BayesianRidge
from sklearn.experimental import enable_iterative_imputer  # обязательный импорт

# IterativeImputer только для числовых признаков
iter_imputer = IterativeImputer(
    estimator=BayesianRidge(),  # внутренний регрессор
    max_iter=10,                # число раундов
    initial_strategy="mean",    # инициализация NaN перед первой итерацией
    imputation_order="ascending",  # по возрастанию доли NaN
    random_state=42,
    verbose=0,
)

# Только числовые колонки
X_num_train = X_train[num_cols].values
X_num_test = X_test[num_cols].values

iter_imputer.fit(X_num_train)
X_num_imputed = iter_imputer.transform(X_num_test)
print("NaN после IterativeImputer:", np.isnan(X_num_imputed).sum())  # 0

# В полном пайплайне:
preprocessor_iter = ColumnTransformer([
    ("num", Pipeline([
        ("impute", IterativeImputer(max_iter=10, random_state=42)),
        ("scale",  StandardScaler()),
    ]), num_cols),
    ("cat", Pipeline([
        ("impute", SimpleImputer(strategy="most_frequent")),
        ("encode", OneHotEncoder(handle_unknown="ignore", sparse_output=False)),
    ]), cat_cols),
])

pipeline_iter = Pipeline([
    ("prep", preprocessor_iter),
    ("clf",  HistGradientBoostingClassifier(random_state=42)),
])
pipeline_iter.fit(X_train, y_train)

KNNImputer как альтернатива

from sklearn.impute import KNNImputer

# Заполняет NaN средним по k ближайшим соседям в пространстве признаков
knn_imp = KNNImputer(n_neighbors=5, weights="distance")
X_knn = knn_imp.fit_transform(X_num_train)

# MissingIndicator: добавляем бинарный признак «был ли NaN»
from sklearn.impute import MissingIndicator
from sklearn.pipeline import FeatureUnion

indicator = MissingIndicator(features="missing-only")
indicator.fit(X_num_train)
print("Признаки с NaN:", indicator.features_)  # индексы столбцов с пропусками

Подводные камни

  • enable_iterative_imputer нужно импортировать явно — без него from sklearn.impute import IterativeImputer вызывает ImportError.
  • SimpleImputer(strategy='mean') не работает на строковых колонках; для категориальных используйте 'most_frequent' или 'constant'.
  • IterativeImputer вызывает fit внутреннего регрессора на каждой итерации для каждого признака — при большом числе признаков и max_iter=10 время fit резко растёт.
  • Если fit вызван на данных без NaN, SimpleImputer сохраняет статистики но не создаёт проблем; однако при transform с NaN применит сохранённые значения — это ожидаемое, не ошибочное поведение.
  • IterativeImputer не поддерживает категориальные признаки напрямую; передавайте только числовые или используйте OrdinalEncoder перед импутацией.
  • При использовании MissingIndicator внутри Pipeline позиция трансформера важна: индикатор должен видеть исходные NaN до импутации.
  • SimpleImputer возвращает numpy array, не DataFrame — имена столбцов теряются; оборачивайте в ColumnTransformer или используйте set_output(transform='pandas') (sklearn >= 1.2).
  • Лечение MNAR (Missing Not At Random) импутацией приводит к bias: если пропуск сам по себе несёт информацию (например, пациент не измерял давление именно при гипертонии), добавляйте бинарный индикатор через MissingIndicator.

Common mistakes

  • Объяснять imputers missing values только синтаксисом без shape, dtype, состояния или режима выполнения.
  • Игнорировать leakage, воспроизводимость, пустые входы и скрытые копии данных.
  • Не проверять production-симптомы: latency, память, ретраи, дрейф качества и несовпадение версий.

What the interviewer is testing

  • Может ли связать imputers missing values с реальным контрактом входов и выходов.
  • Упоминает ли тесты, метрики, reproducibility и диагностику ошибок.
  • Видит ли различие между demo-кодом в ноутбуке и production-пайплайном.

Sources

Related topics