Машинное обучение Данные и признаки: очистка, кодирование, масштабирование, утечки
0%

Данные и признаки: очистка, кодирование, масштабирование, утечки

Данные и признаки: очистка, кодирование, масштабирование, утечки

В карте трека мы условились, что почти любая задача обучения с учителем сводится к матрице признаков X размера n × d и вектору таргета y. Проблема в том, что в природе ни X, ни y не существуют. Существуют логи кликов, выгрузка из CRM с дублями, три разных написания города «Санкт-Петербург», поле salary, где у половины строк NULL, а у трети — -1 вместо NULL, и таблица платежей, куда данные доезжают с задержкой в сутки.

Работа, которая превращает всё это в X и y, называется подготовкой данных и конструированием признаков. По честным оценкам практиков она занимает 60–80% времени ML-проекта, и — что важнее — именно здесь возникает большинство катастроф. Плохую модель видно сразу: она плохо предсказывает. Плохие данные не видно вообще: модель показывает ROC-AUC 0.98 на валидации, вы выкатываете её в прод, и она работает на уровне подбрасывания монетки. Почти всегда причина — утечка.

Эта статья — про то, как получить X так, чтобы цифры на валидации что-то значили.

Почему признаки важнее алгоритма

Есть эмпирическое правило, которое подтверждается снова и снова на соревнованиях и в проде: на фиксированном наборе признаков разница между хорошим градиентным бустингом и хорошей нейросетью обычно составляет единицы процентов, а разница между плохим и хорошим набором признаков — десятки процентов.

Интуиция простая. Модель — это способ провести границу в том пространстве, которое вы ей дали. Если в пространстве нет информации, разделяющей классы, никакая мощность модели её не создаст. Классический пример: предсказание оттока абонента. Признак «количество звонков за месяц» слаб. Признак «отношение количества звонков за последнюю неделю к среднему за предыдущие 8 недель» — сильный, потому что отток начинается со снижения активности. Это одна арифметическая операция, но она кодирует доменное знание, которое модель сама из сырого счётчика не вытащит (точнее, вытащит только при огромном количестве данных и подходящей архитектуре).

Отсюда стратегия: вкладывайте усилия в признаки, пока прирост от них больше прироста от тюнинга гиперпараметров. На практике этот момент наступает нескоро.

Общая картина пайплайна

Ключевой узел здесь — тот, что выделен синим. Разбиение происходит до любых преобразований, зависящих от данных. Все статистики (среднее для импутации, mean/std для стандартизации, список категорий для one-hot, целевые средние для target encoding) вычисляются только на обучающей части и затем применяются к валидации и тесту. Нарушение этого правила — самая частая и самая коварная разновидность утечки; к ней мы вернёмся отдельно и подробно.

Таксономия признаков

Прежде чем что-то делать с колонкой, надо понять, что она такое. pandas.dtype тут почти бесполезен: код региона 77 хранится как int, но арифметика над ним бессмысленна.

Практический ход: составьте явную схему — словарь имя колонки → семантический тип → допустимые значения → политика обработки. Этот словарь пригодится и для валидации данных в проде, и как документация для следующего человека.

Очистка: пропуски

Механизмы пропусков

Стандартная классификация Рубина (Rubin, 1976) различает три механизма, и от механизма зависит, можно ли вообще безопасно импутировать:

  • MCAR (Missing Completely At Random) — пропуск не зависит ни от чего. Датчик сгорел случайно. Удаление строк даёт несмещённую оценку, просто теряется мощность.
  • MAR (Missing At Random) — вероятность пропуска зависит от наблюдаемых признаков. Пожилые реже указывают email. Здесь работает условная импутация по другим колонкам.
  • MNAR (Missing Not At Random) — вероятность пропуска зависит от самого пропущенного значения. Люди с высокой зарплатой чаще не указывают зарплату. Любая импутация смещена; правильный ход — считать сам факт пропуска признаком.

В прикладном ML почти всё — MNAR или близко к нему, потому что пропуски порождаются бизнес-процессом. Поле «дата первой покупки» пустое не случайно: у клиента не было покупок. Импутировать его медианой — значит выдумать несуществующую покупку.

