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.
