Визуализация: как график врёт честными данными
На встрече показывают слайд: столбики растут, стрелка вверх, подпись «конверсия уверенно растёт четвёртый квартал подряд». Никто не спорит — картинка убедительна. Через полгода выясняется, что рост был с 41,0 до 44,0 процента, то есть на семь процентов относительно базы, и целиком укладывается в шум. Все четыре числа на слайде были верными. Подписи стояли. SQL был корректным. Соврала не цифра, а шкала.
Это отличает визуализацию от остальных глав трека. В https://courses.digitable.life/post/data-analytics/08-hypothesis-testing/ ложный вывод получается из-за неверного применения аппарата; здесь аппарат ни при чём. Здесь ложный вывод получается из-за того, что данные кодируются в картинку одним способом, а читаются глазом — другим. Ошибка возникает не в данных и не в коде, а в зазоре между кодированием и декодированием, и её очень трудно заметить автору: он-то знает, что означает его график. Дальше — как устроен этот зазор, шесть механик, которыми он эксплуатируется (обычно непреднамеренно), рабочие SQL-заготовки для честных графиков и чек-лист, который ловит большую часть промахов за две минуты.
1. График — это канал связи с известной пропускной способностью
Формально график — это функция, отображающая значения данных в визуальные переменные: положение, длину, наклон, площадь, цвет, форму, насыщенность. Читатель выполняет обратное преобразование. Если обратное преобразование в среднем даёт не то, что было закодировано, график врёт — независимо от того, хотел этого автор или нет.
Точность декодирования измерена экспериментально. Уильям Кливленд и Роберт Макгилл в 1984 году показывали людям одни и те же отношения, закодированные разными способами, и измеряли ошибку (Graphical Perception, JASA 79(387)). Получилась лестница: позиция на общей шкале читается точнее всего, дальше по убыванию — позиция на разных шкалах, длина, наклон, угол, площадь, объём, насыщенность цвета.
Практические следствия из одной картинки:
- Точки и линии на общей оси — самый точный способ сравнить величины. Не самый красивый, но самый информативный на единицу площади слайда.
- Столбики работают, но только от нулевой базы: длина кодирует величину лишь тогда, когда начало отсчёта общее и равно нулю.
- Круговая диаграмма заставляет читать углы и площади — два способа с большой ошибкой. Отсюда правило Стивена Фью Save the Pies for Dessert: круговая допустима, когда сегментов два-три и точность не важна.
- Heatmap и картограмма дают порядок, но не величину. По цвету никто не скажет, что здесь 1,5 раза, а не 1,2. Если решение зависит от величины — цвет не подходит.
- 3D портит всё сразу: добавляет перспективное искажение, при котором дальние элементы выглядят меньше при равных значениях. Трёхмерная круговая диаграмма — комбинация двух худших решений.
Коэффициент лжи Тафти
Эдвард Тафти в The Visual Display of Quantitative Information (1983) предложил измерять искажение числом:
$$\mathrm{LF} = \frac{\text{относительное изменение на картинке}}{\text{относительное изменение в данных}}$$
Относительное изменение считается одинаково в числителе и знаменателе: модуль разности, делённый на исходное значение. Для честного графика $\mathrm{LF} \approx 1$; Тафти считает приемлемым диапазон примерно от 0,95 до 1,05.
Вернёмся к слайду из начала главы:
Данные: 41,0 → 44,0 процента, изменение $(44-41)/41 = 0{,}0732$, то есть 7,3 процента. На правой панели ось начинается с 40, поэтому первый столбик имеет высоту одной условной единицы, а последний — четырёх: изменение на картинке $(4-1)/1 = 3{,}00$, то есть 300 процентов.
$$\mathrm{LF} = \frac{3{,}00}{0{,}0732} \approx 41$$
График преувеличил эффект в сорок раз, не изменив ни одного числа. Это и есть «врать честными данными» в чистом виде.
2. Сквозная тема: график рисует выборку, а читается как мир
Прежде чем разбирать шкалы, стоит зафиксировать то, что дороже всех приёмов вместе взятых. График не показывает реальность — он показывает то, что попало в ваш способ сбора данных. Читатель этого не знает: он видит линию и достраивает мир.
Три типовых случая, каждый из которых уже разбирался в треке и каждый из которых на графике становится невидимым:
- Выживший источник. События приходят только от клиентов, которые смогли их отправить. Линия «ошибок стало меньше» может означать, что клиенты падают до отправки телеметрии — https://courses.digitable.life/post/data-analytics/02-data-sources/.
- Незакрытый правый край. Последняя точка любого графика по времени события систематически занижена, потому что часть событий ещё не доехала. Читатель видит «падение» — https://courses.digitable.life/post/data-analytics/03-data-quality/.
- Невидимый фильтр.
WHERE user_id IS NOT NULL AND NOT is_botв запросе не виден на картинке никак. Между «данные» и «график» стоит решение аналитика, о котором читатель не подозревает.
Минимальное лекарство — паспорт графика: под каждой картинкой одна строка с окном, размером выборки и фильтрами. И заранее посчитанные отбраковки, чтобы знать, о чём именно строка:
-- Сколько данных дошло до графика и что по дороге отвалилось
SELECT
count(*) AS rows_total,
count(*) FILTER (WHERE user_id IS NULL) AS anonymous,
count(*) FILTER (WHERE coalesce(is_bot, false)) AS bots,
count(*) FILTER (WHERE duration_ms IS NULL) AS no_duration,
count(*) FILTER (
WHERE user_id IS NOT NULL
AND NOT coalesce(is_bot, false)
AND duration_ms IS NOT NULL
) AS on_chart
FROM sessions
WHERE started_at >= date '2026-06-01'
AND started_at < date '2026-07-01';
Обратите внимание на coalesce(is_bot, false): без него NOT is_bot для строки с NULL даёт NULL, строка не попадает ни в один из счётчиков, и суммы не сходятся. Это тот случай, когда трёхзначная логика SQL молча меняет числа на графике — подробнее в https://courses.digitable.life/post/data-analytics/04-sql-for-analysis/. Если на графике осталось 62 процента исходных строк, это должно быть написано под графиком, а не жить в голове аналитика.
3. Оси: где ломается больше всего графиков
Ноль на оси: не догма, а функция типа графика
Спор «всегда ли ось должна начинаться с нуля» решается не вкусом, а тем, что кодирует график.
| Тип | Что кодирует | Нужен ли ноль |
|---|---|---|
| Столбики, площади | величину длиной или площадью | Да, всегда. Без нуля длина перестаёт быть пропорциональной величине |
| Линия, точки | изменение положением | Нет. Ноль часто вреден: сплющивает содержательные колебания |
| Отношения, индексы | изменение относительно базы | База (100 или 1) должна быть видна на оси |
Столбик, обрезанный снизу, врёт механически: глаз читает отношение длин, а длины больше не пропорциональны значениям. Линия обрезанной оси не боится — но обрезку обязательно нужно подписать: диапазон оси на видном месте, а не мелким шрифтом сбоку. Хороший разбор границ этого правила — Datawrapper: Why not to use two axes и соседние заметки того же блога.
Отдельный тонкий случай — процентные шкалы с далёкой базой. Доступность сервиса 99,95 против 99,99 процента на оси от нуля выглядит как две одинаковые полоски, хотя разница в простое — четыре с половиной часа в год против двадцати минут. Здесь ноль вредит: правильная ось — не доступность, а недоступность в логарифмическом масштабе, тогда разница видна честно.
Логарифмическая ось
Логарифмическая ось — рабочий инструмент для величин, растянутых на порядки: длительности запросов, размеры аккаунтов, выручка по клиентам. Правила короткие:
- Подписать явно. На логарифмической оси равные расстояния означают равные отношения, и глаз без подписи читает картинку как линейную.
- Нули и отрицательные значения не отображаются. Их либо не бывает по природе величины, либо нужен другой масштаб (
symlog), либо другой график. - Экспоненциальный рост становится прямой — именно поэтому на такой шкале удобно видеть замедление: прямая начинает загибаться.
Логарифмическая ось часто спасает там, где иначе получается «палка у нуля и пустота справа» — см. https://courses.digitable.life/post/data-analytics/06-distributions/.
Двойная ось Y: приём, у которого нет честного применения
Слева — два ряда, «очевидно связанных». Справа — те же самые ряды, приведённые к общей базе (январь = 100). Продажи выросли на 45 процентов, расходы на рекламу — на 6,7. Никакой связи не видно. Разницу создала одна строчка в настройках: диапазон правой оси 208–226 вместо, скажем, 0–250.
Проблема двойной оси в том, что точка пересечения линий и их взаимный наклон полностью контролируются автором и не несут информации. Автор редко делает это намеренно: инструмент подбирает границы автоматически, «чтобы линии красиво заполнили область». Заполнили — и породили вывод.
Что делать вместо:
- Индексировать оба ряда к базовому периоду (первое значение = 100) и рисовать на общей оси. Тогда наклон означает темп роста и сравним между рядами.
- Small multiples — две панели друг под другом с общей осью X. Это позиция на разных шкалах, второй уровень лестницы точности, а не выдуманное совпадение.
- Диаграмма рассеяния, если вопрос действительно про связь: по осям две величины, точки — периоды, и сразу видно, есть ли зависимость. И это всё равно только корреляция — https://courses.digitable.life/post/data-analytics/10-causality/.
Ось X тоже врёт
- Пропущенные периоды. Если в данных нет строк за 12 и 13 июня,
GROUP BY dayих просто не вернёт, а линия соединит 11-е с 14-м. Двухдневный сбой сбора превратится в ровный участок. - Обрезанное окно (cherry-picking). Выбор начальной даты — самый безобидный на вид способ получить нужный тренд. Правило: окно выбирается из бизнес-логики (год, релизный цикл), а не подгоняется под вывод.
- Инвертированная ось. Реальный случай 2014 года: график убийств во Флориде рисовали с осью, направленной вниз, и рост читался как падение.
Заготовка против пропущенных дней — календарь и левое соединение:
WITH bounds AS (
SELECT date '2026-06-01' AS from_day, date '2026-07-01' AS to_day -- верхняя граница исключается
),
calendar AS (
SELECT g::date AS day
FROM bounds, generate_series(bounds.from_day, bounds.to_day - 1, interval '1 day') AS g
),
raw AS (
SELECT date_trunc('day', occurred_at)::date AS day, count(*) AS n
FROM events, bounds
WHERE occurred_at >= bounds.from_day
AND occurred_at < bounds.to_day
GROUP BY 1
)
SELECT c.day, coalesce(r.n, 0) AS n
FROM calendar c
LEFT JOIN raw r USING (day)
ORDER BY c.day;
Теперь день без событий — это ноль на графике, а не отсутствующая точка. Разница между «ничего не было» и «мы ничего не знаем» существенна, и её тоже стоит различать: если сбор данных был сломан, честнее оставить разрыв в линии, чем нарисовать ноль.
4. Тип графика выбирается из вопроса
задаёт график?"] --> A["Сравнить величины
у категорий"] Q --> B["Показать изменение
во времени"] Q --> C["Показать распределение
одной величины"] Q --> D["Показать связь
двух величин"] Q --> E["Показать состав целого"] A --> A1["Столбики от нуля,
отсортированные по значению,
подписи прямо на столбиках"] B --> B1{"Рядов больше
пяти?"} B1 -- "нет" --> B2["Линии на одной шкале,
обрезка оси подписана"] B1 -- "да" --> B3["Small multiples:
одна панель на ряд,
общая ось Y"] C --> C1["Гистограмма или ECDF;
боксплот только вместе
с точками"] D --> D1["Диаграмма рассеяния,
никогда не две оси Y"] E --> E1{"Категорий больше трёх
или доли надо
сравнивать между периодами?"} E1 -- "да" --> E2["Столбики долей
плюс абсолютные значения
рядом"] E1 -- "нет" --> E3["Круговая допустима,
но столбики точнее"]
Несколько типов заслуживают отдельного предупреждения.
Круговая диаграмма с суммой не 100 процентов. Если данные из вопроса с множественным выбором, доли складываются в 140 процентов, и любая круговая по ним математически бессмысленна. Нужны столбики «доля респондентов, выбравших вариант».
Stacked area с несколькими рядами. У верхних слоёв нет общей базы: их положение зависит от всех нижних, и глаз не может прочитать их динамику. Пригодна только для суммы, а не для сравнения слоёв.
Спагетти-график. Двадцать линий в одном поле не читает никто. Решение — small multiples плюс одна выделенная линия на каждой панели с остальными в фоне серым.
Гладкая интерполяция. Сплайн между месячными точками рисует значения, которых не существовало, и часто выезжает за границы возможного (например, отрицательные продажи в провале). Для дискретных величин — прямые отрезки или ступенька.
Кумулятивные графики. Накопленная сумма всегда растёт, поэтому кумулята выглядит хорошо всегда, включая период полной остановки: в этом случае линия просто перестаёт расти, а глаз всё равно видит «вверх». Информация о темпе спрятана в наклоне — а наклон, как мы видели, читается плохо. Если вопрос про темп, рисуйте темп.
Хороший справочник «вопрос → тип графика» — FT Visual Vocabulary; теоретическая база с примерами хорошего и плохого — бесплатная книга Клауса Уилке Fundamentals of Data Visualization.
5. Агрегация, скрывающая распределение
Самый коварный способ соврать честными данными: не искажать масштаб, а показать корректно посчитанное среднее там, где решение зависит от формы распределения. Классика жанра — квартет Энскома (Graphs in Statistical Analysis, 1973): четыре набора точек с одинаковыми средними, дисперсиями, корреляцией и линией регрессии и совершенно разными картинками. Его современное продолжение — Datasaurus Dozen, где такие наборы генерируются алгоритмом, вплоть до динозавра.
Что происходит в проде без всякого динозавра:
- Столбик «среднее время ответа 220 мс» скрывает, что у 90 процентов пользователей 90 мс, а у оставшихся 10 процентов — полторы секунды. Решение про худший случай нельзя принимать по среднему — https://courses.digitable.life/post/data-analytics/05-descriptive-stats/.
- Боксплот скрывает бимодальность: смесь двух режимов и одно широкое унимодальное распределение дают почти одинаковые ящики — https://courses.digitable.life/post/data-analytics/06-distributions/.
- Агрегат по всем сегментам может двигаться в сторону, противоположную каждому сегменту (парадокс Симпсона). На графике это выглядит как «конверсия падает», хотя в каждом канале она растёт — просто доля дорогого канала выросла.
Заготовки для честного показа распределения
Гистограмма в SQL (PostgreSQL), с явными корзинами и без потери хвоста:
SELECT
width_bucket(duration_ms, 0, 5000, 50) AS bucket, -- 50 корзин по 100 мс
(width_bucket(duration_ms, 0, 5000, 50) - 1) * 100 AS lower_ms, -- левая граница корзины
count(*) AS n
FROM requests
WHERE started_at >= date '2026-07-01'
AND started_at < date '2026-08-01'
GROUP BY 1
ORDER BY 1;
width_bucket(operand, low, high, count) возвращает 0 для значений ниже low и count + 1 для значений выше high. Корзина 51 — это весь хвост «дольше 5 секунд», и выбрасывать её нельзя: именно она обычно и есть предмет разговора. Ширина корзины — параметр, который меняет вывод; разумная отправная точка — правило Фридмана–Дьякониса из https://courses.digitable.life/post/data-analytics/06-distributions/.
ECDF без параметров, которые можно подкрутить, — самый честный дефолт для одной величины:
WITH grid AS (
SELECT (duration_ms / 10) * 10 AS ms, count(*) AS n -- сетка 10 мс, целочисленное деление
FROM requests
WHERE started_at >= date '2026-07-01'
AND started_at < date '2026-08-01'
GROUP BY 1
)
SELECT
ms,
round(
sum(n) OVER (ORDER BY ms ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW)::numeric
/ sum(n) OVER (),
4
) AS share_le -- доля запросов не медленнее ms
FROM grid
ORDER BY ms;
Ряд перцентилей вместо одной линии среднего — минимальное изменение, дающее максимум смысла:
SELECT
date_trunc('day', started_at)::date AS day,
count(*) AS n,
round(avg(duration_ms)) AS avg_ms,
percentile_cont(0.50) WITHIN GROUP (ORDER BY duration_ms) AS p50,
percentile_cont(0.90) WITHIN GROUP (ORDER BY duration_ms) AS p90,
percentile_cont(0.99) WITHIN GROUP (ORDER BY duration_ms) AS p99
FROM requests
WHERE started_at >= current_date - INTERVAL '30 days'
AND started_at < current_date -- сегодняшний день ещё не закрыт
GROUP BY day
ORDER BY day;
Если на графике p99 по пятиминутным интервалам с сотней запросов в каждом — вы рисуете шум: для устойчивой оценки квантиля уровня $q$ нужно порядка $100/(1-q)$ наблюдений, то есть для p99 около десяти тысяч.
Ещё одно дешёвое правило: рядом с любым агрегатом по сегменту выводите count(*) и долю сегмента (round(100.0 * count(*) / sum(count(*)) OVER (), 1)) — иначе столбик для восьми пользователей выглядит ровно так же надёжно, как для восьмидесяти тысяч.
Разрез, который ловит Симпсона на графике: общая линия и линии по сегментам в одном запросе.
SELECT
date_trunc('month', signed_up_at)::date AS month,
round(100.0 * count(*) FILTER (WHERE converted) / count(*), 2) AS conv_all,
round(100.0 * count(*) FILTER (WHERE converted AND channel = 'organic')
/ nullif(count(*) FILTER (WHERE channel = 'organic'), 0), 2) AS conv_organic,
round(100.0 * count(*) FILTER (WHERE converted AND channel = 'paid')
/ nullif(count(*) FILTER (WHERE channel = 'paid'), 0), 2) AS conv_paid,
round(100.0 * count(*) FILTER (WHERE channel = 'paid') / count(*), 1) AS paid_share
FROM signups
WHERE signed_up_at >= date '2026-01-01'
GROUP BY month
ORDER BY month;
nullif(..., 0) защищает от деления на ноль в месяцах, где канала не было. Колонка paid_share здесь не украшение: именно смена состава объясняет расхождение общей линии с линиями сегментов.
Тот же принцип в коде для быстрой проверки перед публикацией. Столбик со средним по этим данным — одна строка ax.bar(["все запросы"], [durations.mean()]), после которой от двадцати тысяч наблюдений остаётся одно число; ECDF стоит столько же и не теряет ничего:
import numpy as np
import matplotlib.pyplot as plt
rng = np.random.default_rng(42)
durations = rng.lognormal(mean=5.2, sigma=0.9, size=20_000) # медиана около 180 мс
x = np.sort(durations) # ECDF: параметров для подкрутки нет
y = np.arange(1, x.size + 1) / x.size
fig, ax = plt.subplots(figsize=(6, 4))
ax.plot(x, y)
ax.set_xscale("log") # логарифм подписан в заголовке оси
ax.set_xlabel("время ответа, мс (логарифмическая шкала)")
ax.set_ylabel("доля запросов не медленнее")
ax.axhline(0.9, linestyle="--", linewidth=1) # уровень p90
ax.set_ylim(0, 1)
ax.set_title(f"ECDF, n = {x.size}")
fig.tight_layout()
Стоимость: сортировка $O(n \log n)$ по времени и $O(n)$ по памяти — для двадцати тысяч точек это доли секунды, а для миллионов ECDF считают на агрегированной сетке, как в SQL выше.
6. Время: отдельный источник вранья
Гранулярность меняет вывод. Суточные точки для продукта с недельной сезонностью — это пила, в которой не виден тренд. Недельные точки прячут провал вторника. Правило: гранулярность выбирается из масштаба решения, а не из того, «как красивее», и по времени всегда сравниваются одинаковые календарные отрезки (полная неделя с полной неделей).
Скользящее среднее сглаживает пилу, но приносит три проблемы: сдвиг фазы, ложную гладкость на краях и потерю всплесков.
WITH daily AS (
SELECT c.day, coalesce(r.n, 0) AS n
FROM calendar c LEFT JOIN raw r USING (day) -- календарь из раздела 3
)
SELECT
day,
n,
CASE WHEN count(*) OVER w = 7
THEN round(avg(n) OVER w, 1)
END AS ma7 -- первые шесть дней остаются NULL: окно ещё неполное
FROM daily
WINDOW w AS (ORDER BY day ROWS BETWEEN 6 PRECEDING AND CURRENT ROW)
ORDER BY day;
CASE WHEN count(*) OVER w = 7 — не педантизм. Без него первая точка равна значению первого дня, вторая — среднему из двух, и график начинается с искусственного «роста», которого в данных нет. Второй нюанс: такое окно запаздывающее, его значение относится к концу окна и отстаёт от данных примерно на три дня. Центрированное окно (ROWS BETWEEN 3 PRECEDING AND 3 FOLLOWING) фазу не сдвигает, но заглядывает в будущее и потому не может быть построено для последних трёх дней — их придётся оставить пустыми, а не досчитывать по укороченному окну. Подробности про оконные функции — https://courses.digitable.life/post/data-analytics/04-sql-for-analysis/.
Правый край. Повторим ещё раз, потому что это самая частая ошибка на дашбордах: последняя точка по времени события почти всегда занижена. Лечится либо исключением незакрытого периода, либо визуальной пометкой.
-- Недельный график: текущая неделя не показывается вовсе
SELECT
date_trunc('week', occurred_at)::date AS week_start,
count(*) AS events
FROM events
WHERE occurred_at >= date '2026-04-01'
AND occurred_at < date_trunc('week', now())
GROUP BY 1
ORDER BY 1;
Изменение определения метрики посреди ряда — линия делает скачок, читатель видит эффект продукта. На графике должна стоять вертикальная линия-аннотация с датой и подписью «с 14 мая в активных пользователей входят мобильные сессии». Без аннотации это самый дорогой вид вранья: оно повторяется каждый раз, когда кто-то открывает график.
Индексация к базе. Приведение рядов к «база = 100» — правильный инструмент для сравнения темпов, но выбор базы становится параметром: возьмите провальный месяц, и все ряды покажут рост. База выбирается по содержательной причине (начало года, дата релиза), и она подписывается.
7. Цвет, площадь и карты
Три вида шкал, и их нельзя путать.
- Качественная (категории): различимые оттенки одной светлоты, не больше семи-восьми штук.
- Последовательная (величина от малого к большому): один тон, монотонная светлота.
- Расходящаяся (отклонение от осмысленного центра): два тона, нейтраль посередине.
Расходящаяся шкала с произвольно выбранной серединой — способ соврать, ровно как обрезанная ось: центр цветовой шкалы читается как «норма». Если середина не имеет смысла (ноль, цель, среднее по рынку), нужна последовательная шкала.
Радужная палитра (jet) вредна. Она немонотонна по светлоте: глаз видит границы там, где в данных плавный переход, и не видит переходов там, где они есть. В чёрно-белой печати она разваливается. Замена — viridis, magma и родственные монотонные палитры (matplotlib: choosing colormaps), для категорий и карт — ColorBrewer. И цвет не должен быть единственным каналом: около 8 процентов мужчин не различают красный и зелёный. Светофорная раскраска «красный плохо / зелёный хорошо» для них — два одинаковых серых. Дублируйте цвет формой, штриховкой, порядком или подписью; проверьте контраст — детали в https://courses.digitable.life/post/accessibility/06-visual/ и https://courses.digitable.life/post/ux-design/10-accessibility-in-design/.
Площадь и радиус. Если величина кодируется кругом, кодировать нужно площадь:
$$r = r_0 \sqrt{\frac{v}{v_0}}$$
Частая ошибка — задать радиус пропорционально величине. Тогда при отношении значений 2 отношение площадей равно 4, и картинка преувеличивает вдвое. Но и корректная площадь читается с занижением: воспринимаемая величина растёт примерно как площадь в степени 0,7, поэтому отношение 1,5 глаз читает примерно как 1,3. Вывод: пузыри годятся для порядка величин, а не для сравнения близких значений.
Карты. Картограмма (choropleth) абсолютных значений — почти всегда карта численности населения: в Москве всего больше, потому что там больше людей. Показывать нужно нормированную величину (на тысячу жителей, на клиента), а способ разбиения на классы (равные интервалы, квантили, естественные разрывы) менять вывод не должен — если меняет, вывод неустойчив и это надо сказать вслух.
8. Неопределённость должна быть на картинке
Столбик без усов — это утверждение о точности, которой у вас нет. Правила из https://courses.digitable.life/post/data-analytics/07-uncertainty/ в визуальной формулировке:
- Показывайте интервал для разности, а не два интервала для групп. Вопрос был про разницу. Перекрывающиеся усы двух групп не означают отсутствия различий — это одна из самых распространённых ошибок чтения графиков.
- Подписывайте, что именно за усы. Стандартное отклонение, стандартная ошибка и 95-процентный интервал различаются в разы. Без подписи усы декоративны.
- Не рисуйте линию там, где точек мало. Пять когорт по девять человек дают красивую кривую удержания, которая целиком состоит из шума — https://courses.digitable.life/post/data-analytics/11-cohorts-and-retention/.
- Не сглаживайте так, чтобы исчезла неопределённость. Гладкая линия внушает уверенность, которой в данных нет: если данные шумные, пусть шум будет виден, а тренд — отдельной линией.
- Держите
nрядом с числом. Подпись «конверсия 12,5 % (n = 16)» лечит больше споров, чем любые доверительные интервалы, потому что её понимают все.
9. График переживает свой контекст
Есть механика, из-за которой аккуратный график всё равно приводит к неверному решению, и она не про пиксели.
Вывод простой и жёсткий: всё, что важно для правильного чтения, должно быть внутри картинки — в заголовке, на оси, в аннотации, в подписи под графиком. Устная оговорка не переживает первого копирования в слайд, а картинка живёт годами.
Отсюда же правило заголовка. Заголовок графика — это не название измерения, а вывод:
- Плохо: «Конверсия по неделям».
- Лучше: «Конверсия выросла на 0,4 п.п. за квартал; интервал включает ноль».
- Ещё лучше, если график про решение: «Конверсия не достигла порога 3,5 %, запуск переносим».
Такой заголовок вынуждает автора сформулировать, что он утверждает, — и половина сомнительных графиков умирает на этом шаге, ещё до публикации. Про то, как это встроить в регулярную отчётность, — https://courses.digitable.life/post/data-analytics/13-dashboards/, а про то, как вопрос определяет всю работу, — https://courses.digitable.life/post/data-analytics/01-question-first/.
10. Карта способов соврать честными данными
Пять веток, в одну из которых попадает почти любой спорный график — карту удобно держать рядом при ревью чужих картинок:
честными данными)) Шкала Обрезанная ось у столбиков Двойная ось Y Логарифм без подписи Расходящаяся палитра без центра Выборка Выживший источник Незакрытый правый край Невидимый фильтр в запросе Подобранное окно дат Агрегация Среднее вместо распределения Парадокс Симпсона Кумулята вместо темпа Сглаживание, съедающее всплески Кодирование Площадь вместо длины Радиус вместо площади Радужная палитра Гладкая интерполяция Контекст Заголовок без вывода Нет n и окна Смена определения без аннотации Оговорка только устно
11. Пять точек в истории «график как аргумент»
Точка 1986 года — самая поучительная для нашей сквозной темы. В ночь перед пуском инженеры обсуждали зависимость повреждений уплотнительных колец от температуры. В материалах, которые рассматривались на совещании, фигурировали полёты с повреждениями; полёты без повреждений, проходившие при более высоких температурах, в этой подборке не участвовали. По разбору Тафти (Visual Explanations, 1997), картинка, включающая все пуски и температуру по оси X, показала бы зависимость гораздо отчётливее. Историки техники спорят о том, насколько данные вообще позволяли сделать однозначный вывод, — но сам механизм несомненен и знаком любому аналитику: на графике оказалось только то, что казалось релевантным, и отсутствие остального стало невидимым. Способ отбора данных для картинки — часть картинки, даже когда он в неё не попал.
12. Чек-лист перед публикацией
Две минуты, которые ловят большую часть промахов.
- Какой вопрос задаёт график и какое решение изменится от ответа?
- Заголовок — это вывод, а не название измерения?
- Столбики начинаются от нуля? Если ось обрезана или логарифмическая — это подписано?
- Нет ли второй оси Y? Если есть — можно ли заменить индексом или двумя панелями?
- Указаны окно,
nи все фильтры, применённые в запросе? - Незакрытый последний период исключён или помечен, а пропущенные периоды видны как пропуски, а не заглажены линией?
- Показана ли форма распределения там, где решение зависит от хвоста?
- Есть ли на графике мера неопределённости и подпись, какая именно?
- Проверен ли график в разрезе главных сегментов (не Симпсон ли это)?
- Цвет дублирован формой или подписью, а палитра монотонна по светлоте? Круги кодируют площадь, а не радиус? На карте величина нормирована?
- Была ли смена определения метрики внутри окна и стоит ли аннотация?
- Что подумает человек, увидевший только картинку без вашего комментария?
Зеркальная версия для чтения чужого графика: посмотрите сначала на оси, потом на подпись под графиком, потом на n — и только затем на линии. Если первые три шага дали пустоту, линии можно не смотреть.
13. Типичные ошибки
| Ошибка | Что видит читатель | Как поймать |
|---|---|---|
| Обрезанная ось у столбиков | Кратный рост вместо процентного | Коэффициент лжи: изменение на картинке / изменение в данных |
| Двойная ось Y | Связь двух рядов | Привести оба ряда к индексу 100 и посмотреть снова |
| Среднее вместо распределения | «Всё в порядке» при катастрофе у части пользователей | Построить ECDF или ряд перцентилей |
| Правый край без пометки | Падение в конце периода | Исключить незакрытый период из выборки |
| Скользящее среднее без проверки полноты окна | Ложный рост в начале ряда | count(*) OVER w равен ширине окна |
| Кумулятивный график | Рост даже при полной остановке | Нарисовать производную — приросты по периодам |
| Смена определения метрики | Эффект продукта вместо смены счётчика | Аннотация с датой изменения |
| Агрегат без разреза | Тренд, противоположный сегментному | Разрез по двум-трём главным измерениям |
| Радиус вместо площади | Двукратное преувеличение | Проверить формулу: $r \propto \sqrt{v}$ |
| Столбик без усов | Уверенность, которой нет | Добавить интервал разности и n |
Мини-итог
- График — канал с измеренной пропускной способностью. Позиция читается точнее длины, длина — точнее угла, угол — точнее площади и цвета. Выбор кодирования определяет ошибку чтения ещё до того, как в дело вступит недобросовестность.
- Ноль на оси — вопрос не вкуса, а типа графика. Столбики кодируют величину длиной и требуют нуля всегда; линии кодируют изменение, и обрезка оси для них законна, но должна быть подписана.
- Двойная ось Y создаёт связь настройками масштаба; замена — индекс к базе или две панели с общей осью X. Коэффициент лжи превращает спор о вкусах в число: обрезка оси на примере из главы дала преувеличение эффекта в сорок раз.
- Агрегация, скрывающая распределение, опаснее любой шкалы, потому что выглядит корректно. Там, где решение зависит от хвоста или от состава, показывают ECDF, перцентили и разрезы.
- Время требует отдельной дисциплины: незакрытый правый край, пропущенные дни, фаза скользящего среднего, выбор базы индексации и аннотация смены определения.
- Всё важное должно быть внутри картинки. Устная оговорка не переживает копирования в слайд, а картинка живёт годами и продолжает принимать решения без вас.
- Способ сбора данных — часть графика, даже если он в него не попал. Строка «окно,
n, фильтры» под картинкой стоит дешевле, чем любое из последствий её отсутствия.
Дополнительное чтение: Edward Tufte, The Visual Display of Quantitative Information — книга, с которой начался разговор о честности графиков; Claus Wilke, Fundamentals of Data Visualization — бесплатная и самая практичная современная книга по теме, с разделами «плохо / лучше» почти на каждый случай; Carl Bergstrom, Jevin West, Calling Bullshit — курс о том, как читать чужие графики и числа; FT Visual Vocabulary — шпаргалка «вопрос → тип графика» на одном листе; блог Datawrapper — короткие разборы конкретных решений от людей, которые делают графики для газет каждый день.
Мысль, к которой мы подошли вплотную: почти всё в этой главе — про один график. Дашборд — это двадцать графиков сразу, и там к визуальным проблемам добавляется главная организационная: никто не сформулировал, какое решение изменится хотя бы от одного из них.
Что дальше
Дашборды: почему их не смотрят и как сделать иначе — почему большинство дашбордов открывает только их автор, чем отличается дашборд под решение от витрины с цифрами, как устроен слой «метрика — порог — действие», сколько графиков помещается на один экран без потери смысла и что делать со старыми дашбордами, которые никто не удаляет.