Что делать на практике

import numpy as np
import pandas as pd
from sklearn.impute import SimpleImputer

# 1. Первым делом ищем "замаскированные" пропуски: сентинелы вместо NaN.
#    -1, 0, 9999, "N/A", "", "unknown", 1900-01-01 — типичные подозреваемые.
def find_sentinels(s: pd.Series, top: int = 5) -> pd.Series:
    """Самые частые значения: аномально высокая доля одного числа — красный флаг."""
    return s.value_counts(dropna=False).head(top) / len(s)

# 2. Флаг пропуска почти всегда полезен и почти никогда не вреден.
def add_missing_indicators(df: pd.DataFrame, cols: list[str]) -> pd.DataFrame:
    out = df.copy()
    for c in cols:
        out[f"{c}__is_na"] = df[c].isna().astype("int8")
    return out

# 3. Импутация: константа-маркер вне диапазона для деревьев,
#    медиана/среднее — для линейных моделей и всего, что чувствительно к геометрии.
num_imputer_trees = SimpleImputer(strategy="constant", fill_value=-999)
num_imputer_linear = SimpleImputer(strategy="median", add_indicator=True)

Три правила, которые экономят много боли:

  1. Для деревьев и бустингов импутация часто не нужна вовсе. LightGBM, XGBoost и CatBoost обрабатывают NaN нативно: при построении сплита пропуски отправляются в ту ветку, которая даёт больший прирост. Это строго лучше, чем ваша медиана, потому что направление выбирается по данным. См. раздел про missing values в документации LightGBM.
  2. Флаг пропуска (__is_na) добавляйте почти всегда. Он стоит один бит и часто оказывается в топе важности признаков — именно потому, что пропуски MNAR несут сигнал.
  3. Импутация — это обучаемое преобразование. Медиана считается на train и сохраняется. Если посчитать медиану по всему датасету, тест «протечёт» в train через это число. Эффект слабый, но он есть, и он складывается с другими.

Продвинутые методы — итеративная импутация (MICE, sklearn.impute.IterativeImputer) и kNN-импутация — в реальных задачах окупаются редко: они дороги, нестабильны на новых данных и требуют, чтобы весь их «обученный» стейт ехал в прод. Начинайте с простого.

Очистка: выбросы, дубли и грязь

Дубли. Полные дубли строк обычно артефакт джойна и удаляются. Гораздо опаснее частичные дубли — одна сущность под двумя id. Они портят и обучение (модель запоминает конкретный объект), и валидацию (один и тот же объект попадает в train и test — это утечка групп, см. ниже).

Выбросы. Различайте три вещи:

Тип Пример Что делать
Ошибка данных возраст 300, цена −5 Исправить или занулить в NaN
Редкое, но реальное покупка на 4 млн ₽ Оставить, возможно, лог-преобразовать
Другой режим бот вместо человека Выделить в отдельный признак/сегмент

Удалять «выбросы» по правилу трёх сигм механически — плохая идея: в тяжёлохвостых распределениях (деньги, длительности, сетевой трафик) вы выкинете самую содержательную часть выборки. Лучше:

# Робастное сжатие хвостов (winsorization) — параметры считаем ТОЛЬКО на train.
def fit_winsor(s: pd.Series, lo: float = 0.001, hi: float = 0.999) -> tuple[float, float]:
    return s.quantile(lo), s.quantile(hi)

def apply_winsor(s: pd.Series, bounds: tuple[float, float]) -> pd.Series:
    return s.clip(*bounds)

# Логарифм для правостороннего хвоста: log1p безопасен для нулей.
df["amount_log"] = np.log1p(df["amount"].clip(lower=0))

Грязь в категориях. "Москва", "москва", "МОСКВА", " Москва ", "Moskva" — это пять категорий для one-hot и одна для человека. Минимальная нормализация: stripcasefold → замена множественных пробелов → словарь синонимов для частых вариантов. Для адресов и названий компаний — специализированные библиотеки нормализации, а не регулярки.

Кодирование категориальных признаков

Модели считают числа. Категории надо перевести в числа так, чтобы не навязать модели ложный порядок и не взорвать размерность.

