Files
clickstream-data-platform/generator/src/clickstream_generator/ids.py
T
ddadminandClaude Opus 5 722dbe22b7 feat(generator): торговые события — корзина, покупка, сырой ecommerce
- Зачем:
  - клиентская сторона мира становится целой: без add_to_cart и purchase
    в данных нет ни таксономии событий, ни вложенного JSON, ни денег,
    а метка «покупатель» из плана состава ни на что не влияла (#40).
- Что:
  - добавлен модуль commerce: корзина шире заказа, номер заказа вида
    ГГГГММДД-NNNN, промокод без скидки в сумме, сырой ecommerce через orjson;
  - метка покупателя получила два рычага — долгую жизнь куки в плане и
    свою воронку в дне; CART_PERCENT опущен с 8 до 6, чтобы конверсия
    мира осталась около 2%;
  - часть цен каталога получила копейки: productPrice округляется форматом,
    purchaseRevenue несёт точную сумму — расхождение живёт внутри события;
  - граница суток забирает страницу подтверждения вместе с её покупкой:
    потерь на клиентской стороне этот этап не заводит;
  - решения и перемеренные числа мира записаны в спеку генератора, §9.
- Проверка:
  - make lint && make typecheck && make test — 383 passed (было 353).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-02 19:13:30 +03:00

48 lines
3.0 KiB
Python

"""Идентификаторы событий: неповторяющиеся числа, которые переживут JSON.
Уникальность обещана не для красоты: `WatchID` — ключ дедупликации при
переигровке дня (спека генератора, раздел 4), и два одинаковых номера
склеили бы разные события. Строк дня две породы — трафиковые и торговые, —
и рисует их разная случайность, поэтому обещание держится здесь, в одном
месте на весь генератор: у приёма одно определение, иначе дисциплина живёт
копиями и расходится с ними.
"""
import numpy as np
from numpy.typing import NDArray
# Потолок идентификаторов: выше 2^53 числа в JSON (jq, консоль браузера)
# округляются при разборе, и id перестаёт быть собой. Настоящая Метрика так
# не делает — её id длиннее (мастер-спека, раздел 1.1).
LIMIT = 2**53
def unique(rng: np.random.Generator, size: int) -> NDArray[np.uint64]:
"""Неповторяющиеся id ниже 2^53, разбросанные по диапазону.
Не броски наугад, а шаги случайной длины — они не повторяются по
построению, — и потом перемешивание, чтобы номер не выдавал порядок
строк. Средний шаг — весь диапазон, делённый на число событий, поэтому в
среднем ряд занимает его половину.
"""
step = LIMIT // (size + 1)
numbers = np.cumsum(rng.integers(1, step + 1, size, dtype=np.uint64))
return numbers[np.argsort(rng.integers(0, size * size + 1, size), kind="stable")]
def unique_apart_from(
rng: np.random.Generator, size: int, taken: NDArray[np.uint64]
) -> NDArray[np.uint64]:
"""То же, но и с занятыми номерами ряд не пересекается.
Торговые события берут случайность из своего подпотока — иначе правка
торгового поведения сдвинула бы трафик, — а значит, про уже розданные
трафиковые номера их ряд ничего не знает. Совпадение двух рядов на
диапазоне в 2^53 невероятно, но обещание уникальности держит дедуп, и
проверить его дешевле, чем предположить.
"""
while True:
numbers = unique(rng, size)
if not np.intersect1d(numbers, taken).size:
return numbers