- Зачем: - генератор должен создавать учебно полезный визит с общим click_id, правдоподобным путём и честным тиковым бюджетом событий. - Что: - добавлена марковская цепочка страниц, запланированные метки времени и потолок GEN_MAX_SESSION_EVENTS. - добавлен набор тикового батча из нескольких визитов до рассчитанного бюджета событий. - обновлены тесты, README, KNOWN_ISSUES и статусы задач 01/02. - Проверка: - uv run --with pytest --with-requirements generator/requirements.txt pytest generator/tests -q
131 lines
9.7 KiB
Markdown
131 lines
9.7 KiB
Markdown
# Генератор: известные проблемы и контекст для доработки
|
||
|
||
> **Статус (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 курса).
|