One-hot encoding

Каждой категории — своя бинарная колонка. Честный вариант по умолчанию для линейных моделей: он не навязывает порядок, коэффициенты интерпретируются как поправка относительно базовой категории.

Стоимость: d растёт на число уникальных значений. Для k категорий это O(n·k) памяти в плотном виде — при k = 10 000 и n = 10^6 это 10^10 ячеек, то есть нужно разреженное представление (sparse_output=True). Линейные модели и SVM с разреженными матрицами работают отлично; деревья — плохо, потому что каждый сплит по бинарной колонке отрезает крошечный кусок выборки, и дерево вырождается в цепочку.

Обязательно задавайте политику для невиданных категорий: handle_unknown="infrequent_if_exist" или "ignore". Иначе первая же новая категория в проде уронит пайплайн.

Ordinal encoding

Просто номер категории. Для порядковых признаков — корректно и компактно. Для номинальных с деревьями — приемлемо: дерево может «нарезать» числовую ось на куски и восстановить произвольные группировки, хотя и ценой лишней глубины. Для линейных моделей — категорически нет: вы утверждаете, что Казань − Москва = Новосибирск − Казань.

Target (mean) encoding

Заменяем категорию средним значением таргета в ней. Сжимает признак произвольной кардинальности в одно число и часто даёт заметный прирост. И это же — самый популярный способ устроить себе утечку.

Наивная реализация df.groupby(cat)[y].mean() использует таргет той самой строки, для которой считает признак. Для редкой категории, встретившейся один раз, закодированное значение равно её таргету — модель просто читает ответ. На train метрика улетает в потолок, на тесте — обвал.

Лечится двумя вещами: сглаживанием к глобальному среднему и вычислением out-of-fold.

from sklearn.model_selection import KFold

def target_encode_oof(
    X: pd.DataFrame, y: pd.Series, col: str, n_splits: int = 5, m: float = 20.0, seed: int = 0
) -> tuple[pd.Series, dict, float]:
    """Out-of-fold target encoding со сглаживанием.

    Для каждой строки признак считается по фолдам, куда эта строка НЕ входит.
    Сглаживание: enc = (sum_y + m * prior) / (count + m).
    m — сила притяжения к глобальному среднему: чем реже категория, тем ближе к prior.

    Сложность: O(n) на фолд, итого O(n * n_splits) времени, O(k) памяти на маппинг.
    """
    prior = y.mean()
    oof = pd.Series(np.nan, index=X.index, dtype="float64")

    for tr_idx, va_idx in KFold(n_splits, shuffle=True, random_state=seed).split(X):
        stats = y.iloc[tr_idx].groupby(X[col].iloc[tr_idx]).agg(["sum", "count"])
        mapping = (stats["sum"] + m * prior) / (stats["count"] + m)
        oof.iloc[va_idx] = X[col].iloc[va_idx].map(mapping).astype("float64")

    oof = oof.fillna(prior)  # категории, не встретившиеся в обучающих фолдах

    # Маппинг для инференса считается по ВСЕМ обучающим данным — так он устойчивее.
    full = y.groupby(X[col]).agg(["sum", "count"])
    full_map = ((full["sum"] + m * prior) / (full["count"] + m)).to_dict()
    return oof, full_map, prior

Тонкости, которые ломают target encoding даже у опытных людей:

  • OOF-схема должна совпадать со схемой валидации. Если валидация по времени, то и кодирование считается по прошлому, а не случайными фолдами.
  • При группировке (несколько строк на пользователя) фолды режутся по группам, иначе кодирование по одной строке пользователя протечёт в другую.
  • Сглаживание обязательно. Без него редкие категории — чистый шум с идеальной корреляцией на train.
  • CatBoost решает это системно через ordered target statistics: для каждого объекта статистика считается только по предшествующим в случайной перестановке объектам. Разбор — в статье CatBoost: unbiased boosting with categorical features. Если категорий много и они важны — часто проще взять CatBoost, чем писать своё.

Хеширование и частотное кодирование

