feat(generator): день-функция — трафик, визиты и просмотры страниц
Зачем: план состава отдаёт дневную аудиторию, но событий у мира ещё не было. День-функция превращает аудиторию в поток просмотров — на нём стоят лабы про сборку визитов и про витрины, а следующий этап вешает на него торговые события. Что: - `day.py` — день как чистая функция зерна и номера дня: суточная волна в местном времени посетителя, визиты по документированным правилам нарезки, все 47 колонок выгрузки; шов для торговых событий — ряды `page` и `product`; - `reference.py` — справочники-литералы: профили устройств, города Поволжья с настоящими гео-id Яндекса, источники трафика, карта сайта; - `catalog.py` и `data/catalog/products.csv` — каталог на 180 позиций, общий у генератора и будущего словаря ClickHouse; - `weights.py` — выбор по целым весам, один на план и на день; - паспорт куки (устройство и город) переехал в план состава; броски приписаны последними, поэтому измеренные числа канонического мира не сдвинулись; - словарь: «визит» закреплён за сессией, одноимённое понятие плана стало «днём активности»; статьи в `CONTEXT.md`; - решения по ходу — в спеку генератора, раздел 9; наполнение `ParsedParamsKey1` отложено тикетом #47. Проверка: `make lint`, `make typecheck`, `make test` — 353 passed (было 297). Счётчики плана после правки те же: приток 3827,64/день, дневная аудитория 6235–7124, 68 119 посетителей за 14 дней, 170 двухкуковых пар. День 0 — 45 810 событий за 0,6 с, снимок 14 дней — 5,9 с при пороге 30 с на день. Две слепые линии ревью, десять находок, все закрыты и перепроверены. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -55,7 +55,7 @@
|
||||
торговые счётчики сложатся, когда торговое поведение определит #40.
|
||||
- **Состав не замкнут: посетители появляются и затухают.** План состава
|
||||
задаёт календарь появления — у каждого посетителя есть дата первого
|
||||
визита и профиль возвратов, включая затухание: заметная доля кук
|
||||
дня и профиль возвратов, включая затухание: заметная доля кук
|
||||
одноразовая, как в живом трафике. Новые посетители появляются на всём
|
||||
протяжении оси: uniq(ClientID) растёт с горизонтом, дневная и накопленная
|
||||
аудитории не сходятся в одно число. Приток — часть плана, а не мутация:
|
||||
@@ -67,7 +67,7 @@
|
||||
ветвящийся по номеру дня (позиция в дереве — раздел 2), отдельный от
|
||||
подпотока дня-функции. Аудитория дня — когорта D плюс возвраты когорт
|
||||
последних дней: окно активности — хвост возвратов (константа мира —
|
||||
раздел 9), отсчитанный от первого визита человека и общий на обе его
|
||||
раздел 9), отсчитанный от первого дня человека и общий на обе его
|
||||
куки (уточнение при исполнении #38, 2026-08-02: окно от рождения
|
||||
каждой куки растянуло бы жизнь когорты вдвое, а с ней и загляд назад,
|
||||
которым ленивая форма и держится). За краем окна кука не
|
||||
@@ -89,7 +89,7 @@
|
||||
Равные шансы по всему окну означали бы вторую куку у давно ушедшего
|
||||
человека — и вчетверо меньше пар, реализованных внутри снимка.
|
||||
Каждой паре план назначает дни гарантированных заказов: по
|
||||
одному на куку, из дней визитов этой куки, на оси от D0 и позже;
|
||||
одному на куку, из дней активности этой куки, на оси от D0 и позже;
|
||||
человеку предыстории, чьё окно активности таких дней не оставляет,
|
||||
пара не назначается. День-функция обязана назначенные заказы
|
||||
реализовать; остальные покупки — вольные, их решает генератор торговых
|
||||
@@ -405,12 +405,13 @@ pytest-тест с маркером `perf` и таймаутом-обрубан
|
||||
Решено при исполнении #38 (2026-08-02):
|
||||
|
||||
- **Числа притока и состава.** Приток — ~3 800 новых кук в средний день,
|
||||
модулируется тем же недельным профилем, что трафик (иначе доля
|
||||
новичков скакала бы по дням недели). Доля одноразовых кук — 75%;
|
||||
модулируется недельным профилем мира; трафик наследует эту волну через
|
||||
дневную аудиторию, а не отдельным умножением (уточнение при исполнении
|
||||
#39 — ниже). Доля одноразовых кук — 75%;
|
||||
возвращающиеся — в среднем 3–4 возврата, профиль убывающий: почти все
|
||||
в первые 7–10 дней, тонкий хвост поздних возвратов и повторных
|
||||
покупок — до края окна (цикл повторной покупки магазина — месяцы).
|
||||
Хвост возвратов — окно активности человека от его первого визита,
|
||||
Хвост возвратов — окно активности человека от его первого дня,
|
||||
общее на обе его куки, — и глубина предыстории: 90 дней (решение
|
||||
владельца 2026-08-02: дольше квартала стенд никто не гоняет, а заказы
|
||||
старых посетителей продолжаются весь прогон; плата — чуть меньше пар,
|
||||
@@ -447,6 +448,100 @@ pytest-тест с маркером `perf` и таймаутом-обрубан
|
||||
смыслу равен модулю, но платит загрузчиком и валидацией (довод
|
||||
раздела 3 против YAML).
|
||||
|
||||
Решено при исполнении #39 (2026-08-02) — решения владельца до реализации:
|
||||
|
||||
- **Каталог товаров заводится здесь, а не в #40.** У карточки товара есть
|
||||
`Title` — имя товара, а имена живут в каталоге: карточка без каталога
|
||||
невозможна, и без карточек #39 сделал бы примерно половину трафика.
|
||||
Файл `data/catalog/products.csv` (`sku`, `name`, `category`, `brand`,
|
||||
`price`) — общий у генератора и словаря ClickHouse; #40 получает готовый.
|
||||
Решена форма, а не длина: артикул — четыре буквы категории и четыре
|
||||
цифры, цена — целые копейки, категорий шесть. Строки дописываются
|
||||
механически, поэтому ни код, ни тесты их не считают, а товар для карточки
|
||||
выбирается равномерно внутри категории.
|
||||
- **Ассортимент — непродовольственная розница** (товары для дома, текстиль,
|
||||
посуда, мелкая бытовая техника, детское, одежда и обувь), ~150–200 sku.
|
||||
Довод не вкусовой: числа мира приняты под этот профиль — цикл повторной
|
||||
покупки в месяцы, 75% одноразовых кук, конверсия ~2% на визит.
|
||||
Продуктовая сеть требовала бы других чисел, то есть переоткрытия #38.
|
||||
- **География — один регион присутствия**: Поволжье с центром в Самаре,
|
||||
города своего региона и тонкий хвост остальной страны. «Топ городов
|
||||
России» дал бы магазину с одним складом карту, которой у него не бывает.
|
||||
- **Модельные сутки считаются в часовом поясе счётчика** (UTC+4), как в
|
||||
выгрузке Метрики: `EventDate` — дата в поясе счётчика, `UTCEventTime` —
|
||||
абсолютная метка. Шов суток приходится на местную полночь и не режет
|
||||
утренние сессии, а регион мира выбирается свободно. Суточная волна задана
|
||||
в местном времени посетителя: гостю из другого пояса профиль
|
||||
поворачивается на разницу, и пик слегка размазывается — как в жизни.
|
||||
Следствие для соседних этапов: `toDate(UTCEventTime)` ≠ `EventDate`; оно
|
||||
записано в описание выгрузки, иначе сторона хранилища выведет дату сама и
|
||||
разойдётся на несколько часов данных.
|
||||
- **Паспорт куки — постоянная часть мира, а не поведение дня.** Браузер, ОС,
|
||||
устройство, экран и город у куки одни во всех её днях: кука — это браузер
|
||||
на устройстве. Держит их план состава (`Cohort` и `DayAudience` прирастают
|
||||
двумя массивами), разворачивает в поля события день-функция. Выводить
|
||||
паспорт арифметикой из `ClientID` дешевле, но про двухкуковые пары знает
|
||||
только план, а два города у одного человека — ложь в данных; пара
|
||||
получает один город и разные устройства («телефон и ноутбук»,
|
||||
мастер-спека, раздел 5). Броски паспорта приписаны последними, поэтому
|
||||
измеренные числа канонического мира не сдвинулись: прогон счётчиков до и
|
||||
после дал те же 3 828 / 6 235–7 124 / 68 119 / 170.
|
||||
- **Справочники — таблицы-литералы** рядом с конфигурацией мира; Faker и
|
||||
mimesis не подключаются. Нужны не случайные строки, а связки (город → id
|
||||
региона → часовой пояс; телефон → Safari → iOS → размер экрана) — их
|
||||
фейкер не даёт, таблицу пришлось бы написать всё равно, а новая
|
||||
зависимость молча меняла бы мир при обновлении своих словарей.
|
||||
- **Гео-id — настоящие числа геобазы Яндекса.** Проверка 2026-08-02: id
|
||||
подтверждались обращением к живым сервисам Яндекса по тому же номеру
|
||||
(`yandex.ru/pogoda/<id>`, `yandex.ru/maps/225/russia/`) — страница
|
||||
открывает ожидаемое место. Оговорка стоит в коде: опубликованной таблицы
|
||||
геобазы найти не удалось, а что `RegionCityID` Метрики нумерует регионы
|
||||
той же геобазой — обоснованное допущение, не подтверждённый источником
|
||||
факт. Выдуманные по памяти числа не годятся ни в каком случае.
|
||||
- **IP-адреса — нероутируемые диапазоны**: документационные сети RFC 5737 и
|
||||
benchmark-сеть 198.18/15 по ломтю на город, телефонам — CGNAT 100.64/10,
|
||||
как у настоящих операторов. Правдоподобные публичные адреса принадлежат
|
||||
живым организациям, и в учебных данных им не место.
|
||||
- **Словарь: «визит» остаётся за сессией** и полем `VisitID`. Одноимённое
|
||||
понятие плана состава переименовано в «день активности»: у куки он один на
|
||||
день, а визитов внутри дня бывает несколько.
|
||||
|
||||
Решено при исполнении #39 самой реализацией:
|
||||
|
||||
- **Правила резки визитов** (мастер-спека, раздел 1.2 требовала их
|
||||
задокументировать): визит принадлежит одной куке; пауза дольше 30 минут
|
||||
рвёт визит надвое; граница модельных суток режет визит, события за
|
||||
полночь в день не попадают. Единственное исключение — визит с заказом,
|
||||
обещанным планом двухкуковых пар: его старт сдвигается назад, чтобы
|
||||
воронка уместилась в сутки. Это принятое ограничение модели, а не
|
||||
недосмотр: обещание плана — гарантия, на которой стоит лаба склейки, и
|
||||
ради неё мы сужаем свободу старта. Цена названа и мала: около 24 визитов
|
||||
в день никогда не начинаются в последние минуты суток (из ~9,5 тыс.).
|
||||
Правила лежат в докстринге `day.py`, и тест собирает визиты по ним
|
||||
заново, сверяя сборку с `VisitID`.
|
||||
- **Числа дня**: ≈1,4 визита на день активности куки и ≈4,8 страницы на
|
||||
визит — 9,5 тыс. визитов и ~45 тыс. pageview в средний день; воронка
|
||||
8% визитов до корзины → 45% из них до оформления → 55% из них до
|
||||
подтверждения, то есть конверсия визита ~2%. Итог сложится после
|
||||
торговых событий (#40) и ляжет в манифест.
|
||||
- **Недельная волна применяется один раз.** Профиль ведёт приток, трафик
|
||||
наследует его через дневную аудиторию; измеренный размах трафика — ±7%
|
||||
против ±10% у притока. Второе умножение удвоило бы недельный размах. Если
|
||||
недельной лабе однажды не хватит сигнала, принципиальный путь — отдельный,
|
||||
более слабый профиль активности, а не повторное применение профиля
|
||||
притока: механизмы разные, и «на выходных приходит меньше новых людей» —
|
||||
не тот же факт, что «на выходных каждый ходит меньше».
|
||||
- **`ParsedParamsKey1` остаётся пустой** — это отложенное решение, а не
|
||||
дыра: вариант A/B-теста был бы постоянной куки, а не поведением дня, и
|
||||
заводится тикетом #47.
|
||||
- **Шов для #40**: день отдаёт, кроме колонок, два выровненных по строкам
|
||||
ряда — какая это страница магазина и какой товар показывала карточка. По
|
||||
ним #40 знает, куда вешать торговое событие и что посетитель на самом
|
||||
деле смотрел: товар в корзине, которого никто не открывал, — видимая
|
||||
глупость в воронке. Визит с назначенным заказом всегда доходит до
|
||||
`/confirmation`, а перед корзиной у него всегда есть карточка. Своей
|
||||
случайности #39 у #40 не занимает: подпоток `COMMERCE` не тронут.
|
||||
|
||||
Остаётся открытым, за тикетами:
|
||||
|
||||
- интерфейс запуска генератора (CLI / цели make) и как он делит режимы
|
||||
@@ -454,4 +549,7 @@ pytest-тест с маркером `perf` и таймаутом-обрубан
|
||||
оценки мастер-спеки (раздел 9) — и в каком контейнере он живёт (#41);
|
||||
- как фиксируется «зерновой» мир конца этапа 2 (раздел 9 мастер-спеки):
|
||||
с манифестным решением напрашивается мини-манифест зернового мира —
|
||||
форма за #42.
|
||||
форма за #42;
|
||||
- паспорт мира в манифесте (зерно и версия генератора) файл каталога не
|
||||
накрывает: правка цены в CSV меняет мир молча. Манифесту нужен хеш
|
||||
каталога — хвост для #42.
|
||||
|
||||
Reference in New Issue
Block a user