Диаграммы рассеяния и пузырьки

Каждая точка — наблюдение. Облако точек показывает связь, которую не видно в таблице.

«Диаграмма рассеяния отвечает на вопрос: связаны ли две величины? И сразу показывает выбросы, о которых вы не подозревали».

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.

Проверьте себя
1. Что показывает диаграмма рассеяния?
AДолю каждой категории в целом
BСвязь между двумя числовыми переменными
CТренд одной величины во времени
DТолько средние значения
2. Почему bubble chart не годится для точного сравнения величин?
AПузырьки слишком яркие
BГлаз плохо сравнивает площади, кодирующие третью переменную
CBubble не поддерживается
DЦвет важнее размера