Визуализация: как график врёт честными данными
На встрече показывают слайд: столбики растут, стрелка вверх, подпись «конверсия уверенно растёт четвёртый квартал подряд». Никто не спорит — картинка убедительна. Через полгода выясняется, что рост был с 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. Тип графика выбирается из вопроса
Несколько типов заслуживают отдельного предупреждения.
Круговая диаграмма с суммой не 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. Карта способов соврать честными данными
Пять веток, в одну из которых попадает почти любой спорный график — карту удобно держать рядом при ревью чужих картинок:
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 — короткие разборы конкретных решений от людей, которые делают графики для газет каждый день.
Мысль, к которой мы подошли вплотную: почти всё в этой главе — про один график. Дашборд — это двадцать графиков сразу, и там к визуальным проблемам добавляется главная организационная: никто не сформулировал, какое решение изменится хотя бы от одного из них.
Что дальше
Дашборды: почему их не смотрят и как сделать иначе — почему большинство дашбордов открывает только их автор, чем отличается дашборд под решение от витрины с цифрами, как устроен слой «метрика — порог — действие», сколько графиков помещается на один экран без потери смысла и что делать со старыми дашбордами, которые никто не удаляет.