- Зачем: - визит после восстановления не должен менять браузер и источники перехода внутри одного 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.
9.7 KiB
Генератор: известные проблемы и контекст для доработки
Статус (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(): день (9–18) ×1.2, ночь (0–5) ×0.7. - Jitter ±
GEN_JITTER_PCT% и границыmin/max_events_per_tick.
Это адекватная модель интенсивности во времени. Претензий к ней нет.
Исторический корневой дефект: старая модель сущностей
До переработки 2026-06-11 прежняя реализация в монолитном
generator/generator.py на каждое событие в батче делала примерно следующее:
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 наследуется из сида
Три уровня иерархии (пользователь → сессия → событие) схлопываются в плоскую выборку:
click_idсвежий на каждое событие → один визит = одно событие. Визит как группа из нескольких событий никогда не формируется.user_domain_idпереиспользуется из сид-пула (~99 значений), но без привязки к сессиям — события одного пользователя разлетаются отдельнымиclick_id. Иерархию «пользователь → его сессии → события сессии» из потока не восстановить.- Время у всех событий батча ≈
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 нёс осмысленную семантику визита, а старый генератор её
ломал.
Что сделано в новой модели
Чинили именно генеративную модель сущностей, не переписывая математику интенсивности:
- Иерархическая генерация вместо плоской выборки:
- есть ограниченная популяция пользователей с постоянным
user_domain_id; - пользователь со временем открывает 1..N визитов;
- визит порождает последовательность событий с одним
click_id, общим device/geo-контекстом и путём по страницам.
- есть ограниченная популяция пользователей с постоянным
- Тиковый слой:
- событийный бюджет тика превращается в рождения визитов через среднюю длину визита;
- активные визиты живут между тиками;
- выпускаются только события, у которых наступила запланированная метка времени.
- Время событий внутри визита строго растёт и не прилипает к одному
now()для всего батча. - Состояние сохраняет популяцию, активные визиты, накопленный бюджет рождения визитов, номер тика и состояние ГПСЧ.
После этой переделки поток на длинном окне и при штатных параметрах даёт
здоровую пирамиду 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 курса).