Очистка данных в 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, а какие — нулём? - Изменились ли сумма, количество заказов и диапазон дат?
Сначала профилируй источник, потом исправляй
Очистка — это не набор универсальных вызовов dropna и drop_duplicates. Сначала зафиксируй исходное состояние: число строк, обязательные колонки, диапазон дат, уникальные ключи и долю пропусков. Без baseline невозможно понять, какое решение изменило результат и сколько данных было потеряно.
Профиль смотри по смысловым группам. Пропуски в стране могут быть проблемой регистрации, а пропуски в discount — нормальным отсутствием скидки. Дубли в таблице событий могут быть повторной доставкой сообщения, а дубли в таблице заказов — ошибкой загрузки. Один и тот же метод для всех колонок уничтожает этот контекст.
Очищенный набор должен сопровождаться отчётом об изменениях. Храни количество строк до и после, причины quarantine и контрольные суммы. Тогда очистка становится проверяемой частью анализа, а не невидимой подготовкой перед графиком.
| Проверка | Зачем |
|---|---|
| rows и columns | увидеть изменение размера и схемы |
| min/max даты | поймать неверный период или парсинг |
| nunique ключа | понять ожидаемую уникальность |
| missing share | отделить системную проблему от единичных строк |
| sum контрольного поля | сверить цену очистки |
Quarantine лучше тихого удаления
Если строка не проходит контракт, не обязательно удалять её навсегда. Отложи её в quarantine с причиной: invalid_date, missing_user_id, negative_revenue или duplicate_order. Основной расчёт работает на clean-слое, а владелец данных получает список проблемных случаев. Это сохраняет возможность расследования.
Разные ошибки требуют разных действий. Непарсящуюся дату нельзя превращать в начало эпохи. Отсутствующий channel можно показать как unknown, если неизвестность является допустимым состоянием. Отрицательная выручка может быть возвратом, а может быть поломкой источника. Название причины должно отражать решение, а не только технический симптом.
Quarantine не должен стать кладбищем строк. Считай его долю по дням и источникам, поставь порог, после которого pipeline останавливается. Если сегодня в quarantine ушло 0.1%, это одно обсуждение; если после релиза 25%, отчёт нельзя публиковать без проверки.
invalid_date = orders['created_at'].isna()
invalid_user = orders['user_id'].isna()
invalid_revenue = orders['revenue'].lt(0)
bad = invalid_date | invalid_user | invalid_revenue
quarantine = orders.loc[bad].copy()
quarantine['reason'] = np.select(
[invalid_date, invalid_user, invalid_revenue],
['invalid_date', 'missing_user_id', 'negative_revenue'],
default='other',
)
clean = orders.loc[~bad].copy()Дубликат определяется ключом и временем
Полный дубль строки — только один из вариантов. В заказах ключом может быть order_id, в событиях — комбинация user_id, event_name и occurred_at, а в таблице статусов — ключ плюс версия обновления. Сначала опиши, сколько строк должно быть на ключ. Затем реши, какую запись оставить: последнюю по updated_at, первую подтверждённую или все как историю.
Порядок удаления имеет значение. Если оставить первую строку без сортировки, результат будет зависеть от порядка выгрузки. Сначала отсортируй по полю, которое выражает бизнес-приоритет, затем используй drop_duplicates. Сохрани отброшенные строки или хотя бы их число и период.
Дедупликация событий и дедупликация фактов — разные задачи. Повторное событие может быть техническим дублем, а два платежа на одну сумму — двумя реальными операциями. Не удаляй по user_id только потому, что метрике нужны уникальные пользователи: это можно сделать на этапе агрегации, не разрушая источник.
Если не можешь словами сказать, что является ключом строки, ещё рано удалять дубли. Сначала разберись со схемой и владельцем источника.
До и после очистки должны объяснять разницу
Сравни итоговые показатели на raw и clean: строки, уникальные заказы, уникальные пользователи, сумма выручки и окно дат. Для каждого расхождения должна быть причина. Если clean revenue выше raw, возможно, ты убрал отрицательные возвраты; это не обязательно ошибка, но отчёт должен назвать показатель как gross или net.
Не сравнивай только среднее. Удаление десяти крупных заказов может почти не изменить количество строк, но сильно изменить p95 и LTV. Сохрани несколько квантилей и долю сегментов до и после. Так видно, не исчезла ли целая категория пользователей вместе с «плохими» строками.
Последний шаг — повторный профиль. Чистый набор должен соответствовать контракту, но неизвестные значения и quarantine должны быть видны в отчёте. Если после очистки всё идеально, это повод проверить, не отфильтровал ли код сам источник ошибки.
- Зафиксировать baseline до изменения.
- Разделить допустимые неизвестные и реальные ошибки.
- Сохранить quarantine с причиной.
- Удалять дубли по явному ключу и правилу приоритета.
- Сверить показатели и распределения до и после.
Материалы по теме

Как тестировать аналитические расчёты на Python и pandas
Практический гайд по тестам для аналитика: проверить метрики на маленьком датасете, поймать регрессию и защитить расчёт от тихих изменений.

Как ускорить pandas: память, типы и обработка больших файлов
Что делать, если pandas медленно работает или не помещает файл в память: категории, downcast, chunksize, Parquet и контроль размера данных.

Pandas apply и векторизация: как писать быстрее и понятнее
Когда использовать apply, почему векторные операции быстрее и как переписать медленный построчный расчёт в pandas.