Дашборды: почему их не смотрят и как сделать иначе
В любой компании старше трёх лет есть папка с дашбордами, которую никто не открывает. Их не удаляют — неловко: кто-то делал, кто-то заказывал, на каком-то из них написано «для правления». Раз в квартал появляется новый, «чтобы наконец был единый источник правды», и через два месяца присоединяется к остальным.
Это не лень пользователей и не проблема инструмента: на Metabase, Superset, Tableau, Grafana и Looker одинаково легко построить и мёртвую витрину, и рабочий инструмент. Разница возникает раньше, чем открывается конструктор, — в момент, когда кто-то решает, что «надо бы видеть эти цифры». «Видеть цифры» — не задача. Задача звучит иначе: вот решение, которое кто-то принимает регулярно; вот число, от которого оно зависит; вот порог, при котором решение меняется. Панель без такого предложения — витрина, а витрину смотрят один раз.
Сквозная тема трека здесь никуда не девается: дашборд не может показать больше, чем позволяет способ сбора данных под ним, и прячет это ограничение эффективнее любого другого формата. Число на большом экране выглядит одинаково уверенно независимо от того, посчитано оно по миллиону сессий или по трёмстам, полны ли данные за вчера и считает ли ваша воронка ботов.
Кладбище дашбордов: это измеряется
Утверждение «большинство дашбордов никто не смотрит» звучит как публицистика, пока вы не
посчитаете. Все BI-инструменты пишут журнал обращений в свою служебную базу, и это
первый отчёт, который стоит построить в новой компании. В Metabase нужны
report_dashboard (сами дашборды), query_execution (каждое исполнение запроса с
контекстом) и core_user:
-- Кто на самом деле открывает дашборды (служебная база Metabase, PostgreSQL)
WITH views AS (
SELECT qe.dashboard_id,
qe.executor_id AS user_id,
qe.started_at
FROM query_execution qe
WHERE qe.context = 'dashboard' -- не разовые вопросы и не API
AND qe.started_at >= now() - INTERVAL '90 days'
)
SELECT d.id,
d.name,
u.email AS creator,
d.created_at::date AS created,
COUNT(DISTINCT v.user_id) AS viewers_90d, -- NULL не считается: у мёртвых будет 0
COUNT(v.started_at) AS queries_90d,
MAX(v.started_at)::date AS last_seen
FROM report_dashboard d
LEFT JOIN views v ON v.dashboard_id = d.id
LEFT JOIN core_user u ON u.id = d.creator_id
WHERE NOT d.archived
GROUP BY d.id, d.name, u.email, d.created_at
ORDER BY viewers_90d ASC, queries_90d ASC;
В Superset тот же отчёт строится по таблице logs (action, dashboard_id, user_id,
dttm), в Tableau — по historical_events из репозитория. Схемы служебных баз меняются
между версиями и не являются публичным API — проверьте имена колонок на своей установке
(https://www.metabase.com/docs/latest/usage-and-performance-tools/usage-analytics).
Результат почти всегда один и хорошо описывается тем, что вы уже знаете про распределения: это не колокол, а тяжёлый хвост. Три-пять дашбордов собирают почти весь трафик, ещё десяток открывают раз в месяц, остальные шестьдесят — ноль просмотров за квартал. При этом каждый из них ежечасно обновляется, греет хранилище и создаёт иллюзию, что «аналитика в компании есть».
И здесь же ловушка, идеально иллюстрирующая тему трека: этот отчёт сам страдает от
того, чем меряет. Просмотр из кэша может не порождать записи об исполнении — реально
открытый дашборд выглядит мёртвым. Наоборот, подписки, прогрев кэша и мониторинг создают
исполнения без единого человека у экрана: панель с 4000 «просмотров» может открываться
только планировщиком раз в 15 минут, поэтому служебных пользователей и лишние значения
context надо отфильтровать. Один человек, открывший дашборд десять раз за день, — это
не десять пользователей, отсюда COUNT(DISTINCT user_id).
Но и уникальный зритель ничего не говорит о том, изменилось ли хоть одно решение. Просмотр — прокси-метрика: идеальный дашборд мониторинга можно открывать раз в неделю на двадцать секунд, и это успех, а мёртвый — ежедневно и ритуально. Использование говорит, где копать; ответ даёт разговор со зрителями из списка: «что вы делаете после того, как посмотрели?»
Три жанра, которые называют одним словом
Половина мёртвых дашбордов мертва потому, что в одном экране смешали три разных жанра с разными требованиями.
Мониторинг — постоянное наблюдение за небольшим набором чисел, у каждого из которых есть норма; цель — заметить отклонение; читается за 15 секунд; обновляется часто. Исследование — инструмент для того, кто задаёт вопросы: срезы, фильтры, детализация, возможность провалиться в сырые данные; читается за 15 минут; обновляется по требованию. Нарратив — ответ на конкретный вопрос, зафиксированный во времени: «повлияла ли переделка корзины на выручку»; это текст с выводом, методом, оговорками и датой; читается один раз и не обновляется вовсе.
Классическая ошибка — заказчик просит нарратив («почему упало?»), а получает мониторинг («вот двадцать графиков, смотрите сами»). Ответа там нет, и после третьего раза человек перестаёт открывать. Обратная ошибка — вместо алерта делают мониторинг: «пусть команда следит за очередью возвратов». Люди не умеют круглосуточно следить, для этого есть пороговые уведомления.
«сделай дашборд»"] --> A{"Кто и как часто
принимает решение
по этому числу?"} A -->|"Никто не назван"| STOP["Дашборд не нужен.
Сначала владелец решения"] A -->|"Один раз, сейчас"| ONE["Разовый разбор:
текст с выводом и оговорками"] A -->|"Регулярно, по расписанию"| MON{"Порог решения
можно записать заранее?"} A -->|"Каждый раз новый срез"| EXP["Инструмент исследования:
фильтры, детализация, экспорт"] MON -->|"Да, реакция однозначна"| AL["Алерт.
Дашборд — только для разбора после сигнала"] MON -->|"Нет, нужно суждение"| DASH["Мониторинг:
5–9 блоков, норма, свежесть"] ONE --> ARCH["Архивируется
вместе с решением"] AL --> RUN["Учения: проверить,
что сигнал доходит"] DASH --> REV["Ревизия раз в квартал"]
Разделение не бюрократия, а экономия: нарратив пишется за день и не требует поддержки; мониторинг требует поддержки вечно; инструмент исследования требует ещё и обучения пользователей. Осознанный выбор жанра снимает половину будущих претензий, а формулировка «какое решение изменится» — та же самая, что в первой главе трека. Кто именно принимает решение и как с ним об этом разговаривать — соседний трек, Стейкхолдеры.
Спецификация плитки: семь строк перед тем, как открыть конструктор
Рабочий приём: до первого графика заполнить короткую карточку на каждый блок. Если хоть одна строка не заполняется — блока не будет.
| Строка | Пример заполнения |
|---|---|
| Вопрос | Не портится ли оплата после релиза корзины? |
| Решение | Откатить релиз / завести дефект / ничего не делать |
| Владелец | Дежурный платёжной команды (не «продукт», а роль с именем) |
| Метрика и зерно | Доля сессий с успешной оплатой; зерно — сессия, срез — платформа |
| Норма и порог | Обычно 3,4–4,6 %; ниже 3,4 % два дня подряд → эскалация |
| Источник и лаг | analytics.fct_funnel_daily, обновление каждый час, лаг до 45 минут |
| Срок жизни | До закрытия проекта корзины, ревизия 1 октября |
Строка «решение» отсеивает больше всего: «чтобы держать руку на пульсе» решением не является. Строка «норма» отсеивает следующую порцию — если никто не может сказать, какое значение нормально, метрику никогда не смотрели всерьёз, и первым делом нужен не дашборд, а разбор истории. «Владелец» — роль, а не отдел: у отдела нет глаз. «Срок жизни» — единственное, что останавливает рост кладбища.
Число без сравнения — не информация
Плитка «4,1 %» не сообщает ничего. Информация — это разница между ожиданием и наблюдением, поэтому у любого числа на панели должно быть сравнение, а у сравнения — масштаб неопределённости.
Три вида сравнения в порядке полезности: с полосой обычного разброса («4,1 % при обычных 3,4–4,6 %») — сразу видно, новость это или нет; с целью («4,1 % при плане 4,5 %») — работает, если план осмысленный, а не назначенный сверху круглым числом; с прошлым периодом («4,1 % против 3,9 % неделю назад») — самый популярный и самый опасный, потому что разница двух шумных точек шумнее каждой из них: её стандартная ошибка равна $\sqrt{\mathrm{SE}_1^2 + \mathrm{SE}_2^2}$, примерно в 1,4 раза больше, — а выглядит как факт.
И почти всегда нужен знаменатель. «Конверсия 6,2 %» по сегменту из 40 сессий — это две покупки; одна лишняя сдвинет цифру до 7,7 %. Показывайте рядом $n$: самая дешёвая прививка от ложных выводов. Про то, как интервал признаёт неопределённость, — глава 7.
Полоса обычного разброса: как перестать реагировать на шум
Самая дорогая привычка, которую воспитывает дашборд, — ежедневная реакция на дневной график. Метрика упала на 6 % — совещание. Выросла — «наши изменения работают». Обычно не произошло ничего: это дрожание доли.
Для доли $\hat p$, посчитанной по $n$ наблюдениям, стандартная ошибка равна
$$\mathrm{SE}(\hat p) = \sqrt{\frac{\hat p(1-\hat p)}{n}}$$
При базе 4 % и 10 000 сессий в сутки $\mathrm{SE} = \sqrt{0{,}04 \cdot 0{,}96 / 10000} \approx 0{,}00196$, то есть 0,20 процентного пункта. Границы $\hat p \pm 3\mathrm{SE}$ дают полосу 3,41–4,59 %. Всё, что внутри, — не новость.
Такая полоса — контрольная карта долей (p-chart), инструмент из статистического управления процессами. Правил чтения нужно ровно два, и второе не менее важно первого:
- точка вне полосы — смотреть сегодня;
- серия из восьми и более точек подряд по одну сторону от центральной линии — сдвиг процесса, даже если ни одна точка не вышла за границы. На картинке выше десять точек подряд ниже центра: медленная деградация, которую правило «вне полосы» не поймает никогда.
Полный набор эвристик (правила Нельсона, правила Western Electric) описан в справочнике NIST/SEMATECH — https://www.itl.nist.gov/div898/handbook/pmc/pmc.htm; практическое изложение для управленцев — Donald Wheeler, «Understanding Variation: The Key to Managing Chaos». Больше двух-трёх правил на дашборде обычно вредно: каждое добавляет ложных тревог.
Тот же расчёт в SQL, готовый под линию и полосу на графике:
-- Контрольная карта долей: центральная линия по окну, границы — по n каждого дня
WITH daily AS (
SELECT f.day, f.sessions, f.conversions,
f.conversions::numeric / NULLIF(f.sessions, 0) AS p
FROM analytics.fct_funnel_daily f
WHERE f.day >= CURRENT_DATE - 60
AND f.day < CURRENT_DATE -- сегодняшний день неполон, он не участвует
),
base AS ( -- центр считаем по всем сессиям окна,
SELECT SUM(conversions)::numeric -- а не как среднее дневных долей
/ NULLIF(SUM(sessions), 0) AS p_bar
FROM daily
),
banded AS (
SELECT d.day, d.sessions, d.p, b.p_bar,
sqrt(b.p_bar * (1 - b.p_bar) / d.sessions) AS se
FROM daily d CROSS JOIN base b
)
SELECT day,
round(100 * p, 2) AS conv_pct,
round(100 * (p_bar - 3 * se), 2) AS lcl_pct,
round(100 * (p_bar + 3 * se), 2) AS ucl_pct,
CASE WHEN p > p_bar + 3 * se THEN 'выброс вверх'
WHEN p < p_bar - 3 * se THEN 'выброс вниз' END AS out_of_band,
-- правило серии: 8 из 8 подряд ниже центра
SUM(CASE WHEN p < p_bar THEN 1 ELSE 0 END)
OVER (ORDER BY day ROWS BETWEEN 7 PRECEDING AND CURRENT ROW) AS below_run
FROM banded
ORDER BY day;
Две оговорки, без которых формула соврёт.
Первая: границы шире, чем кажется. Формула предполагает независимые наблюдения, а сессии одного пользователя зависимы, дни недели различаются систематически, трафик приходит пачками из рассылок. Реальный разброс всегда больше биномиального. Если по историческим данным дневная доля выходит за $\pm 3\mathrm{SE}$ чаще, чем раз в несколько месяцев, — не радуйтесь сигналам, а расширьте полосу по фактическому разбросу остатков после снятия недельной сезонности. Хорошая практика: считать полосу по стандартному отклонению дневных значений за прошлые 8–12 недель, а биномиальную формулу использовать как нижнюю оценку.
Вторая: маленький сегмент нельзя смотреть по дням вообще. Тот же расчёт при 300 сессиях в сутки даёт $\mathrm{SE} \approx 1{,}13$ п.п. и полосу 0,6–7,4 %. График конверсии такого сегмента по дням — это генератор случайных чисел с подписью «динамика». Единственное честное решение — агрегировать до недели (при 2100 сессиях в неделю полоса 2,7–5,3 %, всё ещё широко) или не показывать срез вовсе. Правило для дашборда: зернистость времени выбирается по объёму данных в самом мелком показанном срезе, а не по привычке.
Двадцать плиток — это двадцать проверок гипотез
Про множественные сравнения обычно вспоминают в контексте A/B-тестов, но дашборд — худший случай той же проблемы: человек смотрит на два десятка чисел одновременно, каждый день, и реагирует на самое отклонившееся.
Если на панели $k$ независимых метрик, каждая со своей полосой уровня $\alpha$, вероятность хотя бы одной ложной тревоги за один просмотр равна
$$P = 1 - (1-\alpha)^{k}$$
Для границ $\pm 3\mathrm{SE}$ ($\alpha = 0{,}0027$) и $k = 20$ получаем 5,3 % за день — и $1 - (1 - 0{,}053)^{30} \approx 80,%$ за месяц. То есть на панели из двадцати плиток примерно четыре месяца из пяти найдётся хотя бы одна «аномалия», за которой ничего не стоит. Соблазнительные границы $\pm 2\mathrm{SE}$ ($\alpha = 0{,}0455$) дают 61 % за один день: ложная тревога в среднем чаще, чем через день. Именно так дашборды приучают команду игнорировать собственные сигналы.
Практические следствия: держите на панели мониторинга 5–9 блоков, а не 30; выделяйте цветом только выходы за границы, а не всё подряд; для метрик, за которыми действительно следят, используйте правило подтверждения (два периода подряд) вместо одиночной точки. Логика та же, что при подглядывании в A/B-тест — от частого взгляда порог перестаёт значить то, что написано на этикетке; честный разбор самого порога — глава 8.
Что дашборд наследует от способа сбора данных
Между действием пользователя и числом на экране — конвейер, и каждое его звено что-то теряет или задерживает.
догоняет через часы, иногда дни Q->>R: загрузка микропакетами, 5 минут Note over R: дубли при повторной доставке
снимаются дедупликацией по event_id R->>M: пересборка последних 3 суток каждый час Note over M: пересборка нужна ровно потому,
что события приходят задним числом M->>C: запрос дашборда, TTL 15 минут C->>D: плитка 4,1 процента Note over D: суммарный лаг до 45 минут,
последние сутки заведомо неполны
Отсюда три обязательных элемента любой панели.
Свежесть на виду. Не «обновлено 5 минут назад» (это про кэш), а «данные полны по вчерашний день включительно, последняя загрузка в 09:15». Разница принципиальная: кэш может быть свежим при сломанном конвейере.
-- Плитка свежести: одна строка, три числа, никакой красоты
SELECT MAX(f.loaded_at) AS last_load,
date_trunc('minute', now() - MAX(f.loaded_at)) AS lag,
MAX(f.day) FILTER (WHERE f.is_complete) AS last_complete_day,
(now() - MAX(f.loaded_at)) < INTERVAL '2 hours' AS is_fresh
FROM analytics.fct_funnel_daily f;
Неполный период отрезан или помечен. Самая частая ложная тревога в мире дашбордов: столбик за сегодня ниже вчерашнего, потому что сегодня ещё идёт. То же самое на недельных и месячных графиках — последний столбик всегда «падает». Лечение: либо не показывать незакрытый период, либо рисовать его штриховкой и подписывать. Никогда не позволяйте линии тренда доходить до неполной точки.
-- Дневная динамика в часовом поясе отчётности, без сегодняшнего дня
WITH bounds AS (
SELECT (date_trunc('day', now() AT TIME ZONE 'Europe/Moscow'))
AT TIME ZONE 'Europe/Moscow' AS today_start -- обратно в timestamptz
)
SELECT date_trunc('day', e.event_ts AT TIME ZONE 'Europe/Moscow') AS day_msk,
COUNT(DISTINCT e.user_id) AS users
FROM events e, bounds b
WHERE e.event_ts >= b.today_start - INTERVAL '30 days'
AND e.event_ts < b.today_start
GROUP BY 1
ORDER BY 1;
Часовой пояс здесь не педантизм: если витрина считает сутки по UTC, а компания живёт по Москве, три часа заказов каждый вечер уезжают в «завтра», и утренние сравнения с прошлым днём становятся ложью. Договоритесь о поясе один раз и напишите его в подписи к дашборду.
Оговорка о том, чего в данных нет. Один абзац мелким шрифтом внизу панели: «учитываются только веб-события; трафик из мобильного приложения попадает сюда с задержкой до суток; сессии без cookie-согласия не считаются вовсе — это около 12 % трафика в ЕС». Это не юридическая формальность, а защита от вывода, который данные не выдерживают. Механика потерь разобрана в главе 2, поздние и дублирующиеся события — в главе 3, а устройство самого конвейера — в соседнем треке, Оркестрация.
Агрегация, которая прячет ответ
Дашборд по природе своей агрегатор, и три ошибки агрегации встречаются на нём чаще, чем где-либо ещё.
Среднее из средних. Классика: дневную конверсию считают как среднее дневных долей.
-- НЕВЕРНО: каждый день получает одинаковый вес независимо от трафика
SELECT AVG(conversions::numeric / NULLIF(sessions, 0)) AS conv_wrong
FROM analytics.fct_funnel_daily
WHERE day >= CURRENT_DATE - 30;
-- ВЕРНО: доля — это отношение сумм
SELECT SUM(conversions)::numeric / NULLIF(SUM(sessions), 0) AS conv_right
FROM analytics.fct_funnel_daily
WHERE day >= CURRENT_DATE - 30;
Два дня: в первый 100 сессий и 10 покупок (10 %), во второй 900 сессий и 45 покупок (5 %). Правильный ответ — 55/1000 = 5,5 %. Среднее дневных долей — 7,5 %. Ошибка в полтора раза, и заметить её на глаз невозможно. Следствие для архитектуры витрин: витрина отдаёт числитель и знаменатель, а не готовую долю — тогда её можно пересобирать по любым срезам и периодам без вранья.
Среднее вместо распределения. Плитка «среднее время ответа 240 мс» при медиане 90 мс и 99-м перцентиле 3 с описывает несуществующего пользователя. На дашбордах, где речь про время, деньги или размеры, показывайте перцентили — они устойчивы и складываются в понятную картину.
SELECT date_trunc('hour', started_at) AS h,
COUNT(*) AS n,
percentile_cont(0.5) WITHIN GROUP (ORDER BY duration_ms)::int AS p50,
percentile_cont(0.95) WITHIN GROUP (ORDER BY duration_ms)::int AS p95,
percentile_cont(0.99) WITHIN GROUP (ORDER BY duration_ms)::int AS p99
FROM api_requests
WHERE started_at >= now() - INTERVAL '24 hours'
GROUP BY 1
ORDER BY 1;
Отдельная ловушка: перцентили нельзя усреднять. Среднее из дневных p95 не равно p95
за неделю; чтобы получить недельный перцентиль, нужны исходные наблюдения или
пригодная к слиянию структура — t-digest, quantileTDigest в ClickHouse,
HDR-гистограмма. Если витрина хранит только дневные p95, недельной цифры у вас нет, и
честнее показать семь точек, чем одну выдуманную. Почему средние врут именно здесь —
глава 5 и глава 6.
Срез, спрятанный агрегатом. Общая конверсия стоит на месте, потому что мобильная упала, а десктопная выросла. На дашборде мониторинга это лечится не двадцатью дополнительными плитками, а одним приёмом: главная метрика плюс маленький ряд одинаковых мини-графиков по 3–5 ключевым срезам (small multiples в терминологии Тафти). Крайний случай, когда агрегат и срезы говорят противоположное, — парадокс Симпсона, разбор в главе 10. Выбор типа графика и то, как он врёт честными данными, — глава 12.
Дашборд не доказывает причину — но может помочь её искать
Самая частая сцена вокруг панели: метрика дёрнулась, и в комнате мгновенно рождается объяснение. «Это из-за вчерашнего релиза». Дашборд не содержит информации, из которой такой вывод следует: он показывает одну реализацию процесса без контрфактического сценария. Что было бы без релиза — на графике нет и быть не может.
Единственное, чем панель реально помогает, — слой аннотаций. Релизы, промоакции, инциденты, изменения трекинга, праздники и рекламные закупки должны рисоваться прямо на временных осях. Половина «загадочных» скачков объясняется строкой «включили новый источник трафика», а вторая половина — «поменяли определение события».
CREATE TABLE analytics.annotations (
id bigserial PRIMARY KEY,
happened_at timestamptz NOT NULL,
kind text NOT NULL CHECK (kind IN ('release','incident','promo','tracking')),
title text NOT NULL,
author text NOT NULL
);
-- Присоединяем к дневному ряду: несколько событий в день дадут несколько строк,
-- поэтому для одной точки графика их обычно сворачивают в список
SELECT d.day,
d.conv_pct,
string_agg(a.kind || ': ' || a.title, '; ' ORDER BY a.happened_at) AS notes
FROM daily_conv d
LEFT JOIN analytics.annotations a
ON a.happened_at >= d.day::timestamptz
AND a.happened_at < (d.day + 1)::timestamptz
GROUP BY d.day, d.conv_pct
ORDER BY d.day;
Изменение трекинга — самая коварная категория аннотаций. Если 12 марта поменяли момент
отправки события add_to_cart, то ряд до и после 12 марта — две разные метрики на одной
линии, и любое сравнение через эту дату бессмысленно. На графике должна быть вертикальная
черта и подпись, а лучше — разрыв линии.
Дальше работает обычный порядок: аннотация даёт гипотезу, а подтверждает её эксперимент или квазиэкспериментальный метод из главы 10. Дашборд — то место, где вопрос рождается, а не то, где на него отвечают.
Если решение автоматизируемо — это алерт, а не дашборд
Когда порог записан заранее и реакция однозначна, человек у экрана не нужен: нужно уведомление. Ровно тот же принцип, что в мониторинге сервисов, — см. соседний трек, Алертинг и Мониторинг.
Правила, которые переносятся в аналитику один в один:
- Сигнал адресован человеку, а не каналу. У алерта есть дежурный, иначе его читают все и не реагирует никто.
- Порог с подтверждением. «Ниже 3,4 % два дня подряд» вместо «ниже 3,4 %». Второе условие срезает большую часть ложных тревог, как показал расчёт выше.
- Дедупликация и тишина. Один инцидент — одно уведомление, а не по одному в час.
- У каждого алерта есть инструкция. Что открыть, что проверить, кому написать. Без инструкции получатель через месяц заводит фильтр в почте.
- Шумный алерт хуже отсутствующего. Алерт, который срабатывает вхолостую хотя бы раз в неделю, перестают читать — и он молча пропустит настоящий сбой.
Дашборд в этой схеме нужен, но роль у него другая: это место, куда приходят после сигнала, чтобы понять, где именно сломалось. Такой дашборд не нужно смотреть каждый день — и его низкая посещаемость не признак смерти.
Семантический слой: одна метрика — одно определение
Три дашборда с тремя разными числами для «конверсии» — обычное дело: где-то знаменатель сессии, где-то пользователи, где-то заказы; где-то боты отфильтрованы, где-то нет. После второго такого случая доверие к аналитике заканчивается — и справедливо, потому что верить нечему.
Лечится это не совещанием, а кодом: определение метрики живёт в одном месте, в репозитории, под ревью, и все панели читают его оттуда.
-- Витрина отдаёт составные части, а не готовую долю: так её можно
-- пересобрать по любому срезу без ошибки «среднее из средних»
CREATE OR REPLACE VIEW analytics.metric_checkout AS
SELECT s.day,
s.platform,
s.user_segment,
COUNT(*) FILTER (WHERE s.reached_cart) AS carts,
COUNT(*) FILTER (WHERE s.reached_paid) AS paid
FROM analytics.fct_sessions s
WHERE NOT s.is_bot -- определение бота — тоже часть определения метрики
GROUP BY 1, 2, 3;
Инструменты, которые делают это системно: dbt Semantic Layer / MetricFlow (https://docs.getdbt.com/docs/build/about-metricflow), Cube (https://cube.dev/docs), LookML в Looker. Общий смысл один: метрика описана декларативно (мера, зерно, допустимые срезы, фильтры), а SQL под каждый разрез генерируется. Даже без этих инструментов половину пользы даёт дисциплина: слой витрин с общими определениями и запрет писать в дашбордах «сырой» SQL по продакшн-таблицам.
Реестр метрик полезно завести явно — как таблицу, а не как страницу в вики, потому что страница устаревает молча:
Поле guardrail_for в реестре — не украшение: у каждой целевой метрики должна быть
парная защитная, ломающаяся при попытке накрутить основную. Это тема
следующей главы, но место ей — в реестре,
рядом с определением.
Инженерная часть: скорость, тесты, версии
Скорость — это функциональное требование. Дашборд, который открывается 40 секунд, не смотрят, каким бы правильным он ни был. Практический ориентир: до 3 секунд — инструмент, до 10 — терпимо для исследования, больше 30 — мёртв. Лечение: не «оптимизация запроса» в последнюю очередь, а предагрегация как проектное решение — витрина с нужной зернистостью, обновляемая по расписанию, вместо расчёта по сырым событиям на каждый клик. Колоночные хранилища и то, почему они здесь уместнее OLTP-базы, — ClickHouse и OLAP; разбор планов запросов — Индексы и планы.
-- Инкрементальная пересборка: последние 3 суток целиком, потому что события
-- приходят задним числом. Требуется UNIQUE (day, platform).
INSERT INTO analytics.fct_funnel_daily (day, platform, sessions, conversions, is_complete, loaded_at)
SELECT s.day,
s.platform,
COUNT(*) AS sessions,
COUNT(*) FILTER (WHERE s.reached_paid) AS conversions,
s.day < CURRENT_DATE AS is_complete,
now()
FROM analytics.fct_sessions s
WHERE s.day >= CURRENT_DATE - 3
GROUP BY s.day, s.platform
ON CONFLICT (day, platform) DO UPDATE
SET sessions = EXCLUDED.sessions,
conversions = EXCLUDED.conversions,
is_complete = EXCLUDED.is_complete,
loaded_at = EXCLUDED.loaded_at;
Тесты на данные, а не только на код. Пайплайн зелёный, а цифры чушь — обычная ситуация: схема не менялась, поменялось содержимое. Минимальный набор проверок, который окупается в первую неделю:
# dbt: тесты витрины и свежести источника
version: 2
sources:
- name: raw
tables:
- name: events
loaded_at_field: ingested_at
freshness:
warn_after: {count: 2, period: hour}
error_after: {count: 6, period: hour}
models:
- name: fct_funnel_daily
description: "Воронка по дням; зерно — (day, platform)"
tests:
- dbt_utils.unique_combination_of_columns:
combination_of_columns: [day, platform]
- dbt_utils.expression_is_true:
expression: "conversions <= sessions"
columns:
- name: day
tests: [not_null]
- name: sessions
tests:
- not_null
- dbt_utils.accepted_range:
min_value: 0
inclusive: true
Документация по тестам dbt: https://docs.getdbt.com/docs/build/data-tests. Отдельно
стоит завести сверку с эталоном: выручка из витрины против выручки из
биллинга/бухгалтерии, ежедневно, с допуском. Расхождение больше допуска — красная плитка
на дашборде и письмо владельцу. Один такой тест ловит больше реальных ошибок, чем десять
проверок на NOT NULL. Про качество и владение данными в целом —
Качество данных и governance.
Версии и ревью. Определения метрик и SQL дашбордов — код: git, пул-реквесты, код-ревью. Изменение определения метрики оформляется как изменение контракта: старое значение не переписывается задним числом молча, на графике появляется аннотация. Иначе через полгода никто не сможет объяснить, почему цифра в презентации за апрель не воспроизводится.
Доступ. Дашборд с персональными данными, открытый «всем сотрудникам», — инцидент, который просто ещё не заметили. Агрегаты по мелким срезам тоже раскрывают личность: одна строка «регион X, тариф Y, 1 клиент» — это конкретный человек. Практика: минимальная численность группы для показа (обычно 5–10), маскирование идентификаторов, отдельные права на детализацию.
Доступность. Красно-зелёная подсветка статуса нечитаема для примерно 4 % мужской аудитории; статус должен дублироваться формой или подписью, а не только цветом. Контраст, размер шрифта и работа с клавиатуры на панели, которую смотрят каждый день, — не формальность: Визуальная доступность.
Жизненный цикл: дашборды должны умирать
Кладбище растёт потому, что у дашборда есть рождение и нет смерти. Введите состояния явно — и половина проблемы исчезнет без единого совещания.
или владелец ушёл ПодВопросом --> Рабочий: нашёлся владелец,
переписана спецификация ПодВопросом --> Архив: никто не заявил права Рабочий --> Архив: проект закрыт,
наступила дата sunset Архив --> Рабочий: восстановлен по запросу
в течение 90 дней Архив --> [*]: удалён вместе с расписанием обновления note right of ПодВопросом Уведомление создателю и владельцу, две недели на ответ. Молчание — это ответ. end note note right of Архив Расписание обновления выключается сразу: мёртвый дашборд перестаёт греть хранилище end note
Что делает автоматика: раз в неделю прогоняет запрос использования из начала главы, переводит в состояние «под вопросом» всё, что не открывали 90 дней, шлёт создателю письмо, через две недели архивирует. Восстановление — одна кнопка, поэтому сопротивление почти нулевое: людей пугает не архивация, а необратимость.
Отдельно выключайте расписания обновления. Архивированный дашборд, продолжающий каждые 15 минут перестраивать витрину, — чистые деньги на ветер; в облачном хранилище это заметная строка счёта. Проверьте заодно подписки: рассылки, которые никто не открывает, живут годами.
Антипаттерны, которые узнаются с одного взгляда
| Антипаттерн | Почему не работает | Что вместо |
|---|---|---|
| «Обзорный дашборд для руководства» на 30 плиток | Нет ни одного решения, привязанного к конкретной плитке | 5–9 метрик с нормой и владельцем, остальное — по ссылке |
| Голая цифра без сравнения | Не отличить норму от аварии | Значение + полоса обычного разброса + $n$ |
| Дневной график по маленькому сегменту | Ширина полосы больше любого реального эффекта | Недельная агрегация или отказ от среза |
| Последний неполный столбик как часть тренда | Ежедневная ложная тревога «упало» | Отрезать или штриховать и подписывать |
| Сравнение «к прошлой неделе» на всех плитках | Шум двух точек складывается | Сравнение с полосой, а не с одной точкой |
| Пончиковая диаграмма из десяти долей | Углы не сравниваются глазом | Горизонтальные столбики, отсортированные по значению |
| Три дашборда с разной «конверсией» | Нет единого определения | Семантический слой и реестр метрик |
| Красный/зелёный как единственный признак статуса | Нечитаемо при дальтонизме и на проекторе | Цвет + форма + подпись |
| «Обновлено 2 минуты назад» вместо полноты данных | Свежий кэш при сломанном конвейере | Плитка полноты: последний полный день, лаг, статус тестов |
| Дашборд без даты ревизии | Живёт вечно, устаревает молча | Поле sunset и автоархивация |
| Прогноз-линия без интервала | Выглядит как знание, является догадкой | Веер сценариев или отказ от прогноза на панели |
| Цель, нарисованная задним числом под факт | Метрика становится ритуалом | Цель фиксируется до периода, изменения — с аннотацией |
Практика: как оживить существующую панель за один день
- Померить. Запрос использования из начала главы. Отсортировать по числу зрителей, отдельно посчитать, сколько обновлений приходится на дашборды с нулём просмотров.
- Поговорить. Три-пять разговоров по 15 минут с теми, кто в списке значится зрителем: «что вы смотрите первым; что делаете после; какого числа не хватает». Ответ «просто смотрю, всё ли нормально» означает, что нужен алерт, а не панель.
- Определить жанр. Одна панель — один жанр. Всё лишнее вынести в отдельный инструмент исследования или в разовый разбор.
- Заполнить спецификации. Семь строк на блок. Блоки, у которых не заполняется строка «решение», удалить — обычно это 60–70 % содержимого.
- Добавить контекст. Каждой оставшейся плитке: полосу обычного разброса, размер выборки, порог решения.
- Добавить полноту и аннотации. Плитка свежести, отрезанный неполный период, слой релизов и инцидентов на временных осях.
- Перевести пороги в алерты. Всё, где реакция однозначна, уходит в уведомления с подтверждением и дежурным.
- Закрыть тестами. Уникальность зерна, диапазоны, свежесть источника, сверка с эталоном.
- Назначить срок жизни. Дата ревизии в описании панели и в реестре.
- Проверить скорость. Если открытие дольше 10 секунд — предагрегат, а не индекс в последний момент.
Хороший признак успеха на выходе: панель стало можно прочитать за 15 секунд, а число блоков уменьшилось. Плохой признак: «мы добавили ещё три графика, чтобы точно ничего не упустить».
Мини-итог
- Мёртвые дашборды — не случайность, а следствие постройки без ответа на вопрос, какое решение изменится. Это измеримо: журнал обращений BI показывает тяжёлый хвост, где большая часть панелей имеет ноль зрителей за квартал.
- Метрика использования сама подвержена дефектам сбора: кэш скрывает просмотры, планировщик их выдумывает. Считайте уникальных людей и проверяйте контекст.
- Три жанра — мониторинг, исследование, нарратив — не смешиваются. Просьба «сделай дашборд» чаще всего означает нарратив или алерт.
- Спецификация из семи строк на блок (вопрос, решение, владелец, зерно, норма, источник, срок жизни) отсеивает большинство лишнего до открытия конструктора.
- Число без сравнения не информация. Лучшее сравнение — полоса обычного разброса $\hat p \pm 3\mathrm{SE}$ плюс правило серии из восьми точек; сравнение с одной прошлой точкой — самое шумное из возможных.
- Двадцать плиток — двадцать одновременных проверок: при границах $\pm 3\mathrm{SE}$ это около 80 % шанса ложной тревоги за месяц, при $\pm 2\mathrm{SE}$ — 61 % за один день. Меньше блоков, подтверждение порога, выделение только выходов за границы.
- Дашборд наследует лаги, потери и поздние события своего конвейера. Обязательны плитка полноты данных, отрезанный неполный период и оговорка о том, чего в данных нет.
- Доля — это отношение сумм, а не среднее долей; перцентили не усредняются; агрегат прячет расходящиеся срезы.
- Причинность на панели не живёт: максимум, что она даёт, — аннотации релизов и инцидентов как источник гипотез.
- Если порог записан и реакция однозначна — это алерт с дежурным, а не панель.
- Определение метрики — код в репозитории; скорость открытия — функциональное требование; данные закрываются тестами и сверкой с эталоном.
- У дашборда должна быть дата смерти. Автоархивация по 90 дням без просмотров и выключение расписаний экономят и хранилище, и доверие.
Источники
- Stephen Few. Information Dashboard Design — канонический разбор того, как не превращать панель в приборную доску самолёта: https://www.oreilly.com/library/view/information-dashboard-design/0596100167/
- Edward Tufte. The Visual Display of Quantitative Information — про плотность информации и small multiples: https://www.edwardtufte.com/book/the-visual-display-of-quantitative-information/
- Donald J. Wheeler. Understanding Variation: The Key to Managing Chaos — контрольные карты как способ перестать реагировать на шум.
- NIST/SEMATECH e-Handbook of Statistical Methods, раздел о мониторинге процессов: https://www.itl.nist.gov/div898/handbook/pmc/pmc.htm
- Google SRE Workbook, «Alerting on SLOs» — почему шумный алерт хуже отсутствующего: https://sre.google/workbook/alerting-on-slos/
- Metabase, внутренняя аналитика использования: https://www.metabase.com/docs/latest/usage-and-performance-tools/usage-analytics
- Apache Superset, документация: https://superset.apache.org/docs/intro
- dbt: тесты данных https://docs.getdbt.com/docs/build/data-tests и семантический слой https://docs.getdbt.com/docs/build/about-metricflow
- Cube — семантический слой поверх хранилища: https://cube.dev/docs
- Grafana, документация по панелям и алертам: https://grafana.com/docs/grafana/latest/
Смежное чтение на портале: Продуктовые метрики и Аналитика и решения — про то, какие числа вообще стоит выносить на панель; UX-метрики — про измерение самого интерфейса; Обратные связи и Локальная оптимизация — про то, почему панель, на которую смотрит вся компания, меняет поведение компании.
Что дальше
Мы закончили на том, что у каждой метрики на панели должен быть владелец и порог. Но как только у числа появляется владелец, у которого от этого числа зависит оценка работы, включается механизм, ломающий саму метрику: люди начинают оптимизировать цифру, а не то, что она измеряла. Это не злой умысел и не редкость — это закон, воспроизводящийся в любой организации.