Продолжение #40 в той же ветке feat/40-trade-events. Тикет заведён отдельно,
потому что #40 уже отчитан и закрыт по своим критериям: дописывать в него
новую работу значило бы портить след.
Все числа ниже замерены на каноническом зерне seeds.CANONICAL_SEED
(20260601) за 14 дней — том самом мире, на котором стоят тесты.
Зачем
Приёмка #40 нашла в торговых данных два точных равенства, каких в живом
магазине не бывает.
Первое. Множество визитов с событием корзины в точности совпадает с
множеством визитов, открывших страницу /cart. Проверено на всех 14 днях:
706 и 706, 828 и 828, 690 и 690 — ни одного расхождения. Значит, факт
«положил в корзину» идеально предсказывается более поздним просмотром
страницы. В жизни наоборот: положить и корзину не открыть — самый массовый
сюжет магазина.
Второе. Внутри визита, дошедшего до корзины, в неё ложатся все разные
карточки, которые он открывал: 100%. Вне такого визита — 0%. Решение
принимается один раз на визит, а не на товар, поэтому привлекательность
товара измерить нечем: конверсия «просмотр карточки → корзина» по sku даёт
чистый шум, а вопрос «какие товары кладут чаще» ответа не имеет. По всему дню
до корзины доходит 6,8% просмотров карточек — и эти 6,8% размазаны ровно.
Оба равенства аналитик читает как склейку в разметке, а не как поведение
людей. Решения по обеим правкам приняты и записаны в спеку генератора,
раздел 9, блок «Решено при исполнении #40 после приёмки (2026-08-02)» —
тикет их не пересказывает, а исполняет.
Замер до правки (канонический мир, 14 дней)
величина
значение
дневная аудитория
6637–7595, средняя 7166
визитов в день
9879
событий в день
45 951 – 52 770, средних 49 509
просмотров карточек в день
26 903
событий корзины в день
1836, в 765 визитах
визиты с событием корзины / со страницей /cart
совпадают все 14 дней
просмотров карточек, дошедших до корзины
6,82%
конверсия визита в покупку
2,43%
позиций на заказ
1,87; однопозиционных заказов 50,3%
брошено позиций в покупающих корзинах
12,0%
Что сделать
Отвязать корзину от страницы корзины. Класть может любой визит, открывший
карточку. Страница /cart остаётся шагом воронки и решается как прежде — в
день-функции, торговая половина её решение не трогает. Заказ по-прежнему
рисуется только у визита, дошедшего до подтверждения.
Ввести уровень спроса у товара. Колонка каталога с тремя значениями
(магнит, обычный, залёживается); вероятности трёх уровней — числа мира в world.py, рядом с остальными. Уровень назначается каждой строке каталога и
живёт в файле: это постоянная часть мира, а не бросок дня.
Что решать исполнителю
Форма вероятности — за исполнителем, и это главная работа тикета. Вилки мира
узкие, а требования тянут в разные стороны:
у визита, дошедшего до /cart, хотя бы одна позиция в корзине есть всегда:
страница корзины при пустой корзине невозможна. Это включение остаётся
намеренным — ломается не оно, а обратное;
при этом таких визитов около 765 в день, и если вероятность у них та же,
что у прочих, почти всем придётся «спасать» позицию принудительно — тогда
корзина этих визитов схлопнется до одной позиции, а заказ станет почти
всегда однопозиционным (сейчас 1,87 позиции на заказ, половина заказов
однопозиционные);
намерение визита в жизни на добавление влияет, и корреляция «открыл
корзину — что-то положил» законна. Запрещено не влияние, а точное
совпадение множеств именно этой пары;
бюджет событий держится тем, что общая доля просмотров карточек, доходящих
до корзины, остаётся около нынешних 6,8%: меняется не сколько кладут, а кто
и что.
Откуда брать намерение — из шага воронки, из метки покупателя, из того и
другого — решает исполнитель и записывает довод в отчёт.
Критерии приёмки
Множество визитов с событием корзины больше не совпадает со множеством
визитов со страницей /cart: визиты, положившие товар и корзину не
открывавшие, составляют не менее трети всех визитов с событием корзины.
Сторож меряет долю, а не факт различия одним визитом.
У каждого визита, дошедшего до /cart, в корзине хотя бы одна позиция.
Проверено сторожем на нескольких днях недели, а не наблюдением на одном.
Запрет на точное совпадение касается только пары «событие корзины ↔
страница /cart». Прочие точные соответствия в торговой половине
намеренны и остаются: подтверждение ↔ покупка, цель ↔ породившее её
событие. Их сторожа должны остаться зелёными.
data/catalog/products.csv несёт уровень спроса у каждой строки; три
значения; вероятности уровней — именованные числа в world.py.
Конверсия «просмотр карточки → корзина» различима по уровням спроса: за
14 канонических дней доля разных карточек, дошедших до корзины, у
магнитов не менее чем вдвое выше, чем у залёживающихся, и порядок трёх
уровней строгий. Знаменатель назвать в тесте явно: просмотры карточек
товаров этого уровня, повторный просмотр внутри визита считается один
раз — так же, как считается позиция.
Бюджет событий не поехал: доля просмотров карточек, дошедших до
корзины, остаётся в пределах 5–9% (сейчас 6,82%), средний день — в
пределах 48–52 тыс. событий (сейчас 49 509).
Вилки раздела 5 держатся: дневная аудитория 6–8 тыс., конверсия визита
в покупку 2,0–3,0% (сейчас 2,43%).
Заказ по-прежнему уже корзины: среднее число позиций на заказ 1,6–2,2
(сейчас 1,87), доля однопозиционных заказов 40–60% (сейчас 50,3%), доля
брошенных позиций внутри покупающих корзин ненулевая (сейчас 12,0%).
Трафиковая половина не сдвинулась: набор просмотров страниц (страница,
время, WatchID) на каноническом дне совпадает со снимком, снятым до
правки. Снимок снять первым ходом, до изменений — иначе сверять будет
не с чем.
Детерминизм не ослаб: существующий сторож повторяемости дня зелёный и
накрывает всё, что тикет добавил.
Документация правится тем же изменением: измеренные числа в спеке
генератора (раздел 9, блок «после приёмки») приведены к новым; модуль catalog.py больше не утверждает, что популярность товаров не
моделируется.
make lint, make typecheck, make test зелёные.
Границы
Воронку день-функции не трогать: доли CART_PERCENT, CHECKOUT_OF_CART_PERCENT, CONFIRMATION_OF_CHECKOUT_PERCENT и их
«покупательские» близнецы решают страницы, и решают их как прежде. Если
калибровка требует их сдвинуть — это решение уровня владельца, вернуть
вопросом, а не двигать молча.
План состава мира (#38) не трогать: метка покупателя, пары, приток.
Контракт схемы события и сериализатор не трогать: новых колонок события
тикет не заводит. Уровень спроса — свойство каталога, в событие не попадает.
Форма файла каталога решена (спека, раздел 9): колонка добавляется, порядок
и смысл прежних колонок не меняется. Словарь ClickHouse над тем же файлом
ещё не построен (#37/#43) — сдвиг сейчас дёшев, но заметить его в спеке
стоит.
Коммит не делать.
Сначала прочитать
docs/specs/2026-08-01-generator.md — раздел 5 (вилки мира) и раздел 9,
блоки «Решено при исполнении #39», «#40» и «#40 после приёмки». Последний —
источник истины по этому тикету.
generator/src/clickstream_generator/day.py — воронка, чтобы понимать, что
именно не трогаем.
Проверка
cd generator
make lint
make typecheck
make test
Замер мира — своим скриптом на 14 днях канонического зерна. Осторожно с
маской: строка покупки тоже несёт массивы товаров, поэтому «строка с
товарами» — это не «событие корзины»; событие корзины — строка с товарами и
без номера заказа. На этом уже споткнулась постановка: первый замер завысил
события корзины на число покупок. Числа замера — в отчёт.
Part of #4
Продолжение #40 в той же ветке `feat/40-trade-events`. Тикет заведён отдельно,
потому что #40 уже отчитан и закрыт по своим критериям: дописывать в него
новую работу значило бы портить след.
Все числа ниже замерены на каноническом зерне `seeds.CANONICAL_SEED`
(20260601) за 14 дней — том самом мире, на котором стоят тесты.
## Зачем
Приёмка #40 нашла в торговых данных два точных равенства, каких в живом
магазине не бывает.
**Первое.** Множество визитов с событием корзины в точности совпадает с
множеством визитов, открывших страницу `/cart`. Проверено на всех 14 днях:
706 и 706, 828 и 828, 690 и 690 — ни одного расхождения. Значит, факт
«положил в корзину» идеально предсказывается более поздним просмотром
страницы. В жизни наоборот: положить и корзину не открыть — самый массовый
сюжет магазина.
**Второе.** Внутри визита, дошедшего до корзины, в неё ложатся все разные
карточки, которые он открывал: 100%. Вне такого визита — 0%. Решение
принимается один раз на визит, а не на товар, поэтому привлекательность
товара измерить нечем: конверсия «просмотр карточки → корзина» по sku даёт
чистый шум, а вопрос «какие товары кладут чаще» ответа не имеет. По всему дню
до корзины доходит 6,8% просмотров карточек — и эти 6,8% размазаны ровно.
Оба равенства аналитик читает как склейку в разметке, а не как поведение
людей. Решения по обеим правкам приняты и записаны в спеку генератора,
раздел 9, блок «Решено при исполнении #40 после приёмки (2026-08-02)» —
тикет их не пересказывает, а исполняет.
## Замер до правки (канонический мир, 14 дней)
| величина | значение |
| --- | --- |
| дневная аудитория | 6637–7595, средняя 7166 |
| визитов в день | 9879 |
| событий в день | 45 951 – 52 770, средних 49 509 |
| просмотров карточек в день | 26 903 |
| событий корзины в день | 1836, в 765 визитах |
| визиты с событием корзины / со страницей `/cart` | совпадают все 14 дней |
| просмотров карточек, дошедших до корзины | 6,82% |
| конверсия визита в покупку | 2,43% |
| позиций на заказ | 1,87; однопозиционных заказов 50,3% |
| брошено позиций в покупающих корзинах | 12,0% |
## Что сделать
**Отвязать корзину от страницы корзины.** Класть может любой визит, открывший
карточку. Страница `/cart` остаётся шагом воронки и решается как прежде — в
день-функции, торговая половина её решение не трогает. Заказ по-прежнему
рисуется только у визита, дошедшего до подтверждения.
**Ввести уровень спроса у товара.** Колонка каталога с тремя значениями
(магнит, обычный, залёживается); вероятности трёх уровней — числа мира в
`world.py`, рядом с остальными. Уровень назначается каждой строке каталога и
живёт в файле: это постоянная часть мира, а не бросок дня.
## Что решать исполнителю
Форма вероятности — за исполнителем, и это главная работа тикета. Вилки мира
узкие, а требования тянут в разные стороны:
- у визита, дошедшего до `/cart`, хотя бы одна позиция в корзине есть всегда:
страница корзины при пустой корзине невозможна. Это включение остаётся
намеренным — ломается не оно, а обратное;
- при этом таких визитов около 765 в день, и если вероятность у них та же,
что у прочих, почти всем придётся «спасать» позицию принудительно — тогда
корзина этих визитов схлопнется до одной позиции, а заказ станет почти
всегда однопозиционным (сейчас 1,87 позиции на заказ, половина заказов
однопозиционные);
- намерение визита в жизни на добавление влияет, и корреляция «открыл
корзину — что-то положил» законна. Запрещено не влияние, а точное
совпадение множеств именно этой пары;
- бюджет событий держится тем, что общая доля просмотров карточек, доходящих
до корзины, остаётся около нынешних 6,8%: меняется не сколько кладут, а кто
и что.
Откуда брать намерение — из шага воронки, из метки покупателя, из того и
другого — решает исполнитель и записывает довод в отчёт.
## Критерии приёмки
- [x] Множество визитов с событием корзины больше не совпадает со множеством
визитов со страницей `/cart`: визиты, положившие товар и корзину не
открывавшие, составляют не менее трети всех визитов с событием корзины.
Сторож меряет долю, а не факт различия одним визитом.
- [x] У каждого визита, дошедшего до `/cart`, в корзине хотя бы одна позиция.
Проверено сторожем на нескольких днях недели, а не наблюдением на одном.
- [x] Запрет на точное совпадение касается только пары «событие корзины ↔
страница `/cart`». Прочие точные соответствия в торговой половине
намеренны и остаются: подтверждение ↔ покупка, цель ↔ породившее её
событие. Их сторожа должны остаться зелёными.
- [x] `data/catalog/products.csv` несёт уровень спроса у каждой строки; три
значения; вероятности уровней — именованные числа в `world.py`.
- [x] Конверсия «просмотр карточки → корзина» различима по уровням спроса: за
14 канонических дней доля разных карточек, дошедших до корзины, у
магнитов не менее чем вдвое выше, чем у залёживающихся, и порядок трёх
уровней строгий. Знаменатель назвать в тесте явно: просмотры карточек
товаров этого уровня, повторный просмотр внутри визита считается один
раз — так же, как считается позиция.
- [x] Бюджет событий не поехал: доля просмотров карточек, дошедших до
корзины, остаётся в пределах 5–9% (сейчас 6,82%), средний день — в
пределах 48–52 тыс. событий (сейчас 49 509).
- [x] Вилки раздела 5 держатся: дневная аудитория 6–8 тыс., конверсия визита
в покупку 2,0–3,0% (сейчас 2,43%).
- [x] Заказ по-прежнему уже корзины: среднее число позиций на заказ 1,6–2,2
(сейчас 1,87), доля однопозиционных заказов 40–60% (сейчас 50,3%), доля
брошенных позиций внутри покупающих корзин ненулевая (сейчас 12,0%).
- [x] Трафиковая половина не сдвинулась: набор просмотров страниц (страница,
время, `WatchID`) на каноническом дне совпадает со снимком, снятым до
правки. Снимок снять первым ходом, до изменений — иначе сверять будет
не с чем.
- [x] Детерминизм не ослаб: существующий сторож повторяемости дня зелёный и
накрывает всё, что тикет добавил.
- [x] Документация правится тем же изменением: измеренные числа в спеке
генератора (раздел 9, блок «после приёмки») приведены к новым; модуль
`catalog.py` больше не утверждает, что популярность товаров не
моделируется.
- [x] `make lint`, `make typecheck`, `make test` зелёные.
## Границы
- Воронку день-функции не трогать: доли `CART_PERCENT`,
`CHECKOUT_OF_CART_PERCENT`, `CONFIRMATION_OF_CHECKOUT_PERCENT` и их
«покупательские» близнецы решают страницы, и решают их как прежде. Если
калибровка требует их сдвинуть — это решение уровня владельца, вернуть
вопросом, а не двигать молча.
- План состава мира (#38) не трогать: метка покупателя, пары, приток.
- Контракт схемы события и сериализатор не трогать: новых колонок события
тикет не заводит. Уровень спроса — свойство каталога, в событие не попадает.
- Форма файла каталога решена (спека, раздел 9): колонка добавляется, порядок
и смысл прежних колонок не меняется. Словарь ClickHouse над тем же файлом
ещё не построен (#37/#43) — сдвиг сейчас дёшев, но заметить его в спеке
стоит.
- Коммит не делать.
## Сначала прочитать
- `docs/specs/2026-08-01-generator.md` — раздел 5 (вилки мира) и раздел 9,
блоки «Решено при исполнении #39», «#40» и «#40 после приёмки». Последний —
источник истины по этому тикету.
- `CONTEXT.md` — термины «Уровень спроса», «Торговое событие», «Каталог
товаров», «Покупатель», «Визит».
- `generator/src/clickstream_generator/commerce.py` — вся торговая половина;
правка живёт в основном в `_baskets` и `_kept`.
- `generator/src/clickstream_generator/catalog.py`,
`data/catalog/products.csv`, `generator/src/clickstream_generator/world.py`.
- `generator/src/clickstream_generator/day.py` — воронка, чтобы понимать, что
именно не трогаем.
## Проверка
```
cd generator
make lint
make typecheck
make test
```
Замер мира — своим скриптом на 14 днях канонического зерна. Осторожно с
маской: строка покупки тоже несёт массивы товаров, поэтому «строка с
товарами» — это не «событие корзины»; событие корзины — строка с товарами и
без номера заказа. На этом уже споткнулась постановка: первый замер завысил
события корзины на число покупок. Числа замера — в отчёт.
Что получилось. Класть в корзину теперь может любой визит, открывший
карточку: 41,4% кладущих визитов корзину не открывают вовсе — раньше таких
не было ни одного. У визита, дошедшего до /cart, корзина по-прежнему
непуста, ни разу за 14 дней. У товара появился уровень спроса, и конверсия
«просмотр карточки → корзина» стала различима: 13,1% у магнитов, 9,2% у
обычных, 5,8% у залёживающихся — магнит выше залёживающегося в 2,25 раза.
По отдельным товарам разброс от 15,4% до 5,1%.
Числа мира после правки (14 дней канонического зерна): средний день
49 834 события, конверсия карточки в корзину 8,62%, конверсия визита в
покупку 2,43%, аудитория 6637–7595, позиций на заказ 1,74, однопозиционных
заказов 56,4%, брошено 7,25% позиций в покупающих корзинах. Трафиковая
половина побайтово прежняя — sha256 по всем просмотрам за 14 дней совпал со
снимком до правки.
Решение уровня задачи, принятое исполнителем: намерение визита берётся
из шага воронки, а не из метки покупателя — метка уже действует через
воронку, и второе слагаемое посчитало бы её дважды, протянув план состава
сквозь шов между днём и торговой половиной. Двух рядов вероятностей вместо
одного ряда с множителем потребовал потолок: у визита с намерением ряд
обязан быть высоким, иначе корзина схлопывается, а у потолка разница уровней
съедается.
Ревью. Две слепые линии, пересечение — ноль. Линия задачи и вкуса —
7 находок (1 MAJOR), линия кода — 1 MINOR при 120 командах. Триаж: 9 FIXED,
0 отклонено, два круга; обе перепроверки закрыли свои находки.
Крупнейшая находка: нулевая вероятность у залёживающегося товара возвращала
ровно то тождество, ради уничтожения которого тикет заведён, — для целого
уровня спроса и 27% каталога. Закрыта не тем маршрутом, который предложил
ревьюер: предложенный ряд был перемерен и отвергнут (давал магнит ÷
залёживается = 1,84 при требуемых 2), ревьюер это подтвердил своим замером.
Вторая по весу: уровни спроса были разложены идеально ровно, 6/16/8 в каждой
категории, — каталог пересыпан целиком, все числа перемерены.
Сверх тикета, обе линии признали обоснованным: ABANDONED_POSITION_PERCENT
15 → 10 (корзина сузилась, прежняя доля выносила её целиком) и правка
мастер-спеки, раздел 3 — список колонок каталога, по которому будет строиться
словарь ClickHouse (#37/#43).
Проверка: make lint && make typecheck && make test из корня
репозитория — 394 passed (было 383).
Осталось владельцу, не блокирует: конверсия карточки в корзину 8,62% при
вилке 5–9% — запаса 0,4 п.п., и обе границы тикета давят вверх. Следующая
правка торговой половины этот запас потратит; вилку стоит пересмотреть до
того, как #42 заморозит манифест.
Сделано, коммит в ветке `feat/40-trade-events`.
**Что получилось.** Класть в корзину теперь может любой визит, открывший
карточку: 41,4% кладущих визитов корзину не открывают вовсе — раньше таких
не было ни одного. У визита, дошедшего до `/cart`, корзина по-прежнему
непуста, ни разу за 14 дней. У товара появился уровень спроса, и конверсия
«просмотр карточки → корзина» стала различима: 13,1% у магнитов, 9,2% у
обычных, 5,8% у залёживающихся — магнит выше залёживающегося в 2,25 раза.
По отдельным товарам разброс от 15,4% до 5,1%.
**Числа мира после правки** (14 дней канонического зерна): средний день
49 834 события, конверсия карточки в корзину 8,62%, конверсия визита в
покупку 2,43%, аудитория 6637–7595, позиций на заказ 1,74, однопозиционных
заказов 56,4%, брошено 7,25% позиций в покупающих корзинах. Трафиковая
половина побайтово прежняя — sha256 по всем просмотрам за 14 дней совпал со
снимком до правки.
**Решение уровня задачи, принятое исполнителем**: намерение визита берётся
из шага воронки, а не из метки покупателя — метка уже действует через
воронку, и второе слагаемое посчитало бы её дважды, протянув план состава
сквозь шов между днём и торговой половиной. Двух рядов вероятностей вместо
одного ряда с множителем потребовал потолок: у визита с намерением ряд
обязан быть высоким, иначе корзина схлопывается, а у потолка разница уровней
съедается.
**Ревью.** Две слепые линии, пересечение — ноль. Линия задачи и вкуса —
7 находок (1 MAJOR), линия кода — 1 MINOR при 120 командах. Триаж: 9 FIXED,
0 отклонено, два круга; обе перепроверки закрыли свои находки.
Крупнейшая находка: нулевая вероятность у залёживающегося товара возвращала
ровно то тождество, ради уничтожения которого тикет заведён, — для целого
уровня спроса и 27% каталога. Закрыта не тем маршрутом, который предложил
ревьюер: предложенный ряд был перемерен и отвергнут (давал магнит ÷
залёживается = 1,84 при требуемых 2), ревьюер это подтвердил своим замером.
Вторая по весу: уровни спроса были разложены идеально ровно, 6/16/8 в каждой
категории, — каталог пересыпан целиком, все числа перемерены.
**Сверх тикета**, обе линии признали обоснованным: `ABANDONED_POSITION_PERCENT`
15 → 10 (корзина сузилась, прежняя доля выносила её целиком) и правка
мастер-спеки, раздел 3 — список колонок каталога, по которому будет строиться
словарь ClickHouse (#37/#43).
**Проверка**: `make lint && make typecheck && make test` из корня
репозитория — 394 passed (было 383).
**Осталось владельцу, не блокирует**: конверсия карточки в корзину 8,62% при
вилке 5–9% — запаса 0,4 п.п., и обе границы тикета давят вверх. Следующая
правка торговой половины этот запас потратит; вилку стоит пересмотреть до
того, как #42 заморозит манифест.
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
Part of #4
Продолжение #40 в той же ветке
feat/40-trade-events. Тикет заведён отдельно,потому что #40 уже отчитан и закрыт по своим критериям: дописывать в него
новую работу значило бы портить след.
Все числа ниже замерены на каноническом зерне
seeds.CANONICAL_SEED(20260601) за 14 дней — том самом мире, на котором стоят тесты.
Зачем
Приёмка #40 нашла в торговых данных два точных равенства, каких в живом
магазине не бывает.
Первое. Множество визитов с событием корзины в точности совпадает с
множеством визитов, открывших страницу
/cart. Проверено на всех 14 днях:706 и 706, 828 и 828, 690 и 690 — ни одного расхождения. Значит, факт
«положил в корзину» идеально предсказывается более поздним просмотром
страницы. В жизни наоборот: положить и корзину не открыть — самый массовый
сюжет магазина.
Второе. Внутри визита, дошедшего до корзины, в неё ложатся все разные
карточки, которые он открывал: 100%. Вне такого визита — 0%. Решение
принимается один раз на визит, а не на товар, поэтому привлекательность
товара измерить нечем: конверсия «просмотр карточки → корзина» по sku даёт
чистый шум, а вопрос «какие товары кладут чаще» ответа не имеет. По всему дню
до корзины доходит 6,8% просмотров карточек — и эти 6,8% размазаны ровно.
Оба равенства аналитик читает как склейку в разметке, а не как поведение
людей. Решения по обеим правкам приняты и записаны в спеку генератора,
раздел 9, блок «Решено при исполнении #40 после приёмки (2026-08-02)» —
тикет их не пересказывает, а исполняет.
Замер до правки (канонический мир, 14 дней)
/cartЧто сделать
Отвязать корзину от страницы корзины. Класть может любой визит, открывший
карточку. Страница
/cartостаётся шагом воронки и решается как прежде — вдень-функции, торговая половина её решение не трогает. Заказ по-прежнему
рисуется только у визита, дошедшего до подтверждения.
Ввести уровень спроса у товара. Колонка каталога с тремя значениями
(магнит, обычный, залёживается); вероятности трёх уровней — числа мира в
world.py, рядом с остальными. Уровень назначается каждой строке каталога иживёт в файле: это постоянная часть мира, а не бросок дня.
Что решать исполнителю
Форма вероятности — за исполнителем, и это главная работа тикета. Вилки мира
узкие, а требования тянут в разные стороны:
/cart, хотя бы одна позиция в корзине есть всегда:страница корзины при пустой корзине невозможна. Это включение остаётся
намеренным — ломается не оно, а обратное;
что у прочих, почти всем придётся «спасать» позицию принудительно — тогда
корзина этих визитов схлопнется до одной позиции, а заказ станет почти
всегда однопозиционным (сейчас 1,87 позиции на заказ, половина заказов
однопозиционные);
корзину — что-то положил» законна. Запрещено не влияние, а точное
совпадение множеств именно этой пары;
до корзины, остаётся около нынешних 6,8%: меняется не сколько кладут, а кто
и что.
Откуда брать намерение — из шага воронки, из метки покупателя, из того и
другого — решает исполнитель и записывает довод в отчёт.
Критерии приёмки
визитов со страницей
/cart: визиты, положившие товар и корзину неоткрывавшие, составляют не менее трети всех визитов с событием корзины.
Сторож меряет долю, а не факт различия одним визитом.
/cart, в корзине хотя бы одна позиция.Проверено сторожем на нескольких днях недели, а не наблюдением на одном.
страница
/cart». Прочие точные соответствия в торговой половиненамеренны и остаются: подтверждение ↔ покупка, цель ↔ породившее её
событие. Их сторожа должны остаться зелёными.
data/catalog/products.csvнесёт уровень спроса у каждой строки; тризначения; вероятности уровней — именованные числа в
world.py.14 канонических дней доля разных карточек, дошедших до корзины, у
магнитов не менее чем вдвое выше, чем у залёживающихся, и порядок трёх
уровней строгий. Знаменатель назвать в тесте явно: просмотры карточек
товаров этого уровня, повторный просмотр внутри визита считается один
раз — так же, как считается позиция.
корзины, остаётся в пределах 5–9% (сейчас 6,82%), средний день — в
пределах 48–52 тыс. событий (сейчас 49 509).
в покупку 2,0–3,0% (сейчас 2,43%).
(сейчас 1,87), доля однопозиционных заказов 40–60% (сейчас 50,3%), доля
брошенных позиций внутри покупающих корзин ненулевая (сейчас 12,0%).
время,
WatchID) на каноническом дне совпадает со снимком, снятым доправки. Снимок снять первым ходом, до изменений — иначе сверять будет
не с чем.
накрывает всё, что тикет добавил.
генератора (раздел 9, блок «после приёмки») приведены к новым; модуль
catalog.pyбольше не утверждает, что популярность товаров немоделируется.
make lint,make typecheck,make testзелёные.Границы
CART_PERCENT,CHECKOUT_OF_CART_PERCENT,CONFIRMATION_OF_CHECKOUT_PERCENTи их«покупательские» близнецы решают страницы, и решают их как прежде. Если
калибровка требует их сдвинуть — это решение уровня владельца, вернуть
вопросом, а не двигать молча.
тикет не заводит. Уровень спроса — свойство каталога, в событие не попадает.
и смысл прежних колонок не меняется. Словарь ClickHouse над тем же файлом
ещё не построен (#37/#43) — сдвиг сейчас дёшев, но заметить его в спеке
стоит.
Сначала прочитать
docs/specs/2026-08-01-generator.md— раздел 5 (вилки мира) и раздел 9,блоки «Решено при исполнении #39», «#40» и «#40 после приёмки». Последний —
источник истины по этому тикету.
CONTEXT.md— термины «Уровень спроса», «Торговое событие», «Каталогтоваров», «Покупатель», «Визит».
generator/src/clickstream_generator/commerce.py— вся торговая половина;правка живёт в основном в
_basketsи_kept.generator/src/clickstream_generator/catalog.py,data/catalog/products.csv,generator/src/clickstream_generator/world.py.generator/src/clickstream_generator/day.py— воронка, чтобы понимать, чтоименно не трогаем.
Проверка
Замер мира — своим скриптом на 14 днях канонического зерна. Осторожно с
маской: строка покупки тоже несёт массивы товаров, поэтому «строка с
товарами» — это не «событие корзины»; событие корзины — строка с товарами и
без номера заказа. На этом уже споткнулась постановка: первый замер завысил
события корзины на число покупок. Числа замера — в отчёт.
Сделано, коммит в ветке
feat/40-trade-events.Что получилось. Класть в корзину теперь может любой визит, открывший
карточку: 41,4% кладущих визитов корзину не открывают вовсе — раньше таких
не было ни одного. У визита, дошедшего до
/cart, корзина по-прежнемунепуста, ни разу за 14 дней. У товара появился уровень спроса, и конверсия
«просмотр карточки → корзина» стала различима: 13,1% у магнитов, 9,2% у
обычных, 5,8% у залёживающихся — магнит выше залёживающегося в 2,25 раза.
По отдельным товарам разброс от 15,4% до 5,1%.
Числа мира после правки (14 дней канонического зерна): средний день
49 834 события, конверсия карточки в корзину 8,62%, конверсия визита в
покупку 2,43%, аудитория 6637–7595, позиций на заказ 1,74, однопозиционных
заказов 56,4%, брошено 7,25% позиций в покупающих корзинах. Трафиковая
половина побайтово прежняя — sha256 по всем просмотрам за 14 дней совпал со
снимком до правки.
Решение уровня задачи, принятое исполнителем: намерение визита берётся
из шага воронки, а не из метки покупателя — метка уже действует через
воронку, и второе слагаемое посчитало бы её дважды, протянув план состава
сквозь шов между днём и торговой половиной. Двух рядов вероятностей вместо
одного ряда с множителем потребовал потолок: у визита с намерением ряд
обязан быть высоким, иначе корзина схлопывается, а у потолка разница уровней
съедается.
Ревью. Две слепые линии, пересечение — ноль. Линия задачи и вкуса —
7 находок (1 MAJOR), линия кода — 1 MINOR при 120 командах. Триаж: 9 FIXED,
0 отклонено, два круга; обе перепроверки закрыли свои находки.
Крупнейшая находка: нулевая вероятность у залёживающегося товара возвращала
ровно то тождество, ради уничтожения которого тикет заведён, — для целого
уровня спроса и 27% каталога. Закрыта не тем маршрутом, который предложил
ревьюер: предложенный ряд был перемерен и отвергнут (давал магнит ÷
залёживается = 1,84 при требуемых 2), ревьюер это подтвердил своим замером.
Вторая по весу: уровни спроса были разложены идеально ровно, 6/16/8 в каждой
категории, — каталог пересыпан целиком, все числа перемерены.
Сверх тикета, обе линии признали обоснованным:
ABANDONED_POSITION_PERCENT15 → 10 (корзина сузилась, прежняя доля выносила её целиком) и правка
мастер-спеки, раздел 3 — список колонок каталога, по которому будет строиться
словарь ClickHouse (#37/#43).
Проверка:
make lint && make typecheck && make testиз корнярепозитория — 394 passed (было 383).
Осталось владельцу, не блокирует: конверсия карточки в корзину 8,62% при
вилке 5–9% — запаса 0,4 п.п., и обе границы тикета давят вверх. Следующая
правка торговой половины этот запас потратит; вилку стоит пересмотреть до
того, как #42 заморозит манифест.