Hashing trick: index = hash(category) mod B. Фиксированная размерность B, не нужен словарь, новые категории обрабатываются сами собой, O(1) на значение. Цена — коллизии: две категории делят колонку. При B, в 5–10 раз большем числа частых категорий, потери обычно малы. Незаменим для потоковых данных и признаков вроде user_agent. Теория — Weinberger et al., Feature Hashing for Large Scale Multitask Learning.

Частотное кодирование: категория → её частота в обучающей выборке. Ноль риска утечки таргета, дёшево, для деревьев часто удивительно эффективно (популярность сама по себе сигнал). Минус — категории с одинаковой частотой сливаются.

Эмбеддинги

Обучаемый вектор на категорию, оптимизируемый вместе с моделью. Даёт осмысленную геометрию (похожие товары рядом) и хорошо работает при большой кардинальности и больших данных. Требует нейросети — подробности в треке нейросетей. Практическая эвристика размерности: dim ≈ min(50, round(1.6 * k^0.56)).

Масштабирование и трансформации распределений

Кому это нужно

Масштаб влияет на всё, где есть расстояние или сумма с весами:

Семейство моделей Нужно масштабирование Почему
kNN, k-means, DBSCAN Обязательно Расстояние доминируется признаком с большим разбросом
SVM (особенно RBF) Обязательно Ядро — функция расстояния; плюс сходимость
Линейные с L1/L2 Обязательно Штраф на коэффициент несопоставим при разных масштабах
Линейные без регуляризации Желательно Обусловленность и скорость градиентного спуска
PCA, SVD Обязательно Максимизируется дисперсия — она в единицах измерения
Нейросети Обязательно Стабильность градиентов, работа инициализации
Деревья, RF, GBDT Не нужно Сплиты инвариантны к монотонным преобразованиям

Картинка объясняет геометрию лучше слов:

Влияние масштабирования на окрестность точки

В сырых единицах доход измеряется сотнями тысяч, а возраст — десятками. Квадрат разности по доходу на восемь порядков больше квадрата разности по возрасту, поэтому евклидово расстояние — это, по сути, расстояние по доходу. kNN формально использует два признака, фактически — один.

Методы

from sklearn.preprocessing import (
    StandardScaler, MinMaxScaler, RobustScaler, QuantileTransformer, PowerTransformer
)

# z-score: (x - mean) / std. Дефолт. Чувствителен к выбросам (они портят mean и std).
StandardScaler()

# в [0, 1]: (x - min) / (max - min). Для ограниченных величин и сетей с sigmoid.
# Один выброс сжимает все остальные точки в микроскопический интервал.
MinMaxScaler()

# (x - median) / IQR. Тот же z-score, но на робастных статистиках — при тяжёлых хвостах.
RobustScaler()

# ранговое преобразование в равномерное/нормальное. Убивает выбросы полностью,
# но и нелинейно искажает расстояния — сначала пробуйте что-то попроще.
QuantileTransformer(output_distribution="normal")

# Yeo-Johnson / Box-Cox: параметрическая нормализация скошенных распределений.
PowerTransformer(method="yeo-johnson")

Отдельно про трансформации формы распределения. Линейные модели предполагают линейную связь; сильно скошенный признак этому мешает. log1p для денег и счётчиков, квадратный корень для счётчиков Пуассона, логит для долей — дешёвые приёмы, которые часто дают больше, чем смена модели. Для деревьев они бесполезны (монотонное преобразование не меняет сплиты), но и не вредны.

Циклические признаки

Час 23 и час 0 отстоят на единицу, а не на 23. Кодируйте циклы парой синус/косинус:

def cyclical(values: pd.Series, period: int) -> pd.DataFrame:
    """Час суток -> (sin, cos). Расстояние между 23:00 и 00:00 становится малым."""
    angle = 2 * np.pi * values / period
    return pd.DataFrame({"sin": np.sin(angle), "cos": np.cos(angle)}, index=values.index)

X = pd.concat([X, cyclical(X["hour"], 24).add_prefix("hour_")], axis=1)

Для деревьев альтернатива проще и часто лучше: оставить час как есть и позволить модели самой нарезать интервалы.

Feature engineering: где брать сигнал

Несколько семейств приёмов, дающих наибольшую отдачу на табличных данных.

