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>
This commit is contained in:
1 parent
bfbaa96696
commit
722dbe22b7
19 files changed
+1591
-210
No files matched your search
@@ -9,7 +9,9 @@
|
||||
|
||||
Что план решает до генерации событий и чем связывает дни между собой:
|
||||
|
||||
- приток — кто и когда впервые появился, и сколько раз вернётся;
|
||||
- приток — кто и когда впервые появился, и сколько раз вернётся; кука
|
||||
помеченного покупателя живёт дольше прочих — это один из двух рычагов
|
||||
метки, второй лежит в воронке дня;
|
||||
- двухкуковые пары — какой человек завёл вторую куку и в какие дни
|
||||
каждая из двух кук обязана оформить заказ;
|
||||
- паспорт куки — устройство и город: они у куки одни и те же во всех её
|
||||
@@ -32,17 +34,14 @@ from functools import lru_cache
|
||||
import numpy as np
|
||||
from numpy.typing import NDArray
|
||||
|
||||
from clickstream_generator import reference, world
|
||||
from clickstream_generator import ids, reference, world
|
||||
from clickstream_generator.seeds import cohort_stream
|
||||
from clickstream_generator.weights import pick
|
||||
|
||||
# Куки живут числами ниже 2^53: выше JSON округляет — тот же довод, что у
|
||||
# `WatchID` в контракте схемы. Граница не достигается: 2^53 сам уже за ней.
|
||||
CLIENT_ID_LIMIT = 2**53
|
||||
|
||||
# Кумулятивные веса: выбор по ним — целочисленный, бросок попадает в чью-то
|
||||
# долю общего веса.
|
||||
_RETURN_COUNT_CUMULATIVE = np.cumsum(world.RETURN_COUNT_WEIGHTS)
|
||||
_BUYER_RETURN_COUNT_CUMULATIVE = np.cumsum(world.BUYER_RETURN_COUNT_WEIGHTS)
|
||||
_RETURN_DELAY_CUMULATIVE = np.cumsum(world.RETURN_DELAY_WEIGHTS)
|
||||
_CITY_CUMULATIVE = np.cumsum([city.weight for city in reference.CITIES])
|
||||
_DEVICE_CUMULATIVE = np.cumsum(
|
||||
@@ -182,15 +181,19 @@ def cohort(seed: int, day: int) -> Cohort:
|
||||
]
|
||||
|
||||
cookies = people + paired.size
|
||||
client_id = rng.integers(1, CLIENT_ID_LIMIT, cookies, dtype=np.uint64)
|
||||
# Кука живёт числом ниже 2^53 — тот же потолок, что у номера события:
|
||||
# выше JSON округляет при разборе. Граница не достигается.
|
||||
client_id = rng.integers(1, ids.LIMIT, cookies, dtype=np.uint64)
|
||||
birth_day = np.full(cookies, day, dtype=np.int64)
|
||||
# Вторая кука рождается, пока человек ещё ходит: тем же затухающим
|
||||
# профилем, что и возвраты, — обычно через дни, изредка через месяцы.
|
||||
# Фиксированного зазора нет, иначе пары в данных узнавались бы по нему.
|
||||
birth_day[people:] += 1 + pick(rng, _RETURN_DELAY_CUMULATIVE, paired.size)
|
||||
|
||||
# Вторая кука принадлежит покупателю — как и первая кука его пары.
|
||||
buyer_cookie = np.concatenate((buyer, np.ones(paired.size, dtype=bool)))
|
||||
active_cookie, active_day = _active_days(
|
||||
rng, birth_day, day + world.RETURN_TAIL_DAYS
|
||||
rng, birth_day, buyer_cookie, day + world.RETURN_TAIL_DAYS
|
||||
)
|
||||
twins = np.column_stack((paired, np.arange(people, cookies, dtype=np.int64)))
|
||||
pair_cookies, pair_order_days = _assign_orders(
|
||||
@@ -204,8 +207,7 @@ def cohort(seed: int, day: int) -> Cohort:
|
||||
people=people,
|
||||
client_id=client_id,
|
||||
birth_day=birth_day,
|
||||
# Вторая кука принадлежит покупателю — как и первая кука его пары.
|
||||
buyer=np.concatenate((buyer, np.ones(paired.size, dtype=bool))),
|
||||
buyer=buyer_cookie,
|
||||
active_cookie=active_cookie,
|
||||
active_day=active_day,
|
||||
pair_cookies=pair_cookies,
|
||||
@@ -271,13 +273,30 @@ def _influx(rng: np.random.Generator, day: int) -> int:
|
||||
|
||||
|
||||
def _active_days(
|
||||
rng: np.random.Generator, birth_day: NDArray[np.int64], window_end: int
|
||||
rng: np.random.Generator,
|
||||
birth_day: NDArray[np.int64],
|
||||
buyer: NDArray[np.bool_],
|
||||
window_end: int,
|
||||
) -> tuple[NDArray[np.int64], NDArray[np.int64]]:
|
||||
"""Дни активности каждой куки: день рождения и возвраты, пока окно открыто."""
|
||||
"""Дни активности каждой куки: день рождения и возвраты, пока окно открыто.
|
||||
|
||||
Помеченный планом покупатель живёт дольше прочих: одноразовым бывает
|
||||
много реже и возвращается чаще. Это первый из двух рычагов метки
|
||||
(второй — воронка дня): метки в событии нет, поэтому «постоянный
|
||||
покупатель» читается в данных только как кука, которая ходит неделями и
|
||||
покупает не раз. Одним лифтом конверсии этого не добиться — кука живёт
|
||||
меньше двух визитов за снимок, и второй покупке негде случиться (спека
|
||||
генератора, раздел 9).
|
||||
"""
|
||||
cookies = birth_day.size
|
||||
returns = np.zeros(cookies, dtype=np.int64)
|
||||
returning = rng.integers(0, 100, cookies) >= world.ONE_SHOT_PERCENT
|
||||
returns[returning] = 1 + pick(rng, _RETURN_COUNT_CUMULATIVE, int(returning.sum()))
|
||||
one_shot = np.where(buyer, world.BUYER_ONE_SHOT_PERCENT, world.ONE_SHOT_PERCENT)
|
||||
returning = rng.integers(0, 100, cookies) >= one_shot
|
||||
for here, weights in (
|
||||
(returning & ~buyer, _RETURN_COUNT_CUMULATIVE),
|
||||
(returning & buyer, _BUYER_RETURN_COUNT_CUMULATIVE),
|
||||
):
|
||||
returns[here] = 1 + pick(rng, weights, int(here.sum()))
|
||||
|
||||
owner = np.repeat(np.arange(cookies, dtype=np.int64), returns)
|
||||
delay = 1 + pick(rng, _RETURN_DELAY_CUMULATIVE, owner.size)
|
||||
|
||||
Reference in new issue
Block a user