Перейти к содержимому
Ступень 22.4 Практический ML на табличных данных

Разведочный анализ, визуализация и признаки

Как смотреть на данные до модели: распределения, выбросы, корреляции и парадокс Симпсона, утечки и дубликаты, честные графики в matplotlib и первые признаки — от one-hot до target encoding, через который протекает целевая переменная.

УРОКstage-2/2-4-tabular-practice/02-eda-visualization.mdx2 чУрок 2 из 4
После урока вы сможете
  • Провести разведочный анализ таблицы по чек-листу и найти выбросы, дубликаты, дисбаланс и подозрительные признаки
  • Отличить настоящую связь от артефакта — тяжёлого хвоста, выброса, смешения групп (парадокс Симпсона)
  • Построить в matplotlib гистограмму, «ящик с усами», диаграмму рассеяния, тепловую карту и small multiples по правилам честного графика
  • Сделать признаки из чисел, категорий, дат и текста и объяснить, как target encoding протекает целевой переменной и как это лечат

Андрей Карпатый в «Рецепте обучения нейросетей» ставит первым шагом не выбор архитектуры, а «стать единым целым с данными»: часами листать примеры, смотреть на распределения, искать дубликаты, битые записи и странные закономерности, не написав ни строчки кода модели. Причина простая: модель выучит всё, что есть в данных, включая ошибки разметки, утечки и перекосы. Обнаружить их после обучения гораздо дороже.

Разведочный анализ данныхразведочный анализ данных (EDA)EN exploratory data analysis (EDA)Систематический первый взгляд на данные до модели — распределения, выбросы, связи, утечки, дубликаты. (EDA, exploratory data analysis) — это систематический первый взгляд на таблицу до модели. Он отвечает на вопросы: как распределены признаки, где выбросы и пропуски, что с чем связано, нет ли подозрительно хороших признаков, сбалансированы ли классы. В мире LLM ровно то же делают с корпусом претрейна (урок 5.1.2: длины, языки, дубликаты, мусор) и с результатами эвалов (урок 7.3: где модель ошибается, не перекошен ли набор). Чек-лист из конца урока пригодится вам там без изменений.

Работаем с логами LLM-сервиса из прошлого урока: logs = make_logs(), 2000 запросов, задержки, токены и статусы.

flowchart LR
A["Структура:<br/>shape, типы,<br/>пропуски"] --> B["Каждый признак:<br/>распределение,<br/>выбросы"]
B --> C["Связи:<br/>корреляции,<br/>группы"]
C --> D["Цель:<br/>дисбаланс,<br/>утечки"]
D --> E["Дубликаты<br/>и чистка"]
E --> F["Признаки<br/>для модели"]
Разведка идёт от простого к сложному; на каждом шаге — график и вопрос «так и должно быть?»

Первый шаг вы уже умеете: shape, info, describe, value_counts из урока 2.4.1. Дальше — по порядку.

В describe у задержки среднее 4032 мс, медиана 2802 мс, максимум 51 208 мс. Среднее намного больше медианы: у распределения тяжёлый правый хвост. Так устроены почти все «размеры» в мире: длины текстов в токенах, цены, доходы, время ответа. Число, которое это измеряет, — асимметрия (skewness): для симметричного распределения она около 0, у нашей задержки logs["latency_ms"].skew() равна 3.35.

На обычной гистограмме такой признак выглядит как высокий столбик у нуля и длинный пустой хвост: формы не видно. Помогает логарифмическая шкалалогарифмическая шкалаEN logarithmic scaleРавные отрезки означают умножение (×10), а не прибавление. Степенные законы на ней — прямые линии. (урок 1.0.2): равные отступы по оси означают равные отношения (1000 → 10 000 → 100 000), а не равные разности. После логарифма асимметрия задержки падает до 0.17 — распределение становится почти симметричным «колоколом» (левый верхний график на рисунке ниже).

Для моделей вывод такой: признак с тяжёлым хвостом часто заменяют на log⁡(1+x)\log(1 + x) (np.log1p). Линейной модели это помогает: разница между 1000 и 2000 токенов становится такой же, как между 10 000 и 20 000. Деревьям всё равно: они сравнивают с порогом, а логарифм порядок не меняет.

Выброс (outlier) — значение, далёкое от основной массы. Стандартный детектор — правило межквартильного размаха (IQR). Квартили Q1Q_1 и Q3Q_3 — значения, ниже которых лежит 25% и 75% данных; IQR=Q3−Q1\mathrm{IQR} = Q_3 - Q_1 — ширина «середины». Выбросами считают всё, что дальше 1.5⋅IQR1.5 \cdot \mathrm{IQR} от краёв середины:

x<Q1−1.5 IQRилиx>Q3+1.5 IQR.x < Q_1 - 1.5\,\mathrm{IQR} \quad \text{или} \quad x > Q_3 + 1.5\,\mathrm{IQR}.

Читаем: «отступаем от первого квартиля вниз и от третьего вверх на полторы ширины середины; всё, что за этими границами, — подозрительно». Именно эти границы рисуют «усы» у ящика с усамиящик с усами (boxplot)EN box plotЯщик — от Q1 до Q3 с линией медианы; усы — до Q1 − 1.5·IQR и Q3 + 1.5·IQR; точки за усами — кандидаты в выбросы. (boxplot).

На числах. У задержки Q1=1584Q_1 = 1584, Q3=4997Q_3 = 4997, IQR=3413\mathrm{IQR} = 3413. Верхняя граница 4997+1.5⋅3413=10 1154997 + 1.5 \cdot 3413 = 10\,115 мс. За ней 144 запроса — 7% данных, и 140 из них приходятся на две большие модели. Это не ошибки: большие модели генерируют медленнее, и длинный ответ на deepseek-v3 законно идёт 15 секунд. Правило, придуманное для симметричных данных, на тяжёлом хвосте «ловит» нормальные значения. Если применить его к логарифму задержки, выбросов остаётся 5 (четыре запроса дольше 28 секунд и один быстрее 0.3 секунды), а если считать по логарифму внутри каждой модели отдельно — 7 (по сырой задержке внутри моделей — всё ещё 85).

Отсюда правило работы с выбросами:

  1. Найти — IQR, z-оценка x−xˉs\frac{x - \bar x}{s} больше 3 (у нас 28 точек), а лучше — график.
  2. Понять, что это. Опечатка (площадь квартиры 3540 м² вместо 35.4), сбой датчика, единицы измерения (секунды вперемешку с миллисекундами) — ошибки. Долгий запрос на большой модели — редкость, но правда.
  3. Решить. Ошибки исправить или удалить. Редкие настоящие значения оставить, а модели помочь: логарифм, устойчивая к выбросам метрика (MAE вместо MSE, урок 2.2.3), обрезка по перцентилю.

Удалять «всё, что за усами» вслепую нельзя: вы выбросите самые интересные случаи.

Коэффициент корреляции Пирсонакорреляция (коэффициент Пирсона)EN correlation (Pearson coefficient)Число от −1 до 1 — сила ЛИНЕЙНОЙ связи двух признаков; косинус угла между центрированными векторами. Корреляция Спирмена — то же по рангам. измеряет, насколько два признака связаны линейно:

r=∑i(xi−xˉ)(yi−yˉ)∑i(xi−xˉ)2 ∑i(yi−yˉ)2.r = \frac{\sum_i (x_i - \bar x)(y_i - \bar y)}{\sqrt{\sum_i (x_i - \bar x)^2}\,\sqrt{\sum_i (y_i - \bar y)^2}}.

Числитель — сумма произведений отклонений от средних: положительная, если xx и yy вместе бывают выше или вместе ниже своих средних. Знаменатель нормирует, и rr всегда лежит от −1 до 1. Это косинус угла между центрированными векторами x−xˉx - \bar x и y−yˉy - \bar y — то самое косинусное сходство из урока 1.1.1.

На числах. x=(1,2,3,4)x = (1, 2, 3, 4), y=(2,4,5,9)y = (2, 4, 5, 9). Средние 2.5 и 5, отклонения (−1.5,−0.5,0.5,1.5)(-1.5, -0.5, 0.5, 1.5) и (−3,−1,0,4)(-3, -1, 0, 4). Числитель 4.5+0.5+0+6=114.5 + 0.5 + 0 + 6 = 11, знаменатель 5⋅26≈2.236⋅5.099\sqrt{5} \cdot \sqrt{26} \approx 2.236 \cdot 5.099, итого r≈0.965r \approx 0.965. В pandas: df[cols].corr() — сразу вся матрица.

В логах: completion_tokens и latency_ms — 0.72, prompt_tokens и latency_ms — 0.01 (в нашей синтетике задержка от длины промпта не зависит). Четыре ловушки корреляции:

  • Нелинейность. Для y=x2y = x^2 на отрезке от −1 до 1 корреляция ровно 0, хотя yy полностью определяется xx. r=0r = 0 значит «нет линейной связи», а не «нет связи». Смотрите на диаграмму рассеяния.
  • Выбросы. Добавьте к четырём точкам выше одну пятую (40,0)(40, 0) — и rr станет −0.61. Одна точка перевернула знак. Устойчивая альтернатива — корреляция Спирмена: та же формула, но по рангам (corr(method="spearman")).
  • Смешение групп. Внутри каждой модели корреляция токенов ответа и задержки 0.92–0.95, а по всем данным — 0.72: у моделей разная скорость, и облака точек смещены друг относительно друга (левый нижний график ниже).
  • Корреляция — не причинность. Продажи мороженого коррелируют с числом утоплений: обоих больше летом.

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

ЛёгкиеТрудныеВсего
Модель A18 из 20 (90%)48 из 80 (60%)66 из 100 (66%)
Модель B64 из 80 (80%)10 из 20 (50%)74 из 100 (74%)

A точнее B в каждой категории, но хуже в сумме. Противоречия нет: модели A достались в основном трудные вопросы, модели B — лёгкие, и общая точность — это среднее, взвешенное по составу. Это парадокс Симпсонапарадокс СимпсонаEN Simpson's paradoxСвязь в каждой группе направлена в одну сторону, а в объединённых данных — в другую из-за разного состава групп.. Когда в урок 7.3 вы будете сравнивать модели на бенчмарках, первым вопросом должно быть «на одних и тех же ли вопросах?», а вторым — «как выглядит разбивка по категориям?».

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

print(logs["status"].value_counts(normalize=True).round(3)) # ok 0.938, timeout 0.032, error 0.030

Таймаутов 3.2%. Это дисбаланс классовдисбаланс классовEN class imbalance из урока 2.2.3: модель «никогда не таймаут» получит 96.8% accuracy и не принесёт пользы. Сразу выбираем метрику (recall, PR-кривые, ROC-AUC) и при разбиении сохраняем доли классов (стратификация, урок 2.4.4).

Утечки через пропуски, идентификаторы и время

Заголовок раздела «Утечки через пропуски, идентификаторы и время»

Вспомним утечка данныхутечка данныхEN data leakage из урока 2.1.2: признак, которого не будет в момент предсказания. Простой детектор — обучить модель на каждом признаке по отдельности и посмотреть на ROC-AUC. Признак latency_ms.isna() даёт для таймаутов AUC = 1.0: у таймаутов задержку не записали. Но предсказывать таймаут нужно до ответа модели, когда задержки ещё нет. Это утечка через пропуск. Честный признак вроде completion_tokens даёт 0.45 — у нас таймауты случайны.

Типичные каналы утечек в таблицах:

  • Пропуски и служебные поля, заполняемые по итогам события (задержка у таймаута, «причина отказа», «дата расторжения договора»).
  • Идентификаторы. Если записи нумеровали после сортировки по чему-то, связанному с ответом (сначала все одобренные заявки, потом отклонённые), номер строки предсказывает ответ. В соревнованиях Kaggle утечки через ID и порядок строк находили не раз.
  • Время. Признак «сколько всего запросов у пользователя за неделю» (groupby("user_id")["ts"].transform("size")) для запроса в понедельник знает будущее. Честный вариант — «сколько было до этого запроса»: groupby("user_id").cumcount() на отсортированных по времени данных.
  • Признаки-следствия цели. Комиссия агента = 3% от цены сделки; число звонков в отдел удержания у ушедших клиентов.

Правило: про каждый признак спросите «известен ли он в момент, когда модель будет делать предсказание?». Признак, который в одиночку даёт AUC 0.99, — почти всегда утечка, а не удача.

df.duplicated().sum() считает строки, полностью повторяющие предыдущие. В логах их 0, но по паре (user_id, prompt) повторов 1647: пользователи отправляют одни и те же запросы. Это нормально для логов, но если повторы окажутся и в train, и в test, модель будет проверяться на том, что уже видела. Для текстов дубликаты ищут после нормализации (регистр, пробелы, знаки препинания), а почти-дубликаты — через MinHash (урок 5.1.2). У LLM это та же проблема, что и загрязнение бенчмарков.

Четыре графика закрывают почти всю разведку. Код запускайте локально (matplotlib — урок 0.3.3):

import matplotlib.pyplot as plt
import numpy as np
fig, axes = plt.subplots(2, 2, figsize=(11, 8))
ax = axes[0, 0] # 1) распределение задержки в лог-шкале
bins = np.logspace(np.log10(200), np.log10(60_000), 40) # корзины равной ширины на лог-оси
ax.hist(logs["latency_ms"].dropna(), bins=bins)
ax.set_xscale("log")
ax.set_xlabel("задержка, мс (лог-шкала)"); ax.set_ylabel("запросов")
ax.set_title(f"Задержка (n = {logs['latency_ms'].notna().sum()})")
ax = axes[0, 1] # 2) ящики с усами по моделям
order = ["qwen3-8b", "qwen3-32b", "deepseek-v3"]
ax.boxplot([logs.loc[logs["model"] == m, "latency_ms"].dropna() for m in order])
ax.set_xticks([1, 2, 3], order) # подписи под ящиками
ax.set_yscale("log"); ax.set_ylabel("задержка, мс (лог-шкала)"); ax.set_title("Задержка по моделям")
ax = axes[1, 0] # 3) диаграмма рассеяния, цвет — модель
for m in order:
g = logs[logs["model"] == m]
ax.scatter(g["completion_tokens"], g["latency_ms"], s=6, alpha=0.5, label=m)
ax.set_xlabel("токенов в ответе"); ax.set_ylabel("задержка, мс"); ax.legend(); ax.set_title("Токены ответа и задержка")
ax = axes[1, 1] # 4) тепловая карта корреляций
cols = ["prompt_tokens", "completion_tokens", "latency_ms"]
corr = logs[cols].corr()
im = ax.imshow(corr, vmin=-1, vmax=1, cmap="RdBu_r") # симметричная шкала: 0 — белый
ax.set_xticks(range(3), cols, rotation=20); ax.set_yticks(range(3), cols)
for i in range(3):
for j in range(3):
ax.text(j, i, f"{corr.iloc[i, j]:.2f}", ha="center", va="center") # число в каждой клетке
fig.colorbar(im, ax=ax); ax.set_title("Корреляции Пирсона")
fig.tight_layout()
fig.savefig("eda_overview.png", dpi=90)

Четыре графика разведки логов: гистограмма задержки в логарифмической шкале похожа на колокол; ящики с усами показывают, что qwen3-8b быстрее двух других моделей; на диаграмме рассеяния задержка растёт с числом токенов ответа тремя веерами по моделям; тепловая карта корреляций с числом 0.72 между токенами ответа и задержкой

Что читается с картинки. Гистограмма в лог-шкале — колокол без странных пиков, значит, задержки записаны единообразно. «Ящики» показывают, что медиана у qwen3-8b втрое ниже, чем у deepseek-v3, а кружки над усами — те самые «выбросы», которые на деле просто длинные ответы. Диаграмма рассеяния — три веера с разным наклоном: наклон — это секунды на токен, как в линейной регрессии из урока 2.2.1.