Агрегаты по сущности. Если строка — транзакция, а предсказываем поведение клиента, добавьте агрегаты по клиенту: сумма/среднее/медиана/max/std за окна 1, 7, 30, 90 дней, число уникальных категорий, время с последнего события. Это основной источник сигнала в задачах про поведение. Критично: агрегаты считаются строго по данным до момента предсказания.

Отношения и разности. Модели плохо изобретают деление. цена / средняя_цена_категории, траты_неделя / траты_месяц, возраст_аккаунта − возраст_карты — часто мощнее исходных величин, потому что нормируют на контекст.

Взаимодействия. Для линейных моделей произведения и попарные комбинации категорий (город × канал) — способ добавить нелинейность. Деревья находят взаимодействия сами, но неглубокие модели выигрывают от явных подсказок.

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

Текст. От простого к сложному: длина, количество слов, доля цифр/капса → TF-IDF по словам и символьным n-граммам → предобученные эмбеддинги. Символьные n-граммы (analyzer="char_wb", ngram_range=(3,5)) удивительно устойчивы к опечаткам и морфологии русского языка.

Гео. Координаты сами по себе слабы. Полезнее: расстояние до центра города, до ближайшего метро, идентификатор ячейки (geohash / H3) как категория, плотность объектов вокруг.

Утечки данных: главная тема

Утечка (data leakage) — ситуация, когда в признаках оказывается информация, недоступная в момент реального предсказания. Формальный разбор и таксономия — в работе Kaufman et al., Leakage in Data Mining. Признак утечки один и всегда один и тот же: подозрительно хорошая метрика. ROC-AUC 0.99 в задаче про поведение людей — это не успех, это баг, пока не доказано обратное.

Четыре типа

1. Целевая утечка (target leakage). Признак содержит следствие таргета. Классика: в задаче «уйдёт ли клиент» есть колонка дата_закрытия_счёта; в задаче «одобрить ли кредит» — сумма_первого_платежа; в медицинской задаче — назначенное_лечение. Все они заполняются после события, которое мы предсказываем. Приём диагностики: для каждого топ-признака спросите «в какой момент физически появляется это значение в проде и появляется ли оно до предсказания?». Если ответа нет — уберите признак и посмотрите на падение метрики.

2. Утечка через препроцессинг (train-test contamination). StandardScaler обучен на всём датасете — значит, среднее и дисперсия теста подмешаны в train. То же с импутацией, отбором признаков по корреляции с таргетом на всей выборке, PCA на всём датасете, подбором порога бинаризации. По отдельности каждая даёт небольшой оптимизм; вместе они дают уверенный разрыв между валидацией и продом. Лечится структурно: всё внутри sklearn.Pipeline, вся оценка через cross_val_score по этому пайплайну.

3. Временная утечка. Самая дорогая. Если данные имеют время, случайное разбиение даёт модели доступ к будущему.

Случайное и временное разбиение

Обратите внимание на зазор (эмбарго) во втором варианте. Он нужен, когда таргет размечается с горизонтом: если y — «совершит ли покупку в ближайшие 30 дней», то объекты за 30 дней до границы содержат информацию из тестового периода. Такой же зазор нужен, если признаки — скользящие окна. Подробная методология для финансовых данных (purging и embargo) — у Маркоса Лопеса де Прадо, Advances in Financial Machine Learning, глава 7.

4. Групповая утечка. Одна сущность представлена несколькими строками (несколько фото одного пациента, несколько сессий одного пользователя, дубли одного товара). При случайном разбиении часть строк объекта попадает в train, часть в test, и модель «узнаёт» объект, а не закономерность. Лечится GroupKFold / StratifiedGroupKFold по идентификатору сущности.

Как ловить утечку системно

from sklearn.pipeline import Pipeline
from sklearn.compose import ColumnTransformer
from sklearn.preprocessing import StandardScaler, OneHotEncoder
from sklearn.impute import SimpleImputer
from sklearn.linear_model import LogisticRegression
from sklearn.model_selection import cross_val_score, StratifiedGroupKFold

