Files
clickstream-ch-kafka-supers…/generator/KNOWN_ISSUES.md
T
Dmitry Dementiev 642789d484 refactor(generator): разнесён сервис генератора по src-пакету
- Зачем:
  - перед активными визитами нужно отделить генеративную модель от Kafka, состояния и сервисного цикла.
- Что:
  - перенесены модули генератора в пакет `src/clickstream_generator`.
  - `generator.py` оставлен фасадом и точкой входа с совместимыми импортами.
  - обновлены Dockerfile, тесты, README, спека и issue 02.5.
- Проверка:
  - `docker build -t generator:test generator`.
  - `docker run --rm -v /home/dmitry/sources/clickstream-ch-kafka-superset-demo:/workspace -w /workspace/generator generator:test pytest tests/ -q`.
  - `python -m py_compile generator.py src/clickstream_generator/*.py` в Docker.
2026-06-11 15:00:50 +03:00

133 lines
9.9 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Генератор: известные проблемы и контекст для доработки
> **Статус (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/src/clickstream_generator/intensity.py`,
`calculate_events_count()`:
- **Poisson-процесс** прихода событий: λ на минуту → λ на тик → розыгрыш Пуассона.
- **Дневной коэффициент** `_hour_factor()`: день (918) ×1.2, ночь (05) ×0.7.
- **Jitter** ±`GEN_JITTER_PCT`% и границы `min/max_events_per_tick`.
Это адекватная модель *интенсивности во времени*. Претензий к ней нет.
## Исторический корневой дефект: модель сущностей в `generate_batch()`
До среза от 2026-06-11 прежняя реализация `generate_batch()` в монолитном
`generator/generator.py` на каждое событие в батче делала примерно следующее:
```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/src/clickstream_generator/intensity.py`
`calculate_events_count()`; `generator/src/clickstream_generator/generation.py`
`EventGenerator.generate_batch()`.
- Доменная модель: `sql/ddl/dds/30_dds.sql`, `sql/ddl/dm/40_dm.sql`.
- Контекст обсуждения: ветка `docs/advanced-clickstream-course`, дизайн
Superset-дашборда (урок 6 курса).