Files
clickstream-ch-kafka-supers…/generator/KNOWN_ISSUES.md
T
ddadminandDmitry Dementiev dd2c3cdaf9 docs(generator): зафиксирован дефект генеративной модели и план доработки
- Зачем:
  - генератор не влит и неочевидно почему; при возврате к нему легко
    переоткрывать заново вывод, что click_id на событие ломает семантику визита.
- Что:
  - добавлен KNOWN_ISSUES.md: модель интенсивности ок, модель сущностей неверна,
    план перехода на иерархию пользователь -> сессия -> событие.
  - в шапку README.md добавлено предупреждение со ссылкой на KNOWN_ISSUES.md.
- Проверка:
  - прочитать generator/KNOWN_ISSUES.md и сверить с generate_batch() в generator.py.
2026-06-09 17:27:17 +03:00

8.6 KiB
Raw Blame History

Генератор: известные проблемы и контекст для доработки

Статус (2026-06-06): ветка feature/data-generator не влита в main. Генератор не используется как источник данных для витрин DM/дашборда. Витрины и Superset-дашборд строятся на статическом сиде (data/*.jsonl). Причина — ниже. Это не «сырой код по мелочи», а концептуальный дефект генеративной модели, который надо осознанно чинить перед использованием.

Заметка написана при дизайне 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..7 событий (dds.click = «контекст сессии пользователя», см. sql/ddl/dds/30_dds.sql).
  • user_domain_id живёт в device_events, привязан к click_id.

Что генератор делает правильно

Файл generator/generator.py, _calculate_events_count() (стр. ~232260):

  • Poisson-процесс прихода событий: λ на минуту → λ на тик → розыгрыш Пуассона.
  • Дневной коэффициент _hour_factor(): день (918) ×1.2, ночь (05) ×0.7.
  • Jitter ±GEN_JITTER_PCT% и границы min/max_events_per_tick.

Это адекватная модель интенсивности во времени. Претензий к ней нет.

Корневой дефект: модель сущностей в generate_batch()

Файл generator/generator.py, generate_batch() (стр. ~262–330). На каждое событие в батче:

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..7 событий) свежий на событие → 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() (стр. ~232260), generate_batch() (стр. ~262330).
  • Доменная модель: sql/ddl/dds/30_dds.sql, sql/ddl/dm/40_dm.sql.
  • Контекст обсуждения: ветка docs/advanced-clickstream-course, дизайн Superset-дашборда (урок 6 курса).