- Зачем: - приёмка #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 канонических дней совпадает со снимком до правки.
225 lines
17 KiB
Python
225 lines
17 KiB
Python
"""Конфигурация мира: все числа, которыми задан модельный магазин.
|
|
|
|
Модуль — приглашение крутить: поменяйте число, пересоберите снимок и
|
|
посмотрите, что стало с данными. Правка любой константы здесь — смена мира,
|
|
поэтому чек манифеста честно покраснеет: манифест сторожит только канонический
|
|
мир, свои миры менти собирает без его гарантий (спека генератора, раздел 9).
|
|
|
|
Числа решены спекой и связаны между собой; связки сторожат тесты
|
|
`test_world.py`, чтобы правка одного числа не рассыпала вывод соседнего.
|
|
Здесь только числа — как в контракте схемы, никакого поведения; длинные
|
|
таблицы записаны коротко, но остаются таблицами.
|
|
"""
|
|
|
|
from datetime import date
|
|
|
|
# Счётчик стенда: сайт один, номер — константа мира.
|
|
COUNTER_ID = 42150607
|
|
|
|
# Часовой пояс счётчика, минуты от UTC: Самара, UTC+4. Модельные сутки
|
|
# считаются в этом поясе, как в выгрузке Метрики: `EventDate` — дата в поясе
|
|
# счётчика, `UTCEventTime` — абсолютная метка. Отсюда следствие, о котором
|
|
# сторона хранилища должна знать заранее: `toDate(UTCEventTime)` ≠ `EventDate`
|
|
# у ночных событий (спека генератора, раздел 9).
|
|
COUNTER_TIMEZONE_MINUTES = 240
|
|
|
|
# D0 — первый день оси модельного времени, понедельник. Реальный календарь в
|
|
# модели не участвует: дата нужна лишь затем, чтобы дни оси легли в
|
|
# `EventDate`/`UTCEventTime` конкретными числами. От даты запуска мир не
|
|
# зависит — иначе манифест перестал бы быть воспроизводимым.
|
|
ORIGIN = date(2026, 6, 1)
|
|
|
|
# Приток: сколько новых людей приходит в мир в средний день. Каждый приводит
|
|
# свою куку, поэтому число это же — приток кук; вторые куки двухкуковых пар
|
|
# добавляют к нему меньше процента.
|
|
DAILY_INFLUX = 3_800
|
|
|
|
# Недельная волна мира, проценты от среднего: понедельник … воскресенье.
|
|
# Волна задана притоку — по ней в мир приходят новые люди. Трафик наследует
|
|
# её через дневную аудиторию, а не вторым умножением (довод — у суточной
|
|
# волны выходного дня), поэтому доля новичков по дням недели ровная, а
|
|
# недельный размах трафика выходит мягче притока: ±7% против ±10%. В сумме
|
|
# ровно 700: за неделю средний день остаётся средним.
|
|
WEEKLY_PROFILE_PERCENT = (105, 108, 107, 105, 95, 88, 92)
|
|
|
|
# Разброс притока изо дня в день, проценты: ровный приток выдал бы себя в
|
|
# первом же графике по дням.
|
|
INFLUX_JITTER_PERCENT = 5
|
|
|
|
# Доля одноразовых кук: пришли раз и не вернулись — как в живом трафике.
|
|
ONE_SHOT_PERCENT = 75
|
|
|
|
# Сколько раз возвращается кука, которая вернулась хоть раз: веса для 1, 2,
|
|
# 3 … возвратов. В среднем выходит 3–4 возврата; вместе с одноразовыми это
|
|
# ≈1,9 активного дня на куку — отсюда дневная аудитория 6–8 тыс. при
|
|
# притоке 3 800 (спека, разделы 5 и 9).
|
|
RETURN_COUNT_WEIGHTS = (25, 20, 15, 12, 9, 7, 5, 4, 2, 1)
|
|
|
|
# Хвост возвратов: окно активности человека от его первого дня, общее на
|
|
# обе его куки. За краем окна кука не возвращается. Оно же — глубина
|
|
# предыстории: столько когорт живёт до D0, чтобы дневная аудитория была на
|
|
# полке с самого первого дня.
|
|
RETURN_TAIL_DAYS = 90
|
|
|
|
# Профиль возвратов по дням от первого дня куки: почти всё в первую неделю,
|
|
# дальше тонкий хвост до края окна — повторные покупки в магазине случаются
|
|
# и через месяцы. Профиль затухает к краю, поэтому обрыв на нём в данных не
|
|
# виден. Читается по парам «сколько дней — с каким весом»; дней в сумме
|
|
# ровно `RETURN_TAIL_DAYS`.
|
|
RETURN_DELAY_WEIGHTS = tuple(
|
|
weight
|
|
for days, weight in ((3, 100), (7, 40), (20, 8), (60, 1))
|
|
for _ in range(days)
|
|
)
|
|
|
|
# Доля покупателей среди людей когорты. Считается людьми, не куками: человек
|
|
# с двумя куками — один покупатель. Число плана, а не торгового поведения:
|
|
# без него не отобрать двухкуковые пары.
|
|
BUYER_PERCENT = 5
|
|
|
|
# Доля покупателей, у которых заведётся вторая кука (мастер-спека, раздел 5).
|
|
# Такой паре план назначает по гарантированному заказу с каждой куки — на
|
|
# этом стоит лаба про склейку личности.
|
|
PAIRED_BUYER_PERCENT = 15
|
|
|
|
# Первый рычаг метки покупателя: помеченный дольше живёт и чаще возвращается.
|
|
# Одноразовым он бывает много реже прочих, а возвращается вдвое чаще —
|
|
# отсюда кука, которая ходит неделями. Без этого рычага «постоянный
|
|
# покупатель» в данных не читается: метки в событии нет и не будет
|
|
# (кликстрим анонимен), а кука живёт меньше двух визитов за снимок — второй
|
|
# покупке негде случиться (спека генератора, раздел 9).
|
|
BUYER_ONE_SHOT_PERCENT = 35
|
|
BUYER_RETURN_COUNT_WEIGHTS = (10, 11, 12, 12, 11, 10, 9, 7, 5, 4, 3, 2)
|
|
|
|
# --- Числа дня: суточная волна, визиты, воронка ---------------------------
|
|
|
|
# Суточная волна буднего дня: проценты от среднего часа, от 00 до 23 часов
|
|
# местного времени посетителя. Ночной провал, обеденный и вечерний пики до
|
|
# ~2× среднего (спека генератора, раздел 2). В сумме ровно 2400: средний час
|
|
# остаётся средним, и суточный объём от формы волны не зависит.
|
|
WEEKDAY_HOURS_PERCENT = (
|
|
35, 20, 12, 8, 8, 12, 25, 45, 70, 105, 115, 130,
|
|
170, 165, 140, 130, 130, 140, 160, 185, 210, 180, 130, 75,
|
|
) # fmt: skip
|
|
|
|
# Выходной день: подъём позже, обеденного пика нет — день ровнее, вечер
|
|
# ниже буднего. Здесь только форма, поэтому сумма та же — 2400: объём
|
|
# выходного день-функция не трогает, он приходит сам, потому что дневная
|
|
# аудитория уже дышит недельной волной через приток. Умножить на неё
|
|
# второй раз значило бы удвоить недельный размах.
|
|
WEEKEND_HOURS_PERCENT = (
|
|
45, 30, 20, 12, 10, 10, 14, 22, 40, 70, 105, 140,
|
|
165, 160, 175, 175, 170, 165, 165, 175, 180, 165, 120, 67,
|
|
) # fmt: skip
|
|
|
|
# Сколько визитов у куки в её день активности: веса для 1, 2 и 3 визитов.
|
|
# В среднем ≈1,4 — при дневной аудитории 6–7 тыс. это 8–10 тыс. визитов
|
|
# (спека, раздел 5).
|
|
VISITS_PER_ACTIVE_DAY_WEIGHTS = (70, 22, 8)
|
|
|
|
# Длина визита в страницах: веса для 1, 2, 3 … страниц. Первая доля — отказы
|
|
# (посмотрел одну страницу и ушёл), дальше затухающий хвост. В среднем ≈4,8
|
|
# страницы: вместе с числом визитов это ~45 тыс. pageview в средний день,
|
|
# и до ~50 тыс. добирают торговые события.
|
|
VISIT_PAGES_WEIGHTS = (250, 150, 120, 100, 88, 78, 68, 58, 50, 42, 35, 28, 22, 16)
|
|
|
|
# Таймаут визита: пауза дольше этой рвёт визит надвое. Правило резки, по
|
|
# которому лаба сессий сверяет свою сборку с `VisitID` (мастер-спека,
|
|
# раздел 1.2), поэтому паузы внутри визита всегда короче, а соседние визиты
|
|
# одной куки всегда разведены дальше.
|
|
VISIT_TIMEOUT_SECONDS = 1800
|
|
|
|
# Пауза между соседними страницами визита, секунды: обычная и «задумался».
|
|
# Обе целиком внутри таймаута — иначе визит распался бы там, где генератор
|
|
# этого не обещал.
|
|
PAGE_PAUSE_SECONDS = (8, 300)
|
|
LONG_PAUSE_SECONDS = (300, 1500)
|
|
LONG_PAUSE_PERCENT = 12
|
|
|
|
# Воронка: доля визитов, дошедших до корзины, и доли следующих шагов от
|
|
# предыдущего. Произведение — конверсия визита в оформленный заказ
|
|
# (спека генератора, раздел 9). Гарантированные планом заказы двухкуковых
|
|
# пар проходят воронку целиком независимо от этих долей.
|
|
CART_PERCENT = 6
|
|
CHECKOUT_OF_CART_PERCENT = 45
|
|
CONFIRMATION_OF_CHECKOUT_PERCENT = 55
|
|
|
|
# Второй рычаг метки покупателя: помеченный отличается на обоих шагах
|
|
# воронки — и до корзины доходит чаще, и бросает её реже. В жизни
|
|
# различаются оба: кто пришёл смотреть, тот и кладёт реже, и до конца
|
|
# доводит реже; один шаг дал бы половину картины. Шаг подтверждения общий:
|
|
# оплата — про магазин, а не про склонность покупать.
|
|
BUYER_CART_PERCENT = 18
|
|
BUYER_CHECKOUT_OF_CART_PERCENT = 60
|
|
|
|
# --- Числа торговых событий: корзина, заказ, деньги ------------------------
|
|
|
|
# Торговое событие встаёт на несколько секунд позже своей страницы: корзина
|
|
# позже карточки, покупка позже подтверждения. Задержка короче самой
|
|
# короткой паузы между страницами — иначе событие корзины обогнало бы
|
|
# страницу, на которой посетитель его нажал.
|
|
TRADE_DELAY_SECONDS = (2, 8)
|
|
|
|
# Кто и что кладёт в корзину: доля открытых карточек, которая до неё дошла.
|
|
# Оба ряда читаются по уровням спроса товара (`catalog.DEMAND_LEVELS`):
|
|
# магнит, обычный, залёживается.
|
|
#
|
|
# Первый ряд — визит, дошедший до страницы корзины. Он пришёл покупать и
|
|
# кладёт почти всё, что сравнивал; уровень решает, что именно из сравненного
|
|
# он выберет, и залежавшийся товар проигрывает соседям по вкладке. Верх ряда
|
|
# высок намеренно: опусти его — и корзина такого визита схлопнется до одной
|
|
# позиции, а заказ до одного товара.
|
|
#
|
|
# Второй ряд — визит, который до страницы корзины не дошёл. Кладёт он редко,
|
|
# и магнит уходит у него из карточки втрое чаще обычного товара и вшестеро
|
|
# чаще залежавшегося: случайного посетителя решает соблазнить сам товар,
|
|
# больше решать нечему. Событий при этом выходит почти поровну — магнитов
|
|
# 47% таких добавлений, обычных 42%: обычных в каталоге вдвое с лишним
|
|
# больше, и разница уровней живёт в доле открытых карточек, а не в счёте
|
|
# событий. Нулю в этом ряду места нет: обнулить уровень значило бы вернуть
|
|
# по его товарам ровно то равенство «положил — открыл корзину», ради
|
|
# которого правка и делалась.
|
|
#
|
|
# Спрос растянут во втором ряду сильнее, чем в первом, и это не подгонка:
|
|
# намерение заслоняет товар, поэтому привлекательность видна там, где
|
|
# намерения нет.
|
|
SHOPPING_ADD_PERCENT = (98, 94, 55)
|
|
BROWSING_ADD_PERCENT = (6, 2, 1)
|
|
|
|
# Доля позиций корзины, которые остались брошенными: заказ уже корзины.
|
|
# Иначе событие корзины не рассказывало бы ничего сверх покупки — заказ был
|
|
# бы её точной копией, и сравнивать было бы нечего. Пустым заказ не бывает:
|
|
# если брошены все позиции, одна остаётся (спека генератора, раздел 9).
|
|
# Число опущено с 15% вместе с отвязкой корзины от страницы корзины: корзина
|
|
# стала уже (1,7 позиции вместо 2,4), и прежние 15% чаще выносили её целиком,
|
|
# а спасённая позиция возвращала заказ к одному товару.
|
|
ABANDONED_POSITION_PERCENT = 10
|
|
|
|
# Сколько штук одного товара берут: веса для 1, 2, 3 штук. Обычно одна,
|
|
# изредка две-три — непродовольственная розница.
|
|
ITEM_QUANTITY_WEIGHTS = (85, 11, 4)
|
|
|
|
# Валюта магазина: один регион присутствия — одна валюта.
|
|
CURRENCY = "RUB"
|
|
|
|
# Доля заказов с промокодом.
|
|
COUPON_PERCENT = 20
|
|
|
|
# Промокоды: код и скидка в процентах. Таблица — число мира, а не выдумка
|
|
# бэкенда: этап 3 берёт её готовой и обязан дать заказу с кодом скидку,
|
|
# иначе данные соврут. В клиентскую выручку скидка не входит — код на сайте
|
|
# знает корзину, а не итог расчёта (спека генератора, раздел 9). Цифры в
|
|
# коде — те же проценты: скидка в рублях потребовала бы второго правила
|
|
# чтения таблицы, а вместе с ним и второго вида скидки на стороне бэкенда.
|
|
COUPONS = (
|
|
("VESNA10", 10),
|
|
("DOMASHNIY5", 5),
|
|
("PERVYY15", 15),
|
|
("UYUT7", 7),
|
|
)
|
|
|
|
# Цели счётчика: корзина и покупка. Цели дублируют торговые события — в бою
|
|
# так и бывает (мастер-спека, раздел 1.2).
|
|
GOAL_CART_ID = 42150001
|
|
GOAL_PURCHASE_ID = 42150002
|