feat(generator): корзина отвязана от страницы, у товара — уровень спроса

- Зачем:
  - приёмка #40 нашла в торговых данных два точных равенства, каких в живом
    магазине не бывает: событие корзины случалось ровно у визитов со
    страницей /cart, а внутри такого визита в корзину уходили все открытые
    карточки. Привлекательность товара было нечем измерить, а аналитик читал
    бы эти равенства как склейку в разметке.
- Что:
  - класть в корзину может любой визит, открывший карточку; страница /cart
    осталась шагом воронки, а у визита, дошедшего до неё, корзина непуста.
  - у товара появился уровень спроса — колонка каталога с тремя значениями
    и два ряда вероятностей в числах мира: намерение визита берётся из шага
    воронки, а не из метки покупателя, которая уже действует через неё.
  - уровни рассыпаны по каталогу одной колодой: одинакового расклада по
    категориям нет, связи с ценой нет, и то и другое сторожится тестами.
  - числа мира перемерены: конверсия карточки в корзину 8,62% (по уровням
    13,1 / 9,2 / 5,8), кладут без открытия корзины 41,4% таких визитов,
    средний день 49 834 события, конверсия визита в покупку 2,43%.
  - спека генератора и мастер-спека приведены к новой форме каталога.
- Проверка:
  - make lint && make typecheck && make test — 394 passed (было 383).
  - трафиковая половина побайтово та же: sha256 по (URL, времени, WatchID)
    всех просмотров за 14 канонических дней совпадает со снимком до правки.
This commit is contained in:
2026-08-02 22:46:30 +03:00
parent 4165f10cdd
commit c0f4d21df9
10 changed files with 734 additions and 248 deletions
+18 -1
View File
@@ -6,7 +6,7 @@
«в среднем 3–4 возврата» и «средняя кука активна ≈1,9 дня».
"""
from clickstream_generator import world
from clickstream_generator import catalog, world
def mean_by_weights(values: tuple[int, ...], weights: tuple[int, ...]) -> float:
@@ -150,6 +150,23 @@ def test_the_buyer_mark_shows_on_both_steps_of_the_funnel():
assert 2 <= lift <= 6
def test_the_demand_levels_keep_their_order_and_yield_to_the_intent():
"""Ряды вероятностей читаются по уровням каталога, и оба убывают.
Совпадение длин — не формальность: ряды индексируются номером уровня,
и новый уровень в файле каталога обязан получить здесь своё число.
"""
rows = (world.SHOPPING_ADD_PERCENT, world.BROWSING_ADD_PERCENT)
for row in rows:
assert len(row) == len(catalog.DEMAND_LEVELS)
assert row[0] > row[1] > row[2], row
# Намерение сильнее товара: визит, пришедший покупать, кладёт чаще на
# любом уровне — иначе слово «намерение» ничего бы не значило.
assert all(shopping > browsing for shopping, browsing in zip(*rows, strict=True))
# Но и не всё подряд: товар решает у обоих, спрос не декорация.
assert min(world.SHOPPING_ADD_PERCENT) < 100
def test_the_buyer_mark_makes_the_cookie_live_longer():
"""Второй рычаг метки: без долгой жизни второй покупке негде случиться."""
assert world.BUYER_ONE_SHOT_PERCENT < world.ONE_SHOT_PERCENT