feat(generator): подключена новая модель к steady-stream сервису

- Зачем:
  - после калибровки потока и state v2 генератор нужно принять как рабочий steady-stream источник, а не как исторически сломанный прототип.
- Что:
  - добавлен сервисный тест multi-event визита с мок-публикацией во все четыре Kafka-топика.
  - compose позволяет переопределять демо-параметры генератора без правки файла, сохраняя внутренние контейнерные адреса.
  - README, OPERATIONS, KNOWN_ISSUES и карточка задачи синхронизированы с новой моделью и state v2.
- Проверка:
  - uv run --with-requirements generator/requirements.txt pytest generator/tests -q.
  - git diff --check.
  - GEN_STATE_RESET=true GEN_POPULATION_MAX=123 docker compose config.
This commit is contained in:
Dmitry Dementiev
2026-06-11 18:26:39 +03:00
parent 4a08465f7d
commit 6c4e0f4d16
6 changed files with 220 additions and 79 deletions
+53 -46
View File
@@ -1,35 +1,41 @@
# Генератор: известные проблемы и контекст для доработки
> **Статус (2026-06-06):** ветка `feature/data-generator` **не влита** в `main`.
> Генератор **не используется** как источник данных для витрин DM/дашборда.
> Витрины и Superset-дашборд строятся на **статическом сиде** (`data/*.jsonl`).
> Причина — ниже. Это не «сырой код по мелочи», а концептуальный дефект
> генеративной модели, который надо осознанно чинить перед использованием.
> **Статус (2026-06-11):** исторический дефект старой плоской генерации закрыт
> для режима `steady-stream`. Генератор строит визиты с общим `click_id`,
> монотонным временем событий, путём по страницам воронки, популяцией
> возвращающихся пользователей и состоянием v2 для активных визитов.
>
> **Обновление (2026-06-11):** первый срез модели визита реализован в
> `generate_batch()`: один публичный вызов строит один `click_id` с несколькими
> событиями, марковским путём по страницам и запланированными строго растущими
> метками времени. Остальные пункты ниже остаются полезным историческим
> контекстом и списком следующих шагов: популяция возвращающихся пользователей,
> межсессионные паузы и полноценное состояние активных визитов ещё не закрыты.
> Эта заметка больше не является предупреждением «генератор концептуально
> сломан». Она оставлена как учебный разбор старого дефекта и как место для
> небольших остаточных ограничений.
Заметка написана при дизайне Superset-дашборда (ветка
`docs/advanced-clickstream-course`): разбирались, почему на дашборде
`Unique Users == Unique Sessions`, и по ходу вскрылось, что генератор
семантику сессии не чинит, а ломает сильнее. Чтобы при возвращении к
генератору не переоткрывать это заново — фиксирую понимание целиком.
`Unique Users == Unique Sessions`, и по ходу вскрылось, что старая реализация
генератора на тот момент семантику сессии не чинила, а ломала сильнее. Разбор
оставлен, чтобы не переоткрывать этот дефект заново и показать, почему новая
модель устроена иначе.
## TL;DR
- **Модель интенсивности потока (сколько событий и когда) — нормальная.**
Poisson по тикам + дневной коэффициент + jitter. Её можно оставить.
- **Генеративная модель сущностей исправляется по шагам.** Срез одного визита
уже не штампует свежий `click_id` на каждое событие, но полная иерархия
пользователь → несколько визитов → события ещё требует популяции
возвращающихся пользователей.
- **Вывод:** прежде чем использовать генератор как полноценный источник,
доделать оставшиеся уровни модели сущностей. Математику интенсивности
трогать не обязательно.
Poisson по тикам + дневной коэффициент + jitter сохранены.
- **Генеративная модель сущностей переписана.** Поток больше не штампует свежий
`click_id` на каждое событие: визит живёт несколько событий, пользователь
может вернуться в новом визите после кулдауна.
- **Вывод:** старый дефект `Sessions == Events` не считается актуальным
блокером. Дальше генератор можно улучшать уже как работающую учебную модель,
а не как концептуально сломанный источник.
## Текущие ограничения
- Тип события остаётся `pageview` для всех событий. Это осознанное ограничение:
текущий дашборд и уроки строят воронку по `page_url_path`, а не по
`event_type`.
- Device/geo-профиль пользователя стабилен между визитами. Смену устройства
генератор пока не моделирует.
- При долгом простое больше 30 минут активный визит закрывается без досылки
остатка. Популяция пользователей при этом сохраняется.
## Доменная модель (как задумано в DDL)
@@ -59,9 +65,9 @@ user_domain_id (постоянный пользователь, cookie)
Это адекватная модель *интенсивности во времени*. Претензий к ней нет.
## Исторический корневой дефект: модель сущностей в `generate_batch()`
## Исторический корневой дефект: старая модель сущностей
До среза от 2026-06-11 прежняя реализация `generate_batch()` в монолитном
До переработки 2026-06-11 прежняя реализация в монолитном
`generator/generator.py` на каждое событие в батче делала примерно следующее:
```python
@@ -94,33 +100,34 @@ device_event = {**base_device, "click_id": new_click_id} # user_domain_i
| `user_domain_id` | 1:1 с `click_id` | переиспользуется (потолок ~99) |
| Семантика | `Sessions == Users` (вырождено по пользователю) | `Sessions == Events` (сессия = одно событие) |
Парадокс: **статический сид как учебная основа лучше**, потому что на нём
`click_id` несёт осмысленную семантику визита. Генератор её ломает.
Парадокс был таким: **статический сид как учебная основа был лучше**, потому что
на нём `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).
- есть ограниченная популяция пользователей с постоянным `user_domain_id`;
- пользователь со временем открывает 1..N визитов;
- визит порождает последовательность событий с одним `click_id`, общим
device/geo-контекстом и путём по страницам.
2. **Тиковый слой:**
- событийный бюджет тика превращается в рождения визитов через среднюю длину
визита;
- активные визиты живут между тиками;
- выпускаются только события, у которых наступила запланированная метка
времени.
3. **Время событий** внутри визита строго растёт и не прилипает к одному
`now()` для всего батча.
4. **Состояние v2** сохраняет популяцию, активные визиты, накопленный бюджет
рождения визитов, номер тика и состояние ГПСЧ.
После такой переделки на потоке естественно получится здоровая пирамида
`users < sessions < events`, и дашборд сможет показывать разницу
«пользователь vs сессия» честными числами.
После этой переделки поток на длинном окне и при штатных параметрах даёт
здоровую пирамиду `users < sessions < events`, и дашборд может показывать
разницу «пользователь vs сессия» честными числами.
## Ссылки