Small multiples («много маленьких») — один и тот же график для каждой группы рядом, в общих осях: fig, axes = plt.subplots(1, 3, sharex=True, sharey=True) и по гистограмме на модель. Глаз сравнивает формы мгновенно, но только если оси общие: иначе одинаковые на вид столбики значат разное.

Признаки: из сырых столбцов — в числа для модели

Заголовок раздела «Признаки: из сырых столбцов — в числа для модели»

конструирование признаковконструирование признаков (feature engineering)EN feature engineeringПревращение сырых столбцов в признаки для модели: масштабирование, логарифм, кодирование категорий, признаки из дат и текста. — превращение сырых столбцов в признаки, удобные модели. Главные приёмы:

Числа. Стандартизация (урок 2.2.1) нужна линейным моделям, kNN и нейросетям; деревьям — нет. Тяжёлые хвосты — через np.log1p. Статистики (среднее, стандартное отклонение) считают только по train — это правило урока 2.1.2 никуда не делось.

Категории. Модель умножает признаки на веса, а «deepseek-v3» на вес не умножишь. Самый простой перевод — one-hot кодированиеone-hot кодирование (унитарный код)EN one-hot encodingВектор длины V из нулей с единицей в позиции номера токена. Все one-hot векторы попарно ортогональны.-кодирование: по столбцу-индикатору на каждое значение (pd.get_dummies(logs["model"]) даёт три столбца из нулей и единиц). Беда — категории с тысячами значений (город, промокод, ID товара): столбцов станет тысячи, большинство почти пустые.

Target encodingкодирование средним целевой переменной (target encoding)EN target encodingКатегория → среднее цели по ней. Без out-of-fold и сглаживания протекает целевой переменной: у редких категорий кодировка почти равна метке. заменяет категорию средним значением цели по ней: промокоду — долю ушедших клиентов среди его владельцев. Один плотный столбец вместо тысячи. Но здесь спрятана утечка: среднее по категории включает ответ самой строки. У промокода, встретившегося один раз, кодировка в точности равна метке.

Эксперимент: 1000 клиентов, 500 случайных промокодов, метки случайные (20% единиц) — предсказывать нечего.

Способ кодированияROC-AUC на тех же данных
среднее цели по категории (наивно)0.919
то же со сглаживанием, m=10m = 100.911
out-of-fold: среднее по другим фолдам0.529

Наивная кодировка «нашла» сигнал в шуме, как отбор признаков в эксперименте урока 2.1.2. Лекарства два:

  • Out-of-fold. Делим train на 5 частей; кодировку для строк части jj считаем по остальным четырём частям. Строка никогда не видит своего ответа. Так работает TargetEncoder в scikit-learn (с версии 1.3), а CatBoost считает среднее только по «предыдущим» строкам в случайной перестановке (урок 2.3.1).
  • Сглаживание тянет кодировку редких категорий к общему среднему:
enc(c)=ncyˉc+myˉnc+m,\mathrm{enc}(c) = \frac{n_c \bar y_c + m \bar y}{n_c + m},

где ncn_c — сколько раз встретилась категория, yˉc\bar y_c — среднее цели в ней, yˉ\bar y — общее среднее, mm — «сила доверия» общему среднему. Категория из 3 строк, все три — единицы, общее среднее 0.2, m=10m = 10: (3⋅1+10⋅0.2)/13≈0.385(3 \cdot 1 + 10 \cdot 0.2) / 13 \approx 0.385 вместо 1.0. Само по себе сглаживание утечку не убирает (0.911 в таблице), но в паре с out-of-fold делает кодировку устойчивой.

Даты. Из ts получаются час, день недели, «выходной ли», «дней с регистрации». Час — циклический признак: 23:00 и 00:00 соседние, а числа 23 и 0 далеки. Переводят в точку на окружности: (sin⁡2πh24,cos⁡2πh24)(\sin \frac{2\pi h}{24}, \cos \frac{2\pi h}{24}). Для 0 часов это (0,1)(0, 1), для 23 — (−0.259,0.966)(-0.259, 0.966), расстояние между ними 0.26, а между полуночью и полуднем — 2.

