Аналитика данных Дашборды: почему их не смотрят и как сделать иначе
0%

Дашборды: почему их не смотрят и как сделать иначе

Дашборды: почему их не смотрят и как сделать иначе

В любой компании старше трёх лет есть папка с дашбордами, которую никто не открывает. Их не удаляют — неловко: кто-то делал, кто-то заказывал, на каком-то из них написано «для правления». Раз в квартал появляется новый, «чтобы наконец был единый источник правды», и через два месяца присоединяется к остальным.

Это не лень пользователей и не проблема инструмента: на 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 минут; обновляется по требованию. Нарратив — ответ на конкретный вопрос, зафиксированный во времени: «повлияла ли переделка корзины на выручку»; это текст с выводом, методом, оговорками и датой; читается один раз и не обновляется вовсе.

Классическая ошибка — заказчик просит нарратив («почему упало?»), а получает мониторинг («вот двадцать графиков, смотрите сами»). Ответа там нет, и после третьего раза человек перестаёт открывать. Обратная ошибка — вместо алерта делают мониторинг: «пусть команда следит за очередью возвратов». Люди не умеют круглосуточно следить, для этого есть пороговые уведомления.

Разделение не бюрократия, а экономия: нарратив пишется за день и не требует поддержки; мониторинг требует поддержки вечно; инструмент исследования требует ещё и обучения пользователей. Осознанный выбор жанра снимает половину будущих претензий, а формулировка «какое решение изменится» — та же самая, что в первой главе трека. Кто именно принимает решение и как с ним об этом разговаривать — соседний трек, Стейкхолдеры.

Спецификация плитки: семь строк перед тем, как открыть конструктор

Рабочий приём: до первого графика заполнить короткую карточку на каждый блок. Если хоть одна строка не заполняется — блока не будет.

Строка Пример заполнения
Вопрос Не портится ли оплата после релиза корзины?
Решение Откатить релиз / завести дефект / ничего не делать
Владелец Дежурный платёжной команды (не «продукт», а роль с именем)
Метрика и зерно Доля сессий с успешной оплатой; зерно — сессия, срез — платформа
Норма и порог Обычно 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.

Что дашборд наследует от способа сбора данных

Между действием пользователя и числом на экране — конвейер, и каждое его звено что-то теряет или задерживает.

Отсюда три обязательных элемента любой панели.

Свежесть на виду. Не «обновлено 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 % мужской аудитории; статус должен дублироваться формой или подписью, а не только цветом. Контраст, размер шрифта и работа с клавиатуры на панели, которую смотрят каждый день, — не формальность: Визуальная доступность.

Жизненный цикл: дашборды должны умирать

Кладбище растёт потому, что у дашборда есть рождение и нет смерти. Введите состояния явно — и половина проблемы исчезнет без единого совещания.

Что делает автоматика: раз в неделю прогоняет запрос использования из начала главы, переводит в состояние «под вопросом» всё, что не открывали 90 дней, шлёт создателю письмо, через две недели архивирует. Восстановление — одна кнопка, поэтому сопротивление почти нулевое: людей пугает не архивация, а необратимость.

Отдельно выключайте расписания обновления. Архивированный дашборд, продолжающий каждые 15 минут перестраивать витрину, — чистые деньги на ветер; в облачном хранилище это заметная строка счёта. Проверьте заодно подписки: рассылки, которые никто не открывает, живут годами.

Антипаттерны, которые узнаются с одного взгляда

Антипаттерн Почему не работает Что вместо
«Обзорный дашборд для руководства» на 30 плиток Нет ни одного решения, привязанного к конкретной плитке 5–9 метрик с нормой и владельцем, остальное — по ссылке
Голая цифра без сравнения Не отличить норму от аварии Значение + полоса обычного разброса + $n$
Дневной график по маленькому сегменту Ширина полосы больше любого реального эффекта Недельная агрегация или отказ от среза
Последний неполный столбик как часть тренда Ежедневная ложная тревога «упало» Отрезать или штриховать и подписывать
Сравнение «к прошлой неделе» на всех плитках Шум двух точек складывается Сравнение с полосой, а не с одной точкой
Пончиковая диаграмма из десяти долей Углы не сравниваются глазом Горизонтальные столбики, отсортированные по значению
Три дашборда с разной «конверсией» Нет единого определения Семантический слой и реестр метрик
Красный/зелёный как единственный признак статуса Нечитаемо при дальтонизме и на проекторе Цвет + форма + подпись
«Обновлено 2 минуты назад» вместо полноты данных Свежий кэш при сломанном конвейере Плитка полноты: последний полный день, лаг, статус тестов
Дашборд без даты ревизии Живёт вечно, устаревает молча Поле sunset и автоархивация
Прогноз-линия без интервала Выглядит как знание, является догадкой Веер сценариев или отказ от прогноза на панели
Цель, нарисованная задним числом под факт Метрика становится ритуалом Цель фиксируется до периода, изменения — с аннотацией

