Files
ddadmin e2d06841de fix(generator): сохранена фактура визита при восстановлении
- Зачем:
  - визит после восстановления не должен менять браузер и источники перехода внутри одного click_id.
- Что:
  - добавлен base_click_id в state v3 для восстановления донора фактуры.
  - исправлено восстановление timestamp offset без потери микросекунд.
  - расширены тесты и стыковая проверка browser/source и device/os/geo.
- Проверка:
  - uv run --with-requirements generator/requirements.txt pytest generator/tests -q.
  - bash -n scripts/check_generated_analytics.sh.
  - git diff --cached --check.
2026-07-04 21:46:23 +03:00

140 lines
9.7 KiB
Markdown
Raw Permalink 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-11):** исторический дефект старой плоской генерации закрыт
> для режима `steady-stream`. Генератор строит визиты с общим `click_id`,
> монотонным временем событий, путём по страницам воронки, популяцией
> возвращающихся пользователей и состоянием для активных визитов.
>
> Эта заметка больше не является предупреждением «генератор концептуально
> сломан». Она оставлена как учебный разбор старого дефекта и как место для
> небольших остаточных ограничений.
Заметка написана при дизайне Superset-дашборда (ветка
`docs/advanced-clickstream-course`): разбирались, почему на дашборде
`Unique Users == Unique Sessions`, и по ходу вскрылось, что старая реализация
генератора на тот момент семантику сессии не чинила, а ломала сильнее. Разбор
оставлен, чтобы не переоткрывать этот дефект заново и показать, почему новая
модель устроена иначе.
## TL;DR
- **Модель интенсивности потока (сколько событий и когда) — нормальная.**
Poisson по тикам + дневной коэффициент + jitter сохранены.
- **Генеративная модель сущностей переписана.** Поток больше не штампует свежий
`click_id` на каждое событие: визит живёт несколько событий, пользователь
может вернуться в новом визите после кулдауна.
- **Вывод:** старый дефект `Sessions == Events` не считается актуальным
блокером. Дальше генератор можно улучшать уже как работающую учебную модель,
а не как концептуально сломанный источник.
## Текущие ограничения
- Тип события остаётся `pageview` для всех событий. Это осознанное ограничение:
текущий дашборд и уроки строят воронку по `page_url_path`, а не по
`event_type`.
- Device/geo-профиль пользователя стабилен между визитами. Смену устройства
генератор пока не моделирует.
- При долгом простое больше 30 минут активный визит закрывается без досылки
остатка. Популяция пользователей при этом сохраняется.
## Доменная модель (как задумано в 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`.
Это адекватная модель *интенсивности во времени*. Претензий к ней нет.
## Исторический корневой дефект: старая модель сущностей
До переработки 2026-06-11 прежняя реализация в монолитном
`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`, общим
device/geo-контекстом и путём по страницам.
2. **Тиковый слой:**
- событийный бюджет тика превращается в рождения визитов через среднюю длину
визита;
- активные визиты живут между тиками;
- выпускаются только события, у которых наступила запланированная метка
времени.
3. **Время событий** внутри визита строго растёт и не прилипает к одному
`now()` для всего батча.
4. **Состояние** сохраняет популяцию, активные визиты, накопленный бюджет
рождения визитов, номер тика и состояние ГПСЧ.
После этой переделки поток на длинном окне и при штатных параметрах даёт
здоровую пирамиду `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 курса).