Текст. Длина в символах и байтах, доля кириллицы, наличие слов «переведи/übersetze/translate» (урок 2.4.1). Настоящее представление смысла текста — эмбеддинги (ступень 4), и в таблицах их тоже используют как признаки.

Пропуски. Заполняют медианой (по train!) и добавляют столбец-индикатор «был пропуск» — факт отсутствия значения часто информативен. Бустинг (урок 2.4.3) умеет работать с NaN сам.

  1. Структура: сколько строк и столбцов, типы, пропуски по столбцам, что означает одна строка.
  2. Каждый признак: гистограмма (при тяжёлом хвосте — в лог-шкале), диапазон, невозможные значения, выбросы.
  3. Категории: value_counts, редкие и неожиданные значения, разные написания одного и того же.
  4. Связи: матрица корреляций и диаграммы рассеяния для пар с высокой корреляцией; разбивка по группам (Симпсон!).
  5. Цель: доли классов или распределение; одиночные признаки с подозрительно высоким качеством.
  6. Утечки: про каждый признак — «известен ли он в момент предсказания?»; ID, время, пропуски.
  7. Дубликаты: полные, по ключевым полям, почти-дубликаты; повторы между train и test.
  8. Запишите выводы — что починили, что оставили и почему. Это начало отчёта (урок 2.4.4).

Для текстов и сырых рабочих выгрузок к чек-листу добавляются ещё пять пунктов — они в уроке 11.1.1.

Перед вами «объявления о квартирах» — синтетика с тремя спрятанными проблемами: выбросом, утечкой и дубликатами. Реализуйте функции разведки, найдите проблемы, почистите данные и соберите small multiples из гистограмм. Рисуем через matplotlib.figure.Figure без pyplot: так графики работают и в браузере, а тесты проверяют их устройство (число графиков, заголовки, столбики).

Детектив данных: выброс, утечка, дубликатыpandas, matplotlib · нужен интернет6 тестов
import numpy as np
import pandas as pd
from matplotlib.figure import Figure   # рисуем без pyplot: так графики работают и в браузере


def make_flats(seed=7):
    """Синтетические объявления о продаже квартир (цены условные, млн ₽). В данных спрятаны три проблемы."""
    rng = np.random.default_rng(seed)
    n = 300
    district = rng.choice(["Центр", "Север", "Юг", "Запад"], size=n, p=[0.2, 0.3, 0.3, 0.2])
    area = rng.normal(55, 15, n).clip(20, 120).round(1)
    rooms = np.clip(np.round(area / 22 + rng.normal(0, 0.4, n)), 1, 5).astype(int)
    year = rng.integers(1965, 2025, n)
    floor = rng.integers(1, 26, n)
    per_m2 = pd.Series(district).map({"Центр": 0.32, "Север": 0.19, "Юг": 0.17, "Запад": 0.22}).to_numpy()
    price = area * per_m2 * (1 + 0.006 * (year - 1995)) * rng.lognormal(0, 0.15, n)
    df = pd.DataFrame({
        "listing_id": np.arange(1001, 1001 + n),
        "district": district, "area": area, "rooms": rooms, "year": year, "floor": floor,
        "commission": (0.03 * price * rng.normal(1, 0.01, n)).round(3),
        "price": price.round(2),
    })
    df.loc[42, "area"] = 3540.0
    dup = df.sample(6, random_state=1)
    return pd.concat([df, dup], ignore_index=True)


def iqr_bounds(s, k=1.5):
    """Границы «усов» по правилу IQR: (Q1 − k·IQR, Q3 + k·IQR), где IQR = Q3 − Q1.

    Квартили считайте методом Series.quantile (по умолчанию — линейная интерполяция).
    Вернуть кортеж из двух float.
    """
    # TODO: ваш код
    ...


def find_outliers(df, col, k=1.5):
    """Список меток индекса (по возрастанию) строк, где df[col] выходит за границы iqr_bounds."""
    # TODO: ваш код
    ...


