feat(generator): заказы дня — проекция покупок с деньгами магазина

- Зачем:
  - тикет #90: второй источник должен смотреть на торговую половину дня
    глазами бэкенда — заказ есть проекция покупки, а не второе порождение;
    учебный результат — два источника согласованы по построению, а не
    сверкой.
- Что:
  - `commerce` отдаёт вторым выходом покупки дня: номер заказа, корзину,
    выручку клиента, промокод и человека за кукой; новых бросков в
    подпоток `COMMERCE` не добавилось, события дня не сдвинулись.
  - заведена заказная сторона: подпоток `ORDERS` на позиции 2 дерева зерна
    (прежнее мёртвое имя `DISCREPANCIES`), ветвление — по дню рождения
    заказа; `LATECOMERS` не тронут, его наполнит этап 6.
  - новый модуль `orders.py`: деньги заказа целыми копейками —
    `items_total` из корзины, `discount` по таблице «код → скидка»
    (округление вниз), `delivery` броском по таблице весов на полную длину
    дня, `total` = `items_total` − `discount` + `delivery`.
  - план состава завёл `person_id` — последним броском когорты, после
    паспортов: вторая кука пары повторяет ID своего человека, заказ
    показывает его как `user_id`, в событие Метрики он не попадает.
  - `world`: окно изменяемости `ORDER_WINDOW_DAYS` = 7 рядом с D0 и поясом,
    черновая таблица стоимости доставки.
  - слепок дня в тестах сторожит обе половины: заказы сравниваются наравне
    с потоком событий.
  - доки: имя подпотока заказной стороны и состав денег заказа записаны в
    `docs/architecture/orders`, README генератора знает про новый модуль.
- Проверка:
  - из `generator/`: make lint, make typecheck, make test (417 тестов);
    в корне — make lint.
  - опись мира не покраснела: байты восьми дней те же, заказная сторона
    события не сдвинула.

Closes #90

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
ddadminandClaude Opus 5 committed 2026-08-18 12:30:13 +03:00
1 parent 716eafe865
commit b2fcc7e593
14 files changed
+650 -52

No files matched your search

+34 -15
View File
@@ -36,15 +36,21 @@
гарантия, ради неё мы сужаем свободу старта. Цена названа — около 24
визитов в день из ~9,5 тыс. не начинаются в последние минуты суток.
**Шов с торговыми событиями.** Поток несёт, кроме колонок, два выровненных
по строкам ряда: `page` — какая это страница магазина, и `product` — какой
товар показывала карточка (−1 у прочих страниц). По ним `commerce` знает и
то, куда сажать событие (карточка, подтверждение), и то, что посетитель на
самом деле смотрел: товар в корзине, которого никто не открывал, — видимая
глупость в воронке. Визит с назначенным заказом всегда доходит до
`/confirmation`, а перед корзиной у него всегда есть карточка товара.
Случайность у половин разная: трафик берёт подпоток `TRAFFIC`, торговля —
`COMMERCE`, и правка одной не сдвигает другую.
**Шов с торговыми событиями.** Поток несёт, кроме колонок, три выровненных
по строкам ряда: `page` — какая это страница магазина, `product` — какой
товар показывала карточка (−1 у прочих страниц), и `person` — человек за
кукой этой строки. По первым двум `commerce` знает и то, куда сажать событие
(карточка, подтверждение), и то, что посетитель на самом деле смотрел: товар
в корзине, которого никто не открывал, — видимая глупость в воронке. Третий
в события не попадает вовсе: личность нужна заказу, а кликстрим анонимен.
Визит с назначенным заказом всегда доходит до `/confirmation`, а перед
корзиной у него всегда есть карточка товара. Случайность у половин разная:
трафик берёт подпоток `TRAFFIC`, торговля — `COMMERCE`, заказы — свой, и
правка одной не сдвигает другие.
**Заказы бэкенда** день отдаёт второй половиной: покупки, которые собрала
торговая половина, заказная превращает в заказы дня — с личностью
покупателя и деньгами магазина (`orders`).
"""
from dataclasses import dataclass
@@ -53,7 +59,7 @@ from typing import Any
import numpy as np
from numpy.typing import NDArray
from clickstream_generator import catalog, commerce, ids, plan, reference, world
from clickstream_generator import catalog, commerce, ids, orders, plan, reference, world
from clickstream_generator.reference import Page
from clickstream_generator.seeds import Component, day_stream
from clickstream_generator.weights import pick, pick_row
@@ -78,18 +84,22 @@ _SOURCE_CUMULATIVE = np.cumsum([source.weight for source in reference.TRAFFIC_SO
@dataclass(frozen=True, slots=True)
class Day:
"""Поток событий одного дня: колонки выгрузки и страницы за ними.
"""Прожитый день: поток событий трекера и заказы бэкенда.
Строки упорядочены по времени — так их и проиграет проигрыватель.
`columns` — колонки контракта схемы по его порядку, все до одной;
`page` и `product` выровнены по тем же строкам (см. шов в докстринге
модуля).
модуля). `orders` — вторая половина дня: заказы, рождённые в нём.
Половины уезжают разными командами и в разные топики, но считаются
вместе: заказ — проекция покупки, и порознь им разойтись негде.
"""
day: int
columns: dict[str, NDArray[Any]]
page: NDArray[np.uint8]
product: NDArray[np.int64]
orders: orders.Orders
def __len__(self) -> int:
return self.page.size
@@ -149,15 +159,24 @@ def stream(seed: int, day: int) -> Day:
order = np.lexsort((columns["WatchID"], columns["UTCEventTime"]))
# Торговые события садятся на готовый трафиковый поток и отдают его
# целиком: в нём же они и упорядочиваются.
columns, page, product = commerce.weave(
# целиком: в нём же они и упорядочиваются. Вторым выходом приходят
# покупки дня — из них заказная сторона собирает заказы.
person = np.repeat(audience.person_id[visits.cookie], visits.pages)
columns, page, product, purchases = commerce.weave(
seed,
day,
{name: value[order] for name, value in columns.items()},
page[alive][order],
product[alive][order],
person[alive][order],
)
return Day(
day=day,
columns=columns,
page=page,
product=product,
orders=orders.of_day(seed, day, purchases),
)
return Day(day=day, columns=columns, page=page, product=product)
def _visits(rng: np.random.Generator, audience: plan.DayAudience) -> _Visits: