Очистка данных в pandas: пропуски, дубликаты и типы
Практический порядок очистки таблицы в pandas: найти дубликаты, обработать пропуски, привести типы и не удалить важные бизнес-сценарии.

Очистка данных — это не “сделать таблицу красивой”. Нужно понять, какие строки являются дублями, что означает пропуск и какой тип должен быть у каждого поля. Если удалить всё подозрительное без правил, можно убрать реальные возвраты, повторные события или пользователей с неполным профилем.
Коротко
Надёжная очистка начинается с контракта: какое зерно таблицы, какие поля обязательны, какие значения допустимы и что считается дубликатом. После каждого изменения сохраняй количество строк и контрольные суммы, чтобы не потерять данные молча.
- Не удаляй строки до того, как описал причину удаления.
- Проверяй дубли по бизнес-ключу, а не по всей строке.
- Пропуски в категориальном поле и в сумме денег требуют разных решений.
- Приводи типы явно и считай число значений, которые не удалось преобразовать.
Опиши контракт учебной таблицы
Пусть в `orders` одна строка — один заказ. `order_id` обязателен и уникален, `created_at` должен быть датой, `revenue` — неотрицательным числом, а `channel` может быть неизвестным, но не пустой строкой. Такой контракт превращает спор о данных в набор проверок.
| Поле | Правило | Что делать при нарушении |
|---|---|---|
| order_id | не NULL, уникален | остановить расчёт и проверить источник |
| created_at | валидная дата в известной timezone | вынести строки в отчёт ошибок |
| revenue | число >= 0 | проверить возвраты и валюту, не ставить ноль автоматически |
| channel | категория или unknown | нормализовать регистр и пустые значения |
Типы: строка с числом ещё не число
CSV часто отдаёт сумму как текст: `"1 250"`, `"1250,50"` или `"n/a"`. `pd.to_numeric` помогает привести значения, но после `errors="coerce"` нужно отдельно посмотреть, сколько строк превратилось в NaN. Иначе плохая запись растворится в среднем.
before = orders['revenue'].isna().sum()
orders['revenue'] = (
orders['revenue'].astype('string')
.str.replace(' ', '', regex=False)
.str.replace(',', '.', regex=False)
.pipe(pd.to_numeric, errors='coerce')
)
after = orders['revenue'].isna().sum()
print('new missing revenue:', after - before)
orders['created_at'] = pd.to_datetime(orders['created_at'], errors='coerce', utc=True)Дубликаты: сначала понять ключ
`drop_duplicates()` без subset сравнивает всю строку. Для заказа это может быть неправильно: одна и та же покупка могла получить обновлённый статус или комментарий. Если источник гарантирует одну строку на заказ, проверяй `order_id`; если это поток событий, ключом может быть user_id, event_name и occurred_at с допустимой точностью.
duplicates = orders.loc[orders.duplicated('order_id', keep=False)]
print('duplicate rows:', len(duplicates))
orders = (
orders.sort_values('updated_at')
.drop_duplicates('order_id', keep='last')
)
assert orders['order_id'].is_uniqueВ таблице статусов повторный order_id может быть историей изменений. В таблице фактов оплаты повтор может означать дубль загрузки. Решение зависит от зерна и бизнес-смысла, а не от самого названия метода.
Пропуски: unknown, zero и отдельный статус
Категориальный пропуск часто лучше превратить в явную группу `unknown`, чтобы он не исчез из группировки. Ноль подходит только тогда, когда отсутствие записи действительно означает отсутствие значения: например, скидка не применялась. Для неизвестной выручки ноль создаст ложное снижение среднего.
orders['channel'] = orders['channel'].replace('', pd.NA).fillna('unknown')
orders['discount'] = orders['discount'].fillna(0)
invalid_revenue = orders['revenue'].isna() | orders['revenue'].lt(0)
quarantine = orders.loc[invalid_revenue].copy()
clean_orders = orders.loc[~invalid_revenue].copy()Чеклист перед расчётом
Очищенная таблица должна быть не просто без пропусков. В финальном отчёте сохрани количество отброшенных строк, причины и контрольные числа. Так другой аналитик поймёт, почему выручка после очистки отличается от файла источника.
- Сколько строк было в источнике и сколько осталось?
- Сколько дублей найдено и по какому ключу?
- Сколько значений не удалось привести к нужному типу?
- Какие пропуски стали `unknown`, а какие — нулём?
- Изменились ли сумма, количество заказов и диапазон дат?
Материалы по теме
Как тестировать аналитические расчёты на Python и pandas
Практический гайд по тестам для аналитика: проверить метрики на маленьком датасете, поймать регрессию и защитить расчёт от тихих изменений.
Читать материал
Как ускорить pandas: память, типы и обработка больших файлов
Что делать, если pandas медленно работает или не помещает файл в память: категории, downcast, chunksize, Parquet и контроль размера данных.
Читать материал
Pandas apply и векторизация: как писать быстрее и понятнее
Когда использовать apply, почему векторные операции быстрее и как переписать медленный построчный расчёт в pandas.
Читать материал