Диаграммы рассеяния и пузырьки
Каждая точка — наблюдение. Облако точек показывает связь, которую не видно в таблице.
«Диаграмма рассеяния отвечает на вопрос: связаны ли две величины? И сразу показывает выбросы, о которых вы не подозревали».
Scatter наносит каждое наблюдение точкой по двум числовым осям. Форма облака рассказывает о связи: восходящее облако — положительная корреляция, нисходящее — отрицательная, бесформенное — связи нет. Точка вдали от облака — выброс. Группы точек — кластеры.
Диаграмма рассеяния — главный инструмент разведочного анализа (EDA): прежде чем строить модель или считать средние, аналитик смотрит именно на облако точек. Оно сразу отвечает на вопросы, которые таблица скрывает: есть ли вообще зависимость, линейна ли она, нет ли двух раздельных групп клиентов, не тянет ли единственный гигантский выброс всю статистику. Классический довод в пользу графика — квартет Энскомба: четыре набора данных с одинаковыми средними, дисперсиями и коэффициентом корреляции выглядят на scatter совершенно по-разному (прямая, парабола, идеальная линия с одним выбросом). Это доказывает, что сводные числа без картинки обманчивы.
Scatter масштабируется до трёх и четырёх измерений за счёт дополнительных визуальных каналов. Размер маркера (s) превращает диаграмму в bubble chart и кодирует третью величину — численность, объём, вес наблюдения. Цвет (c + cmap) добавляет четвёртое измерение: непрерывное — через градиентную палитру, категориальное — через дискретные цвета. Форма маркера может кодировать пятый, качественный признак. Но каждый новый канал нагружает восприятие, поэтому больше трёх-четырёх кодировок на одном графике делают его нечитаемым: лучше разбить данные на несколько панелей, чем втиснуть всё в одно облако.
fig, ax = plt.subplots()
ax.scatter(area, price, alpha=0.6)
# bubble: третья переменная -> размер точки
ax.scatter(area, price, s=[r*5 for r in rooms], alpha=0.5)
# цвет -> категория (четвёртое измерение)
ax.scatter(area, price, c=district_codes, cmap="viridis")
ax.set_xlabel("Площадь, м2")
ax.set_ylabel("Цена, млн")
Как работает под капотом
Scatter рисует маркер-artist в точке (x, y) для каждого наблюдения. Параметр s задаёт площадь маркера (внимание: глаз плохо сравнивает площади, поэтому bubble — для качественного, не точного сравнения). c + cmap кодирует категорию или число цветом. При тысячах точек включают прозрачность (alpha), чтобы увидеть плотность.
Важная техническая деталь bubble: чтобы радиус пузырька воспринимался пропорционально величине, кодировать нужно именно площадь, а не диаметр. Если задать s прямо равным значению, то наблюдение вдвое большее по величине получит вчетверо большую площадь и зрительно покажется гораздо «весомее», чем оно есть. Поэтому корректный приём — брать s пропорционально значению (Matplotlib и так трактует s как площадь в квадратных пунктах) и подбирать масштабный коэффициент так, чтобы самый крупный пузырёк не перекрывал соседей. Цветовую кодировку тоже выбирают по типу данных: для непрерывной величины — секвенциальная палитра (viridis), для расходящейся вокруг нуля — diverging, для категорий — качественная; радужный jet искажает восприятие и не дружелюбен к дальтоникам.
При большом числе наблюдений главная проблема — overplotting: точки накладываются и плотные области выглядят как сплошное пятно, скрывая, где данных тысячи, а где десятки. Полупрозрачность (alpha) частично решает это: наложение тёмных полупрозрачных точек даёт градиент плотности. Но при десятках тысяч точек даже alpha не спасает — тогда переходят к агрегирующим формам: hexbin разбивает плоскость на шестиугольные ячейки и красит их по числу попавших точек, а 2D-гистограмма или контурный KDE-график показывают плотность напрямую. Это превращает «облако» обратно в честную карту распределения.
Связь измеряют коэффициентом корреляции. Посчитаем Пирсона вручную — это число, которое «прячется» за наклоном облака.
# Корреляция Пирсона вручную: насколько связаны X и Y
xs = [30, 45, 50, 60, 75, 80, 95]
ys = [3.1, 4.0, 4.4, 5.2, 6.0, 6.3, 7.1]
n = len(xs)
mx = sum(xs) / n
my = sum(ys) / n
cov = sum((x - mx) * (y - my) for x, y in zip(xs, ys))
sx = sum((x - mx) ** 2 for x in xs) ** 0.5
sy = sum((y - my) ** 2 for y in ys) ** 0.5
r = cov / (sx * sy)
print("Коэффициент корреляции r =", round(r, 3))
print("Близко к 1 -> сильная положительная связь")
«Попробуй сам ▶» — значение r и есть числовая мера того наклона, что вы видите глазами на scatter. r≈0.99 — почти прямая линия.
Частые ошибки
Путать корреляцию с причинностью — облако показывает связь, но не «что причина». Перегруженный scatter без прозрачности — точки сливаются в чёрное пятно (overplotting). Bubble для точного сравнения — площади обманывают. Радужная палитра для непрерывного цвета. Игнорировать выбросы вместо их разбора.
Несколько менее очевидных ловушек. Доверять одному коэффициенту корреляции, не глядя на сам график: r измеряет только линейную связь, поэтому у идеальной параболы r может быть близок к нулю, хотя зависимость очевидна. Эффект «лестницы» при дискретных или округлённых данных (например, возраст в целых годах) — точки выстраиваются в столбцы и сливаются; помогает джиттер, лёгкий случайный сдвиг. Парадокс Симпсона: общая линия тренда по всем точкам может идти вверх, а внутри каждой подгруппы — вниз; вскрывается только раскраской по hue. И наконец, кодирование размером величины, которая может быть отрицательной, — площадь не бывает отрицательной, поэтому такой признак ставят на цвет, а не на размер пузырька.
Best practices
- При большом числе точек — прозрачность или гексбин.
- Bubble — только для качественной оценки «больше/меньше».
- Подписывайте или выделяйте важные выбросы.
- Помните: корреляция ≠ причинность.
Итог: scatter раскрывает связи и аномалии. Дальше — графики распределения: гистограмма, boxplot, violin.