1.2 EDA и подготовка данных: пропуски, выбросы, кодирование, масштабирование

Урок 1.2. EDA и подготовка данных

После урока вы сможете:

  • находить и удалять дубликаты;
  • распознавать выбросы и ошибки ввода;
  • выбирать стратегию работы с пропусками;
  • кодировать категориальные признаки и масштабировать числовые;
  • собирать всю предобработку в один ColumnTransformer.

Зачем нужен EDA

EDA (exploratory data analysis, разведочный анализ) — это знакомство с данными до моделирования. Модель не знает, что квартира площадью 800 м² — опечатка. Она честно попытается выучить и эту «закономерность». Поэтому первым делом мы ищем в данных то, что не похоже на правду.

Шаг 1. Дубликаты

Одно и то же объявление могло попасть в выгрузку дважды. Дубликаты искажают модель: она придаёт повторённым квартирам двойной вес.

# Считаем полные дубликаты строк и удаляем их
print('Полных дубликатов:', df.duplicated().sum())
df = df.drop_duplicates().reset_index(drop=True)

В данных BishkekDom 12 дубликатов. После удаления остаётся 1 500 объявлений.

Шаг 2. Выбросы и ошибки ввода

Ящик с усами (boxplot) быстро показывает значения, которые сильно отличаются от остальных.

# Boxplot для площади и цены
df['area_m2'].plot.box(title='Площадь, м²')

Важно различать два случая:

  • Ошибка ввода. Квартира 833 м² — это почти наверняка 83,3 м² с лишним нулём. Цена $57 — цена, введённая в тысячах долларов.
  • Настоящий редкий объект. Пентхаус за $170 000 — редкость, но не ошибка. Такие строки удалять нельзя.
# Находим подозрительные объявления
suspicious = df[(df.area_m2 > 300) | (df.price_usd < 5000)]

# Их всего 7 — удаляем, угадывать исправление рискованно
df = df[(df.area_m2 <= 300) & (df.price_usd >= 5000)].reset_index(drop=True)

Не удаляйте выбросы автоматически. Правило «удалить всё за пределами усов boxplot» выбросит дорогие элитки и сделает модель слепой к премиум-сегменту. Каждое удаление должно иметь бизнес-объяснение.

Шаг 3. Связь признаков с целевой переменной

# Медианная цена по районам
df.groupby('district')['price_usd'].median().sort_values().plot.barh()

# Зависимость цены от площади
df.plot.scatter(x='area_m2', y='price_usd', alpha=0.3)

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

Шаг 4. Пропуски

# Столбцы с пропусками
df.isna().sum()[lambda s: s > 0]

В данных не указаны год постройки (около 45 объявлений), состояние (около 45) и расстояние до центра (около 30). Основные стратегии:

Стратегия Когда подходит
Удалить строки Пропусков очень мало, и они случайны
Заполнить медианой (числа) Числовой признак, есть выбросы
Заполнить самым частым значением (категории) Категориальный признак
Отдельная категория «Не указано» Сам факт пропуска может быть информативен

Не заполняйте пропуски на всём датасете сразу. Если посчитать медиану по всем данным, в неё попадёт информация из тестовой выборки. Заполнение должно «учиться» только на train. Это делает SimpleImputer внутри pipeline — его мы соберём ниже.

Шаг 5. Кодирование категорий

Модели работают только с числами. Район «Первомайский» нужно превратить в числа. Нельзя просто пронумеровать районы (1, 2, 3, 4): модель решит, что Свердловский «в четыре раза больше» Октябрьского.

Правильный способ — one-hot encoding: для каждого района создаётся отдельный столбец со значением 0 или 1.

district → Октябрьский Первомайский Ленинский Свердловский
Первомайский 0 1 0 0
Ленинский 0 0 1 0

Шаг 6. Масштабирование чисел

Площадь измеряется десятками, год постройки — тысячами, наличие парковки — нулём и единицей. Многим моделям удобнее, когда все числа в одном масштабе. StandardScaler приводит каждый признак к среднему 0 и стандартному отклонению 1.

Собираем всё вместе: ColumnTransformer

from sklearn.compose import ColumnTransformer
from sklearn.pipeline import Pipeline
from sklearn.impute import SimpleImputer
from sklearn.preprocessing import OneHotEncoder, StandardScaler

num_features = ['rooms', 'area_m2', 'floor', 'floors_total',
                'year_built', 'has_parking', 'distance_center_km']
cat_features = ['district', 'building_series', 'condition', 'heating']

preprocess = ColumnTransformer([
    # Числа: заполняем медианой, затем масштабируем
    ('num', Pipeline([('impute', SimpleImputer(strategy='median')),
                      ('scale', StandardScaler())]), num_features),
    # Категории: заполняем самым частым значением, затем one-hot
    ('cat', Pipeline([('impute', SimpleImputer(strategy='most_frequent')),
                      ('onehot', OneHotEncoder(handle_unknown='ignore'))]), cat_features),
])

Почему pipeline — это профессиональный стандарт: вся обработка описана в одном объекте. Её нельзя случайно забыть применить к новым данным, и она автоматически защищает от утечки через пропуски и масштабирование. Когда агент BishkekDom введёт новую квартиру, она пройдёт ровно те же шаги.

Итоги урока

  • EDA начинается с поиска того, что не похоже на правду: дубликатов, опечаток, пропусков.
  • Ошибки ввода удаляем или исправляем; настоящие редкие объекты оставляем.
  • Категории кодируем через one-hot, числа масштабируем.
  • Вся предобработка собирается в ColumnTransformer и обучается только на train.