num_pipe = Pipeline([
    ("imp", SimpleImputer(strategy="median", add_indicator=True)),
    ("sc", StandardScaler()),
])
cat_pipe = Pipeline([
    ("imp", SimpleImputer(strategy="constant", fill_value="__NA__")),
    ("ohe", OneHotEncoder(handle_unknown="infrequent_if_exist", min_frequency=20,
                          sparse_output=True)),
])

prep = ColumnTransformer([
    ("num", num_pipe, num_cols),
    ("cat", cat_pipe, cat_cols),
], remainder="drop")

model = Pipeline([("prep", prep), ("clf", LogisticRegression(max_iter=2000, C=1.0))])

# Группировка по клиенту + стратификация по классу: две защиты сразу.
cv = StratifiedGroupKFold(n_splits=5, shuffle=True, random_state=42)
scores = cross_val_score(model, X, y, groups=X["client_id"], cv=cv, scoring="roc_auc")
print(f"ROC-AUC: {scores.mean():.4f} ± {scores.std():.4f}")

Чек-лист диагностики, когда метрика подозрительно хороша:

  1. Посмотрите важность признаков. Один признак с долей важности > 0.5 — почти всегда утечка.
  2. Обучите модель на одном подозрительном признаке. Если AUC 0.95 — вопрос закрыт.
  3. Для каждого признака выпишите время его появления относительно таргета. Это скучная таблица, которая спасает проекты.
  4. Сравните распределения признака в train и в свежих проде-данных. Расхождение — признак того, что офлайн-признак невоспроизводим онлайн.
  5. Проверьте, нет ли id-подобных колонок (порядковый номер строки, автоинкремент) — они часто коррелируют со временем, а значит и с таргетом.
  6. Устройте «бэктест»: обучитесь на данных до даты T, оцените на T…T+месяц. Если офлайн-метрика при этом резко падает — вы нашли временную утечку.

Правильный порядок операций при кросс-валидации

Если вы читаете этот код в чужом проекте и видите fit_transform на полном X до разбиения — вы нашли баг, каким бы аккуратным ни выглядел остальной пайплайн.

Отбор признаков

Больше признаков — не всегда лучше: растёт дисперсия оценок, дороже инференс, больше поверхность для дрейфа. Три семейства методов:

  • Фильтры. Дисперсия ≈ 0, попарная корреляция > 0.95, взаимная информация с таргетом. Дёшево (O(n·d)), но игнорируют взаимодействия. Важно: фильтр, использующий таргет, — обучаемое преобразование, ему место внутри пайплайна.
  • Обёртки. Рекурсивное исключение (RFE), последовательный отбор. Качественно, но O(d) обучений модели — дорого.
  • Встроенные. L1-регуляризация зануляет коэффициенты; важность в деревьях. Дёшево, потому что бесплатно идёт с обучением. Осторожно: feature_importances_ в деревьях смещена в сторону высококардинальных признаков — надёжнее permutation importance или SHAP.

Практичный компромисс: permutation importance на отложенной выборке. Она измеряет реальный вклад в качество, а не внутреннюю статистику модели (документация sklearn).

Как это устроено в проде

Ключевая проблема продакшена — train/serving skew: признак, посчитанный в офлайн-пайплайне на SQL, и признак, посчитанный в онлайн-сервисе на Python, различаются. Разные округления, разные часовые пояса, разные границы окон, разный порядок джойнов. Модель, обученная на одном, получает другое.

Три инструмента борьбы:

  1. Единая реализация признака. Один код, две среды исполнения. Именно это и продают feature store (Feast, Tecton, Databricks Feature Store): определение признака описано один раз, платформа материализует его и в батч-хранилище для обучения, и в онлайн-хранилище с низкой задержкой.
  2. Point-in-time correctness. Джойн признаков к обучающим примерам должен возвращать значение, актуальное на момент события, а не текущее. Это самое частое требование к feature store и самая частая ошибка в самописных пайплайнах: JOIN по ключу без условия на время — готовая временная утечка. Подробнее — в документации Feast про point-in-time joins.
  3. Валидация схемы и мониторинг дрейфа. Контракт на входные данные (Great Expectations, pandera, TFDV) с проверками типов, диапазонов, доли пропусков и множества допустимых категорий. Нарушение контракта должно ронять пайплайн громко, а не тихо превращаться в NaN.

