- Зачем:
- после смены чарта на row-lineage пользовательская дока, README и урок 6
описывали несуществующий «Data Quality Summary» и старый состав KPI;
термин «зерно (grain)» использовался без пояснения.
- Что:
- SUPERSET_DASHBOARD.md, README, урок 6 описывают «Rows by Layer (event)»,
выровнен состав KPI/чартов; термин «зерно» поясняется в уроке простыми
словами с якорем из данных (события 1000 / визиты 99).
- термин выровнен на «визит» по CONTEXT.md (click_id = визит/сессия).
- в спеку редизайна добавлена секция «Пересмотр после приёмки» как след решения.
- Проверка:
- grep по «Data Quality Summary»/«Row Count» вне handoffs пуст.
- визуальная вычитка изменённых разделов.
174 lines
12 KiB
Markdown
174 lines
12 KiB
Markdown
# Редизайн KPI-полосы и состава чартов дашборда «E-commerce Analytics»
|
||
|
||
Дата: 2026-06-06
|
||
Статус: Implemented (`superset/create_dashboard.py`, `docs/SUPERSET_DASHBOARD.md`,
|
||
`docs/course/lessons/06_superset_bi.md`)
|
||
Связано: [`CONTEXT.md`](../../CONTEXT.md), [ADR-0002](../adr/0002-specs-as-durable-design-docs.md),
|
||
урок [`docs/course/lessons/06_superset_bi.md`](../course/lessons/06_superset_bi.md)
|
||
|
||
## Problem
|
||
|
||
Дашборд-эталон показывает две KPI-плитки — `Unique Users` и `Unique Sessions` — с
|
||
одинаковым числом (26 на срезе, 99 на полных данных), потому что в датасете
|
||
`user_domain_id ↔ click_id` строго 1:1. Две одинаковые цифры читаются как баг
|
||
расчёта и ничему не учат менти, который копирует дашборд как образец. Нужен
|
||
честный, копируемый состав KPI и чартов на полных данных.
|
||
|
||
## Context
|
||
|
||
- Дашборд — учебный эталон для менти. Требований обратной совместимости нет:
|
||
состав можно менять свободно, критерий — учебная ценность.
|
||
- Доменные термины зафиксированы в `CONTEXT.md`: **пользователь** =
|
||
`user_domain_id`, **визит/сессия** = `click_id`, **событие** = `event_id`.
|
||
- Источник данных — статический сид `data/*.jsonl`. Синтетический генератор
|
||
(ветка `feature/data-generator`) источником **не** является и семантику ломает
|
||
(см. `generator/KNOWN_ISSUES.md` на той ветке).
|
||
|
||
## Goals
|
||
|
||
- KPI-полоса из 4 различающихся честных метрик (без дубля одинаковых чисел).
|
||
- Превратить слабые места данных в учебные объекты, а не маскировать их.
|
||
- Консистентность с `CONTEXT.md` и уроком 6.
|
||
|
||
## Non-goals
|
||
|
||
- Не модернизируем **типы** виджетов (`pie`/`world_map`/`dist_bar` → ECharts) —
|
||
отдельный косметический заход.
|
||
- Не трогаем эмодзи в тайтлах, секции-заголовки, языковой винегрет.
|
||
- Не трогаем генератор.
|
||
- Не правим код `create_dashboard.py` в рамках этой спеки — это реализация.
|
||
|
||
## Current state
|
||
|
||
Полный датасет (`make data` без `LIMIT`):
|
||
|
||
```
|
||
события: 1000 | визиты (click_id): 99 | пользователи (user_domain_id): 99 (1:1)
|
||
avg событий на визит: 10.10 | bounce (визиты с 1 событием): 4/99 = 4.0%
|
||
event_type: 100% pageview (других типов НЕТ)
|
||
page_url_path — воронка: /home 426 → /product_a 236 + /product_b 157 →
|
||
/cart 93 → /payment 53 → /confirmation 35 (~8% в конец)
|
||
geo_country: 40 | device_type: 2 (Mobile/Computer) | utm_source: 5 | utm_medium: 4
|
||
```
|
||
|
||
Текущие чарты и триаж по учебной ценности:
|
||
|
||
| Чарт | Тип | Вердикт |
|
||
|---|---|---|
|
||
| Total Events, Unique Users | big_number_total | оставить (KPI) |
|
||
| Unique Sessions | big_number_total | **убрать** (дубль Users) |
|
||
| Avg Events/Session | big_number_total | оставить, переименовать `/Visit` |
|
||
| Top Pages | dist_bar | **апгрейд в Funnel** (центральный учебный объект) |
|
||
| UTM Effectiveness | table | оставить; **выкинуть колонки purchases/add_to_cart** (всегда 0) |
|
||
| Geography Map | world_map | оставить (40 стран; тип модернизировать отдельно) |
|
||
| Traffic by Device | pie | оставить |
|
||
| Events by Hour | line | **проверить на пустоту**, иначе дропнуть |
|
||
| Data Quality Summary | dist_bar | оставить (⚠️ позже пересмотрено — см. ниже) |
|
||
|
||
## Options considered
|
||
|
||
- **A — честная воронка вовлечённости (стандартный KPI-strip).** 4 плитки
|
||
объём→охват→глубина→качество; различие user/session уходит в текст.
|
||
- **B — оставить обе плитки + Markdown-панель**, объясняющая равенство 1:1.
|
||
- **C — strip как форма модели:** воронка `Events → Visits → Users` вместо
|
||
бизнес-KPI; вырожденное 1:1 становится наглядным.
|
||
|
||
## Recommended approach
|
||
|
||
**A для KPI-полосы + идея C отдельным чартом-воронкой.** KPI-полоса —
|
||
стандартная и чистая (копируемый паттерн); иерархия модели подаётся отдельным
|
||
чартом, а `Top Pages` превращается в **Funnel** по страницам.
|
||
|
||
KPI-полоса:
|
||
|
||
```
|
||
📊 Total Events 1000 · 👤 Unique Users 99 · 📈 Avg Events/Visit 10.1 · 🎯 Conversion to /confirmation ~8%
|
||
```
|
||
|
||
## Rationale
|
||
|
||
- Конверсия через `page_url_path` честнее блёклого bounce (4%) и превращает
|
||
«всё pageview» из слабости в сильный урок: воронка реально затухает.
|
||
- Различие user/session учим текстом (`CONTEXT.md` + урок 6), а не двумя
|
||
одинаковыми числами — это убирает сигнал «баг».
|
||
- Стандартный KPI-strip ценнее для копирования, чем нестандартный вариант C;
|
||
но идею C сохраняем как отдельный наглядный чарт.
|
||
|
||
## Decisions and rejected alternatives
|
||
|
||
- **Данные: полный датасет** и для дашборда, и для урока 6. Срез `LIMIT=50`
|
||
отклонён: числа эталона разойдутся с тем, что менти получит у себя. Урок 6 —
|
||
убрать `LIMIT=50 make data`. (Срез `LIMIT` оставить в доке как явный приём
|
||
отладки, не как основной прогон. Замечание: ограничение `head -n` исходно
|
||
возникло, чтобы агентский контекст не давился полным jsonl — это ограничение
|
||
для агента, не для пайплайна.)
|
||
- **Убрать `Unique Sessions` из KPI** — дубль `Unique Users` при 1:1.
|
||
- **4-я KPI = Conversion**, не bounce. Bounce 4% отклонён как блёклый.
|
||
- **Top Pages → Funnel.** Воронка по страницам — главный учебный объект.
|
||
- **UTM-таблица: убрать колонки purchases/add_to_cart** — всегда 0, т.к.
|
||
`event_type` только `pageview`.
|
||
- **Гео/device/DQ не удаляем** — данные оправдывают ценность; модернизация их
|
||
типов виджетов — отдельно (non-goal здесь).
|
||
- **«Снести всё legacy»** на практике = убрать дубль-KPI + мёртвые колонки +
|
||
проверить Events by Hour. Инсайты не вырезаем — у большинства есть данные.
|
||
|
||
## Risks and mitigations
|
||
|
||
- **Funnel в Superset 4.1.2** — проверить доступность `viz_type` воронки до
|
||
реализации; при отсутствии — упорядоченный bar.
|
||
- **Формула Conversion** — определить точно и валидировать SQL (см. Open
|
||
questions).
|
||
- **Идемпотентность `create_dashboard.py`** при смене состава (чарты по
|
||
`slice_name`, дашборд по `slug`) — переименование `Unique Sessions`/`Top Pages`
|
||
не должно плодить дубли; проверить апдейт на месте.
|
||
|
||
## Validation
|
||
|
||
- KPI-полоса: 4 разных числа, дубля нет.
|
||
- `GET /api/v1/dashboard/<id>/datasets` → 200; DQ Summary без
|
||
`Columns missing in datasource`.
|
||
- Визуальная проверка через playwright-cli (скриншот в `/tmp`).
|
||
- Числа на дашборде сходятся с прямым запросом в ClickHouse.
|
||
|
||
## Documentation impact
|
||
|
||
- `docs/SUPERSET_DASHBOARD.md` — обновить раздел «Структура дашборда»
|
||
(состав чартов/KPI/фильтров) **в момент реализации**, чтобы пользовательская
|
||
дока не описывала несуществующие чарты.
|
||
- `docs/course/lessons/06_superset_bi.md` — убрать `LIMIT=50`, синхронизировать
|
||
состав чартов и скриншоты.
|
||
- `CONTEXT.md` — уже отражает user/visit/event; правок не требует.
|
||
|
||
## Open questions
|
||
|
||
- **Resolved: точная формула Conversion.** Используем page-funnel conversion:
|
||
`countIf(page_url_path = '/confirmation') / countIf(page_url_path = '/home')`.
|
||
На полном датасете это `35 / 426 = 8.2%`. Визитовая формула даёт 25.3% и
|
||
отклонена, потому что KPI должен совпадать с логикой chart `Page Funnel`.
|
||
- **Resolved: Events by Hour.** Оставляем: на полном датасете есть два часовых
|
||
бакета (`20 → 256`, `21 → 744`), график не пустой.
|
||
- **Resolved: Funnel в Superset 4.1.2.** MCP Context7 по `/apache/superset` не
|
||
дал точной строки `viz_type`; установленный Superset 4.1.2 проверен по bundled
|
||
example `Featured Charts/Funnel.yaml` и frontend assets. Используем
|
||
`viz_type: funnel`.
|
||
- **Требования к будущему генератору**, вытекающие из упёртостей этих данных
|
||
(разнообразие `event_type`, возвраты пользователей → sessions>users,
|
||
реалистичная воронка) — при возврате к генератору перенести в его
|
||
`KNOWN_ISSUES.md`.
|
||
|
||
## Пересмотр после приёмки (2026-06-06)
|
||
|
||
Решение «Data Quality Summary | dist_bar | оставить» **пересмотрено** при визуальной
|
||
проверке дашборда. Чарт суммировал `total_rows` по всем таблицам слоя, складывая
|
||
таблицы разного зерна (события 1000 + визиты 99 + пустые error-таблицы) в один столбец,
|
||
и рисовал убывающую «воронку потерь» (stg≈4250 → ods≈2198 → dds≈1099), которой в данных
|
||
нет. Для учебного стенда это активно вводит в заблуждение.
|
||
|
||
Чарт переделан в честный row-lineage **одного event-зерна**
|
||
(`browser_raw → browser_event → event → v_events_enriched`) и переименован в
|
||
`🧱 Rows by Layer (event)`. Теперь убывание настоящее: шаг `1050 → 1000` — это
|
||
дедупликация at-least-once потока по `event_id` в ODS (`ReplacingMergeTree`).
|
||
В `dm.dq_summary` добавлена строка для слоя `dm` (`sql/dm/40_dds_to_dm.sql`), чтобы
|
||
цепочка замыкалась до витрины. Настоящие сигналы качества (`rows_with_errors`,
|
||
`orphan_events`) на чистых демо-данных = 0 и остаются отдельными `check_name`.
|