Визуализация в Python: как строить понятные графики в matplotlib и seaborn
Как выбрать тип графика и собрать его в Python: линии, столбцы, распределения и сравнение сегментов без декоративного шума.
Содержание статьи
График не должен показывать всё, что есть в таблице. Его задача — помочь увидеть один паттерн: тренд, разницу между сегментами, распределение или выброс. В Python удобство matplotlib и seaborn начинается не с выбора цвета, а с вопроса, на который должна ответить визуализация.
Коротко
Линия показывает изменение во времени, столбцы сравнивают категории, histogram показывает распределение, boxplot помогает увидеть медиану и выбросы. Seaborn даёт удобные статистические шаблоны, matplotlib — полный контроль над композицией и подписями.
- Один график — один главный вывод.
- Подписывайте единицу измерения, период и знаменатель.
- Сортируйте категории по смыслу, а не по случайному порядку в файле.
- Цвет должен разделять смысл, а не просто украшать холст.
Сначала выберите форму вопроса
Если команда спрашивает «как меняется DAU по дням», нужен line chart. Если «какой канал принёс больше заказов», подойдут горизонтальные bars. Для «есть ли длинный хвост чеков» нужен histogram. Неподходящий тип графика может сделать правильные данные трудными для чтения.
| Вопрос | График | На что смотреть |
|---|---|---|
| Как меняется метрика во времени? | line | тренд, сезонность, перелом |
| Какие сегменты отличаются? | bar | уровень и порядок категорий |
| Как распределены значения? | histogram | центр, хвост, выбросы |
| Есть ли связь двух чисел? | scatter | направление и необычные точки |
| Как ведут себя когорты? | heatmap | градиент и провалы по возрасту |
Линия для тренда
Перед построением линии отсортируйте даты и приведите данные к одному уровню: например, один день и одна строка на канал. Если на одном дне несколько строк, matplotlib соединит их в порядок чтения DataFrame, и график может выглядеть как случайная пила.
import matplotlib.pyplot as plt
daily = (
events.groupby('event_date', as_index=False)
.agg(dau=('user_id', 'nunique'))
.sort_values('event_date')
)
fig, ax = plt.subplots(figsize=(9, 4))
ax.plot(daily['event_date'], daily['dau'], color='#6d5dfc', linewidth=2)
ax.set(title='DAU by day', xlabel='Date', ylabel='Unique active users')
ax.grid(axis='y', alpha=0.2)
fig.tight_layout()Сравнение сегментов через seaborn
Seaborn удобен, когда данные уже находятся в «длинном» формате: отдельные колонки для даты, сегмента и значения. Он сам создаёт легенду и даёт аккуратные статистические шаблоны. Но подписи и порядок категорий всё равно нужно задать самостоятельно.
Для бизнес-решения рядом нужны количество заказов и доверительный диапазон, а не только среднее.
import seaborn as sns
import matplotlib.pyplot as plt
channel_avg = orders.groupby('channel', as_index=False).agg(
average_order=('revenue', 'mean'),
)
channel_avg = channel_avg.sort_values('average_order')
fig, ax = plt.subplots(figsize=(8, 4))
sns.barplot(data=channel_avg, x='average_order', y='channel', color='#0f9f76', ax=ax)
ax.set(xlabel='Average order value, RUB', ylabel='Channel')
fig.tight_layout()Почему красивый график может врать
Обрезанная ось превращает маленькую разницу в драматичный разрыв. Среднее скрывает несколько крупных заказов. Сглаженная линия создаёт впечатление причинности. График помогает сформировать вопрос, но не отвечает на него без определения метрики, размера базы и проверки сегментов.
После визуального сигнала вернитесь к зерну, знаменателю и статистической неопределённости. Особенно если график должен привести к продуктовой или финансовой рекомендации.
- Показывайте размер сегмента рядом с процентом или средним.
- Для ratio-метрик не смешивайте среднее по пользователям с отношением сумм.
- Не используйте двойную ось без крайней необходимости.
- Отмечайте неполный текущий период.
- Сохраняйте исходную таблицу, из которой построен график.
От вопроса к форме графика
Начинайте не с выбора функции matplotlib, а с вопроса. Если нужно увидеть изменение во времени, нужна линия с одинаковым шагом периода. Если нужно сравнить категории, подойдут горизонтальные столбцы, отсортированные по показателю. Если вопрос про распределение чеков, среднее и один bar chart скроют форму — нужны histogram, boxplot или квантили.
Один график должен отвечать на одну основную задачу. Не помещай на одну ось DAU, revenue, конверсию и количество заказов только потому, что так помещается больше информации. Лучше сделать небольшой набор согласованных графиков: общий тренд, разрез по сегменту и таблицу с базой. Читателю проще связать три ясных объекта, чем расшифровывать один перегруженный.
Пишите подпись как вывод о том, что нужно проверить: «DAU снизился после 15 августа, но падение видно только на Android». Такая подпись направляет чтение, но не подменяет анализ. В примечании укажите период, размер базы и определение активного пользователя.
| Вопрос | График | Обязательная оговорка |
|---|---|---|
| как меняется метрика | линия по времени | неполный текущий период |
| какой сегмент больше | отсортированные bars | размер базы и единицы |
| где теряются пользователи | воронка | порядок и окно событий |
| как распределены значения | histogram или boxplot | выбросы и размер выборки |
| как ведут себя когорты | heatmap | возраст когорты и знаменатель |
Сорок категорий и пятьдесят тысяч точек ломают правильно выбранный тип графика
Вопрос определяет тип графика, а размер данных определяет, останется ли он читаемым. «Какой канал принёс больше заказов» на шести каналах — это отсортированные горизонтальные полосы. На сорока utm-метках это сорок полос на четырёхсот пикселях высоты: подписи наезжают друг на друга, а глаз всё равно сравнивает только первые пять. Менять тип тут не нужно, нужно признать, что вопрос был про топ: десять строк плюс «прочее» с суммой и числом скрытых категорий в подписи.
Scatter на пятидесяти тысячах заказов — не облако точек, а залитая область. Точки перекрываются, и плотность перестаёт быть видимой: там, где кажется сгусток, может не быть ничего кроме наложения. Выходов три. Прозрачность около alpha=0.05 возвращает градиент, пока точек десятки тысяч. hexbin заменяет точки ячейками и показывает плотность числом. Случайная выборка в пару тысяч точек сохраняет форму связи, если подписать, что это выборка, и указать её размер.
Обратная крайность встречается не реже. Линия по трём месяцам соединяет три числа и выглядит как тренд, хотя тренда в трёх точках нет — есть три значения и два перехода между ними. Три столбца или три числа прямо в тексте не обещают читателю того, чего в данных не лежит. Линия начинает работать, когда шаг периода одинаковый и наблюдений хватает, чтобы отличить изменение от обычного разброса: это десятки точек, а не три.
У histogram форма зависит не только от данных, но и от числа корзин, и это единственный график, который легко подделать дефолтом. Пусть чеки лежат в диапазоне от 0 до 20 000 ₽. Десять корзин дают ширину 2 000 ₽: промо-цена 990 ₽ и обычная 1 490 ₽ попадают в одну корзину, распределение выглядит одногорбым. Сорок корзин дают ширину 500 ₽, и те же данные расходятся в два пика — вывод, которого до этого не было видно. Поэтому ширину корзины берут из смысла величины: шага цены, шага тарифа, круглой суммы пополнения. Если такого шага нет, ecdfplot и квантили от этого выбора не зависят вовсе.
Heatmap на двух когортах — это таблица, которую зачем-то раскрасили: две строки читаются числами быстрее, чем градиентом. Матрица начинает выигрывать, когда строк около десятка и взгляд ищет диагональ, провал по возрасту когорты или выбившуюся неделю. Круговая диаграмма — почти единственный тип, который не выигрывает ни одного сравнения: девять секторов на глаз не сопоставляются, а две-три доли короче написать словами в предложении.
Перед тем как оставить график в отчёте, посчитайте, сколько элементов в нём различимо. Больше двадцати категорий на оси, больше нескольких тысяч точек в scatter, корзина шире шага изучаемой величины — в каждом из этих случаев читатель видит форму рендера, а не форму данных. Замена обычно не в другом типе графика, а в сужении вопроса: топ вместо всех, один сегмент вместо решётки, период покрупнее вместо ежедневной пилы.
| Ситуация | Что перестаёт работать | Чем заменить |
|---|---|---|
| 40 категорий в bar | подписи и сравнение соседних полос | топ-10 и «прочее» с числом скрытых категорий |
| 50 000 точек в scatter | плотность: точки накладываются | alpha, hexbin или подписанная выборка |
| 3 точки в линии | линия обещает тренд | столбцы или числа в тексте |
| корзина шире шага цены | два пика сливаются в один | ширина корзины от смысла величины, ECDF |
| 2 когорты в heatmap | градиент вместо двух чисел | таблица или две линии |
| 9 секторов в pie | сравнение долей на глаз | отсортированные горизонтальные полосы |
import numpy as np
import matplotlib.pyplot as plt
step = 500 # шаг цен в продукте
revenue = orders['revenue'].clip(upper=orders['revenue'].quantile(0.99))
bins = np.arange(0, revenue.max() + step, step)
fig, ax = plt.subplots(figsize=(9, 4))
ax.hist(revenue, bins=bins, color='#6d5dfc')
ax.set(
xlabel='Чек, ₽ — корзина 500 ₽, верхний 1% обрезан',
ylabel='Заказов',
)
fig.tight_layout()Число категорий, число точек и ширина корзины известны до вызова matplotlib — это три запроса к DataFrame. Если по ним видно, что полос будет сорок, решение принимается на уровне данных: агрегировать, обрезать хвост или разбить на два графика. Подбирать размер фигуры под неудачный срез бесполезно.
Подготовьте таблицу до вызова seaborn
График не должен сам решать, что считать. Сначала соберите DataFrame на нужном зерне: дата, сегмент, значение, база и, если нужно, доверительный интервал. Не передавайте в визуализацию сырые события, если задача — сравнить дневной DAU. Иначе библиотека посчитает частоты строк, а не смысловую метрику.
Для категорий с длинными названиями используйте горизонтальные bars и явно сортируйте результат. Для временных рядов отсортируйте дату и добавьте пропущенные периоды, если отсутствие строки означает нулевое наблюдение. Но не заполняйте нулём период, для которого данные ещё не загрузились. В отчёте нужно различать «значение равно нулю» и «источник не прислал день».
Перед рендером проверьте единицы: рубли, тысячи рублей, проценты или доли. Подпись оси и форматирование чисел должны соответствовать данным. Ошибка в единицах визуально выглядит профессионально, поэтому её ловит только отдельная проверка.
daily = (
events.assign(event_date=pd.to_datetime(events['occurred_at']).dt.date)
.groupby('event_date', as_index=False)
.agg(dau=('user_id', 'nunique'))
.sort_values('event_date')
)
ax = daily.plot(x='event_date', y='dau', marker='o')
ax.set(xlabel='Дата', ylabel='Уникальные пользователи')Неопределённость и размер базы должны быть видны
Процент без знаменателя почти бесполезен. На графике конверсия 40% может относиться к 20 пользователям, а конверсия 8% — к 50 000. Показывайте рядом размер базы или подпишите его в таблице. Для сравнения групп добавляйте доверительный интервал или хотя бы количество наблюдений, если точная статистика не входит в задачу.
Сглаживание и rolling mean помогают увидеть тренд, но скрывают короткие скачки. Показывайте исходную линию тоньше и подписывайте окно сглаживания. Иначе команда может принять задержанный показатель за сегодняшнюю реакцию продукта.
Неполный день или неделя должны иметь отдельный визуальный статус. Серый фон, пунктир или подпись «данные по 13:00» лучше, чем сравнение незавершённого периода с полным. Это особенно важно для revenue и DAU, которые естественно растут внутри дня.
Покажите период, единицы, базу и определение. Если есть фильтр, назовите его. Читатель должен понять график без поиска по пяти ячейкам ноутбука.
Практика: один график для продуктового решения
Возьмите падение метрики и сначала постройте общий тренд, затем тот же показатель по платформе или каналу. Не начинайте с десяти сегментов: они быстро превращают исследование в перебор. Выберите один разрез по гипотезе и один контрольный разрез, который может опровергнуть объяснение.
Для каждой линии запишите размер базы, период и правило активного пользователя. Если сегмент отличается, проверьте, не изменился ли его состав. Затем сравните график с таблицей контрольных чисел и сформулируйте следующий вопрос. Хорошая визуализация заканчивается не «красиво растёт», а конкретным действием: проверить событие, запустить эксперимент или не менять продукт.
В финальном отчёте оставьте исходный DataFrame и код построения. Тогда любой коллега сможет поменять период, увидеть фильтр и проверить, что визуальный вывод не появился из ручного форматирования.
- Назвать вопрос до выбора типа графика.
- Собрать данные на смысловом зерне.
- Показать базу, единицы и неполные периоды.
- Отделить сигнал от сглаживания.
- Связать визуальный сигнал с проверяемым действием.
Материалы по теме

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

NumPy для аналитика: массивы, маски и векторные расчёты
Что нужно знать аналитику о NumPy: массивы, типы, boolean-маски, np.where и векторизация, которая лежит под многими операциями pandas.

Python для аналитика: рабочий стек из Jupyter, pandas и графиков
Какие инструменты нужны аналитику в Python: Jupyter, pandas, NumPy и matplotlib, как они связаны и с какого рабочего сценария начать.