docs(generator): зафиксирован дефект генеративной модели и план доработки

- Зачем:
  - генератор не влит и неочевидно почему; при возврате к нему легко
    переоткрывать заново вывод, что click_id на событие ломает семантику визита.
- Что:
  - добавлен KNOWN_ISSUES.md: модель интенсивности ок, модель сущностей неверна,
    план перехода на иерархию пользователь -> сессия -> событие.
  - в шапку README.md добавлено предупреждение со ссылкой на KNOWN_ISSUES.md.
- Проверка:
  - прочитать generator/KNOWN_ISSUES.md и сверить с generate_batch() в generator.py.
This commit is contained in:
2026-06-09 17:27:17 +03:00
committed by Dmitry Dementiev
parent 40633602f5
commit dd2c3cdaf9
2 changed files with 125 additions and 0 deletions
+120
View File
@@ -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()` (стр. ~232260):
- **Poisson-процесс** прихода событий: λ на минуту → λ на тик → розыгрыш Пуассона.
- **Дневной коэффициент** `_hour_factor()`: день (918) ×1.2, ночь (05) ×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()` (стр. ~232260),
`generate_batch()` (стр. ~262330).
- Доменная модель: `sql/ddl/dds/30_dds.sql`, `sql/ddl/dm/40_dm.sql`.
- Контекст обсуждения: ветка `docs/advanced-clickstream-course`, дизайн
Superset-дашборда (урок 6 курса).
+5
View File
@@ -1,5 +1,10 @@
# Генератор событий (MVP rev5)
> ⚠️ **Перед использованием как источник витрин — прочитать
> [KNOWN_ISSUES.md](./KNOWN_ISSUES.md).** Генеративная модель сущностей неверна
> (свежий `click_id` на каждое событие ломает семантику визита/сессии); ветка
> не влита в `main` именно поэтому. Математику интенсивности это не затрагивает.
Автономный генератор событий для Kafka с режимом `steady-stream`.
## Архитектура