# Генератор: известные проблемы и контекст для доработки > **Статус (2026-06-06):** ветка `feature/data-generator` **не влита** в `main`. > Генератор **не используется** как источник данных для витрин DM/дашборда. > Витрины и Superset-дашборд строятся на **статическом сиде** (`data/*.jsonl`). > Причина — ниже. Это не «сырой код по мелочи», а концептуальный дефект > генеративной модели, который надо осознанно чинить перед использованием. > > **Обновление (2026-06-11):** первый срез модели визита реализован в > `generate_batch()`: один публичный вызов строит один `click_id` с несколькими > событиями, марковским путём по страницам и запланированными строго растущими > метками времени. Остальные пункты ниже остаются полезным историческим > контекстом и списком следующих шагов: популяция возвращающихся пользователей, > межсессионные паузы и полноценное состояние активных визитов ещё не закрыты. Заметка написана при дизайне 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..27 событий, медиана 10 (`dds.click` = «контекст сессии пользователя», см. `sql/ddl/dds/30_dds.sql`; точные цифры — профиль сид-датасета в `CONTEXT.md`, ранняя оценка «1..7» была замером по срезу файла). - `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()` До среза от 2026-06-11 файл `generator/generator.py`, `generate_batch()` на каждое событие в батче делал примерно следующее: ```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..27 событий, медиана 10) | свежий на событие → `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 курса).