- Зачем:
- закрыть 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>
22 KiB
Математическая модель генератора: распределения, тики, персистентность
Дата: 2026-06-10 (ревизия после адверсариального ревью в тот же день)
Статус: Draft
Связано: ADR-0004 (решение
«генератор, не реплей»), спека формы доработки
(требования к иерархии; эта спека закрывает её раздел Open questions),
CONTEXT.md (доменный язык и профиль сид-датасета —
опорные цифры для калибровки), generator/KNOWN_ISSUES.md
(диагноз дефекта).
Уровень документа: модель и инварианты. Детальный план реализации и код — за исполнителем; конкретные технические решения (структуры данных, точные алгоритмы розыгрыша, имена новых переменных окружения, не названные здесь) он строит сам в рамках зафиксированных здесь правил.
Картина целиком
Генератор имитирует жизнь сайта тремя уровнями:
популяция пользователей — кто вообще ходит на сайт
└── визиты (сессии, click_id) — пользователь время от времени заходит
└── события (event_id) — за один заход ходит по страницам
Поверх иерархии — сохранённая модель интенсивности (Пуассон + часовой
коэффициент + jitter из _calculate_events_count()): она задаёт, сколько
активности происходит в единицу времени. Путь по страницам внутри визита
задаёт марковская цепочка — таблица вероятностей переходов между страницами.
Принятые решения (развилки закрыты 2026-06-10)
- Типы событий: 100%
pageview. Воронка остаётся страничной (page_url_path), как в сиде и на дашборде. Другие типы (purchase/add_to_cart/click) — отдельная итерация после урока 7: они тянут согласованную правку дашборда и урока 6, что выходит за рамки переработки. - Session-timeout не вводим. Генератор порождает визиты явно и знает их
границы по построению; правило «молчал полчаса — новая сессия» — это приём
восстановления сессий из событий, который здесь не нужен.
CONTEXT.md(сессия ≡ визит ≡click_id) не меняется. Вместо механизма — инвариант пауз (см. ниже): данные не должны противоречить привычной 30-минутной семантике. - Популяция: постоянное ядро + медленная ротация. Размер активной популяции ограничен (память не растёт), но новые пользователи понемногу приходят, давно неактивные — выбывают. Кумулятивное число уникальных пользователей растёт со временем — стенд выглядит живым.
- Путь по страницам — марковская цепочка. Для каждой страницы задана вероятность перехода на каждую другую страницу или ухода с сайта. Один механизм даёт сразу: длину визита, затухающую воронку, петли (возвраты на главную, просмотр обоих товаров) и «походил после покупки» — всё то, что реально есть в сиде. Простая «затухающая цепочка без петель» рассмотрена и отклонена: она не может дать длины визитов как в сиде (медиана 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, текущая страница, момент следующего события. На каждом тике
генератор:
- выпускает события тех активных визитов, у которых подошло время;
- рождает новые визиты по бюджету интенсивности (см. выше);
- завершает визиты, чей путь закончился (исход «ушёл» или потолок длины).
Число одновременно активных визитов ограничено (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 спеки формы доработки
(пирамида, монотонность времени, принадлежность 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— обновляется при реализации; туда же — верхнеуровневый обзор «как работает генератор» (два контура: интенсивность и сущности).