Пропуски и выбросы: что с ними делать

«В данных 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. Удалять выбросы или нет?

Правильный ответ начинается с вопроса «а что это за строка?». Разберите её вручную — почти всегда обнаружится один из трёх случаев:

  1. Ошибка данных: возраст 201, чек с лишними тремя нулями, дата 2170 года. Такое чинят или удаляют, зафиксировав в логе.
  2. Настоящее редкое событие: корпоративный заказ на миллион. Удалять нельзя — это часть бизнеса. Обычно такие клиенты выносятся в отдельный сегмент, чтобы не мешать анализу массового.
  3. Другая природа события: тестовые заказы сотрудников, боты, парсеры. Не ошибка и не клиент — их фильтруют по признаку, а не по величине метрики.

Вместо удаления часто применяют мягкие приёмы: винзоризация (обрезать значения по 1-му и 99-му перцентилю), логарифмирование (сжимает длинный правый хвост, типичный для денег), переход на устойчивые метрики — медиану, усечённое среднее, долю пользователей выше порога.

Отдельно стоит помнить: для A/B-тестов правило обработки выбросов фиксируют до эксперимента и применяют одинаково к обеим группам. Иначе «чистка» превращается в подгонку результата.

Типичные ошибки кандидатов

  • Сразу говорят «заполню средним» — не спросив, что означает пропуск и не связан ли он с самим значением.
  • Удаляют выбросы «потому что портят график», не посмотрев на конкретные строки.
  • Применяют правило трёх сигм к сильно скошенным денежным данным, где оно почти ничего не находит.
  • Заполняют пропуски до разделения на train/test или до разбиения по группам A/B — так информация протекает между группами.
  • Забывают, что заполнение медианой занижает дисперсию, и потом удивляются слишком узким доверительным интервалам.

Как ответить кратко

«Сначала выясняю природу пропуска: случайный он, связан с другими полями или с самим пропущенным значением. От этого зависит всё — при случайных пропусках строки можно удалить, при остальных удаление сместит выборку. Дальше выбираю: оставить NULL (агрегаты его игнорируют), заполнить нулём, если NULL по смыслу ноль, заполнить медианой внутри группы плюс флаг is_missing, для рядов — протянуть предыдущее. Выбросы ищу по IQR, а не по трём сигмам: на скошенных денежных данных сигмы слепнут. И не удаляю их автоматически — сначала смотрю строку: ошибка ввода, редкий крупный клиент или бот. Для устойчивости беру медиану или логарифм вместо удаления».

Проверьте себя
1. Клиенты с высоким доходом систематически не указывают его в анкете. Какой это механизм пропуска?
AMCAR — пропуски полностью случайны
BMAR — пропуск объясняется другими наблюдаемыми полями
CMNAR — пропуск связан с самим пропущенным значением
DЭто не пропуск, а выброс
2. Почему на сильно скошенных денежных данных правило трёх сигм работает хуже IQR?
AТри сигмы применимы только к целым числам
BСреднее и стандартное отклонение сами раздуваются выбросами, поэтому граница уезжает и метод перестаёт их находить
CIQR всегда находит ровно 5% выбросов
DПравило трёх сигм требует минимум 10 000 наблюдений
3. В выборке нашёлся корпоративный заказ на миллион рублей при среднем чеке 3000. Что делать?
AУдалить: он искажает среднее
BРазобрать строку: если это реальная сделка, оставить и вынести такой сегмент отдельно, а для массового анализа взять устойчивые метрики
CЗаменить значение медианой
DОбрезать до трёх сигм автоматически