Модуль 8. Фичи: кодирование, пропуски, утечка данных

Цель модуля: превратить сырую таблицу в честные признаки и не обмануть себя. После модуля вы масштабируете числовые фичи, кодируете категории one-hot и target encoding, разумно закрываете пропуски и, главное, умеете находить target leakage — утечку таргета в признаки, из-за которой AUC 0.99 на валидации оборачивается провалом в проде.

Перед началом: модули 1 (датасет, признаки и таргет), 3 (порог и скоры) и 7 (метрики, AUC, дисбаланс). Инструменты: песочница сайта (чистый Python), сниппет ниже. Трудоёмкость: ~2 ч (чтение 50 мин · сниппет и практика 50 мин).

🔥 AUC 0.99 на валидации — и провал в проде

Команда делает прогноз оттока подписки. Первая модель: AUC 0.71 — обычный рабочий результат, но мало. Второй заход: добавили «побольше данных», и валидационный AUC прыгает до 0.99. В отчёте — триумф, в проде — тишина: модель не отличает уходящих клиентов от остальных, будто валидации и не было.

Что произошло: в таблицу попал признак, который знал ответ. Например, «дата отмены подписки» или «количество обращений после последней оплаты» — конечно, у ушедших клиентов эти поля выглядят иначе. Модель не научилась предсказывать отток — она научилась подсматривать ответ в той же строке. Такая ловушка называется target leakage, утечкой таргета, и по частоте и дороговизне она — главный враг ML-инженера на табличных данных.

📖 8.1. Числовые фичи: масштаб, который решает

Модели не видят «возраст 32» и «980 кликов» — они видят два числа, и если одно на порядки больше другого, расстояния и градиенты перекосятся. Два стандартных преобразования:

  • Min-max: X' = (X − min) / (max − min) — загоняет признак в [0, 1]. Простое, но диапазон определяют минимум и максимум: один выброс сожмёт всех остальных в узкую полоску.
  • Z-score: X' = (X − mean) / std — приводит к среднему 0 и разбросу 1. Тоже чувствительно к выбросам, зато не ограничивает диапазон.

Запустите feature-pipeline.py. В нём крошечная таблица клиентов: возраст (с пропусками), город, клики. Возраст в данных 25–45, клики 4–980, и всё это приводится к [0, 1]. Дословная строка вывода:

Расстояние строк 0 и 2: сырое 220.2, после нормализации 0.503
  (в сырых данных почти весь вклад — от кликов: 120 против 340)

Вот наглядная цена немасштабированных данных: две строки различаются возрастом на 7 лет и кликами на 220 — и «расстояние 220.2» почти целиком про клики, возраст в нём тонет. После нормализации вклад признаков сопоставим: 0.503. Там, где модель считает расстояния (kNN из модуля 1, k-means из модуля 9), масштаб — вопрос работоспособности, а не вкуса.

Итоговая матрица фич — первые строки вывода сниппета:

 №   age_scaled     city=КЗН     city=МСК     city=СПБ  clicks_scaled
 0         0.00         0.00         1.00         0.00         0.12
 1         0.38         0.00         0.00         1.00         0.00

Здесь вся работа модуля в одной таблице: пропущенный возраст строки 1 заполнен медианой 32.5 и после масштабирования равен 0.38; города разложены в one-hot без выдуманного порядка; клики сжаты из диапазона 4–980 в [0, 1]. Все колонки теперь сравнимы, и ни одна не подавляет остальные — это и есть «честные признаки».

Важная оговорка: деревьям и бустингу (модули 4–6) нормализация не нужна — они задают пороговые вопросы вида «признак < порог», а порядок значений от линейных преобразований не меняется. Но как только в пайплайне появились расстояния или линейная комбинация с регуляризацией — нормализуйте.

📖 8.2. Категориальные: one-hot и его цена

Категорию «МСК» нельзя подсунуть модели как число: коды 1, 2, 3 выдумают порядок и величину, которых нет. One-hot убирает выдумку: на каждый город — своя колонка из нулей и одной единицы. В сниппете три города — КЗН, МСК, СПБ — превращаются в три колонки:

Города (one-hot колонки): КЗН, МСК, СПБ

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

Второй способ — target encoding: заменить категорию средним таргетом по этой категории. Колонок не плодит, работает с тысячами категорий, но таит две мины. Первая: средний таргет по всей таблице подглядывает за ответом и создаёт ту самую утечку. Вторая: редкие категории получают среднее по 2–3 строкам и переобучаются. Лекарство одно и называется out-of-fold: статистики для каждой строки считают только по остальным фолдам, как в кросс-валидации (модуль 11). Если у вас категорий немного — начинайте с one-hot: он скучный, но безопасный.

📖 8.3. Пропуски: почему нельзя просто удалять

В сниппете у клиентов не заполнен возраст, и первая строка вывода:

Медиана возраста (заполняем пропуски): 32.5

Медиана выбрана не случайно: она устойчива к выбросам, в отличие от среднего, которое один «возраст 200» утянет за собой. Заполнение (imputation) — стандарт, но полезно помнить о трёх слоях решения:

  1. Почему пропуск? «Не указан» — это тоже информация: люди, не заполнившие профиль, могут вести себя иначе. Поэтому к заполненному значению часто добавляют индикатор пропуска — бинарную колонку «здесь было пусто». Модель сама решит, важен ли факт отсутствия.
  2. Удалять строки — роскошь. Выбросить 20% датасета, чтобы не думать о пропусках, — почти всегда худший вариант: данных и так мало, а пропуски часто коррелируют с чем-то важным.
  3. Заполнить или доверить модели. Бустинги (модуль 6) умеют обрабатывать пропуски сами и часто делают это лучше простой медианы; линейные модели и kNN — нет, им нужно число.

