EDA в pandas: как провести разведывательный анализ данных
Пошаговый EDA в pandas: проверить структуру, распределения, связи и сегменты до того, как строить метрики и делать выводы.
Содержание статьи
Разведывательный анализ нужен до красивого дашборда и до сложной модели. Он помогает понять, что именно лежит в таблице, где данные неполные, какие значения типичны и какие сегменты ведут себя иначе. EDA не даёт автоматического ответа, но заметно снижает риск считать не то.
Коротко
EDA — это короткое исследование формы данных: размер, типы, пропуски, уникальность, распределения, связи и сегменты. Его результатом должен быть список наблюдений и вопросов для следующего анализа, а не коллекция случайных графиков.
- Начните с зерна таблицы и периода данных.
- Смотрите на распределение, а не только на среднее.
- Отделяйте технические пропуски от реального отсутствия значения.
- Проверяйте ключевые срезы до общего вывода.
Шаг 1. Профиль таблицы
Первый экран EDA должен отвечать на простые вопросы: сколько строк, какие поля, какой диапазон дат, где пропуски и сколько уникальных ключей. Если в таблице заказов 200 тысяч строк, но только 80 тысяч order_id, это уже влияет на любой последующий расчёт.
def profile(df: pd.DataFrame) -> pd.DataFrame:
return pd.DataFrame({
'dtype': df.dtypes.astype(str),
'missing': df.isna().sum(),
'missing_share': df.isna().mean().round(3),
'unique': df.nunique(dropna=False),
}).sort_values('missing_share', ascending=False)
print('rows, columns:', orders.shape)
display(profile(orders))
display(orders[['created_at', 'revenue']].describe())Пропавший день нельзя принять за падение заказов
Профиль orders показывает диапазон дат, но две крайние даты не гарантируют непрерывность выгрузки. Между ними может отсутствовать целый день. До анализа среднего чека постройте календарь отчётного периода и присоедините к нему количество заказов, сумму известных revenue и число неизвестных сумм. Предполагаем, что одна строка уже соответствует одному заказу; иначе сначала нужно восстановить это зерно. Для created_at заранее выбрана бизнес-зона, чтобы ночные операции не оказались в соседних сутках.
В учебном фрагменте ниже заказы есть 1 и 3 июля, а за 2 июля строк нет. После reindex у второго дня будет пропуск. Он сообщает только отсутствие наблюдений в файле: магазин мог не работать, а могла остановиться загрузка. Не заполняйте эту ячейку нулём, пока не сверите календарь с журналом доставки данных или контрольной выгрузкой источника. Если нулевой день подтверждён, это отдельное решение с объяснением в отчёте.
Рядом с числом заказов нужен охват денежного поля. Если за день пришли все order_id, но revenue отсутствует, количество покупок останется нормальным, а сумма может выглядеть нулевой. SUM с min_count=1 сохраняет пропуск, когда нет ни одной известной суммы. При частичной загрузке он всё ещё возвращает частичный итог; поэтому сравнивайте revenue_known с orders. Так разрыв трекинга не превращается в историю о падении платёжеспособности клиентов.
Сначала посмотрите сырой ряд, затем сравните одинаковые дни недели в полных неделях. Последний неполный день выгрузки исключите из сравнения или подпишите его отдельно. Рост от понедельника к пятнице может повторяться регулярно; изменение среднего после добавления выходных может быть календарным составом. Но нескольких точек недостаточно, чтобы объявить сезонность установленной. Для устойчивого недельного рисунка нужны повторения, а праздники и рекламные кампании подпишите рядом с датами.
Для гипотезы о пропусках соберите маленькую таблицу «день × канал»: число строк, число известных revenue и доля пропусков. Если провал появляется одновременно во всех каналах, проверьте общую загрузку; если только в одном — его источник или преобразование. Наблюдаемый рисунок помогает выбрать следующую проверку, но не доказывает, что пропуски случайны. В memo передайте конкретные даты, затронутые поля и запрос на повторную выгрузку, чтобы инженер смог воспроизвести сбой.
import pandas as pd
orders = pd.DataFrame({
'order_id': [101, 102, 103],
'created_at': pd.to_datetime(['2026-07-01', '2026-07-01', '2026-07-03']),
'revenue': [1200.0, 800.0, float('nan')],
})
assert orders['order_id'].is_unique
orders['day'] = orders['created_at'].dt.normalize()
daily = orders.groupby('day').agg(
orders=('order_id', 'size'),
revenue_known=('revenue', 'count'),
revenue=('revenue', lambda s: s.sum(min_count=1)),
)
daily = daily.reindex(pd.date_range('2026-07-01', '2026-07-03'))
assert pd.isna(daily.loc['2026-07-02', 'orders'])
assert pd.isna(daily.loc['2026-07-03', 'revenue'])
print(daily)Шаг 2. Смотреть на распределения
Средний чек может быть 1 500 ₽, хотя половина заказов меньше 600 ₽, а несколько крупных клиентов тянут среднее вверх. Поэтому для денег и времени полезно смотреть медиану, квантили и хвост. Выброс — не всегда ошибка: иногда это отдельный тип пользователя или важный бизнес-сценарий.
| Показатель | Что показывает | Осторожность |
|---|---|---|
| median | типичное центральное значение | не показывает размер хвоста |
| quantile(0.9) | уровень, ниже которого 90% наблюдений | зависит от размера выборки |
| min / max | границы наблюдаемых значений | часто чувствительны к ошибкам |
| std | разброс вокруг среднего | плохо объясняет сильно асимметричные данные |
revenue_summary = orders['revenue'].describe(percentiles=[.5, .75, .9, .99])
print(revenue_summary)
p99 = orders['revenue'].quantile(.99)
large_orders = orders.loc[orders['revenue'] > p99]
print('share of orders above p99:', len(large_orders) / len(orders))Шаг 3. Сегменты важнее общей средней
Общий показатель может быть устойчивым, пока внутри него меняются сегменты. Средний заказ по всем каналам скрывает разницу между новым и возвращающимся покупателем. EDA помогает найти срезы, где меняется форма данных, а затем уже решать, нужен ли отдельный KPI или дополнительная гипотеза.
segment_summary = (
orders.groupby('channel')
.agg(
orders=('order_id', 'nunique'),
buyers=('user_id', 'nunique'),
median_revenue=('revenue', 'median'),
p90_revenue=('revenue', lambda s: s.quantile(.9)),
)
.sort_values('median_revenue', ascending=False)
)
display(segment_summary)EDA не доказывает причину
Если пользователи из referral-канала покупают чаще, это ещё не означает, что канал сам по себе создаёт лучший опыт. В него могли попадать более лояльные клиенты или другой тип продукта. Разведывательный анализ формирует вопрос: «почему сегмент отличается?». Для причинности нужны эксперимент, квазиэксперимент или дополнительный дизайн исследования.
- Не удаляй выбросы только ради более красивого среднего.
- Не называйте корреляцию влиянием.
- Запишите, какие фильтры применил к исследованию.
- Отделяйте находки, которые увидел заранее, от найденных перебором срезов.
Сформулируйте карту вопросов до просмотра графиков
EDA легко превращается в бесконечное листание диаграмм. Чтобы этого не произошло, разделите вопросы на четыре группы: полнота данных, распределение, сегменты и время. Сначала проверьте, все ли пользователи и периоды попали в набор. Затем посмотрите форму ключевых числовых полей. Только после этого переходите к сравнениям.
Для каждой находки записывайте статус: факт, гипотеза или вопрос для проверки. Например, «Android revenue ниже» — факт описательного среза. «На Android проблема в оплате» — гипотеза. «Есть ли ошибка после версии 4.2?» — следующий вопрос. Такая маркировка не даёт EDA незаметно превратиться в причинный вывод.
Сохраняйте фильтры и размер выборки рядом с графиком. Если вы посмотрели только платящих пользователей, вывод нельзя переносить на всю аудиторию. Порядок исследования тоже важен: найденная после двадцати срезов разница требует подтверждения на отдельной выборке или в заранее заданном анализе.
| Слой | Вопрос | Пример проверки |
|---|---|---|
| полнота | есть ли все строки и периоды | диапазон дат и объём по дням |
| форма | как распределено значение | квантили, выбросы, пропуски |
| сегменты | где группы отличаются | platform, channel, plan |
| время | когда появилась разница | тренд и точка изменения |
Пропуски и выбросы нужно описать, а не спрятать
Доля пропусков — только начало. Важно понять, какие строки пропущены: один канал, один день, новый тип устройства или случайный набор. Сравнивайте missingness по сегментам и времени. Если revenue отсутствует только после релиза, это уже сигнал о контракте события, а не повод заполнить все значения средним.
Выбросы могут быть ошибкой, крупным клиентом или редким, но важным сценарием. До удаления посмотрите исходную строку и влияние на метрику. Для среднего полезно показать медиану и квантили рядом, а для бизнеса — отдельно посчитать результат с и без спорного сегмента. Это честнее, чем незаметно обрезать верхний процент.
Нормализация масштаба тоже влияет на вывод. Два канала могут иметь одинаковое среднее, но совершенно разную дисперсию. Boxplot или таблица p50/p90/p99 помогает понять, одинаков ли пользовательский опыт или его портит небольшой хвост.
profile = pd.DataFrame({
'missing_share': orders.isna().mean(),
'unique_values': orders.nunique(dropna=False),
})
revenue_stats = orders['revenue'].quantile([.5, .9, .99])
print(profile.sort_values('missing_share', ascending=False))
print(revenue_stats)Сегменты: сравните не только средние
Разница между сегментами может возникнуть из-за состава. Сравнивайте размер группы, долю пропусков, период наблюдения и распределение ключевого показателя. Если одна группа в два раза меньше, один крупный пользователь способен изменить среднее. Для первой проверки используйте медиану, квантили и доверительный интервал, а не только одно число.
Не строй сразу двадцать разрезов и не выбирайте самый красивый. Сначала выберите сегмент по продуктовой гипотезе, затем один независимый разрез для проверки устойчивости. Если эффект появляется только в одном из двадцати случайных срезов, это повод для осторожности, а не для заголовка отчёта.
Кросс-сегментация помогает найти составной эффект, но быстро уменьшает базу. Подписывайте размер каждой ячейки и объединяйте редкие категории, если это соответствует смыслу. Пустая или очень маленькая ячейка не даёт права на сильный вывод.
После каждого сигнала запишите, какие данные или эксперимент смогут его подтвердить. Не превращайте визуальную корреляцию в готовое решение.
Связь скидки с выручкой может целиком состоять из разных корзин
После сравнения каналов возникает следующий вопрос: связана ли скидка с размером заказа. Допустим, к orders добавили discount — скидку в рублях. Начните с определения revenue: сумма до скидки или после неё. Если discount составляет фиксированную долю стоимости корзины, положительная связь с суммой встроена в формулу. Она не показывает, что скидка убедила человека купить больше. Для такой гипотезы нужен другой источник вариации, а не корреляция двух производных одной корзины.
Подготовьте пары значений на одном зерне. Если discount записана на уровне заказа, а revenue повторяется в строках товаров, большой заказ получит больший вес просто потому, что в нём больше позиций. Сверните строки до заказа и проверьте число полных пар после исключения пропусков. При расчёте матрицы корреляций разные пары колонок могут использовать разные подвыборки; одинаковый размер исходного DataFrame не делает их сопоставимыми.
Посмотрите scatterplot отдельно для новых и возвращающихся покупателей, а затем по каналу, который уже исследовал выше. Представьте две группы корзин: небольшие заказы без скидки и крупные заказы с персональным предложением. Между группами связь сильная, внутри каждой может почти отсутствовать. Сравните диапазоны: если без скидки встречаются только маленькие корзины, данные не дают сопоставимых заказов для проверки эффекта скидки на большие. Регрессионная линия сама это ограничение не устранит.
Pearson чувствителен к линейной связи и крайним значениям; Spearman работает с рангами и помогает проверить монотонность. Расхождение двух коэффициентов — повод осмотреть хвост распределения, а не выбрать тот, который подтверждает историю. Если крупные заказы настоящие, оставьте их в основном описании и покажите чувствительность вывода к их исключению отдельно. Для повторных заказов одного пользователя учитывайте, что строки зависимы: обычная оценка неопределённости по независимым заказам будет слишком оптимистичной.
Передайте находку в memo как проверяемую запись: какие поля сопоставлены, сколько полных пар, какие диапазоны пересекаются и что изменилось после разбиения по каналу. Если связь осталась только между группами, следующим шагом будет уточнение правил выдачи скидки. Если видна и внутри сопоставимого среза, сформулируйте эксперимент или дополнительную проверку. Такой итог даёт коллеге понятную точку продолжения исследования и не превращает разведку в преждевременную рекомендацию увеличить скидки.
Практика: закончить EDA коротким memo
Финал EDA — не папка из двадцати графиков, а короткое memo. В нём должны быть размер и период источника, три устойчивых наблюдения, две гипотезы, ограничения и следующий шаг. Для каждого наблюдения приложи ссылку на таблицу или код, из которого оно получено.
Отдельно перечислите, что вы не проверяли: причинность, сезонность, качество трекинга или влияние маркетингового состава. Это не ослабляет исследование. Наоборот, команда понимает границы и не использует описательный срез как доказательство эффекта.
Если EDA повторяется еженедельно, превратите стабильные проверки в функции или SQL-контроль. Исследовательская часть может оставаться интерактивной, а профиль источника и sanity-check должны запускаться одинаково.
- Начать с полноты и схемы источника.
- Посмотреть пропуски, квантили и выбросы.
- Сравнить сегменты с размером базы.
- Разделить факт, гипотезу и следующий вопрос.
- Закончить memo с ограничениями и действием.
Материалы по теме

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

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

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