Практика: как оживить существующую панель за один день

  1. Померить. Запрос использования из начала главы. Отсортировать по числу зрителей, отдельно посчитать, сколько обновлений приходится на дашборды с нулём просмотров.
  2. Поговорить. Три-пять разговоров по 15 минут с теми, кто в списке значится зрителем: «что вы смотрите первым; что делаете после; какого числа не хватает». Ответ «просто смотрю, всё ли нормально» означает, что нужен алерт, а не панель.
  3. Определить жанр. Одна панель — один жанр. Всё лишнее вынести в отдельный инструмент исследования или в разовый разбор.
  4. Заполнить спецификации. Семь строк на блок. Блоки, у которых не заполняется строка «решение», удалить — обычно это 60–70 % содержимого.
  5. Добавить контекст. Каждой оставшейся плитке: полосу обычного разброса, размер выборки, порог решения.
  6. Добавить полноту и аннотации. Плитка свежести, отрезанный неполный период, слой релизов и инцидентов на временных осях.
  7. Перевести пороги в алерты. Всё, где реакция однозначна, уходит в уведомления с подтверждением и дежурным.
  8. Закрыть тестами. Уникальность зерна, диапазоны, свежесть источника, сверка с эталоном.
  9. Назначить срок жизни. Дата ревизии в описании панели и в реестре.
  10. Проверить скорость. Если открытие дольше 10 секунд — предагрегат, а не индекс в последний момент.

Хороший признак успеха на выходе: панель стало можно прочитать за 15 секунд, а число блоков уменьшилось. Плохой признак: «мы добавили ещё три графика, чтобы точно ничего не упустить».

Мини-итог

  • Мёртвые дашборды — не случайность, а следствие постройки без ответа на вопрос, какое решение изменится. Это измеримо: журнал обращений BI показывает тяжёлый хвост, где большая часть панелей имеет ноль зрителей за квартал.
  • Метрика использования сама подвержена дефектам сбора: кэш скрывает просмотры, планировщик их выдумывает. Считайте уникальных людей и проверяйте контекст.
  • Три жанра — мониторинг, исследование, нарратив — не смешиваются. Просьба «сделай дашборд» чаще всего означает нарратив или алерт.
  • Спецификация из семи строк на блок (вопрос, решение, владелец, зерно, норма, источник, срок жизни) отсеивает большинство лишнего до открытия конструктора.
  • Число без сравнения не информация. Лучшее сравнение — полоса обычного разброса $\hat p \pm 3\mathrm{SE}$ плюс правило серии из восьми точек; сравнение с одной прошлой точкой — самое шумное из возможных.
  • Двадцать плиток — двадцать одновременных проверок: при границах $\pm 3\mathrm{SE}$ это около 80 % шанса ложной тревоги за месяц, при $\pm 2\mathrm{SE}$ — 61 % за один день. Меньше блоков, подтверждение порога, выделение только выходов за границы.
  • Дашборд наследует лаги, потери и поздние события своего конвейера. Обязательны плитка полноты данных, отрезанный неполный период и оговорка о том, чего в данных нет.
  • Доля — это отношение сумм, а не среднее долей; перцентили не усредняются; агрегат прячет расходящиеся срезы.
  • Причинность на панели не живёт: максимум, что она даёт, — аннотации релизов и инцидентов как источник гипотез.
  • Если порог записан и реакция однозначна — это алерт с дежурным, а не панель.
  • Определение метрики — код в репозитории; скорость открытия — функциональное требование; данные закрываются тестами и сверкой с эталоном.
  • У дашборда должна быть дата смерти. Автоархивация по 90 дням без просмотров и выключение расписаний экономят и хранилище, и доверие.

Источники

Смежное чтение на портале: Продуктовые метрики и Аналитика и решения — про то, какие числа вообще стоит выносить на панель; UX-метрики — про измерение самого интерфейса; Обратные связи и Локальная оптимизация — про то, почему панель, на которую смотрит вся компания, меняет поведение компании.

Что дальше

Мы закончили на том, что у каждой метрики на панели должен быть владелец и порог. Но как только у числа появляется владелец, у которого от этого числа зависит оценка работы, включается механизм, ломающий саму метрику: люди начинают оптимизировать цифру, а не то, что она измеряла. Это не злой умысел и не редкость — это закон, воспроизводящийся в любой организации.

Метрики и закон Гудхарта: цифра, которая меняет поведение

Нашли неточность? Выделите фрагмент текста — рядом появится жучок.

Нужен разбор именно вашей ситуации?

Статья описывает общий случай. Если у вас частный — можно разобрать его отдельно, платно. А если не хватает целого материала, предложите тему: её оплачивают вскладчину, и она выходит открытой для всех.

Доска запросов