Пропуски и выбросы: что с ними делать
«В данных 15% пропусков и несколько чеков по миллиону рублей. Ваши действия?»
Выброс — это наблюдение, сильно отличающееся от остальных. Оно бывает ошибкой измерения, а бывает самым ценным клиентом. Задача аналитика — различить эти два случая, а не удалить всё подряд.
Вопрос 1. Что делать с пропусками?
Что проверяет интервьюер. Спросите ли вы, почему данных нет. Механизм пропуска важнее метода заполнения, и это главное, что отличает аналитика от человека, который выучил список методов импутации.
Различают три механизма:
| Механизм | Смысл | Пример |
| MCAR | пропуск не связан ни с чем | сбой сети при отправке события |
| MAR | связан с другими наблюдаемыми полями | возраст чаще не указывают в мобильной версии |
| MNAR | связан с самим пропущенным значением | доход не указывают те, у кого он высокий |
Практический смысл прямой: при MCAR удаление строк почти безвредно, при MAR оно смещает выборку (потеряете мобильную аудиторию), при MNAR любое заполнение по среднему систематически врёт — среднее занизится ровно там, где реальные значения выше.
Набор действий:
- Оставить как есть. Часто лучший вариант: SQL-агрегаты сами игнорируют NULL, а «нет данных» — это честная категория в отчёте.
- Удалить строки — если пропусков меньше пары процентов и они MCAR.
- Удалить колонку — если пропущено 70–80% и заполнять нечем.
- Заполнить константой — когда NULL по смыслу означает ноль (не было покупок) или «unknown» для категорий.
- Заполнить медианой/модой — быстрый способ, но он занижает дисперсию: все заполненные значения одинаковы.
- Заполнить по группе — медиана внутри города или сегмента почти всегда лучше глобальной.
- Протянуть предыдущее значение (forward fill) — для временных рядов: курс, остаток на счёте.
- Добавить флаг
is_missing— сам факт пропуска бывает сильным сигналом.
# типовой набор в pandas — здесь только для чтения, библиотека в браузере недоступна
df["revenue"] = df["revenue"].fillna(0) # NULL означает «не покупал»
df["age_missing"] = df["age"].isna().astype(int) # флаг до заполнения
df["age"] = df.groupby("city")["age"].transform(lambda s: s.fillna(s.median()))
df["rate"] = df["rate"].ffill() # временной ряд
Вопрос 2. Как найти выбросы?
Два базовых метода, и надо знать, когда какой не работает.
Правило трёх сигм: выброс — то, что дальше трёх стандартных отклонений от среднего. Проблема в том, что и среднее, и стандартное отклонение сами раздуваются выбросом, поэтому на сильно скошенных данных метод «слепнет».
Метод межквартильного размаха (IQR) устойчив: границы считают от квартилей, на которые единичный гигант почти не влияет.
import statistics
data = [12, 14, 15, 15, 16, 17, 18, 19, 21, 340]
q1, med, q3 = statistics.quantiles(data, n=4)
iqr = q3 - q1
low, high = q1 - 1.5 * iqr, q3 + 1.5 * iqr
print("Q1, Q3, IQR:", q1, q3, iqr)
print("Границы:", round(low, 1), round(high, 1))
print("Выбросы:", [x for x in data if x < low or x > high])
print("Среднее:", round(statistics.mean(data), 1), "| Медиана:", med)
Вывод:
Q1, Q3, IQR: 14.75 19.5 4.75
Границы: 7.6 26.6
Выбросы: [340]
Среднее: 48.7 | Медиана: 16.5
Обратите внимание на последнюю строку: одно значение сдвинуло среднее с 16 до 48. Медиана осталась там, где живёт основная масса данных.
Вопрос 3. Удалять выбросы или нет?
Правильный ответ начинается с вопроса «а что это за строка?». Разберите её вручную — почти всегда обнаружится один из трёх случаев:
- Ошибка данных: возраст 201, чек с лишними тремя нулями, дата 2170 года. Такое чинят или удаляют, зафиксировав в логе.
- Настоящее редкое событие: корпоративный заказ на миллион. Удалять нельзя — это часть бизнеса. Обычно такие клиенты выносятся в отдельный сегмент, чтобы не мешать анализу массового.
- Другая природа события: тестовые заказы сотрудников, боты, парсеры. Не ошибка и не клиент — их фильтруют по признаку, а не по величине метрики.
Вместо удаления часто применяют мягкие приёмы: винзоризация (обрезать значения по 1-му и 99-му перцентилю), логарифмирование (сжимает длинный правый хвост, типичный для денег), переход на устойчивые метрики — медиану, усечённое среднее, долю пользователей выше порога.
Отдельно стоит помнить: для A/B-тестов правило обработки выбросов фиксируют до эксперимента и применяют одинаково к обеим группам. Иначе «чистка» превращается в подгонку результата.
Типичные ошибки кандидатов
- Сразу говорят «заполню средним» — не спросив, что означает пропуск и не связан ли он с самим значением.
- Удаляют выбросы «потому что портят график», не посмотрев на конкретные строки.
- Применяют правило трёх сигм к сильно скошенным денежным данным, где оно почти ничего не находит.
- Заполняют пропуски до разделения на train/test или до разбиения по группам A/B — так информация протекает между группами.
- Забывают, что заполнение медианой занижает дисперсию, и потом удивляются слишком узким доверительным интервалам.
Как ответить кратко
«Сначала выясняю природу пропуска: случайный он, связан с другими полями или с самим пропущенным значением. От этого зависит всё — при случайных пропусках строки можно удалить, при остальных удаление сместит выборку. Дальше выбираю: оставить NULL (агрегаты его игнорируют), заполнить нулём, если NULL по смыслу ноль, заполнить медианой внутри группы плюс флаг
is_missing, для рядов — протянуть предыдущее. Выбросы ищу по IQR, а не по трём сигмам: на скошенных денежных данных сигмы слепнут. И не удаляю их автоматически — сначала смотрю строку: ошибка ввода, редкий крупный клиент или бот. Для устойчивости беру медиану или логарифм вместо удаления».