Чего делать нельзя, так это считать статистики для заполнения по всей таблице сразу — вернёмся к этому в 8.4, потому что это уже утечка.

📖 8.4. Утечка данных: где течёт и как поймать

Посмотрите на диаграмму — это карта всех основных каналов, по которым таргет просачивается в признаки:

Карта утечек: четыре канала утечки таргета, симптом на валидации и три исправления

Разберём её слева направо. Первый канал — признак, который «знает» ответ: поле status=отменён при прогнозе отмены. В проде этого поля в момент предсказания просто не существует (или оно уже содержит ответ), а на валидации оно есть — и модель идеальна.

Второй канал — агрегат по будущему: «средний чек за квартал» в момент, когда квартал ещё не кончился. Тут утечка тоньше: сам признак выглядит приличным, но он посчитан на данных, которых в момент предсказания ещё не было.

Третий канал — время: случайный сплит по строкам перемешивает прошлое и будущее, и модель учится на «завтрашних» примерах, чтобы предсказывать «вчерашние». Четвёртый — препроцессинг до сплита: нормализация и кодирование, посчитанные по всей таблице. Именно здесь всплывает сниппет: вы посчитали медиану 32.5 и min-max по всей таблице — то есть val-часть повлияла на train-признаки. В проде статистики другие, и качество падает.

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

Три исправления из правой части диаграммы:

  • Фичи — только из прошлого относительно момента предсказания: проверяйте каждый признак вопросом «а это было известно тогда?».
  • Сплиты по времени, а не случайные (модуль 11): train — прошлое, val — будущее, как в реальном деплое.
  • Пайплайн fit на train, transform на val и в проде: все статистики (медианы, min/max, категории) считаются на train и применяются к остальным. Об этом и последняя строка вывода сниппета: sklearn объединяет шаги в один transform, который ведёт себя одинаково везде.

И последний инструмент — feature importance как разведка. Важность признаков из дерева или бустинга (модули 4–6) — это не «истина», но отличный детектор: признак с подозрительно гигантской важностью чаще всего оказывается утечкой. Смотрите не на «кто первый», а на «почему у него такое преимущество».

📖 8.5. Компромиссы

  • Нормализация vs деревья. Линейным моделям, kNN и k-means нормализация необходима, деревьям и бустингу безразлична. Единый пайплайн с нормализацией безвреден почти везде — кроме потери интерпретируемости коэффициентов в исходных единицах.
  • One-hot vs target encoding. One-hot безопасен, прозрачен и плодит колонки; target encoding компактен и работает с большими кардинальностями, но требует out-of-fold и дисциплины, иначе утечка.
  • Заполнять пропуски vs удалять строки. Заполнение сохраняет данные, но вносит допущение; удаление честно, но теряет строки и редко оправдано. Индикатор пропуска — компромисс: заполнили, но и факт отсутствия сохранили.
  • Больше фич vs риск утечки и шума. Каждый новый признак — шанс улучшить модель и шанс притащить утечку или шум. Feature importance и проверка «убрал — рухнуло?» — арбитры.
  • Скорость итераций vs чистота сплитов. Быстрее всего считать фичи по всей таблице; правильно — только по train. Разница в скорости мала, разница в честности оценок — решающая.

💻 Практика

  1. (15 мин) Сниппет с вариациями. Запустите feature-pipeline.py. Замените медиану на среднее (мысленно добавьте клиента с возрастом 200) и посчитайте, куда поплывёт заполнение. Затем поменяйте город одного клиента и проследите, как меняется строка matrix.
  2. (15 мин) Эмуляция утечки. В сниппете посчитайте min и max только по строкам 0–5 (это «train») и примените к строкам 6–7 («val»). Сравните с нормализацией по всей таблице — почувствуйте, откуда берётся оптимистичная оценка.
  3. (20 мин) Аудит признаков. Возьмите таблицу с работы или учебную. Для каждого признака ответьте: «известен ли он в момент предсказания?» и «посчитан ли он на прошлом, а не на будущем?». Всё, что не прошло оба вопроса, — кандидаты на удаление.
  4. (10 мин) Диаграмма по памяти. Нарисуйте четыре канала утечки и три исправления из leakage-map без подглядывания и подпишите к каждому по примеру из своей предметной области.

🧩 Quiz "Проверь себя"

1. Сколько колонок даст one-hot для 500 городов?
2. Модель показала AUC 0.99 на валидации и провалилась в проде. Первая гипотеза?
3. Что делать со статистиками пайплайна (медианы, min/max, кодирование)?
4. Зачем нормализовать числовые признаки?
5. Медиана для заполнения пропусков выбирается потому, что…

✅ Итоги модуля

  • Объяснять, когда нормализация обязательна, а когда безразлична.
  • Кодировать категории one-hot и понимать цену роста числа колонок; знать, зачем target encoding нужен out-of-fold.
  • Заполнять пропуски медианой и добавлять индикатор пропуска.
  • Называть четыре канала утечки: будущий признак, агрегат по будущему, случайный сплит по времени, препроцессинг до сплита.
  • Применять проверку «убрал признак — качество рухнуло?» и читать feature importance как разведку.
  • Собирать препроцессинг в пайплайн с fit на train.

🔗 Мостик к Модулю 9

Заметьте, сколько в этом модуле было про расстояния и масштаб: сниппет показал, как немасштабированные клики подавляют возраст — расстояние 220.2 против 0.503 после нормализации. Но в задачах с учителем у нас был таргет, который подтверждал, что признаки работают. А что делать, когда никаких ответов нет — только таблица клиентов и просьба «разделите их на сегменты»? Следующий модуль — кластеризация: k-means, выбор числа кластеров и ровно та же проблема масштаба, которая здесь была подготовкой.