- Зачем:
- ADR-0006 сделал генерацию единственным источником аналитики, а статический
сид — архивным; глоссарий и спеки это ещё не отражали.
- Что:
- CONTEXT.md: «статический сид» переименован в «архивный статический сид»
(короткое имя сохранено), описан как временная кладовка значений с целью
полного вывода; «стартовая история» получила синонимы «стартовый сид» и
«новый сид»; раздел «Слои данных» отмечает переход аналитики на генерацию.
- мат-спека: разделы «Персистентность через рестарты» и «Воспроизводимость»
помечены как переописанные в спеке модельного времени (ссылкой, без повтора).
- спека модельного времени: синоним «стартовый сид» добавлен в определение и
в заметку о влиянии на документацию.
- Проверка:
- git diff: термины и перекрёстные ссылки читаются непротиворечиво; ADR не
правились (статус «архивный» не переносится в документы до ADR-0006).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
284 lines
27 KiB
Markdown
284 lines
27 KiB
Markdown
# Математическая модель генератора: распределения, тики, персистентность
|
||
|
||
Дата: 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. **Популяция: постоянное ядро + медленная ротация.** Размер активной
|
||
популяции ограничен (память не растёт), но новые пользователи понемногу
|
||
приходят, давно неактивные — выбывают. Кумулятивное число уникальных
|
||
пользователей растёт со временем — стенд выглядит живым. «Медленно» — в
|
||
масштабе наблюдения: на ориентирах по умолчанию ядро полностью сменяется
|
||
примерно за 11 часов (это осознанный баланс: возвраты видны за вечер,
|
||
рост uniques — за сутки).
|
||
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`) плюс свежие идентификаторы, а не
|
||
копией всех полей — состояние остаётся компактным.
|
||
|
||
### Визиты (сессии)
|
||
|
||
- Визит = новый `click_id`, общий для всех его событий, с device/geo из профиля
|
||
пользователя.
|
||
- **Каденция публикации device/geo — как в сиде и у текущего генератора:**
|
||
записи `device_events` и `geo_events` публикуются **на каждое событие**
|
||
(в пределах визита дублируются с одинаковым содержимым; в сиде это
|
||
проверено: 1000 строк при 99 уникальных по содержимому). Контракт данных с
|
||
ETL/ODS не меняется — это требование, а не деталь реализации.
|
||
- **Связка с моделью интенсивности.** `_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/мин, цикл ≈ 118 мин, средняя пауза ≈ **~110 мин (~2 часа)**;
|
||
медиана ниже среднего (распределение скошено вправо), порядка 1.5 ч — всё
|
||
равно сильно больше 30-минутного окна, и возвраты видны уже за один вечер
|
||
работы стенда. **Кто меняет λ или размер популяции — обязан пересчитать
|
||
паузу по формуле**: эти величины связаны, их нельзя крутить независимо.
|
||
Ориентиры здесь посчитаны при L=10; после калибровки таблицы переходов
|
||
производные числа пересчитываются от измеренного L.
|
||
|
||
### События и путь по страницам
|
||
|
||
- **Марковская цепочка:** страницы — `/home`, `/product_a`, `/product_b`,
|
||
`/cart`, `/payment`, `/confirmation` плюс исход «ушёл». Визит начинается со
|
||
страницы, разыгранной по **стартовому распределению** (оно — часть модели:
|
||
в сиде `/home` — лишь ~59% входов, остальные входят на страницы товаров),
|
||
дальше каждый следующий шаг разыгрывается по таблице переходов текущей
|
||
страницы. Петли разрешены (вернуться на главную, посмотреть оба товара,
|
||
продолжить ходить после `/confirmation` — как в сиде). Защита от
|
||
зацикливания — потолок длины визита `GEN_MAX_SESSION_EVENTS` (ориентир 30,
|
||
как максимум в сиде).
|
||
- **Калибровка по сиду:** стартовое распределение и значения таблицы подбирает
|
||
исполнитель под целевые показатели из профиля сида (см. `CONTEXT.md`): доля
|
||
визитов, достигших `/confirmation`, ≈ 25%, длина визита с медианой ~10 и
|
||
**средним ~10** (в сиде среднее ≈ медиане; у цепочки с геометрическим
|
||
хвостом среднее легко уезжает выше — следить за обоими). Точного совпадения
|
||
распределений не требуется — требуется сопоставимость, чтобы дашборд на
|
||
потоке показывал привычные по сиду цифры.
|
||
- **Паузы внутри визита:** интервал между соседними событиями — величина
|
||
масштаба *секунд—минут* (например, лог-нормальное: большинство пауз короткие,
|
||
изредка длинные). Ориентир: медиана десятки секунд. Форма распределения и
|
||
параметры — за исполнителем.
|
||
- **Инвариант пауз (вместо session-timeout):** все паузы внутри визита строго
|
||
меньше 30 минут (с запасом: 95-й перцентиль — единицы минут); межсессионные
|
||
паузы — всегда больше кулдауна. Данные не должны противоречить индустриальной
|
||
семантике 30-минутного окна неактивности. (Это сознательно строже сида, где
|
||
встречаются внутрисессионные паузы до 40 минут.)
|
||
- **Время монотонно:** визит живёт несколько тиков, и каждому его событию
|
||
заранее назначается *запланированный* момент (предыдущее событие + пауза).
|
||
В `event_timestamp` пишется именно запланированный момент, а не время
|
||
фактической отправки на тике — иначе метки прилипают к сетке тиков (паузы
|
||
кратны 5 с, события одного тика слипаются в одну метку). В штатной работе
|
||
запланированное время отстаёт от «сейчас» не больше чем на тик; монотонность
|
||
внутри `click_id` строгая по построению.
|
||
|
||
### Раскладка по тикам (активные визиты как состояние)
|
||
|
||
Визит длится дольше тика (5 с), поэтому генератор держит **активные визиты**
|
||
как состояние между тиками. Для каждого активного визита достаточно знать: чей
|
||
он, `click_id`, текущая страница, момент следующего события. На каждом тике
|
||
генератор:
|
||
|
||
1. выпускает события тех активных визитов, у которых подошло время;
|
||
2. рождает новые визиты по бюджету интенсивности (см. выше);
|
||
3. завершает визиты, чей путь закончился (исход «ушёл» или потолок длины).
|
||
|
||
Число одновременно активных визитов ограничено (`GEN_MAX_ACTIVE_SESSIONS`) —
|
||
защита памяти; при достижении потолка новые рождения в этот тик пропускаются
|
||
(бюджет не копится). На ориентирах по умолчанию стационарно активны ~15–30
|
||
визитов — потолок с большим запасом. **Инвариант конфигурации:**
|
||
`GEN_MAX_ACTIVE_SESSIONS < GEN_POPULATION_MAX` — иначе правило вытеснения
|
||
(«без активного визита») может не найти кандидата; валидировать при старте,
|
||
как существующие проверки `Config`.
|
||
|
||
## Персистентность через рестарты
|
||
|
||
> **Приведено в соответствие с модельным временем.** Раздел написан от настенных
|
||
> часов. Правила сохранения и восстановления состояния переописаны в спеке
|
||
> [модельного времени](./2026-06-14-generator-model-time-and-startup-history.md)
|
||
> (§«Сохранение и восстановление состояния»): те же 30 минут отсчитываются по
|
||
> часам генератора. Ниже — исходная формулировка, оставлена как след решения; при
|
||
> расхождении главенствует новая спека.
|
||
|
||
- Расширить состояние в compact-топике `generator_state` (новая `version`):
|
||
к тику и состоянию ГПСЧ добавляются **популяция** (компактно: идентификаторы,
|
||
профили, время последней активности) и **активные визиты**.
|
||
- Объём состояния ограничен потолками популяции и активных визитов — запись в
|
||
Kafka остаётся маленькой (десятки КБ).
|
||
- **Рестарт после простоя — «правило 30 минут», повизитно:** активный визит,
|
||
чьё следующее событие запланировано не дальше 30 минут в прошлом, продолжается —
|
||
созревшие за простой события досылаются со своими (прошлыми, честными)
|
||
метками. Визит, просроченный сильнее, закрывается без досылки остатка —
|
||
посетители «ушли», пока стенд спал. Популяция переживает простой любой длины;
|
||
кулдауны отсчитываются по меткам времени, не по тикам.
|
||
- Уже существующая деградация сохраняется: невалидное/отсутствующее состояние →
|
||
начать с чистого листа (свежая популяция от `GEN_SEED`), предупреждение в лог.
|
||
`GEN_STATE_RESET=true` — явный сброс, как сейчас.
|
||
|
||
## Воспроизводимость (`GEN_SEED`)
|
||
|
||
> **Приведено в соответствие с модельным временем.** Оговорка ниже — «запуск в
|
||
> другой час дня даёт другой поток» — снята: с модельными часами дневной
|
||
> коэффициент считается от точки отсчёта `T0`, а не от настенных часов.
|
||
> Переописано в спеке
|
||
> [модельного времени](./2026-06-14-generator-model-time-and-startup-history.md)
|
||
> (§«Повторяемость»). Ниже — исходная формулировка, оставлена как след решения.
|
||
|
||
При одном `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 ч на дефолтах; медиана ниже, ~1.5 ч).
|
||
- Доля визитов новых пользователей за длинное окно ≈ `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 «Система живёт» (пишется на этапе реализации): марковская таблица
|
||
переходов — учебная «ручка» урока (поменяй вероятность → увидь сдвиг
|
||
конверсии на дашборде). Оговорка скоупа: «увидеть на дашборде» предполагает,
|
||
что поток доезжает до витрин — это зависимость урока 7 от инкрементальной
|
||
загрузки ETL (план v2), а не требование к генератору.
|
||
- `generator/README.md` — обновляется при реализации; туда же — верхнеуровневый
|
||
обзор «как работает генератор» (два контура: интенсивность и сущности).
|