docs(generator): добавлена мат-спека модели и исправлен профиль сида
- Зачем:
- закрыть Open questions спеки формы доработки перед передачей на
реализацию; адверсариальное ревью показало, что прежние ориентиры
(популяция/паузы/интенсивность) взаимно несовместимы, а документы
опираются на неверный факт о сиде («1..7 событий на визит»).
- Что:
- добавлена docs/specs/2026-06-10-generator-math-model.md: марковская
цепочка по страницам, формула связи «популяция-интенсивность-пауза»
(λ по умолчанию 30/мин), кулдаун возврата, правило 30 минут на рестарт,
критерии приёмки.
- в CONTEXT.md добавлен профиль сид-датасета (полный замер: длины визитов
1..27, медиана 10, конверсия 25%, петли и события после /confirmation)
и исправлено ложное «разброс времени внутри click_id <= 1 мин».
- исправлен факт «1..7 событий» в KNOWN_ISSUES.md и ADR-0004; критерии
Validation спеки формы доработки приведены к фактам сида.
- Проверка:
- перекрёстные ссылки между спеками/ADR/CONTEXT.md открываются; цифры
профиля сида воспроизводятся скриптом подсчёта по полным data/*.jsonl.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Fable 5
parent
bce1ded9c4
commit
6efa031023
+24
-2
@@ -22,8 +22,11 @@
|
|||||||
`domain_sessionid`). Поэтому в этой модели **сессия ≡ визит ≡ `click_id`** —
|
`domain_sessionid`). Поэтому в этой модели **сессия ≡ визит ≡ `click_id`** —
|
||||||
группа событий под одним идентификатором, а не окно активности по тайм-ауту.
|
группа событий под одним идентификатором, а не окно активности по тайм-ауту.
|
||||||
Если в будущем понадобится «настоящая» сессия по 30-минутному окну неактивности —
|
Если в будущем понадобится «настоящая» сессия по 30-минутному окну неактивности —
|
||||||
это отдельная производная над `user_domain_id` + `event_ts`; на текущих данных
|
это отдельная производная над `user_domain_id` + `event_ts`, и на сиде она дала
|
||||||
она дала бы те же группы (разброс времени внутри `click_id` ≤ 1 мин).
|
бы **другие** группы: в полном сиде 11 внутрисессионных пауз превышают 30 минут,
|
||||||
|
а разброс времени внутри `click_id` доходит до 49 минут (см. профиль
|
||||||
|
сид-датасета ниже; ранее здесь ошибочно значилось «разброс ≤ 1 мин» — это был
|
||||||
|
замер по малому срезу файла).
|
||||||
|
|
||||||
«Сессию» и «визит» используем как синонимы; в UI предпочитаем «визит», когда важно
|
«Сессию» и «визит» используем как синонимы; в UI предпочитаем «визит», когда важно
|
||||||
подчеркнуть, что это не сессия-по-тайм-ауту.
|
подчеркнуть, что это не сессия-по-тайм-ауту.
|
||||||
@@ -81,6 +84,25 @@ user_domain_id (пользователь, постоянный)
|
|||||||
усугубляет: он штампует свежий `click_id` на каждое событие, и `click_id`
|
усугубляет: он штампует свежий `click_id` на каждое событие, и `click_id`
|
||||||
вырождается в «событие». См. `generator/KNOWN_ISSUES.md` на той ветке.
|
вырождается в «событие». См. `generator/KNOWN_ISSUES.md` на той ветке.
|
||||||
|
|
||||||
|
## Профиль сид-датасета (измерено 2026-06-10, полные файлы)
|
||||||
|
|
||||||
|
Опорные цифры о статическом сиде (`data/*.jsonl`) — для калибровки генератора
|
||||||
|
и проверки гипотез. Измерено по **полным** файлам (1000 событий); внимание:
|
||||||
|
ранние оценки «1..7 событий на визит» делались по малому срезу и неверны.
|
||||||
|
|
||||||
|
- **Объём:** 1000 событий, 99 визитов (`click_id`), 99 пользователей (1:1).
|
||||||
|
- **Длина визита:** 1..27 событий, медиана 10, среднее 10.1.
|
||||||
|
- **Паузы между событиями визита:** медиана ~20 с, p95 ≈ 17 мин,
|
||||||
|
максимум ≈ 40 мин; пауз длиннее 30 минут — 11 штук. Разброс времени внутри
|
||||||
|
одного `click_id` — до 49 минут.
|
||||||
|
- **Воронка:** 25% визитов достигают `/confirmation`. Пути — с петлями:
|
||||||
|
возвраты на `/home`, просмотр обоих товаров; у 21 из 25 «купивших» визитов
|
||||||
|
есть события **после** `/confirmation`, у 9 — `/confirmation` встречается
|
||||||
|
дважды. То есть `/confirmation` — последний *шаг воронки*, но не обязательно
|
||||||
|
последнее *событие визита*.
|
||||||
|
- **Страницы (по событиям):** `/home` 426, `/product_a` 236, `/product_b` 157,
|
||||||
|
`/cart` 93, `/payment` 53, `/confirmation` 35. `event_type` — 100% `pageview`.
|
||||||
|
|
||||||
## Слои данных
|
## Слои данных
|
||||||
|
|
||||||
`STG → ODS → DDS → DM` — см. [`docs/ARCHITECTURE.md`](./docs/ARCHITECTURE.md).
|
`STG → ODS → DDS → DM` — см. [`docs/ARCHITECTURE.md`](./docs/ARCHITECTURE.md).
|
||||||
|
|||||||
@@ -21,7 +21,9 @@ jitter) сохраняется. Режим `bootstrap` (статический
|
|||||||
|
|
||||||
Проект сменил назначение на учебный стенд с курсом. Базовый курс стоит на
|
Проект сменил назначение на учебный стенд с курсом. Базовый курс стоит на
|
||||||
статическом сиде, и это осознанно: сид **честен внутри визита** (`click_id`
|
статическом сиде, и это осознанно: сид **честен внутри визита** (`click_id`
|
||||||
группирует 1..7 событий — настоящая воронка). Но у сида два потолка, которые
|
группирует несколько событий — настоящая воронка; по полному замеру 2026-06-10 —
|
||||||
|
1..27 событий, медиана 10, см. профиль сид-датасета в `CONTEXT.md`). Но у сида
|
||||||
|
два потолка, которые
|
||||||
сид принципиально не закрывает:
|
сид принципиально не закрывает:
|
||||||
|
|
||||||
- **`users == sessions`** — каждый пользователь имеет ровно один `click_id` (1:1),
|
- **`users == sessions`** — каждый пользователь имеет ровно один `click_id` (1:1),
|
||||||
|
|||||||
@@ -49,9 +49,10 @@
|
|||||||
общим device/geo), а не штампуется на каждое событие.
|
общим device/geo), а не штампуется на каждое событие.
|
||||||
- **Время событий внутри сессии монотонно растёт** (правдоподобный путь по
|
- **Время событий внутри сессии монотонно растёт** (правдоподобный путь по
|
||||||
страницам), а не равно `now()` для всего батча.
|
страницам), а не равно `now()` для всего батча.
|
||||||
- **Правдоподобная воронка** по `page_url_path`: затухающая последовательность
|
- **Правдоподобная воронка** по `page_url_path`: путь по страницам
|
||||||
страниц (`/home → … → /confirmation`), где до терминального шага доходит
|
(`/home → … → /confirmation`), где число визитов затухает от шага к шагу,
|
||||||
правдоподобно малая доля — согласованно с дашбордом и сидом.
|
а до последнего шага воронки доходит меньшинство (в сиде ~25%) —
|
||||||
|
согласованно с дашбордом и сидом.
|
||||||
- Сохранена модель интенсивности (жизнеподобные колебания нагрузки).
|
- Сохранена модель интенсивности (жизнеподобные колебания нагрузки).
|
||||||
- Встраивается в курс как **урок 7 «Система живёт»**, сосуществует с bootstrap-сидом
|
- Встраивается в курс как **урок 7 «Система живёт»**, сосуществует с bootstrap-сидом
|
||||||
и уроками 0–6.
|
и уроками 0–6.
|
||||||
@@ -98,8 +99,9 @@
|
|||||||
3. **События сессии.** Сессия порождает 1..M событий, разделяющих `click_id`,
|
3. **События сессии.** Сессия порождает 1..M событий, разделяющих `click_id`,
|
||||||
**упорядоченных по монотонно растущему времени**, образующих правдоподобный
|
**упорядоченных по монотонно растущему времени**, образующих правдоподобный
|
||||||
путь по воронке страниц (`page_url_path`: `/home → … → /confirmation`), где до
|
путь по воронке страниц (`page_url_path`: `/home → … → /confirmation`), где до
|
||||||
терминального шага доходит малая доля. (Вводить ли разнообразие `event_type`
|
последнего шага воронки доходит меньшинство визитов (~25%, как в сиде).
|
||||||
сверх `pageview` — открытый вопрос, см. ниже.)
|
(Вводить ли разнообразие `event_type` сверх `pageview` — открытый вопрос,
|
||||||
|
см. ниже.)
|
||||||
4. **Интенсивность.** Поверх иерархии работает сохранённая модель интенсивности —
|
4. **Интенсивность.** Поверх иерархии работает сохранённая модель интенсивности —
|
||||||
сколько событий и когда (Poisson + дневной коэффициент + jitter).
|
сколько событий и когда (Poisson + дневной коэффициент + jitter).
|
||||||
|
|
||||||
@@ -136,11 +138,14 @@
|
|||||||
|
|
||||||
- `uniqExact(user_domain_id) < uniqExact(click_id) < count(event_id)` —
|
- `uniqExact(user_domain_id) < uniqExact(click_id) < count(event_id)` —
|
||||||
пирамида не вырождена (есть возвраты и мультисобытийные сессии).
|
пирамида не вырождена (есть возвраты и мультисобытийные сессии).
|
||||||
- Внутри одного `click_id` — несколько событий с **возрастающим** `event_ts`.
|
- У `click_id` с несколькими событиями `event_ts` строго **возрастает**;
|
||||||
|
мультисобытийные визиты составляют заметную долю потока (одностраничные
|
||||||
|
визиты-отказы допустимы и правдоподобны).
|
||||||
- Один `click_id` принадлежит ровно одному `user_domain_id`; у пользователя
|
- Один `click_id` принадлежит ровно одному `user_domain_id`; у пользователя
|
||||||
бывает несколько `click_id`.
|
бывает несколько `click_id`.
|
||||||
- Воронка по `page_url_path` затухает (доля доходящих до `/confirmation`
|
- Воронка по `page_url_path` затухает от шага к шагу; доля визитов, доходящих
|
||||||
правдоподобно мала), терминальный шаг — `/confirmation`.
|
до `/confirmation`, согласована с сидом (~25%). `/confirmation` — последний
|
||||||
|
шаг воронки, но визит может продолжаться и после него (как в сиде).
|
||||||
- Поток крутится на слабом железе при дефолтном rate без деградации стенда.
|
- Поток крутится на слабом железе при дефолтном rate без деградации стенда.
|
||||||
- `GEN_SEED` даёт воспроизводимый поток.
|
- `GEN_SEED` даёт воспроизводимый поток.
|
||||||
|
|
||||||
@@ -157,6 +162,12 @@
|
|||||||
|
|
||||||
## Open questions (→ follow-up-спека по математике)
|
## Open questions (→ follow-up-спека по математике)
|
||||||
|
|
||||||
|
> **Закрыто 2026-06-10:** все вопросы ниже решены в
|
||||||
|
> [спеке математической модели](./2026-06-10-generator-math-model.md)
|
||||||
|
> (распределения, ротация популяции, инвариант пауз вместо session-timeout,
|
||||||
|
> активные сессии как состояние, персистентность, воспроизводимость;
|
||||||
|
> `event_type` остаётся 100% `pageview`). Список сохранён для истории.
|
||||||
|
|
||||||
- Конкретные распределения: события на сессию, сессии на пользователя за период,
|
- Конкретные распределения: события на сессию, сессии на пользователя за период,
|
||||||
межсессионные интервалы.
|
межсессионные интервалы.
|
||||||
- **Вводить ли разнообразие `event_type`** (purchase/add_to_cart/click) сверх
|
- **Вводить ли разнообразие `event_type`** (purchase/add_to_cart/click) сверх
|
||||||
|
|||||||
@@ -0,0 +1,241 @@
|
|||||||
|
# Математическая модель генератора: распределения, тики, персистентность
|
||||||
|
|
||||||
|
Дата: 2026-06-10 (ревизия после адверсариального ревью в тот же день)
|
||||||
|
Статус: Draft
|
||||||
|
Связано: [ADR-0004](../adr/0004-steady-stream-synthetic-generator.md) (решение
|
||||||
|
«генератор, не реплей»), [спека формы доработки](./2026-06-09-generator-rework-hierarchical.md)
|
||||||
|
(требования к иерархии; эта спека закрывает её раздел *Open questions*),
|
||||||
|
[`CONTEXT.md`](../../CONTEXT.md) (доменный язык и **профиль сид-датасета** —
|
||||||
|
опорные цифры для калибровки), [`generator/KNOWN_ISSUES.md`](../../generator/KNOWN_ISSUES.md)
|
||||||
|
(диагноз дефекта).
|
||||||
|
|
||||||
|
Уровень документа: **модель и инварианты**. Детальный план реализации и код —
|
||||||
|
за исполнителем; конкретные технические решения (структуры данных, точные
|
||||||
|
алгоритмы розыгрыша, имена новых переменных окружения, не названные здесь) он
|
||||||
|
строит сам в рамках зафиксированных здесь правил.
|
||||||
|
|
||||||
|
## Картина целиком
|
||||||
|
|
||||||
|
Генератор имитирует жизнь сайта тремя уровнями:
|
||||||
|
|
||||||
|
```
|
||||||
|
популяция пользователей — кто вообще ходит на сайт
|
||||||
|
└── визиты (сессии, click_id) — пользователь время от времени заходит
|
||||||
|
└── события (event_id) — за один заход ходит по страницам
|
||||||
|
```
|
||||||
|
|
||||||
|
Поверх иерархии — сохранённая модель интенсивности (Пуассон + часовой
|
||||||
|
коэффициент + jitter из `_calculate_events_count()`): она задаёт, *сколько*
|
||||||
|
активности происходит в единицу времени. Путь по страницам внутри визита
|
||||||
|
задаёт **марковская цепочка** — таблица вероятностей переходов между страницами.
|
||||||
|
|
||||||
|
## Принятые решения (развилки закрыты 2026-06-10)
|
||||||
|
|
||||||
|
1. **Типы событий: 100% `pageview`.** Воронка остаётся страничной
|
||||||
|
(`page_url_path`), как в сиде и на дашборде. Другие типы
|
||||||
|
(purchase/add_to_cart/click) — отдельная итерация после урока 7: они тянут
|
||||||
|
согласованную правку дашборда и урока 6, что выходит за рамки переработки.
|
||||||
|
2. **Session-timeout не вводим.** Генератор порождает визиты явно и знает их
|
||||||
|
границы по построению; правило «молчал полчаса — новая сессия» — это приём
|
||||||
|
*восстановления* сессий из событий, который здесь не нужен. `CONTEXT.md`
|
||||||
|
(сессия ≡ визит ≡ `click_id`) не меняется. Вместо механизма — **инвариант
|
||||||
|
пауз** (см. ниже): данные не должны противоречить привычной 30-минутной
|
||||||
|
семантике.
|
||||||
|
3. **Популяция: постоянное ядро + медленная ротация.** Размер активной
|
||||||
|
популяции ограничен (память не растёт), но новые пользователи понемногу
|
||||||
|
приходят, давно неактивные — выбывают. Кумулятивное число уникальных
|
||||||
|
пользователей растёт со временем — стенд выглядит живым.
|
||||||
|
4. **Путь по страницам — марковская цепочка.** Для каждой страницы задана
|
||||||
|
вероятность перехода на каждую другую страницу или ухода с сайта. Один
|
||||||
|
механизм даёт сразу: длину визита, затухающую воронку, петли (возвраты на
|
||||||
|
главную, просмотр обоих товаров) и «походил после покупки» — всё то, что
|
||||||
|
реально есть в сиде. Простая «затухающая цепочка без петель» рассмотрена и
|
||||||
|
отклонена: она не может дать длины визитов как в сиде (медиана 10, до 27
|
||||||
|
событий) и беднее как учебный объект. Таблица переходов — готовая «ручка»
|
||||||
|
для урока 7: менти меняет вероятность и видит эффект на дашборде.
|
||||||
|
|
||||||
|
## Модель по уровням
|
||||||
|
|
||||||
|
### Популяция пользователей
|
||||||
|
|
||||||
|
- Активная популяция — ограниченное множество пользователей с постоянными
|
||||||
|
`user_domain_id`, не больше `GEN_POPULATION_MAX`.
|
||||||
|
- **Инициализация:** при старте с чистого листа популяция сразу предзаполняется
|
||||||
|
до потолка (детерминированно от `GEN_SEED`) — поток выходит на стационарный
|
||||||
|
режим с первых минут, без длинного «разогрева».
|
||||||
|
- **Ротация через рождение визитов:** каждый новый визит с вероятностью
|
||||||
|
`GEN_P_NEW_USER` достаётся *новому* пользователю (свежий `user_domain_id`),
|
||||||
|
иначе — пользователю из популяции, **доступному** для возврата (см. кулдаун
|
||||||
|
ниже). При переполнении популяции вытесняется дольше всех неактивный
|
||||||
|
пользователь **без активного визита**. Так приток и отток получаются из
|
||||||
|
одного простого правила.
|
||||||
|
- **Профиль пользователя:** при рождении пользователь получает полный
|
||||||
|
device/geo-контекст одной случайной сид-сессии (включая `user_custom_id`;
|
||||||
|
повторы email между пользователями допустимы — демо-данные) с заменой
|
||||||
|
`user_domain_id` на свежий. Профиль используется во всех визитах пользователя
|
||||||
|
(KISS: смену устройства не моделируем).
|
||||||
|
|
||||||
|
### Визиты (сессии)
|
||||||
|
|
||||||
|
- Визит = новый `click_id`, общий для всех его событий, с device/geo из профиля
|
||||||
|
пользователя.
|
||||||
|
- **Связка с моделью интенсивности.** `_calculate_events_count()` сохраняется и
|
||||||
|
выдаёт на тик *бюджет событий*. Бюджет конвертируется в рождения визитов:
|
||||||
|
ожидаемое число новых визитов за тик = бюджет ÷ средняя длина визита
|
||||||
|
(средняя длина — производная калиброванной таблицы переходов, исполнитель
|
||||||
|
измеряет её симуляцией). События же выпускаются по внутрисессионным паузам.
|
||||||
|
Следствие: фактическое число событий за конкретный тик — производная величина,
|
||||||
|
но *средняя* интенсивность за длинное окно совпадает с целевой. Прежние
|
||||||
|
границы `GEN_MIN/MAX_EVENTS_PER_TICK` в старом смысле теряют применимость;
|
||||||
|
ограничивать нужно рождения визитов за тик (имена и значения — за
|
||||||
|
исполнителем, зафиксировать в README).
|
||||||
|
- **Кулдаун возврата:** после завершения визита пользователь недоступен для
|
||||||
|
нового минимум `GEN_MIN_RETURN_MINUTES` (ориентир 30 мин) — это гарантирует
|
||||||
|
нижнюю границу межсессионной паузы (инвариант пауз).
|
||||||
|
- **Межсессионные паузы — эмерджентная величина, не свободный параметр.**
|
||||||
|
Пауза складывается из кулдауна и времени ожидания «своей очереди» и в среднем
|
||||||
|
определяется балансом потока и популяции:
|
||||||
|
|
||||||
|
```
|
||||||
|
средняя пауза ≈ кулдаун + GEN_POPULATION_MAX ÷ (λ ÷ L × (1 − GEN_P_NEW_USER))
|
||||||
|
где λ — событий/мин, L — средняя длина визита
|
||||||
|
```
|
||||||
|
|
||||||
|
На ориентирах по умолчанию (λ=30, L≈10, популяция 300, p_new=0.15):
|
||||||
|
возвратов ≈ 2.6/мин, пауза ≈ **~2 часа** — больше 30-минутного окна и при
|
||||||
|
этом возвраты видны уже за один вечер работы стенда. **Кто меняет λ или
|
||||||
|
размер популяции — обязан пересчитать паузу по формуле**: эти три величины
|
||||||
|
связаны, их нельзя крутить независимо.
|
||||||
|
|
||||||
|
### События и путь по страницам
|
||||||
|
|
||||||
|
- **Марковская цепочка:** страницы — `/home`, `/product_a`, `/product_b`,
|
||||||
|
`/cart`, `/payment`, `/confirmation` плюс исход «ушёл». Визит начинается с
|
||||||
|
входной страницы (как правило `/home`), дальше каждый следующий шаг
|
||||||
|
разыгрывается по таблице переходов текущей страницы. Петли разрешены
|
||||||
|
(вернуться на главную, посмотреть оба товара, продолжить ходить после
|
||||||
|
`/confirmation` — как в сиде). Защита от зацикливания — потолок длины визита
|
||||||
|
`GEN_MAX_SESSION_EVENTS` (ориентир 30, как максимум в сиде).
|
||||||
|
- **Калибровка по сиду:** конкретные значения таблицы подбирает исполнитель под
|
||||||
|
два целевых показателя из профиля сида (см. `CONTEXT.md`): доля визитов,
|
||||||
|
достигших `/confirmation`, ≈ 25%, и длина визита с медианой ~10 событий.
|
||||||
|
Точного совпадения распределений не требуется — требуется сопоставимость,
|
||||||
|
чтобы дашборд на потоке показывал привычные по сиду цифры.
|
||||||
|
- **Паузы внутри визита:** интервал между соседними событиями — величина
|
||||||
|
масштаба *секунд—минут* (например, лог-нормальное: большинство пауз короткие,
|
||||||
|
изредка длинные). Ориентир: медиана десятки секунд. Форма распределения и
|
||||||
|
параметры — за исполнителем.
|
||||||
|
- **Инвариант пауз (вместо session-timeout):** все паузы внутри визита строго
|
||||||
|
меньше 30 минут (с запасом: 95-й перцентиль — единицы минут); межсессионные
|
||||||
|
паузы — всегда больше кулдауна. Данные не должны противоречить индустриальной
|
||||||
|
семантике 30-минутного окна неактивности. (Это сознательно строже сида, где
|
||||||
|
встречаются внутрисессионные паузы до 40 минут.)
|
||||||
|
- **Время монотонно:** визит живёт несколько тиков, и каждому его событию
|
||||||
|
заранее назначается *запланированный* момент (предыдущее событие + пауза).
|
||||||
|
В `event_timestamp` пишется именно запланированный момент, а не время
|
||||||
|
фактической отправки на тике — иначе метки прилипают к сетке тиков (паузы
|
||||||
|
кратны 5 с, события одного тика слипаются в одну метку). В штатной работе
|
||||||
|
запланированное время отстаёт от «сейчас» не больше чем на тик; монотонность
|
||||||
|
внутри `click_id` строгая по построению.
|
||||||
|
|
||||||
|
### Раскладка по тикам (активные визиты как состояние)
|
||||||
|
|
||||||
|
Визит длится дольше тика (5 с), поэтому генератор держит **активные визиты**
|
||||||
|
как состояние между тиками. Для каждого активного визита достаточно знать: чей
|
||||||
|
он, `click_id`, текущая страница, момент следующего события. На каждом тике
|
||||||
|
генератор:
|
||||||
|
|
||||||
|
1. выпускает события тех активных визитов, у которых подошло время;
|
||||||
|
2. рождает новые визиты по бюджету интенсивности (см. выше);
|
||||||
|
3. завершает визиты, чей путь закончился (исход «ушёл» или потолок длины).
|
||||||
|
|
||||||
|
Число одновременно активных визитов ограничено (`GEN_MAX_ACTIVE_SESSIONS`) —
|
||||||
|
защита памяти; при достижении потолка новые рождения в этот тик пропускаются
|
||||||
|
(бюджет не копится). На ориентирах по умолчанию стационарно активны ~15–30
|
||||||
|
визитов — потолок с большим запасом.
|
||||||
|
|
||||||
|
## Персистентность через рестарты
|
||||||
|
|
||||||
|
- Расширить состояние в compact-топике `generator_state` (новая `version`):
|
||||||
|
к тику и состоянию ГПСЧ добавляются **популяция** (компактно: идентификаторы,
|
||||||
|
профили, время последней активности) и **активные визиты**.
|
||||||
|
- Объём состояния ограничен потолками популяции и активных визитов — запись в
|
||||||
|
Kafka остаётся маленькой (десятки КБ).
|
||||||
|
- **Рестарт после простоя — «правило 30 минут», повизитно:** активный визит,
|
||||||
|
чьё следующее событие запланировано не дальше 30 минут в прошлом, продолжается —
|
||||||
|
созревшие за простой события досылаются со своими (прошлыми, честными)
|
||||||
|
метками. Визит, просроченный сильнее, закрывается без досылки остатка —
|
||||||
|
посетители «ушли», пока стенд спал. Популяция переживает простой любой длины;
|
||||||
|
кулдауны отсчитываются по меткам времени, не по тикам.
|
||||||
|
- Уже существующая деградация сохраняется: невалидное/отсутствующее состояние →
|
||||||
|
начать с чистого листа (свежая популяция от `GEN_SEED`), предупреждение в лог.
|
||||||
|
`GEN_STATE_RESET=true` — явный сброс, как сейчас.
|
||||||
|
|
||||||
|
## Воспроизводимость (`GEN_SEED`)
|
||||||
|
|
||||||
|
При одном `GEN_SEED` **и одинаковых условиях запуска** детерминирована
|
||||||
|
последовательность решений: какие пользователи родились, какие визиты открылись,
|
||||||
|
какие пути выпали. Оговорка про условия существенна: часовой коэффициент и
|
||||||
|
метки времени зависят от настенных часов, поэтому запуск в другой час дня даёт
|
||||||
|
другой поток (как и у текущего генератора). Все случайные решения — только
|
||||||
|
через единый ГПСЧ генератора, состояние которого сохраняется; как зафиксировать
|
||||||
|
условия в тесте воспроизводимости — за исполнителем.
|
||||||
|
|
||||||
|
## Параметры (ориентиры по умолчанию)
|
||||||
|
|
||||||
|
Согласованный набор (см. формулу связи в разделе про визиты); дефолты —
|
||||||
|
ориентиры, исполнитель уточняет при калибровке.
|
||||||
|
|
||||||
|
| Параметр | Ориентир | Смысл |
|
||||||
|
|---|---|---|
|
||||||
|
| `GEN_LAMBDA_BASE_PER_MIN` | **30** (сейчас 200) | целевая интенсивность, событий/мин |
|
||||||
|
| `GEN_POPULATION_MAX` | 300 | потолок активной популяции |
|
||||||
|
| `GEN_P_NEW_USER` | 0.15 | доля визитов, достающихся новым пользователям |
|
||||||
|
| `GEN_MIN_RETURN_MINUTES` | 30 | кулдаун возврата пользователя |
|
||||||
|
| `GEN_MAX_SESSION_EVENTS` | 30 | потолок длины визита (защита от петель) |
|
||||||
|
| `GEN_MAX_ACTIVE_SESSIONS` | 200 | потолок одновременных визитов |
|
||||||
|
| средняя длина визита | ~10 событий | производная таблицы переходов (калибровка по сиду) |
|
||||||
|
| пауза внутри визита | медиана ~десятки секунд | распределение — за исполнителем |
|
||||||
|
| межсессионная пауза | ≈ 2 ч | эмерджентная, по формуле |
|
||||||
|
|
||||||
|
Таблица переходов и параметры распределения пауз — тоже конфигурация
|
||||||
|
(формат и имена — за исполнителем; таблица должна быть доступна менти для
|
||||||
|
экспериментов урока 7). Существующие `GEN_TICK_SECONDS`, `GEN_JITTER_PCT`,
|
||||||
|
`GEN_SEED`, `GEN_STATE_*` сохраняют смысл.
|
||||||
|
|
||||||
|
## Критерии приёмки
|
||||||
|
|
||||||
|
Дополняют раздел *Validation* [спеки формы доработки](./2026-06-09-generator-rework-hierarchical.md)
|
||||||
|
(пирамида, монотонность времени, принадлежность `click_id` одному пользователю,
|
||||||
|
воспроизводимость — там; здесь не дублируются):
|
||||||
|
|
||||||
|
- Средняя интенсивность событий за длинное окно (час и больше) соответствует
|
||||||
|
целевой `λ × часовой коэффициент` с разумным отклонением.
|
||||||
|
- Паузы внутри `click_id`: 95-й перцентиль — единицы минут, максимум < 30 минут.
|
||||||
|
- Межсессионные паузы одного пользователя: минимум ≥ кулдауна, медиана —
|
||||||
|
порядка расчётной по формуле (~2 ч на дефолтах).
|
||||||
|
- Доля визитов новых пользователей за длинное окно ≈ `GEN_P_NEW_USER`;
|
||||||
|
кумулятивное число уникальных `user_domain_id` растёт со временем, размер
|
||||||
|
состояния генератора — нет.
|
||||||
|
- Длина визита (медиана ~10, максимум ≤ потолка) и доля дошедших до
|
||||||
|
`/confirmation` (~25%) сопоставимы с профилем сида из `CONTEXT.md`.
|
||||||
|
- Воронка затухает по шагам: число визитов, посетивших страницу, монотонно
|
||||||
|
убывает вдоль `/home → товары → /cart → /payment → /confirmation`.
|
||||||
|
- После рестарта с простоем ≤ 30 минут активные визиты продолжаются (нет
|
||||||
|
скачка «все пользователи новые»); после долгого простоя закрываются только
|
||||||
|
просроченные визиты, популяция сохраняется.
|
||||||
|
|
||||||
|
## Влияние на документацию
|
||||||
|
|
||||||
|
- Спека формы доработки: раздел *Open questions* закрыт ссылкой на этот
|
||||||
|
документ; критерии Validation уточнены по фактическому профилю сида.
|
||||||
|
- `CONTEXT.md`: добавлен **профиль сид-датасета** (измеренные распределения —
|
||||||
|
опора калибровки) и исправлено неверное утверждение о разбросе времени внутри
|
||||||
|
`click_id` (сделано вместе с этой ревизией).
|
||||||
|
- `generator/KNOWN_ISSUES.md`, ADR-0004: исправлен неверный факт «1..7 событий
|
||||||
|
на визит» (по полному замеру — 1..27, медиана 10).
|
||||||
|
- Урок 7 «Система живёт» (пишется на этапе реализации): марковская таблица
|
||||||
|
переходов — учебная «ручка» урока (поменяй вероятность → увидь сдвиг
|
||||||
|
конверсии на дашборде).
|
||||||
|
- `generator/README.md` — обновляется при реализации; туда же — верхнеуровневый
|
||||||
|
обзор «как работает генератор» (два контура: интенсивность и сущности).
|
||||||
@@ -34,8 +34,10 @@ user_domain_id (постоянный пользователь, cookie)
|
|||||||
```
|
```
|
||||||
|
|
||||||
- `click_id` — это **визит**, а не одиночный клик: в статическом сиде один
|
- `click_id` — это **визит**, а не одиночный клик: в статическом сиде один
|
||||||
`click_id` честно группирует 1..7 событий (`dds.click` = «контекст сессии
|
`click_id` честно группирует 1..27 событий, медиана 10 (`dds.click` =
|
||||||
пользователя», см. `sql/ddl/dds/30_dds.sql`).
|
«контекст сессии пользователя», см. `sql/ddl/dds/30_dds.sql`; точные цифры —
|
||||||
|
профиль сид-датасета в `CONTEXT.md`, ранняя оценка «1..7» была замером по
|
||||||
|
срезу файла).
|
||||||
- `user_domain_id` живёт в `device_events`, привязан к `click_id`.
|
- `user_domain_id` живёт в `device_events`, привязан к `click_id`.
|
||||||
|
|
||||||
## Что генератор делает правильно
|
## Что генератор делает правильно
|
||||||
@@ -79,7 +81,7 @@ device_event = {**base_device, "click_id": new_click_id} # user_domain_i
|
|||||||
|
|
||||||
| | Статический сид (`data/*.jsonl`) | Поток из генератора |
|
| | Статический сид (`data/*.jsonl`) | Поток из генератора |
|
||||||
|---|---|---|
|
|---|---|---|
|
||||||
| `click_id` | визит-envelope (1..7 событий) | свежий на событие → `click_id` ≈ событие |
|
| `click_id` | визит-envelope (1..27 событий, медиана 10) | свежий на событие → `click_id` ≈ событие |
|
||||||
| `user_domain_id` | 1:1 с `click_id` | переиспользуется (потолок ~99) |
|
| `user_domain_id` | 1:1 с `click_id` | переиспользуется (потолок ~99) |
|
||||||
| Семантика | `Sessions == Users` (вырождено по пользователю) | `Sessions == Events` (сессия = одно событие) |
|
| Семантика | `Sessions == Users` (вырождено по пользователю) | `Sessions == Events` (сессия = одно событие) |
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user