From dd2c3cdaf919fc95edd1216bb2ad51ce86c9d1b2 Mon Sep 17 00:00:00 2001 From: Dmitry Dementev Date: Sat, 6 Jun 2026 14:49:18 +0300 Subject: [PATCH] =?UTF-8?q?docs(generator):=20=D0=B7=D0=B0=D1=84=D0=B8?= =?UTF-8?q?=D0=BA=D1=81=D0=B8=D1=80=D0=BE=D0=B2=D0=B0=D0=BD=20=D0=B4=D0=B5?= =?UTF-8?q?=D1=84=D0=B5=D0=BA=D1=82=20=D0=B3=D0=B5=D0=BD=D0=B5=D1=80=D0=B0?= =?UTF-8?q?=D1=82=D0=B8=D0=B2=D0=BD=D0=BE=D0=B9=20=D0=BC=D0=BE=D0=B4=D0=B5?= =?UTF-8?q?=D0=BB=D0=B8=20=D0=B8=20=D0=BF=D0=BB=D0=B0=D0=BD=20=D0=B4=D0=BE?= =?UTF-8?q?=D1=80=D0=B0=D0=B1=D0=BE=D1=82=D0=BA=D0=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Зачем: - генератор не влит и неочевидно почему; при возврате к нему легко переоткрывать заново вывод, что click_id на событие ломает семантику визита. - Что: - добавлен KNOWN_ISSUES.md: модель интенсивности ок, модель сущностей неверна, план перехода на иерархию пользователь -> сессия -> событие. - в шапку README.md добавлено предупреждение со ссылкой на KNOWN_ISSUES.md. - Проверка: - прочитать generator/KNOWN_ISSUES.md и сверить с generate_batch() в generator.py. --- generator/KNOWN_ISSUES.md | 120 ++++++++++++++++++++++++++++++++++++++ generator/README.md | 5 ++ 2 files changed, 125 insertions(+) create mode 100644 generator/KNOWN_ISSUES.md diff --git a/generator/KNOWN_ISSUES.md b/generator/KNOWN_ISSUES.md new file mode 100644 index 0000000..87f406f --- /dev/null +++ b/generator/KNOWN_ISSUES.md @@ -0,0 +1,120 @@ +# Генератор: известные проблемы и контекст для доработки + +> **Статус (2026-06-06):** ветка `feature/data-generator` **не влита** в `main`. +> Генератор **не используется** как источник данных для витрин DM/дашборда. +> Витрины и Superset-дашборд строятся на **статическом сиде** (`data/*.jsonl`). +> Причина — ниже. Это не «сырой код по мелочи», а концептуальный дефект +> генеративной модели, который надо осознанно чинить перед использованием. + +Заметка написана при дизайне Superset-дашборда (ветка +`docs/advanced-clickstream-course`): разбирались, почему на дашборде +`Unique Users == Unique Sessions`, и по ходу вскрылось, что генератор +семантику сессии не чинит, а ломает сильнее. Чтобы при возвращении к +генератору не переоткрывать это заново — фиксирую понимание целиком. + +## TL;DR + +- **Модель интенсивности потока (сколько событий и когда) — нормальная.** + Poisson по тикам + дневной коэффициент + jitter. Её можно оставить. +- **Генеративная модель сущностей (кто, какая сессия, какое событие) — неверная.** + На каждое событие штампуется свежий `click_id`, а атрибуты копируются из + случайной сид-строки. Это уничтожает понятие визита/сессии. +- **Вывод:** прежде чем использовать генератор как источник, переделать именно + модель сущностей (иерархия пользователь → сессия → событие). Математику + интенсивности трогать не обязательно. + +## Доменная модель (как задумано в DDL) + +В этой модели сущности связаны иерархически: + +``` +user_domain_id (постоянный пользователь, cookie) + └── click_id (визит/сессия — envelope из 1..N событий с общим device/geo) + └── event_id (отдельное событие: pageview, click, purchase ...) +``` + +- `click_id` — это **визит**, а не одиночный клик: в статическом сиде один + `click_id` честно группирует 1..7 событий (`dds.click` = «контекст сессии + пользователя», см. `sql/ddl/dds/30_dds.sql`). +- `user_domain_id` живёт в `device_events`, привязан к `click_id`. + +## Что генератор делает правильно + +Файл `generator/generator.py`, `_calculate_events_count()` (стр. ~232–260): + +- **Poisson-процесс** прихода событий: λ на минуту → λ на тик → розыгрыш Пуассона. +- **Дневной коэффициент** `_hour_factor()`: день (9–18) ×1.2, ночь (0–5) ×0.7. +- **Jitter** ±`GEN_JITTER_PCT`% и границы `min/max_events_per_tick`. + +Это адекватная модель *интенсивности во времени*. Претензий к ней нет. + +## Корневой дефект: модель сущностей в `generate_batch()` + +Файл `generator/generator.py`, `generate_batch()` (стр. ~262–330). На каждое +событие в батче: + +```python +base_browser = self.rng.choice(self.dictionary.browser_events) # случайная сид-строка +... +new_event_id = self._new_uuid() +new_click_id = self._new_uuid() # СВЕЖИЙ click_id на КАЖДОЕ событие +new_timestamp = self._current_timestamp() # ~now() для всех событий батча +... +device_event = {**base_device, "click_id": new_click_id} # user_domain_id наследуется из сида +``` + +Три уровня иерархии (пользователь → сессия → событие) схлопываются в плоскую +выборку: + +1. **`click_id` свежий на каждое событие** → один визит = одно событие. Визит как + группа из нескольких событий **никогда не формируется**. +2. **`user_domain_id` переиспользуется** из сид-пула (~99 значений), но без + привязки к сессиям — события одного пользователя разлетаются отдельными + `click_id`. Иерархию «пользователь → его сессии → события сессии» из потока + **не восстановить**. +3. **Время** у всех событий батча ≈ `now()` — даже временно́й структуры визита + (последовательность событий внутри сессии) нет. + +### Что это даёт в данных + +| | Статический сид (`data/*.jsonl`) | Поток из генератора | +|---|---|---| +| `click_id` | визит-envelope (1..7 событий) | свежий на событие → `click_id` ≈ событие | +| `user_domain_id` | 1:1 с `click_id` | переиспользуется (потолок ~99) | +| Семантика | `Sessions == Users` (вырождено по пользователю) | `Sessions == Events` (сессия = одно событие) | + +Парадокс: **статический сид как учебная основа лучше**, потому что на нём +`click_id` несёт осмысленную семантику визита. Генератор её ломает. + +## Что перепроверить и переделать перед использованием + +Чинить нужно **генеративную модель сущностей**, а не математику интенсивности: + +1. **Иерархическая генерация вместо плоской выборки:** + - поддерживать популяцию пользователей с *постоянным* `user_domain_id`; + - пользователь со временем открывает 1..N **сессий** (новый `click_id` на + сессию, с межсессионными паузами — модель «вернувшегося пользователя»); + - сессия порождает последовательность из 1..M **событий**, разделяющих один + `click_id` и общий device/geo, упорядоченных по времени (правдоподобный путь + по страницам). +2. **Распределения, требующие проверки математики:** + - события на сессию (например, geometric/NB — длина визита); + - сессии на пользователя за период (возвраты); + - межсессионные интервалы (тайм-аут неактивности как граница сессии). +3. **Время событий** внутри сессии должно расти монотонно, а не быть `now()` для + всего батча. +4. **`event_id`/`click_id`** уже всегда новые (`uuid4`) — при иерархической + модели `click_id` должен переиспользоваться внутри сессии, а не на каждое + событие (см. оговорку в `README.md`, раздел State Recovery). + +После такой переделки на потоке естественно получится здоровая пирамида +`users < sessions < events`, и дашборд сможет показывать разницу +«пользователь vs сессия» честными числами. + +## Ссылки + +- Код: `generator/generator.py` — `_calculate_events_count()` (стр. ~232–260), + `generate_batch()` (стр. ~262–330). +- Доменная модель: `sql/ddl/dds/30_dds.sql`, `sql/ddl/dm/40_dm.sql`. +- Контекст обсуждения: ветка `docs/advanced-clickstream-course`, дизайн + Superset-дашборда (урок 6 курса). diff --git a/generator/README.md b/generator/README.md index 31185ca..66ee32c 100644 --- a/generator/README.md +++ b/generator/README.md @@ -1,5 +1,10 @@ # Генератор событий (MVP rev5) +> ⚠️ **Перед использованием как источник витрин — прочитать +> [KNOWN_ISSUES.md](./KNOWN_ISSUES.md).** Генеративная модель сущностей неверна +> (свежий `click_id` на каждое событие ломает семантику визита/сессии); ветка +> не влита в `main` именно поэтому. Математику интенсивности это не затрагивает. + Автономный генератор событий для Kafka с режимом `steady-stream`. ## Архитектура