Инженерная часть — версионирование признаков, воспроизводимость, откаты — разобрана в статье про MLOps.

Сложность и trade-offs

Операция Время Память Главный риск
One-hot, k категорий O(n) O(n·k) плотно, O(n) разреженно Взрыв размерности, unknown в проде
Ordinal O(n) O(n) Ложный порядок для линейных моделей
Target encoding OOF O(n · folds) O(k) Утечка при неверной схеме фолдов
Hashing, B бакетов O(n) O(n) Коллизии, потеря интерпретируемости
StandardScaler O(n·d) O(d) Чувствительность к выбросам
QuantileTransformer O(n log n) O(q·d) Искажение расстояний
kNN-импутация O(n²·d) O(n·d) Стоимость и перенос стейта в прод
Агрегаты по окнам O(n log n) O(n) Point-in-time ошибки

Общий принцип выбора: начинайте с самого дешёвого и предсказуемого преобразования, усложняйте только под измеренный прирост на честной валидации. Каждое умное преобразование — это дополнительный кусок состояния, который придётся сопровождать в проде годами.

Типичные ошибки

  1. fit_transform на всём датасете до разбиения. Самая частая. Лечится дисциплиной Pipeline.
  2. Наивный target encoding без OOF. Даёт эйфорию на train и провал на тесте.
  3. Случайный KFold на временных данных. Метрика красивая, прод мёртвый.
  4. Случайный KFold при повторяющихся сущностях. Нужен GroupKFold.
  5. Импутация нулём для «количества дней с последней покупки». Ноль означает «покупал сегодня» — вы сообщили модели противоположное истине.
  6. Игнорирование сентинелов. -1, 9999, 1900-01-01 попадают в модель как настоящие числа и портят все статистики.
  7. Масштабирование one-hot колонок. Формально не запрещено, но разрушает разреженность и интерпретацию; в ColumnTransformer держите ветки раздельно.
  8. Отбор признаков по корреляции с таргетом на всей выборке до CV. Тонкая, но реальная утечка: список признаков уже видел тест.
  9. Отсутствие политики для новых категорий. Первый же новый город в проде роняет сервис или молча даёт мусорный вектор.
  10. Признак «удобно доступен в выгрузке», но недоступен онлайн. Проверяйте не только корректность, но и физическую вычислимость в момент инференса с нужной задержкой.

Мини-итог

  • Признаки дают больший прирост, чем выбор модели; вкладывайтесь в них в первую очередь.
  • Разбиение выборки — первая операция, а не последняя. Всё, что «учится» на данных, учится только на train.
  • Пропуски чаще всего информативны: добавляйте флаг, а деревьям вообще отдавайте NaN как есть.
  • Кодирование выбирается по кардинальности и семейству модели; target encoding требует out-of-fold и сглаживания.
  • Масштабирование обязательно для всего, где есть расстояния, суммы с весами и градиенты; для деревьев не нужно.
  • Утечка — главный источник разрыва «офлайн отлично, онлайн ужасно». Четыре типа: целевая, препроцессинговая, временная, групповая.
  • В проде побеждает единая реализация признака для обучения и инференса плюс point-in-time корректность.

Источники

Смежные материалы трека: математический аппарат преобразований — в статье про математику для ML; схемы кросс-валидации и метрики разбираются в оценке моделей; связь размерности признаков с переобучением — в регуляризации; специфика временных признаков — во временных рядах. Инженерная сторона построения пайплайнов данных подробно разобрана в треке Data Engineering.

Что дальше

Матрица признаков готова — пора учить на ней первую модель. В следующей статье разбираем базовое семейство, с которого стоит начинать любую задачу и относительно которого измеряется всё остальное: Линейная и логистическая регрессия.

Нашли неточность? Выделите фрагмент текста — рядом появится жучок.

Нужен разбор именно вашей ситуации?

Статья описывает общий случай. Если у вас частный — можно разобрать его отдельно, платно. А если не хватает целого материала, предложите тему: её оплачивают вскладчину, и она выходит открытой для всех.

Доска запросов