Модуль 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) — стандарт, но полезно помнить о трёх слоях решения:
- Почему пропуск? «Не указан» — это тоже информация: люди, не заполнившие профиль, могут вести себя иначе. Поэтому к заполненному значению часто добавляют индикатор пропуска — бинарную колонку «здесь было пусто». Модель сама решит, важен ли факт отсутствия.
- Удалять строки — роскошь. Выбросить 20% датасета, чтобы не думать о пропусках, — почти всегда худший вариант: данных и так мало, а пропуски часто коррелируют с чем-то важным.
- Заполнить или доверить модели. Бустинги (модуль 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. Разница в скорости мала, разница в честности оценок — решающая.
💻 Практика
- (15 мин) Сниппет с вариациями. Запустите
feature-pipeline.py. Замените медиану на среднее (мысленно добавьте клиента с возрастом 200) и посчитайте, куда поплывёт заполнение. Затем поменяйте город одного клиента и проследите, как меняется строка matrix. - (15 мин) Эмуляция утечки. В сниппете посчитайте min и max только по строкам 0–5 (это «train») и примените к строкам 6–7 («val»). Сравните с нормализацией по всей таблице — почувствуйте, откуда берётся оптимистичная оценка.
- (20 мин) Аудит признаков. Возьмите таблицу с работы или учебную. Для каждого признака ответьте: «известен ли он в момент предсказания?» и «посчитан ли он на прошлом, а не на будущем?». Всё, что не прошло оба вопроса, — кандидаты на удаление.
- (10 мин) Диаграмма по памяти. Нарисуйте четыре канала утечки и три
исправления из
leakage-mapбез подглядывания и подпишите к каждому по примеру из своей предметной области.
🧩 Quiz "Проверь себя"
✅ Итоги модуля
- Объяснять, когда нормализация обязательна, а когда безразлична.
- Кодировать категории one-hot и понимать цену роста числа колонок; знать, зачем target encoding нужен out-of-fold.
- Заполнять пропуски медианой и добавлять индикатор пропуска.
- Называть четыре канала утечки: будущий признак, агрегат по будущему, случайный сплит по времени, препроцессинг до сплита.
- Применять проверку «убрал признак — качество рухнуло?» и читать feature importance как разведку.
- Собирать препроцессинг в пайплайн с fit на train.
🔗 Мостик к Модулю 9
Заметьте, сколько в этом модуле было про расстояния и масштаб: сниппет показал, как немасштабированные клики подавляют возраст — расстояние 220.2 против 0.503 после нормализации. Но в задачах с учителем у нас был таргет, который подтверждал, что признаки работают. А что делать, когда никаких ответов нет — только таблица клиентов и просьба «разделите их на сегменты»? Следующий модуль — кластеризация: k-means, выбор числа кластеров и ровно та же проблема масштаба, которая здесь была подготовкой.