def count_duplicates(df):
    """Сколько строк полностью повторяют одну из ПРЕДЫДУЩИХ строк (первое вхождение не считаем)."""
    # TODO: ваш код
    ...


def find_leaks(df, target, threshold=0.95):
    """Подозрительные признаки: числовые столбцы (кроме target), у которых |корреляция Пирсона|
    с target ≥ threshold. Вернуть список имён, отсортированный по убыванию |корреляции|.
    """
    # TODO: ваш код
    ...


def clean(df, target="price"):
    """Почистить данные по итогам разведки, по шагам:

    1) убрать полные дубликаты строк (оставить первое вхождение);
    2) убрать строки — грубые выбросы по площади: find_outliers(..., "area", k=3);
    3) убрать столбцы-утечки: find_leaks на данных после шагов 1–2.
    Вернуть новую таблицу; исходную не менять.
    """
    # TODO: ваш код
    ...


def overview_figure(df, cols):
    """Small multiples: Figure с гистограммой каждого столбца из cols в один ряд.

    Заголовок каждого графика (ax.set_title) — имя столбца. Вернуть объект Figure.
    Подсказка: fig = Figure(figsize=(4 * len(cols), 3)); axes = fig.subplots(1, len(cols), squeeze=False)[0]
    """
    # TODO: ваш код
    ...


# «Запустить»: разведка по чек-листу урока
flats = make_flats()
print(flats.shape)
print(flats.describe().round(2), "\n")
print("границы IQR для area:", iqr_bounds(flats["area"]))
print("выбросы area (k=3):", find_outliers(flats, "area", k=3))
print("дубликатов:", count_duplicates(flats))
print("подозрение на утечку:", find_leaks(flats, "price"))
cleaned = clean(flats)
if cleaned is not None:
    print("после чистки:", cleaned.shape, "корреляция area–price:", round(cleaned["area"].corr(cleaned["price"]), 3))
fig = overview_figure(flats, ["area", "year", "price"])
if fig is not None:
    print("графиков в фигуре:", len(fig.axes))

После решения посмотрите на число из «Запустить»: с опечаткой 3540 м² корреляция площади и цены — 0.05, без неё — 0.68. Одна строка из трёхсот почти полностью «спрятала» главный признак.

Проверь себя6 вопросов
1 У признака Q1=10Q_1 = 10, Q3=30Q_3 = 30. Какое наибольшее значение ещё НЕ считается выбросом по правилу 1.5⋅IQR1.5 \cdot \mathrm{IQR}?
2 Корреляция Пирсона двух признаков равна 0. Что можно утверждать?
3 Модель A: 9 из 10 на лёгких вопросах и 30 из 90 на трудных. Модель B: 70 из 90 на лёгких и 2 из 10 на трудных. Какие утверждения верны?Можно выбрать несколько ответов
4 Предсказываем, уйдёт ли клиент в следующем месяце. Какие признаки — вероятная утечка?Можно выбрать несколько ответов
5 Категория встретилась 4 раза, все 4 раза цель равна 1, общее среднее цели 0.25, сила сглаживания m=4m = 4. Чему равна сглаженная target-кодировка? Округлите до двух знаков.
6 Почему наивный target encoding завышает качество на train?
  • Разведка идёт по чек-листу: структура → распределения → выбросы → связи → цель и утечки → дубликаты → признаки. Каждый шаг — график и вопрос «так и должно быть?».
  • Тяжёлые хвосты смотрят в лог-шкале и по медиане и перцентилям. Правило IQR находит кандидатов в выбросы, но решает человек: опечатку исправить, редкую правду оставить.
  • Корреляция ловит только линейную связь, ломается от одной точки и от смешения групп; парадокс Симпсона переворачивает выводы при разном составе групп.
  • Признак, который в одиночку предсказывает цель почти идеально, — почти всегда утечка: пропуск, ID, будущее время, следствие цели.
  • One-hot — для категорий с небольшим числом значений; target encoding — только out-of-fold и со сглаживанием.

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