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:
+181
-181
@@ -1,181 +1,181 @@
|
||||
sku,name,category,brand,price
|
||||
HOME-0001,Комплект органайзеров для шкафа,Товары для дома,Домовит,129000
|
||||
HOME-0002,Сушилка для белья напольная,Товары для дома,Домовит,249900
|
||||
HOME-0003,Настольная лампа «Тихий вечер»,Товары для дома,Ладный дом,188990
|
||||
HOME-0004,Набор вешалок 12 штук,Товары для дома,Домовит,79000
|
||||
TEXT-0001,Комплект постельного белья полутораспальный,Текстиль,Сатин,348990
|
||||
TEXT-0002,Плед-покрывало 200×220,Текстиль,Сатин,279000
|
||||
TEXT-0003,Полотенце банное 70×140,Текстиль,Сатин,89000
|
||||
TEXT-0004,Штора блэкаут 200×260,Текстиль,Ладный дом,218990
|
||||
POSU-0001,Сковорода 26 см с антипригарным покрытием,Посуда,Кухмастер,198990
|
||||
POSU-0002,Набор кастрюль 3 предмета,Посуда,Кухмастер,458999
|
||||
POSU-0003,Чайник заварочный стеклянный 1 л,Посуда,Кухмастер,118990
|
||||
POSU-0004,Набор столовых приборов 24 предмета,Посуда,Ладья,289000
|
||||
TECH-0001,Электрочайник 1.7 л,Бытовая техника,Электрон,188990
|
||||
TECH-0002,Блендер погружной 800 Вт,Бытовая техника,Электрон,279000
|
||||
TECH-0003,Отпариватель ручной,Бытовая техника,Электрон,349000
|
||||
TECH-0004,Робот-пылесос,Бытовая техника,Электрон,1899000
|
||||
KIDS-0001,Стульчик для кормления,Детские товары,Малышок,649000
|
||||
KIDS-0002,Ночник-проектор «Звёзды»,Детские товары,Малышок,128990
|
||||
KIDS-0003,Набор детской посуды 5 предметов,Детские товары,Малышок,88999
|
||||
WEAR-0001,Тапочки домашние войлочные,Одежда и обувь,Ходики,98990
|
||||
WEAR-0002,Халат махровый,Одежда и обувь,Сатин,249000
|
||||
WEAR-0003,Носки хлопковые 5 пар,Одежда и обувь,Ходики,59000
|
||||
HOME-0005,Корзина для белья 45 л,Товары для дома,Домовит,119000
|
||||
HOME-0006,Контейнер для хранения 18 л,Товары для дома,Домовит,69000
|
||||
HOME-0007,Чехол для одежды 60×100,Товары для дома,Ладный дом,38990
|
||||
HOME-0008,Коврик придверный резиновый 40×60,Товары для дома,Домовит,59000
|
||||
HOME-0009,Гладильная доска настольная 73×30,Товары для дома,Ладный дом,149000
|
||||
HOME-0010,Ведро хозяйственное с крышкой 12 л,Товары для дома,Домовит,48990
|
||||
HOME-0011,Швабра с отжимом и ведром 8 л,Товары для дома,Домовит,178990
|
||||
HOME-0012,Щётка для пола с черенком 120 см,Товары для дома,Домовит,45000
|
||||
HOME-0013,Совок со щёткой на длинной ручке,Товары для дома,Домовит,79000
|
||||
HOME-0014,Таз хозяйственный круглый 14 л,Товары для дома,Домовит,38990
|
||||
HOME-0015,Этажерка для ванной 3 яруса,Товары для дома,Ладный дом,139000
|
||||
HOME-0016,Полка настенная металлическая 45 см,Товары для дома,Ладный дом,99000
|
||||
HOME-0017,Зеркало настенное круглое 50 см,Товары для дома,Ладный дом,168990
|
||||
HOME-0018,Набор коробок для обуви 6 штук,Товары для дома,Домовит,129000
|
||||
HOME-0019,Подставка для зонтов металлическая,Товары для дома,Ладный дом,158990
|
||||
HOME-0020,Крючки самоклеящиеся 8 штук,Товары для дома,Домовит,29000
|
||||
HOME-0021,Верёвка бельевая 20 м,Товары для дома,Домовит,19000
|
||||
HOME-0022,Прищепки усиленные 24 штуки,Товары для дома,Домовит,24990
|
||||
HOME-0023,Мешки вакуумные для одежды 5 штук,Товары для дома,Домовит,68999
|
||||
HOME-0024,Органайзер подвесной с 12 карманами,Товары для дома,Ладный дом,79000
|
||||
HOME-0025,Часы настенные круглые 30 см,Товары для дома,Ладный дом,88990
|
||||
HOME-0026,Светильник настольный с прищепкой 7 Вт,Товары для дома,Ладный дом,119000
|
||||
HOME-0027,Удлинитель бытовой 3 розетки 3 м,Товары для дома,Ладный дом,99000
|
||||
HOME-0028,Лестница-стремянка 3 ступени,Товары для дома,Домовит,299000
|
||||
HOME-0029,Ящик для инструментов 16 дюймов,Товары для дома,Домовит,138990
|
||||
HOME-0030,Набор плечиков для брюк 6 штук,Товары для дома,Домовит,89000
|
||||
TEXT-0005,Пододеяльник двуспальный 175×215,Текстиль,Сатин,169000
|
||||
TEXT-0006,Простыня на резинке 160×200,Текстиль,Сатин,148990
|
||||
TEXT-0007,Наволочки хлопковые 50×70 2 штуки,Текстиль,Сатин,79000
|
||||
TEXT-0008,Одеяло всесезонное 172×205,Текстиль,Сатин,299000
|
||||
TEXT-0009,Подушка стёганая 50×70,Текстиль,Сатин,128990
|
||||
TEXT-0010,Наматрасник непромокаемый 160×200,Текстиль,Сатин,189000
|
||||
TEXT-0011,Покрывало стёганое 180×210,Текстиль,Сатин,238999
|
||||
TEXT-0012,Плед флисовый 150×200,Текстиль,Сатин,118990
|
||||
TEXT-0013,Полотенце лицевое 50×90,Текстиль,Сатин,49000
|
||||
TEXT-0014,Полотенца кухонные 40×60 3 штуки,Текстиль,Сатин,59000
|
||||
TEXT-0015,Коврик для ванной хлопковый 50×80,Текстиль,Ладный дом,99000
|
||||
TEXT-0016,Штора для ванной 180×200,Текстиль,Ладный дом,79000
|
||||
TEXT-0017,Тюль вуаль 300×260,Текстиль,Ладный дом,199000
|
||||
TEXT-0018,Шторы кухонные 150×170 2 полотна,Текстиль,Ладный дом,179000
|
||||
TEXT-0019,Скатерть водоотталкивающая 140×180,Текстиль,Ладный дом,129000
|
||||
TEXT-0020,Дорожка на стол хлопковая 40×140,Текстиль,Ладный дом,59000
|
||||
TEXT-0021,Салфетки столовые льняные 4 штуки,Текстиль,Сатин,79000
|
||||
TEXT-0022,Чехлы на табурет 35×35 4 штуки,Текстиль,Ладный дом,108990
|
||||
TEXT-0023,Подушка декоративная 40×40,Текстиль,Сатин,68999
|
||||
TEXT-0024,Чехол для диванной подушки 45×45,Текстиль,Сатин,39000
|
||||
TEXT-0025,Фартук кухонный с карманом,Текстиль,Ладный дом,59000
|
||||
TEXT-0026,Прихватки стёганые 20×20 2 штуки,Текстиль,Ладный дом,38990
|
||||
TEXT-0027,Халат вафельный размер 48–50,Текстиль,Сатин,199000
|
||||
TEXT-0028,Комплект полотенец 50×90 и 70×140,Текстиль,Сатин,138999
|
||||
TEXT-0029,Одеяло детское байковое 100×140,Текстиль,Сатин,169000
|
||||
TEXT-0030,Подушка для стула 40×40,Текстиль,Ладный дом,59000
|
||||
POSU-0005,Кастрюля эмалированная 3 л,Посуда,Кухмастер,168990
|
||||
POSU-0006,Ковш нержавеющий с крышкой 1.5 л,Посуда,Кухмастер,138990
|
||||
POSU-0007,Сотейник литой с крышкой 28 см,Посуда,Кухмастер,269000
|
||||
POSU-0008,Сковорода блинная 22 см,Посуда,Кухмастер,149000
|
||||
POSU-0009,Казан алюминиевый 6 л,Посуда,Кухмастер,299000
|
||||
POSU-0010,Форма для запекания стеклянная 2.4 л,Посуда,Кухмастер,118990
|
||||
POSU-0011,Противень стальной 37×27,Посуда,Кухмастер,79000
|
||||
POSU-0012,Набор форм для выпечки 3 штуки,Посуда,Кухмастер,139000
|
||||
POSU-0013,Миска нержавеющая 24 см,Посуда,Кухмастер,68990
|
||||
POSU-0014,Дуршлаг стальной с ручками 22 см,Посуда,Кухмастер,89000
|
||||
POSU-0015,Тёрка четырёхгранная 23 см,Посуда,Кухмастер,49000
|
||||
POSU-0016,Доска разделочная бамбуковая 35×25,Посуда,Кухмастер,78990
|
||||
POSU-0017,Набор ножей с подставкой 6 предметов,Посуда,Кухмастер,248990
|
||||
POSU-0018,Половник и шумовка 2 предмета,Посуда,Кухмастер,58990
|
||||
POSU-0019,Контейнеры пищевые 7 предметов,Посуда,Кухмастер,99000
|
||||
POSU-0020,Банки для сыпучих продуктов 4 штуки,Посуда,Ладья,138990
|
||||
POSU-0021,Тарелки обеденные фарфоровые 6 штук,Посуда,Ладья,178999
|
||||
POSU-0022,Тарелки суповые керамические 4 штуки,Посуда,Ладья,129000
|
||||
POSU-0023,Салатник стеклянный 23 см,Посуда,Ладья,68999
|
||||
POSU-0024,Кружки керамические 350 мл 4 штуки,Посуда,Ладья,118990
|
||||
POSU-0025,Стаканы высокие 400 мл 6 штук,Посуда,Ладья,98990
|
||||
POSU-0026,Бокалы для воды 300 мл 6 штук,Посуда,Ладья,149000
|
||||
POSU-0027,Термос стальной 1 л,Посуда,Ладья,179000
|
||||
POSU-0028,Термокружка с крышкой 450 мл,Посуда,Ладья,99000
|
||||
POSU-0029,Сахарница фарфоровая 400 мл,Посуда,Ладья,59000
|
||||
POSU-0030,Маслёнка стеклянная с крышкой,Посуда,Ладья,68990
|
||||
TECH-0005,Миксер ручной 500 Вт,Бытовая техника,Электрон,248990
|
||||
TECH-0006,Измельчитель с чашей 1.5 л,Бытовая техника,Электрон,298990
|
||||
TECH-0007,Стационарный блендер 1.8 л,Бытовая техника,Электрон,549000
|
||||
TECH-0008,Мясорубка электрическая 1800 Вт,Бытовая техника,Электрон,799000
|
||||
TECH-0009,Тостер на 2 ломтика 850 Вт,Бытовая техника,Электрон,299000
|
||||
TECH-0010,Вафельница электрическая 1000 Вт,Бытовая техника,Электрон,348999
|
||||
TECH-0011,Мультиварка с чашей 5 л,Бытовая техника,Электрон,699000
|
||||
TECH-0012,Электроплитка на 1 конфорку 1500 Вт,Бытовая техника,Электрон,249000
|
||||
TECH-0013,Мини-печь с грилем 30 л,Бытовая техника,Электрон,899000
|
||||
TECH-0014,Микроволновая печь 20 л,Бытовая техника,Электрон,999000
|
||||
TECH-0015,Сушилка для овощей 5 поддонов,Бытовая техника,Электрон,499000
|
||||
TECH-0016,Кофемолка электрическая 200 Вт,Бытовая техника,Электрон,199000
|
||||
TECH-0017,Капельная кофеварка 1.2 л,Бытовая техника,Электрон,399000
|
||||
TECH-0018,Соковыжималка шнековая 150 Вт,Бытовая техника,Электрон,799000
|
||||
TECH-0019,Утюг паровой 2400 Вт,Бытовая техника,Электрон,349000
|
||||
TECH-0020,Швейная машина 12 операций,Бытовая техника,Электрон,1299000
|
||||
TECH-0021,Пылесос контейнерный 2000 Вт,Бытовая техника,Электрон,1099000
|
||||
TECH-0022,Пылесос вертикальный аккумуляторный,Бытовая техника,Электрон,1499000
|
||||
TECH-0023,Электровеник с контейнером 0.5 л,Бытовая техника,Электрон,699000
|
||||
TECH-0024,Фен с диффузором 2200 Вт,Бытовая техника,Электрон,299000
|
||||
TECH-0025,Щипцы для волос 45 Вт,Бытовая техника,Электрон,249000
|
||||
TECH-0026,Машинка для стрижки с 6 насадками,Бытовая техника,Электрон,399000
|
||||
TECH-0027,Весы напольные до 180 кг,Бытовая техника,Электрон,198990
|
||||
TECH-0028,Весы кухонные до 5 кг,Бытовая техника,Электрон,148990
|
||||
TECH-0029,Увлажнитель воздуха 3 л,Бытовая техника,Электрон,499000
|
||||
TECH-0030,Тепловентилятор керамический 1500 Вт,Бытовая техника,Электрон,398990
|
||||
KIDS-0004,Кроватка кукольная деревянная 45 см,Детские товары,Малышок,248999
|
||||
KIDS-0005,Манеж складной 100×100,Детские товары,Малышок,798990
|
||||
KIDS-0006,Горшок детский со спинкой,Детские товары,Малышок,79000
|
||||
KIDS-0007,Ванночка детская 84 см,Детские товары,Малышок,168990
|
||||
KIDS-0008,Подставка детская двухступенчатая,Детские товары,Малышок,89000
|
||||
KIDS-0009,Накладка на унитаз с ручками,Детские товары,Малышок,69000
|
||||
KIDS-0010,Сиденье для купания на присосках,Детские товары,Малышок,138990
|
||||
KIDS-0011,Пелёнки муслиновые 80×100 3 штуки,Детские товары,Малышок,129000
|
||||
KIDS-0012,Одеяло для коляски 90×110,Детские товары,Малышок,148990
|
||||
KIDS-0013,Нагрудники непромокаемые 5 штук,Детские товары,Малышок,58999
|
||||
KIDS-0014,Бутылочка с ручками 250 мл,Детские товары,Малышок,49000
|
||||
KIDS-0015,Поильник непроливайка 300 мл,Детские товары,Малышок,54990
|
||||
KIDS-0016,Контейнер для пустышки 2 штуки,Детские товары,Малышок,25000
|
||||
KIDS-0017,Погремушки мягкие 3 штуки,Детские товары,Малышок,59000
|
||||
KIDS-0018,Кубики развивающие 12 штук,Детские товары,Малышок,89000
|
||||
KIDS-0019,Пирамидка деревянная 8 колец,Детские товары,Малышок,78990
|
||||
KIDS-0020,Сортер с геометрическими фигурами,Детские товары,Малышок,98990
|
||||
KIDS-0021,Конструктор деревянный 50 деталей,Детские товары,Малышок,139000
|
||||
KIDS-0022,Доска для рисования магнитная 30×40,Детские товары,Малышок,128990
|
||||
KIDS-0023,Мольберт детский двусторонний 105 см,Детские товары,Малышок,298999
|
||||
KIDS-0024,Набор для лепки 12 цветов,Детские товары,Малышок,69000
|
||||
KIDS-0025,Коврик игровой складной 180×150,Детские товары,Малышок,399000
|
||||
KIDS-0026,Палатка игровая с окошками,Детские товары,Малышок,199000
|
||||
KIDS-0027,Каталка с ручкой и звуком,Детские товары,Малышок,169000
|
||||
KIDS-0028,Самокат трёхколёсный до 50 кг,Детские товары,Малышок,298999
|
||||
KIDS-0029,Санки со спинкой и ремнём,Детские товары,Малышок,349000
|
||||
KIDS-0030,Рюкзак детский объёмом 8 л,Детские товары,Малышок,128990
|
||||
WEAR-0004,Футболка хлопковая размер 48,Одежда и обувь,Сатин,78990
|
||||
WEAR-0005,Майка хлопковая размер 46,Одежда и обувь,Сатин,58990
|
||||
WEAR-0006,Лонгслив трикотажный размер 50,Одежда и обувь,Сатин,108990
|
||||
WEAR-0007,Водолазка хлопковая размер 44,Одежда и обувь,Сатин,118990
|
||||
WEAR-0008,Толстовка с капюшоном размер 52,Одежда и обувь,Сатин,198999
|
||||
WEAR-0009,Брюки спортивные размер 50,Одежда и обувь,Сатин,169000
|
||||
WEAR-0010,Легинсы хлопковые размер 46,Одежда и обувь,Сатин,98990
|
||||
WEAR-0011,Пижама трикотажная размер 48,Одежда и обувь,Сатин,179000
|
||||
WEAR-0012,Ночная сорочка хлопковая размер 50,Одежда и обувь,Сатин,128999
|
||||
WEAR-0013,Фартук рабочий с тремя карманами,Одежда и обувь,Сатин,79000
|
||||
WEAR-0014,Перчатки хозяйственные 3 пары,Одежда и обувь,Ходики,39000
|
||||
WEAR-0015,Варежки вязаные размер 8,Одежда и обувь,Ходики,68990
|
||||
WEAR-0016,Шапка вязаная с отворотом размер 56,Одежда и обувь,Ходики,88990
|
||||
WEAR-0017,Шарф шерстяной 30×180,Одежда и обувь,Ходики,119000
|
||||
WEAR-0018,Ремень текстильный длиной 120 см,Одежда и обувь,Ходики,78990
|
||||
WEAR-0019,Стельки войлочные 2 пары,Одежда и обувь,Ходики,38990
|
||||
WEAR-0020,Шнурки круглые 120 см 3 пары,Одежда и обувь,Ходики,25000
|
||||
WEAR-0021,Кеды текстильные размер 42,Одежда и обувь,Ходики,179000
|
||||
WEAR-0022,Кроссовки прогулочные размер 39,Одежда и обувь,Ходики,248990
|
||||
WEAR-0023,Ботинки утеплённые размер 41,Одежда и обувь,Ходики,348990
|
||||
WEAR-0024,Сапоги резиновые размер 40,Одежда и обувь,Ходики,199000
|
||||
WEAR-0025,Полуботинки кожаные размер 43,Одежда и обувь,Ходики,398999
|
||||
WEAR-0026,Сандалии с ремешками размер 38,Одежда и обувь,Ходики,189000
|
||||
WEAR-0027,Сланцы резиновые размер 41,Одежда и обувь,Ходики,79000
|
||||
WEAR-0028,Балетки текстильные размер 37,Одежда и обувь,Ходики,149000
|
||||
WEAR-0029,Туфли домашние утеплённые размер 39,Одежда и обувь,Ходики,129000
|
||||
WEAR-0030,Галоши садовые размер 42,Одежда и обувь,Ходики,98990
|
||||
sku,name,category,brand,price,demand
|
||||
HOME-0001,Комплект органайзеров для шкафа,Товары для дома,Домовит,129000,обычный
|
||||
HOME-0002,Сушилка для белья напольная,Товары для дома,Домовит,249900,обычный
|
||||
HOME-0003,Настольная лампа «Тихий вечер»,Товары для дома,Ладный дом,188990,магнит
|
||||
HOME-0004,Набор вешалок 12 штук,Товары для дома,Домовит,79000,магнит
|
||||
TEXT-0001,Комплект постельного белья полутораспальный,Текстиль,Сатин,348990,обычный
|
||||
TEXT-0002,Плед-покрывало 200×220,Текстиль,Сатин,279000,залёживается
|
||||
TEXT-0003,Полотенце банное 70×140,Текстиль,Сатин,89000,обычный
|
||||
TEXT-0004,Штора блэкаут 200×260,Текстиль,Ладный дом,218990,залёживается
|
||||
POSU-0001,Сковорода 26 см с антипригарным покрытием,Посуда,Кухмастер,198990,залёживается
|
||||
POSU-0002,Набор кастрюль 3 предмета,Посуда,Кухмастер,458999,магнит
|
||||
POSU-0003,Чайник заварочный стеклянный 1 л,Посуда,Кухмастер,118990,магнит
|
||||
POSU-0004,Набор столовых приборов 24 предмета,Посуда,Ладья,289000,магнит
|
||||
TECH-0001,Электрочайник 1.7 л,Бытовая техника,Электрон,188990,обычный
|
||||
TECH-0002,Блендер погружной 800 Вт,Бытовая техника,Электрон,279000,залёживается
|
||||
TECH-0003,Отпариватель ручной,Бытовая техника,Электрон,349000,обычный
|
||||
TECH-0004,Робот-пылесос,Бытовая техника,Электрон,1899000,залёживается
|
||||
KIDS-0001,Стульчик для кормления,Детские товары,Малышок,649000,магнит
|
||||
KIDS-0002,Ночник-проектор «Звёзды»,Детские товары,Малышок,128990,магнит
|
||||
KIDS-0003,Набор детской посуды 5 предметов,Детские товары,Малышок,88999,обычный
|
||||
WEAR-0001,Тапочки домашние войлочные,Одежда и обувь,Ходики,98990,обычный
|
||||
WEAR-0002,Халат махровый,Одежда и обувь,Сатин,249000,магнит
|
||||
WEAR-0003,Носки хлопковые 5 пар,Одежда и обувь,Ходики,59000,залёживается
|
||||
HOME-0005,Корзина для белья 45 л,Товары для дома,Домовит,119000,магнит
|
||||
HOME-0006,Контейнер для хранения 18 л,Товары для дома,Домовит,69000,обычный
|
||||
HOME-0007,Чехол для одежды 60×100,Товары для дома,Ладный дом,38990,залёживается
|
||||
HOME-0008,Коврик придверный резиновый 40×60,Товары для дома,Домовит,59000,обычный
|
||||
HOME-0009,Гладильная доска настольная 73×30,Товары для дома,Ладный дом,149000,залёживается
|
||||
HOME-0010,Ведро хозяйственное с крышкой 12 л,Товары для дома,Домовит,48990,залёживается
|
||||
HOME-0011,Швабра с отжимом и ведром 8 л,Товары для дома,Домовит,178990,обычный
|
||||
HOME-0012,Щётка для пола с черенком 120 см,Товары для дома,Домовит,45000,обычный
|
||||
HOME-0013,Совок со щёткой на длинной ручке,Товары для дома,Домовит,79000,магнит
|
||||
HOME-0014,Таз хозяйственный круглый 14 л,Товары для дома,Домовит,38990,магнит
|
||||
HOME-0015,Этажерка для ванной 3 яруса,Товары для дома,Ладный дом,139000,магнит
|
||||
HOME-0016,Полка настенная металлическая 45 см,Товары для дома,Ладный дом,99000,обычный
|
||||
HOME-0017,Зеркало настенное круглое 50 см,Товары для дома,Ладный дом,168990,залёживается
|
||||
HOME-0018,Набор коробок для обуви 6 штук,Товары для дома,Домовит,129000,магнит
|
||||
HOME-0019,Подставка для зонтов металлическая,Товары для дома,Ладный дом,158990,обычный
|
||||
HOME-0020,Крючки самоклеящиеся 8 штук,Товары для дома,Домовит,29000,обычный
|
||||
HOME-0021,Верёвка бельевая 20 м,Товары для дома,Домовит,19000,обычный
|
||||
HOME-0022,Прищепки усиленные 24 штуки,Товары для дома,Домовит,24990,залёживается
|
||||
HOME-0023,Мешки вакуумные для одежды 5 штук,Товары для дома,Домовит,68999,обычный
|
||||
HOME-0024,Органайзер подвесной с 12 карманами,Товары для дома,Ладный дом,79000,обычный
|
||||
HOME-0025,Часы настенные круглые 30 см,Товары для дома,Ладный дом,88990,магнит
|
||||
HOME-0026,Светильник настольный с прищепкой 7 Вт,Товары для дома,Ладный дом,119000,обычный
|
||||
HOME-0027,Удлинитель бытовой 3 розетки 3 м,Товары для дома,Ладный дом,99000,магнит
|
||||
HOME-0028,Лестница-стремянка 3 ступени,Товары для дома,Домовит,299000,обычный
|
||||
HOME-0029,Ящик для инструментов 16 дюймов,Товары для дома,Домовит,138990,обычный
|
||||
HOME-0030,Набор плечиков для брюк 6 штук,Товары для дома,Домовит,89000,обычный
|
||||
TEXT-0005,Пододеяльник двуспальный 175×215,Текстиль,Сатин,169000,обычный
|
||||
TEXT-0006,Простыня на резинке 160×200,Текстиль,Сатин,148990,обычный
|
||||
TEXT-0007,Наволочки хлопковые 50×70 2 штуки,Текстиль,Сатин,79000,залёживается
|
||||
TEXT-0008,Одеяло всесезонное 172×205,Текстиль,Сатин,299000,обычный
|
||||
TEXT-0009,Подушка стёганая 50×70,Текстиль,Сатин,128990,обычный
|
||||
TEXT-0010,Наматрасник непромокаемый 160×200,Текстиль,Сатин,189000,магнит
|
||||
TEXT-0011,Покрывало стёганое 180×210,Текстиль,Сатин,238999,обычный
|
||||
TEXT-0012,Плед флисовый 150×200,Текстиль,Сатин,118990,обычный
|
||||
TEXT-0013,Полотенце лицевое 50×90,Текстиль,Сатин,49000,обычный
|
||||
TEXT-0014,Полотенца кухонные 40×60 3 штуки,Текстиль,Сатин,59000,обычный
|
||||
TEXT-0015,Коврик для ванной хлопковый 50×80,Текстиль,Ладный дом,99000,залёживается
|
||||
TEXT-0016,Штора для ванной 180×200,Текстиль,Ладный дом,79000,обычный
|
||||
TEXT-0017,Тюль вуаль 300×260,Текстиль,Ладный дом,199000,обычный
|
||||
TEXT-0018,Шторы кухонные 150×170 2 полотна,Текстиль,Ладный дом,179000,залёживается
|
||||
TEXT-0019,Скатерть водоотталкивающая 140×180,Текстиль,Ладный дом,129000,обычный
|
||||
TEXT-0020,Дорожка на стол хлопковая 40×140,Текстиль,Ладный дом,59000,залёживается
|
||||
TEXT-0021,Салфетки столовые льняные 4 штуки,Текстиль,Сатин,79000,обычный
|
||||
TEXT-0022,Чехлы на табурет 35×35 4 штуки,Текстиль,Ладный дом,108990,обычный
|
||||
TEXT-0023,Подушка декоративная 40×40,Текстиль,Сатин,68999,магнит
|
||||
TEXT-0024,Чехол для диванной подушки 45×45,Текстиль,Сатин,39000,обычный
|
||||
TEXT-0025,Фартук кухонный с карманом,Текстиль,Ладный дом,59000,магнит
|
||||
TEXT-0026,Прихватки стёганые 20×20 2 штуки,Текстиль,Ладный дом,38990,магнит
|
||||
TEXT-0027,Халат вафельный размер 48–50,Текстиль,Сатин,199000,залёживается
|
||||
TEXT-0028,Комплект полотенец 50×90 и 70×140,Текстиль,Сатин,138999,обычный
|
||||
TEXT-0029,Одеяло детское байковое 100×140,Текстиль,Сатин,169000,обычный
|
||||
TEXT-0030,Подушка для стула 40×40,Текстиль,Ладный дом,59000,обычный
|
||||
POSU-0005,Кастрюля эмалированная 3 л,Посуда,Кухмастер,168990,обычный
|
||||
POSU-0006,Ковш нержавеющий с крышкой 1.5 л,Посуда,Кухмастер,138990,залёживается
|
||||
POSU-0007,Сотейник литой с крышкой 28 см,Посуда,Кухмастер,269000,магнит
|
||||
POSU-0008,Сковорода блинная 22 см,Посуда,Кухмастер,149000,обычный
|
||||
POSU-0009,Казан алюминиевый 6 л,Посуда,Кухмастер,299000,обычный
|
||||
POSU-0010,Форма для запекания стеклянная 2.4 л,Посуда,Кухмастер,118990,магнит
|
||||
POSU-0011,Противень стальной 37×27,Посуда,Кухмастер,79000,залёживается
|
||||
POSU-0012,Набор форм для выпечки 3 штуки,Посуда,Кухмастер,139000,залёживается
|
||||
POSU-0013,Миска нержавеющая 24 см,Посуда,Кухмастер,68990,залёживается
|
||||
POSU-0014,Дуршлаг стальной с ручками 22 см,Посуда,Кухмастер,89000,залёживается
|
||||
POSU-0015,Тёрка четырёхгранная 23 см,Посуда,Кухмастер,49000,обычный
|
||||
POSU-0016,Доска разделочная бамбуковая 35×25,Посуда,Кухмастер,78990,магнит
|
||||
POSU-0017,Набор ножей с подставкой 6 предметов,Посуда,Кухмастер,248990,обычный
|
||||
POSU-0018,Половник и шумовка 2 предмета,Посуда,Кухмастер,58990,обычный
|
||||
POSU-0019,Контейнеры пищевые 7 предметов,Посуда,Кухмастер,99000,залёживается
|
||||
POSU-0020,Банки для сыпучих продуктов 4 штуки,Посуда,Ладья,138990,залёживается
|
||||
POSU-0021,Тарелки обеденные фарфоровые 6 штук,Посуда,Ладья,178999,обычный
|
||||
POSU-0022,Тарелки суповые керамические 4 штуки,Посуда,Ладья,129000,обычный
|
||||
POSU-0023,Салатник стеклянный 23 см,Посуда,Ладья,68999,обычный
|
||||
POSU-0024,Кружки керамические 350 мл 4 штуки,Посуда,Ладья,118990,обычный
|
||||
POSU-0025,Стаканы высокие 400 мл 6 штук,Посуда,Ладья,98990,обычный
|
||||
POSU-0026,Бокалы для воды 300 мл 6 штук,Посуда,Ладья,149000,обычный
|
||||
POSU-0027,Термос стальной 1 л,Посуда,Ладья,179000,залёживается
|
||||
POSU-0028,Термокружка с крышкой 450 мл,Посуда,Ладья,99000,залёживается
|
||||
POSU-0029,Сахарница фарфоровая 400 мл,Посуда,Ладья,59000,магнит
|
||||
POSU-0030,Маслёнка стеклянная с крышкой,Посуда,Ладья,68990,обычный
|
||||
TECH-0005,Миксер ручной 500 Вт,Бытовая техника,Электрон,248990,обычный
|
||||
TECH-0006,Измельчитель с чашей 1.5 л,Бытовая техника,Электрон,298990,обычный
|
||||
TECH-0007,Стационарный блендер 1.8 л,Бытовая техника,Электрон,549000,магнит
|
||||
TECH-0008,Мясорубка электрическая 1800 Вт,Бытовая техника,Электрон,799000,магнит
|
||||
TECH-0009,Тостер на 2 ломтика 850 Вт,Бытовая техника,Электрон,299000,залёживается
|
||||
TECH-0010,Вафельница электрическая 1000 Вт,Бытовая техника,Электрон,348999,обычный
|
||||
TECH-0011,Мультиварка с чашей 5 л,Бытовая техника,Электрон,699000,магнит
|
||||
TECH-0012,Электроплитка на 1 конфорку 1500 Вт,Бытовая техника,Электрон,249000,обычный
|
||||
TECH-0013,Мини-печь с грилем 30 л,Бытовая техника,Электрон,899000,залёживается
|
||||
TECH-0014,Микроволновая печь 20 л,Бытовая техника,Электрон,999000,обычный
|
||||
TECH-0015,Сушилка для овощей 5 поддонов,Бытовая техника,Электрон,499000,обычный
|
||||
TECH-0016,Кофемолка электрическая 200 Вт,Бытовая техника,Электрон,199000,обычный
|
||||
TECH-0017,Капельная кофеварка 1.2 л,Бытовая техника,Электрон,399000,залёживается
|
||||
TECH-0018,Соковыжималка шнековая 150 Вт,Бытовая техника,Электрон,799000,залёживается
|
||||
TECH-0019,Утюг паровой 2400 Вт,Бытовая техника,Электрон,349000,обычный
|
||||
TECH-0020,Швейная машина 12 операций,Бытовая техника,Электрон,1299000,обычный
|
||||
TECH-0021,Пылесос контейнерный 2000 Вт,Бытовая техника,Электрон,1099000,залёживается
|
||||
TECH-0022,Пылесос вертикальный аккумуляторный,Бытовая техника,Электрон,1499000,магнит
|
||||
TECH-0023,Электровеник с контейнером 0.5 л,Бытовая техника,Электрон,699000,магнит
|
||||
TECH-0024,Фен с диффузором 2200 Вт,Бытовая техника,Электрон,299000,обычный
|
||||
TECH-0025,Щипцы для волос 45 Вт,Бытовая техника,Электрон,249000,обычный
|
||||
TECH-0026,Машинка для стрижки с 6 насадками,Бытовая техника,Электрон,399000,залёживается
|
||||
TECH-0027,Весы напольные до 180 кг,Бытовая техника,Электрон,198990,залёживается
|
||||
TECH-0028,Весы кухонные до 5 кг,Бытовая техника,Электрон,148990,обычный
|
||||
TECH-0029,Увлажнитель воздуха 3 л,Бытовая техника,Электрон,499000,обычный
|
||||
TECH-0030,Тепловентилятор керамический 1500 Вт,Бытовая техника,Электрон,398990,магнит
|
||||
KIDS-0004,Кроватка кукольная деревянная 45 см,Детские товары,Малышок,248999,обычный
|
||||
KIDS-0005,Манеж складной 100×100,Детские товары,Малышок,798990,залёживается
|
||||
KIDS-0006,Горшок детский со спинкой,Детские товары,Малышок,79000,магнит
|
||||
KIDS-0007,Ванночка детская 84 см,Детские товары,Малышок,168990,обычный
|
||||
KIDS-0008,Подставка детская двухступенчатая,Детские товары,Малышок,89000,залёживается
|
||||
KIDS-0009,Накладка на унитаз с ручками,Детские товары,Малышок,69000,обычный
|
||||
KIDS-0010,Сиденье для купания на присосках,Детские товары,Малышок,138990,залёживается
|
||||
KIDS-0011,Пелёнки муслиновые 80×100 3 штуки,Детские товары,Малышок,129000,обычный
|
||||
KIDS-0012,Одеяло для коляски 90×110,Детские товары,Малышок,148990,магнит
|
||||
KIDS-0013,Нагрудники непромокаемые 5 штук,Детские товары,Малышок,58999,обычный
|
||||
KIDS-0014,Бутылочка с ручками 250 мл,Детские товары,Малышок,49000,обычный
|
||||
KIDS-0015,Поильник непроливайка 300 мл,Детские товары,Малышок,54990,залёживается
|
||||
KIDS-0016,Контейнер для пустышки 2 штуки,Детские товары,Малышок,25000,магнит
|
||||
KIDS-0017,Погремушки мягкие 3 штуки,Детские товары,Малышок,59000,магнит
|
||||
KIDS-0018,Кубики развивающие 12 штук,Детские товары,Малышок,89000,залёживается
|
||||
KIDS-0019,Пирамидка деревянная 8 колец,Детские товары,Малышок,78990,обычный
|
||||
KIDS-0020,Сортер с геометрическими фигурами,Детские товары,Малышок,98990,обычный
|
||||
KIDS-0021,Конструктор деревянный 50 деталей,Детские товары,Малышок,139000,обычный
|
||||
KIDS-0022,Доска для рисования магнитная 30×40,Детские товары,Малышок,128990,обычный
|
||||
KIDS-0023,Мольберт детский двусторонний 105 см,Детские товары,Малышок,298999,залёживается
|
||||
KIDS-0024,Набор для лепки 12 цветов,Детские товары,Малышок,69000,обычный
|
||||
KIDS-0025,Коврик игровой складной 180×150,Детские товары,Малышок,399000,обычный
|
||||
KIDS-0026,Палатка игровая с окошками,Детские товары,Малышок,199000,обычный
|
||||
KIDS-0027,Каталка с ручкой и звуком,Детские товары,Малышок,169000,залёживается
|
||||
KIDS-0028,Самокат трёхколёсный до 50 кг,Детские товары,Малышок,298999,обычный
|
||||
KIDS-0029,Санки со спинкой и ремнём,Детские товары,Малышок,349000,залёживается
|
||||
KIDS-0030,Рюкзак детский объёмом 8 л,Детские товары,Малышок,128990,обычный
|
||||
WEAR-0004,Футболка хлопковая размер 48,Одежда и обувь,Сатин,78990,залёживается
|
||||
WEAR-0005,Майка хлопковая размер 46,Одежда и обувь,Сатин,58990,обычный
|
||||
WEAR-0006,Лонгслив трикотажный размер 50,Одежда и обувь,Сатин,108990,магнит
|
||||
WEAR-0007,Водолазка хлопковая размер 44,Одежда и обувь,Сатин,118990,обычный
|
||||
WEAR-0008,Толстовка с капюшоном размер 52,Одежда и обувь,Сатин,198999,обычный
|
||||
WEAR-0009,Брюки спортивные размер 50,Одежда и обувь,Сатин,169000,залёживается
|
||||
WEAR-0010,Легинсы хлопковые размер 46,Одежда и обувь,Сатин,98990,залёживается
|
||||
WEAR-0011,Пижама трикотажная размер 48,Одежда и обувь,Сатин,179000,обычный
|
||||
WEAR-0012,Ночная сорочка хлопковая размер 50,Одежда и обувь,Сатин,128999,обычный
|
||||
WEAR-0013,Фартук рабочий с тремя карманами,Одежда и обувь,Сатин,79000,залёживается
|
||||
WEAR-0014,Перчатки хозяйственные 3 пары,Одежда и обувь,Ходики,39000,обычный
|
||||
WEAR-0015,Варежки вязаные размер 8,Одежда и обувь,Ходики,68990,обычный
|
||||
WEAR-0016,Шапка вязаная с отворотом размер 56,Одежда и обувь,Ходики,88990,магнит
|
||||
WEAR-0017,Шарф шерстяной 30×180,Одежда и обувь,Ходики,119000,обычный
|
||||
WEAR-0018,Ремень текстильный длиной 120 см,Одежда и обувь,Ходики,78990,обычный
|
||||
WEAR-0019,Стельки войлочные 2 пары,Одежда и обувь,Ходики,38990,залёживается
|
||||
WEAR-0020,Шнурки круглые 120 см 3 пары,Одежда и обувь,Ходики,25000,обычный
|
||||
WEAR-0021,Кеды текстильные размер 42,Одежда и обувь,Ходики,179000,обычный
|
||||
WEAR-0022,Кроссовки прогулочные размер 39,Одежда и обувь,Ходики,248990,обычный
|
||||
WEAR-0023,Ботинки утеплённые размер 41,Одежда и обувь,Ходики,348990,залёживается
|
||||
WEAR-0024,Сапоги резиновые размер 40,Одежда и обувь,Ходики,199000,обычный
|
||||
WEAR-0025,Полуботинки кожаные размер 43,Одежда и обувь,Ходики,398999,залёживается
|
||||
WEAR-0026,Сандалии с ремешками размер 38,Одежда и обувь,Ходики,189000,обычный
|
||||
WEAR-0027,Сланцы резиновые размер 41,Одежда и обувь,Ходики,79000,обычный
|
||||
WEAR-0028,Балетки текстильные размер 37,Одежда и обувь,Ходики,149000,залёживается
|
||||
WEAR-0029,Туфли домашние утеплённые размер 39,Одежда и обувь,Ходики,129000,магнит
|
||||
WEAR-0030,Галоши садовые размер 42,Одежда и обувь,Ходики,98990,обычный
|
||||
|
||||
|
@@ -223,10 +223,12 @@ smoke — сравнение `system.columns` поднятого стенда с
|
||||
## 3. Каталог товаров
|
||||
|
||||
CSV в репозитории (`data/catalog/products.csv`: `sku`, `name`, `category`,
|
||||
`brand`, `price`) — **словарь ClickHouse** из файла. Тот же файл использует
|
||||
генератор — расхождений нет по построению. Даёт `dictGet` в витринах и
|
||||
разговор о политике обновления словаря. На кластере файл монтируется в обе
|
||||
ноды, словарь создаётся ON CLUSTER.
|
||||
`brand`, `price`, `demand`) — **словарь ClickHouse** из файла. Тот же файл
|
||||
использует генератор — расхождений нет по построению. Даёт `dictGet` в
|
||||
витринах и разговор о политике обновления словаря. На кластере файл
|
||||
монтируется в обе ноды, словарь создаётся ON CLUSTER. Последняя колонка —
|
||||
уровень спроса товара, заведена при исполнении #50 (спека генератора,
|
||||
раздел 9): генератор решает по ней, что уходит из карточки в корзину.
|
||||
|
||||
## 4. Сверка `purchase` против заказов
|
||||
|
||||
|
||||
@@ -754,14 +754,90 @@ pytest-тест с маркером `perf` и таймаутом-обрубан
|
||||
склонность из цены* — жёсткая связь, в которой менти найдёт ровно то,
|
||||
что мы в неё вложили, и ничего сверх.
|
||||
- **Границы правки — те же вилки раздела 5**: аудитория 6–8 тыс., средний
|
||||
день около 50 тыс. событий (сейчас 49 509 — запас невелик, и рост
|
||||
событий корзины считается против него), конверсия визита около 2%
|
||||
(сейчас 2,43%). Заказ рисуется из корзины, а корзина сузилась, поэтому
|
||||
доля брошенных позиций (число мира — 15%, замер по покупающим корзинам —
|
||||
12%) и состав заказа (1,87 позиции) перемеряются вместе с
|
||||
остальным: сохраняется не число, а свойство — заказ уже корзины и
|
||||
однопозиционных заказов около половины. Числа канонического мира после
|
||||
правки перемеряются и переписываются.
|
||||
день около 50 тыс. событий, конверсия визита около 2%. Заказ рисуется из
|
||||
корзины, а корзина сузилась, поэтому доля брошенных позиций и состав
|
||||
заказа перемеряются вместе с остальным: сохраняется не число, а
|
||||
свойство — заказ уже корзины и однопозиционных заказов около половины.
|
||||
|
||||
**Измерено после правки #50** (канонический мир, 14 дней; правый столбец —
|
||||
то же до правки). Доли по уровням и состав заказа считаются по окну снимка
|
||||
целиком, а не как среднее дневных долей: заказов в дне около 240, и на одном
|
||||
дне такая доля гуляет на несколько пунктов от одной случайности выборки.
|
||||
Трафиковая половина не сдвинулась ни на строку: набор просмотров страниц
|
||||
сверен со снимком, снятым до первой правки.
|
||||
|
||||
| величина | стало | было |
|
||||
|---|---|---|
|
||||
| дневная аудитория | 6 637–7 595, средняя 7 166 | без изменений |
|
||||
| визитов в день | 9 879 | без изменений |
|
||||
| событий в день, средних | 49 834 | 49 509 |
|
||||
| просмотров карточек в день | 25 067 | без изменений |
|
||||
| событий корзины в день | 2 161, в 1 306 визитах | 1 836, в 765 визитах |
|
||||
| из них визиты без страницы `/cart` | 541 — 41,4% | 0 |
|
||||
| визитов со страницей `/cart` без позиции | 0 | 0 |
|
||||
| просмотров карточек, дошедших до корзины | 8,62% | 7,32% |
|
||||
| конверсия карточки в корзину: магнит / обычный / залёживается | 13,1% / 9,2% / 5,8% | 7,7% у всех |
|
||||
| событий корзины вне `/cart`: магнит / обычный / залёживается | 42,0% / 20,2% / 17,1% | 0 у всех |
|
||||
| конверсия визита в покупку | 2,43% | без изменений |
|
||||
| позиций на заказ | 1,74; однопозиционных 56,4% | 1,87; 50,3% |
|
||||
| брошено позиций в покупающих корзинах | 7,25% | 12,0% |
|
||||
|
||||
Знаменатель «просмотров карточек» здесь — только просмотры страниц. При
|
||||
постановке #50 в него попали и строки событий корзины: событие садится на
|
||||
карточку и несёт её же страницу, отчего вышло 26 903 просмотра и 6,82%
|
||||
вместо 25 067 и 7,32%. Величина одна и та же, счёт разный.
|
||||
|
||||
Решено при исполнении #50 самой реализацией:
|
||||
|
||||
- **Намерение визита берётся из шага воронки, а не из метки покупателя.**
|
||||
Кладёт всякий визит, открывший карточку, но дошедший до `/cart` кладёт
|
||||
почти всё, что сравнивал, а прочий — редко. Метка покупателя в это
|
||||
решение не заходит, и не потому, что не влияет: она уже влияет — через
|
||||
воронку, где помеченному дано 18% против 6%. Взять её вторым слагаемым
|
||||
значило бы посчитать одно и то же дважды и протянуть план состава сквозь
|
||||
шов, по которому день отдаёт торговой половине только страницы и товары.
|
||||
Плата названа: у визита без намерения добавление ни с чем в человеке не
|
||||
связано — упрощение, но не ложь, потому что склонные покупать и так
|
||||
преобладают среди дошедших до `/cart`.
|
||||
- **Два ряда вероятностей, а не ряд с множителем.** Уровень спроса читается
|
||||
дважды: ряд для визита с намерением и ряд для того, кто смотрит. Один ряд
|
||||
с множителем намерения не годится: вероятности визита с намерением обязаны
|
||||
быть высокими (иначе его корзина схлопывается до одной позиции, а заказ до
|
||||
одного товара), а у потолка в 100% разница уровней съедается, и конверсия
|
||||
по уровням перестаёт различаться. Разрыв уровней внутри рядов поэтому
|
||||
разный: у смотрящего товар решает почти всё, у пришедшего покупать —
|
||||
заслонён намерением.
|
||||
- **Доля брошенных позиций опущена с 15% до 10%.** Корзина сузилась с 2,4
|
||||
позиции до 1,7, и прежние 15% чаще выносили её целиком: спасённая позиция
|
||||
возвращала заказ к одному товару, и однопозиционных заказов набиралось под
|
||||
60%. Свойство сохранено, число перемерено — ровно так, как этот блок и
|
||||
оговаривал.
|
||||
- **Ни одному уровню спроса нельзя дать нулевую вероятность.** Ноль у
|
||||
залежавшегося товара выглядел заманчиво — он дешевле всего разводил
|
||||
конверсию уровней, — но по товарам этого уровня возвращал ровно то
|
||||
равенство, ради которого правка и делалась: событие корзины случалось бы
|
||||
только внутри визита, открывшего `/cart`, без единого исключения на
|
||||
четверти каталога. Отвязка либо про весь ассортимент, либо она не отвязка.
|
||||
Разводить уровни поэтому приходится не нулём внизу второго ряда, а
|
||||
ослаблением залежавшегося в первом: пришедший покупать берёт его реже
|
||||
ходового.
|
||||
- **Уровень спроса — колонка `demand` в конце файла каталога**, значения
|
||||
словами («магнит», «обычный», «залёживается»). Раздан он одной колодой на
|
||||
весь каталог: доли по категориям расходятся сами (магнитов от четырёх до
|
||||
девяти на тридцать строк). Раскладывать колоду поровну в каждую категорию
|
||||
было бы той же неправдоподобной ровностью, какую тикет вычищал из данных.
|
||||
Связь с ценой отклонена выше, и тест сторожит её отсутствие — по четвертям
|
||||
цены и по медиане, потому что одни только края списка обманываются
|
||||
четырьмя исключениями. Хвост соседям: словарь ClickHouse над этим файлом
|
||||
ещё не построен (#37/#43) — колонку надо взять в его описание сразу, а не
|
||||
догонять правкой; манифесту нужен хеш каталога (хвост #42) тем более,
|
||||
потому что теперь в файле живёт ещё и поведение.
|
||||
- **Доля просмотров карточек, доходящих до корзины, выросла с 7,3% до 8,6%
|
||||
и подошла к потолку вилки.** Иначе не сошлось: треть кладущих визитов вне
|
||||
`/cart` требует своих позиций, а состав заказа требует, чтобы визит с
|
||||
намерением клал почти всё сравненное. Обе границы тянут число вверх, и
|
||||
опустить его — значит отпустить одну из них. Место до 9% невелико, и
|
||||
следующая правка торговой половины считается против него.
|
||||
|
||||
Остаётся открытым, за тикетами:
|
||||
|
||||
|
||||
@@ -10,16 +10,22 @@
|
||||
раз в неделю, корзина в двадцать позиций.
|
||||
|
||||
Что решено формой файла, а не его длиной: колонки `sku,name,category,brand,
|
||||
price`; артикул — четыре латинские буквы категории, дефис и четыре цифры;
|
||||
цена — целые копейки (деньги генератор считает целыми, спека, раздел 2).
|
||||
Часть цен кратна рублю, часть несёт копейки — как в обычной рознице
|
||||
(1 289,90 ₽). Без копеек урок про Float64 был бы беспредметным: округлять
|
||||
нечего, и разрыв между `productPrice` и `purchaseRevenue` пришлось бы
|
||||
выдумывать (спека генератора, раздел 9).
|
||||
price,demand`; артикул — четыре латинские буквы категории, дефис и четыре
|
||||
цифры; цена — целые копейки (деньги генератор считает целыми, спека,
|
||||
раздел 2). Часть цен кратна рублю, часть несёт копейки — как в обычной
|
||||
рознице (1 289,90 ₽). Без копеек урок про Float64 был бы беспредметным:
|
||||
округлять нечего, и разрыв между `productPrice` и `purchaseRevenue`
|
||||
пришлось бы выдумывать (спека генератора, раздел 9).
|
||||
Строк в файле может быть сколько угодно: ни генератор, ни тесты их не
|
||||
считают, а товар для карточки выбирается равномерно внутри категории.
|
||||
Популярность товаров не моделируется — придумывать вес каждой строке
|
||||
пришлось бы вручную, а каталог растёт механически.
|
||||
|
||||
**Уровень спроса** — привлекательность товара, а не частота его показа:
|
||||
карточку любого товара открывают одинаково часто, но магнит из открытой
|
||||
карточки уходит в корзину чаще, чем залежавшийся. Уровень приписан строке
|
||||
в файле — он постоянная часть мира, а не бросок дня, — и приписан россыпью:
|
||||
ни с ценой, ни с категорией он не связан. Связь с ценой была бы жёсткой, и
|
||||
менти нашёл бы в данных ровно то, что мы в них вложили (спека генератора,
|
||||
раздел 9). Сами вероятности живут в числах мира, здесь только имена уровней.
|
||||
"""
|
||||
|
||||
import csv
|
||||
@@ -34,7 +40,11 @@ from numpy.typing import NDArray
|
||||
# Каталог не настраивается извне — он часть мира, а не запуска.
|
||||
CATALOG_PATH = Path(__file__).resolve().parents[3] / "data" / "catalog" / "products.csv"
|
||||
|
||||
COLUMNS = ("sku", "name", "category", "brand", "price")
|
||||
COLUMNS = ("sku", "name", "category", "brand", "price", "demand")
|
||||
|
||||
# Уровни спроса по убыванию желанности: порядок здесь — порядок чисел мира,
|
||||
# которыми уровень превращается в вероятность (`world.SHOPPING_ADD_PERCENT`).
|
||||
DEMAND_LEVELS = ("магнит", "обычный", "залёживается")
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
@@ -70,6 +80,7 @@ class Catalog:
|
||||
category: NDArray[np.int64]
|
||||
brand: NDArray[np.object_]
|
||||
price: NDArray[np.int64]
|
||||
demand: NDArray[np.int64]
|
||||
grouped: NDArray[np.int64]
|
||||
first: NDArray[np.int64]
|
||||
count: NDArray[np.int64]
|
||||
@@ -85,12 +96,14 @@ def catalog() -> Catalog:
|
||||
category = np.array([index[row["category"]] for row in rows], dtype=np.int64)
|
||||
grouped = np.argsort(category, kind="stable")
|
||||
count = np.bincount(category, minlength=len(CATEGORIES))
|
||||
level = {name: number for number, name in enumerate(DEMAND_LEVELS)}
|
||||
return Catalog(
|
||||
sku=np.array([row["sku"] for row in rows], dtype=object),
|
||||
name=np.array([row["name"] for row in rows], dtype=object),
|
||||
category=category,
|
||||
brand=np.array([row["brand"] for row in rows], dtype=object),
|
||||
price=np.array([int(row["price"]) for row in rows], dtype=np.int64),
|
||||
demand=np.array([level[row["demand"]] for row in rows], dtype=np.int64),
|
||||
grouped=grouped,
|
||||
first=np.concatenate(([0], np.cumsum(count)[:-1])),
|
||||
count=count,
|
||||
|
||||
@@ -8,12 +8,18 @@
|
||||
граница модельных суток режет всё, что за неё вышло: визит, у которого
|
||||
подтверждение срезано полуночью, покупки не даёт — заказа не было.
|
||||
|
||||
**Корзина шире заказа.** Посетитель кладёт товары тех карточек, которые
|
||||
открывал в этом визите; карточка, открытая дважды, даёт одну позицию —
|
||||
повторный просмотр это раздумье, а не второй товар. Покупает он не всё:
|
||||
часть позиций остаётся брошенной. Иначе событие корзины не рассказывало бы
|
||||
ничего сверх покупки — заказ был бы её точной копией, и сравнивать было бы
|
||||
нечего.
|
||||
**Кладёт любой визит, открывший карточку.** Положить и корзину не открыть —
|
||||
самый массовый сюжет магазина, поэтому событие корзины не привязано к
|
||||
странице `/cart`: иначе факт добавления идеально предсказывался бы более
|
||||
поздним просмотром страницы, а такого равенства в живых данных не бывает.
|
||||
Решается каждая открытая карточка отдельно, а не визит целиком, и решают её
|
||||
двое: намерение визита и уровень спроса товара (числа мира). Карточка,
|
||||
открытая дважды, даёт одну позицию — повторный просмотр это раздумье, а не
|
||||
второй товар.
|
||||
|
||||
**Корзина шире заказа.** Покупает посетитель не всё, что положил: часть
|
||||
позиций остаётся брошенной. Иначе событие корзины не рассказывало бы ничего
|
||||
сверх покупки — заказ был бы её точной копией, и сравнивать было бы нечего.
|
||||
|
||||
**Деньги считаются целыми копейками.** В колонку `productPrice` ложатся
|
||||
целые рубли, как у Метрики, а дробное число в событии одно —
|
||||
@@ -145,7 +151,7 @@ def weave(
|
||||
события встают между ними, и поток пересобирается одним порядком.
|
||||
"""
|
||||
rng = day_stream(seed, day, Component.COMMERCE)
|
||||
baskets = _baskets(page, product, columns["VisitID"])
|
||||
baskets = _baskets(rng, page, product, columns["VisitID"])
|
||||
if not len(baskets):
|
||||
return columns, page, product
|
||||
|
||||
@@ -172,13 +178,24 @@ def _draws(rng: np.random.Generator, baskets: _Baskets) -> _Draws:
|
||||
|
||||
|
||||
def _baskets(
|
||||
page: NDArray[np.uint8], product: NDArray[np.int64], visit: NDArray[np.uint64]
|
||||
rng: np.random.Generator,
|
||||
page: NDArray[np.uint8],
|
||||
product: NDArray[np.int64],
|
||||
visit: NDArray[np.uint64],
|
||||
) -> _Baskets:
|
||||
"""Что посетитель положил в корзину и дошёл ли до заказа.
|
||||
|
||||
Корзина есть у визита, дошедшего до страницы корзины; в ней товары всех
|
||||
карточек этого визита. Карточка, открытая дважды, даёт одну позицию —
|
||||
остаётся первая: тогда товар и кладут.
|
||||
Корзина есть у всякого визита, положившего хоть что-то, — не только у
|
||||
дошедшего до страницы `/cart`. Кандидаты — карточки, открытые в визите,
|
||||
по одной на товар: карточка, открытая дважды, даёт одну позицию, и
|
||||
остаётся первая — тогда товар и кладут.
|
||||
|
||||
Намерение визита берётся из шага воронки: тот, кто дошёл до страницы
|
||||
корзины, пришёл покупать. Позиция у него есть всегда — страница корзины
|
||||
при пустой корзине невозможна, — и держится это высокой вероятностью, а
|
||||
не принуждением: спасать приходится считаные проценты таких визитов.
|
||||
Принуждай мы всех, корзина схлопнулась бы до одной позиции, а заказ до
|
||||
одного товара.
|
||||
"""
|
||||
order = np.argsort(visit, kind="stable")
|
||||
page, product, visit = page[order], product[order], visit[order]
|
||||
@@ -187,54 +204,103 @@ def _baskets(
|
||||
# упорядоченным, а сортировка по визиту устойчивая.
|
||||
started = np.concatenate(([True], visit[1:] != visit[:-1]))
|
||||
number = np.cumsum(started) - 1
|
||||
visits = int(started.sum())
|
||||
|
||||
with_cart = number[page == Page.CART]
|
||||
basket_of_visit = np.full(int(started.sum()), -1, dtype=np.int64)
|
||||
basket_of_visit[with_cart] = np.arange(with_cart.size)
|
||||
|
||||
cards = np.flatnonzero((page == Page.PRODUCT) & (basket_of_visit[number] >= 0))
|
||||
goods = catalog.catalog()
|
||||
cards = np.flatnonzero(page == Page.PRODUCT)
|
||||
# Ключ «визит и товар»: `np.unique` отдаёт индексы первых вхождений,
|
||||
# поэтому вторая карточка того же товара позиции не добавляет.
|
||||
key = number[cards] * catalog.catalog().sku.size + product[cards]
|
||||
positions = np.sort(cards[np.unique(key, return_index=True)[1]])
|
||||
# поэтому вторая карточка того же товара кандидата не добавляет.
|
||||
key = number[cards] * goods.sku.size + product[cards]
|
||||
opened = np.sort(cards[np.unique(key, return_index=True)[1]])
|
||||
holder = number[opened]
|
||||
|
||||
# Подтверждение без корзины невозможно: полночь режет визит с хвоста, а
|
||||
# корзина в нём раньше подтверждения — поэтому у каждого подтверждения
|
||||
# корзина есть, и номер её всегда найдётся.
|
||||
shopping = np.zeros(visits, dtype=np.bool_)
|
||||
shopping[number[page == Page.CART]] = True
|
||||
taken = _taken(rng, goods.demand[product[opened]], shopping[holder])
|
||||
# Кандидаты визита лежат подряд, визиты идут по порядку — поэтому начало
|
||||
# каждого куска находится накопленной суммой.
|
||||
opened_count = np.bincount(holder, minlength=visits)
|
||||
opened_first = np.cumsum(opened_count) - opened_count
|
||||
empty = shopping & (np.bincount(holder[taken], minlength=visits) == 0)
|
||||
_at_least_one(rng, taken, opened_first, opened_count, empty)
|
||||
|
||||
positions, owner = opened[taken], holder[taken]
|
||||
with_basket = np.unique(owner)
|
||||
basket_of_visit = np.full(visits, -1, dtype=np.int64)
|
||||
basket_of_visit[with_basket] = np.arange(with_basket.size)
|
||||
|
||||
# Подтверждение без корзины невозможно: до него визит прошёл страницу
|
||||
# корзины, а у неё позиция есть всегда — поэтому номер корзины найдётся.
|
||||
confirmed = np.flatnonzero(page == Page.CONFIRMATION)
|
||||
confirmation = np.full(with_cart.size, -1, dtype=np.int64)
|
||||
confirmation = np.full(with_basket.size, -1, dtype=np.int64)
|
||||
confirmation[basket_of_visit[number[confirmed]]] = order[confirmed]
|
||||
|
||||
basket = basket_of_visit[number[positions]]
|
||||
count = np.bincount(basket, minlength=with_cart.size)
|
||||
basket = basket_of_visit[owner]
|
||||
count = np.bincount(basket, minlength=with_basket.size)
|
||||
return _Baskets(
|
||||
anchor=order[positions],
|
||||
product=product[positions],
|
||||
basket=basket,
|
||||
# Позиции лежат подряд, корзины идут по порядку визитов — поэтому
|
||||
# начало каждого куска находится накопленной суммой.
|
||||
# Позиции лежат подряд, корзины идут по порядку визитов — начало
|
||||
# каждого куска находится той же накопленной суммой.
|
||||
first=np.cumsum(count) - count,
|
||||
count=count,
|
||||
confirmation=confirmation,
|
||||
)
|
||||
|
||||
|
||||
def _taken(
|
||||
rng: np.random.Generator,
|
||||
demand: NDArray[np.int64],
|
||||
shopping: NDArray[np.bool_],
|
||||
) -> NDArray[np.bool_]:
|
||||
"""Дошла ли открытая карточка до корзины: решают намерение и товар.
|
||||
|
||||
Два ряда чисел мира по уровням спроса: один для визита, пришедшего
|
||||
покупать, другой для того, кто просто смотрит. Товар решает у обоих, но
|
||||
у смотрящего он решает почти всё — потому конверсия «карточка → корзина»
|
||||
по уровням спроса и становится различимой.
|
||||
"""
|
||||
chance = np.where(
|
||||
shopping,
|
||||
np.array(world.SHOPPING_ADD_PERCENT)[demand],
|
||||
np.array(world.BROWSING_ADD_PERCENT)[demand],
|
||||
)
|
||||
return rng.integers(0, 100, demand.size) < chance
|
||||
|
||||
|
||||
def _kept(rng: np.random.Generator, baskets: _Baskets) -> NDArray[np.bool_]:
|
||||
"""Какие позиции корзины дошли до заказа: часть остаётся брошенной.
|
||||
|
||||
Пустым заказ не бывает: если брошены все позиции, одна возвращается —
|
||||
какая, решает свой бросок. Хотя бы одна позиция у корзины есть всегда:
|
||||
перед корзиной визит обязательно открывал карточку.
|
||||
Пустым заказ не бывает: если брошены все позиции, одна возвращается.
|
||||
"""
|
||||
kept = (
|
||||
rng.integers(0, 100, baskets.product.size) >= world.ABANDONED_POSITION_PERCENT
|
||||
)
|
||||
rescued = baskets.first + rng.integers(0, baskets.count)
|
||||
empty = np.bincount(baskets.basket[kept], minlength=len(baskets)) == 0
|
||||
kept[rescued[empty]] = True
|
||||
_at_least_one(rng, kept, baskets.first, baskets.count, empty)
|
||||
return kept
|
||||
|
||||
|
||||
def _at_least_one(
|
||||
rng: np.random.Generator,
|
||||
chosen: NDArray[np.bool_],
|
||||
first: NDArray[np.int64],
|
||||
count: NDArray[np.int64],
|
||||
empty: NDArray[np.bool_],
|
||||
) -> None:
|
||||
"""Возвращает один выбор тем группам, у которых бросок унёс все.
|
||||
|
||||
Приём общий у двух мест: у визита выбираются карточки, у корзины —
|
||||
позиции, а правило одно — пустой ни та, ни другая быть не может. Какой
|
||||
выбор вернуть, решает свой бросок: брать первый значило бы, что ранняя
|
||||
карточка визита переживает любой бросок. Группа без членов сюда не
|
||||
приходит: у визита со страницей корзины карточка была, у корзины —
|
||||
позиция.
|
||||
"""
|
||||
chosen[first[empty] + rng.integers(0, count[empty])] = True
|
||||
|
||||
|
||||
def _coupons(rng: np.random.Generator, baskets: int) -> NDArray[np.int64]:
|
||||
"""Промокод корзины: номер строки в таблице кодов или −1, если кода нет."""
|
||||
code = rng.integers(0, len(world.COUPONS), baskets)
|
||||
|
||||
@@ -258,8 +258,9 @@ def _walk(
|
||||
goods = catalog.catalog()
|
||||
shown = page == Page.PRODUCT
|
||||
row_category = np.repeat(visits.category, visits.pages)[shown]
|
||||
# Товар — равномерно внутри категории визита: популярность строк не
|
||||
# моделируется, каталог дорастает механически (см. `catalog`).
|
||||
# Товар — равномерно внутри категории визита: карточки всех товаров
|
||||
# открывают одинаково часто, а различает их уровень спроса — уже в
|
||||
# корзине, а не в показе (см. `catalog`).
|
||||
inside = rng.integers(0, goods.count[row_category])
|
||||
product = np.full(total, -1, dtype=np.int64)
|
||||
product[shown] = goods.grouped[goods.first[row_category] + inside]
|
||||
|
||||
@@ -160,11 +160,40 @@ 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).
|
||||
ABANDONED_POSITION_PERCENT = 15
|
||||
# Число опущено с 15% вместе с отвязкой корзины от страницы корзины: корзина
|
||||
# стала уже (1,7 позиции вместо 2,4), и прежние 15% чаще выносили её целиком,
|
||||
# а спасённая позиция возвращала заказ к одному товару.
|
||||
ABANDONED_POSITION_PERCENT = 10
|
||||
|
||||
# Сколько штук одного товара берут: веса для 1, 2, 3 штук. Обычно одна,
|
||||
# изредка две-три — непродовольственная розница.
|
||||
|
||||
@@ -6,8 +6,10 @@
|
||||
целая цена.
|
||||
"""
|
||||
|
||||
import collections
|
||||
import csv
|
||||
import re
|
||||
import statistics
|
||||
|
||||
import numpy as np
|
||||
|
||||
@@ -78,6 +80,96 @@ def test_categories_are_told_apart_by_prefix_and_by_address():
|
||||
assert len(prefixes) == len(slugs) == len(catalog.CATEGORIES)
|
||||
|
||||
|
||||
def test_every_row_carries_a_known_demand_level():
|
||||
"""Уровень спроса есть у каждой строки, и уровней ровно три."""
|
||||
known = set(catalog.DEMAND_LEVELS)
|
||||
by_category: dict[str, set[str]] = {}
|
||||
for row in rows():
|
||||
by_category.setdefault(row["category"], set()).add(row["demand"])
|
||||
assert set().union(*by_category.values()) == known
|
||||
# Уровни рассыпаны по всему ассортименту: категории целиком из магнитов
|
||||
# или целиком из залежавшегося в магазине не бывает.
|
||||
for category, here in by_category.items():
|
||||
assert here == known, category
|
||||
|
||||
|
||||
def test_the_demand_is_scattered_and_not_dealt_out_evenly():
|
||||
"""Одинаковый расклад в каждой категории — та же неправдоподобная ровность.
|
||||
|
||||
Ровно её тикет и вычищал из данных: в живом магазине магнитов в посуде и
|
||||
в одежде не поровну. Уровень тянется из одной колоды на весь каталог,
|
||||
поэтому доли категорий расходятся сами.
|
||||
"""
|
||||
magnets: dict[str, int] = {}
|
||||
for row in rows():
|
||||
magnets.setdefault(row["category"], 0)
|
||||
magnets[row["category"]] += row["demand"] == catalog.DEMAND_LEVELS[0]
|
||||
assert len(set(magnets.values())) > 1, magnets
|
||||
|
||||
|
||||
def price_tells_the_level(catalogue: list[dict[str, str]]) -> bool:
|
||||
"""Читается ли уровень спроса по цене товара.
|
||||
|
||||
Спрашивается двумя способами сразу, потому что каждый по отдельности
|
||||
обманывается: по четвертям цены — все ли три уровня встречаются в каждой,
|
||||
и по медиане цены уровня — не съехала ли она от медианы каталога. Одни
|
||||
только края списка ничего не стерегут: четырёх исключений в самых
|
||||
дешёвых и самых дорогих строках хватает, чтобы вернуть в них все уровни.
|
||||
"""
|
||||
known = set(catalog.DEMAND_LEVELS)
|
||||
by_price = sorted(catalogue, key=lambda row: int(row["price"]))
|
||||
step = len(by_price) // 4
|
||||
quarters = [by_price[start : start + step] for start in range(0, 4 * step, step)]
|
||||
if any({row["demand"] for row in part} != known for part in quarters):
|
||||
return True
|
||||
|
||||
whole = statistics.median(int(row["price"]) for row in catalogue)
|
||||
for level in catalog.DEMAND_LEVELS:
|
||||
here = [int(row["price"]) for row in catalogue if row["demand"] == level]
|
||||
if not 0.7 < statistics.median(here) / whole < 1.3:
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def derived_from_price(catalogue: list[dict[str, str]]) -> list[dict[str, str]]:
|
||||
"""Подложный каталог: уровень выведен из цены, по краям — исключения.
|
||||
|
||||
Исключения подобраны так, чтобы в самой дешёвой и самой дорогой четверти
|
||||
нашлись все три уровня: столько и нужно, чтобы обмануть проверку, которая
|
||||
смотрит только на края списка.
|
||||
"""
|
||||
magnet, usual, slow = catalog.DEMAND_LEVELS
|
||||
by_price = sorted(catalogue, key=lambda row: int(row["price"]))
|
||||
counted = collections.Counter(row["demand"] for row in catalogue)
|
||||
deck = [level for level in catalog.DEMAND_LEVELS for _ in range(counted[level])]
|
||||
forged = [
|
||||
dict(row, demand=level) for row, level in zip(by_price, deck, strict=True)
|
||||
]
|
||||
forged[0]["demand"] = slow
|
||||
forged[-1]["demand"] = magnet
|
||||
forged[-2]["demand"] = usual
|
||||
return forged
|
||||
|
||||
|
||||
def test_the_demand_level_is_not_the_price_in_disguise():
|
||||
"""Отклонённый вариант: выводить склонность из цены (спека, раздел 9).
|
||||
|
||||
Жёсткая связь дала бы менти ровно то, что мы в неё вложили: «дорогое
|
||||
залёживается» читалось бы из каталога, а не из данных.
|
||||
"""
|
||||
assert not price_tells_the_level(rows())
|
||||
# Сторож сторожа: подмени уровни ценой — проверка обязана покраснеть,
|
||||
# и краевые исключения её не должны обманывать.
|
||||
assert price_tells_the_level(derived_from_price(rows()))
|
||||
|
||||
|
||||
def test_the_demand_level_reaches_the_arrays_row_by_row():
|
||||
goods = catalog.catalog()
|
||||
assert [catalog.DEMAND_LEVELS[number] for number in goods.demand.tolist()] == [
|
||||
row["demand"] for row in rows()
|
||||
]
|
||||
|
||||
|
||||
def test_the_catalog_is_grouped_by_category_whatever_the_file_order():
|
||||
goods = catalog.catalog()
|
||||
assert goods.count.sum() == goods.sku.size
|
||||
|
||||
@@ -21,6 +21,9 @@ from clickstream_generator.seeds import CANONICAL_SEED, Component
|
||||
|
||||
WEEKDAY = 2
|
||||
WEEK = 7
|
||||
# Окно эталонного снимка: доли по уровням спроса меряются на нём целиком —
|
||||
# на одном дне у самого редкого уровня набирается слишком мало карточек.
|
||||
FORTNIGHT = 14
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
@@ -40,6 +43,13 @@ def week() -> list[day.Day]:
|
||||
return [day.stream(CANONICAL_SEED, number) for number in range(WEEK)]
|
||||
|
||||
|
||||
@pytest.fixture(scope="module")
|
||||
def fortnight(week: list[day.Day]) -> list[day.Day]:
|
||||
"""Две недели: неделя пересчитанной не бывает — день чист от соседей."""
|
||||
later = range(WEEK, FORTNIGHT)
|
||||
return week + [day.stream(CANONICAL_SEED, number) for number in later]
|
||||
|
||||
|
||||
def rows_of(events: day.Day, kind: str) -> dict[str, NDArray[Any]]:
|
||||
"""Строки одного типа события — все колонки разом."""
|
||||
here = events.columns["EventType"] == kind
|
||||
@@ -69,11 +79,58 @@ def test_the_day_carries_both_trade_events(weekday: day.Day):
|
||||
assert carts["WatchID"].size > 2 * orders["WatchID"].size
|
||||
|
||||
|
||||
def visits_with(events: day.Day, page: int) -> set[int]:
|
||||
"""Визиты, у которых эта страница магазина дожила до потока.
|
||||
|
||||
Считаются просмотры страниц, а не строки с такой же страницей: торговое
|
||||
событие садится на свою страницу и несёт её же, поэтому маска по одному
|
||||
`page` посчитала бы событие корзины за просмотр карточки.
|
||||
"""
|
||||
here = (events.columns["EventType"] == "pageview") & (events.page == page)
|
||||
return set(events.columns["VisitID"][here].tolist())
|
||||
|
||||
|
||||
def confirmed_visits(events: day.Day) -> set[int]:
|
||||
"""Визиты, чья страница подтверждения дожила до потока."""
|
||||
return visits_with(events, Page.CONFIRMATION)
|
||||
|
||||
|
||||
def visits_that_put(events: day.Day) -> set[int]:
|
||||
"""Визиты с событием корзины — по типу события, а не по массивам товаров.
|
||||
|
||||
Строка покупки тоже несёт товары, поэтому «строка с `productID`» — это
|
||||
не «событие корзины».
|
||||
"""
|
||||
return set(rows_of(events, commerce.ADD_TO_CART)["VisitID"].tolist())
|
||||
|
||||
|
||||
def cart_products(events: day.Day) -> list[int]:
|
||||
"""Номера товаров, положенных в корзину, — по позиции на строку."""
|
||||
here = events.columns["EventType"] == commerce.ADD_TO_CART
|
||||
return events.product[here].tolist()
|
||||
|
||||
|
||||
def card_views(events: day.Day) -> int:
|
||||
"""Просмотры карточек товара в дне — строками потока, с повторами."""
|
||||
pageview = events.columns["EventType"] == "pageview"
|
||||
here = pageview & (events.page == Page.CONFIRMATION)
|
||||
return set(events.columns["VisitID"][here].tolist())
|
||||
return int((pageview & (events.page == Page.PRODUCT)).sum())
|
||||
|
||||
|
||||
def opened_cards(events: day.Day) -> set[tuple[int, int]]:
|
||||
"""Разные карточки визитов: повторный просмотр внутри визита — один раз.
|
||||
|
||||
Так же считается и позиция корзины, поэтому это и есть честный
|
||||
знаменатель конверсии «просмотр карточки → корзина».
|
||||
"""
|
||||
pageview = events.columns["EventType"] == "pageview"
|
||||
here = pageview & (events.page == Page.PRODUCT)
|
||||
return set(
|
||||
zip(
|
||||
events.columns["VisitID"][here].tolist(),
|
||||
events.product[here].tolist(),
|
||||
strict=True,
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
def test_a_confirmation_in_the_stream_always_has_its_purchase(weekday: day.Day):
|
||||
@@ -238,26 +295,63 @@ def test_the_rounded_price_does_not_add_up_to_the_revenue(weekday: day.Day):
|
||||
assert 0.1 < apart / orders["WatchID"].size < 0.9
|
||||
|
||||
|
||||
def test_the_order_is_narrower_than_the_cart(weekday: day.Day):
|
||||
"""Часть положенных позиций не куплена — иначе сравнивать было бы нечего."""
|
||||
carts = rows_of(weekday, commerce.ADD_TO_CART)
|
||||
orders = rows_of(weekday, commerce.PURCHASE)
|
||||
def basket_and_order(events: day.Day) -> tuple[list[int], list[int]]:
|
||||
"""По каждому заказу — сколько позиций было в корзине и сколько куплено.
|
||||
|
||||
Заодно проверяется само отношение «заказ из корзины»: пустых заказов не
|
||||
бывает, купленного мимо корзины не бывает, позиция в заказе одна на товар.
|
||||
"""
|
||||
carts = rows_of(events, commerce.ADD_TO_CART)
|
||||
orders = rows_of(events, commerce.PURCHASE)
|
||||
put: dict[int, set[str]] = {}
|
||||
for visit, article in zip(
|
||||
carts["VisitID"].tolist(), (cell[0] for cell in carts["productID"]), strict=True
|
||||
):
|
||||
put.setdefault(visit, set()).add(article)
|
||||
|
||||
dropped = whole = 0
|
||||
baskets: list[int] = []
|
||||
sizes: list[int] = []
|
||||
for visit, bought in zip(
|
||||
orders["VisitID"].tolist(), cells(orders["productID"]), strict=True
|
||||
):
|
||||
assert bought, "пустых заказов не бывает"
|
||||
assert set(bought) <= put[visit], "куплено то, чего не клали в корзину"
|
||||
assert len(set(bought)) == len(bought), "позиция в заказе одна на товар"
|
||||
dropped += len(put[visit]) - len(bought)
|
||||
whole += len(put[visit])
|
||||
assert 0.05 < dropped / whole < 0.30
|
||||
baskets.append(len(put[visit]))
|
||||
sizes.append(len(bought))
|
||||
return baskets, sizes
|
||||
|
||||
|
||||
def test_the_order_is_narrower_than_the_cart(weekday: day.Day):
|
||||
"""Часть положенных позиций не куплена — иначе сравнивать было бы нечего."""
|
||||
baskets, sizes = basket_and_order(weekday)
|
||||
assert sum(baskets) > sum(sizes)
|
||||
|
||||
|
||||
def test_the_order_keeps_its_shape_across_the_snapshot(fortnight: list[day.Day]):
|
||||
"""Состав заказа — доли окна снимка, а не одного дня.
|
||||
|
||||
Заказов в дне около 240, и доля однопозиционных гуляет по дням на
|
||||
несколько пунктов от одной случайности выборки; вилки тикета заданы на
|
||||
14 днях, там же они и меряются. Список позиций — единственный носитель
|
||||
урока про вложенный JSON: схлопнись он в один товар, урок бы умер.
|
||||
"""
|
||||
baskets: list[int] = []
|
||||
sizes: list[int] = []
|
||||
for events in fortnight:
|
||||
was, bought = basket_and_order(events)
|
||||
baskets += was
|
||||
sizes += bought
|
||||
|
||||
assert 1.6 < sum(sizes) / len(sizes) < 2.2
|
||||
assert 0.40 < sum(size == 1 for size in sizes) / len(sizes) < 0.60
|
||||
# Брошено не ничего и не всё. Вилка та же, что и до правки: число мира
|
||||
# опустилось с 15% до 10%, замер по покупающим корзинам — с 12,0% до
|
||||
# 7,2%, и запас до нижней границы остался в полтора раза. Ниже её
|
||||
# ронять незачем: с ней сторож замечает, что число мира срезали вдвое,
|
||||
# а без неё — уже нет.
|
||||
dropped = 1 - sum(sizes) / sum(baskets)
|
||||
assert 0.05 < dropped < 0.30
|
||||
|
||||
|
||||
def test_the_cart_holds_only_what_the_visitor_opened(weekday: day.Day):
|
||||
@@ -284,6 +378,102 @@ def test_the_cart_holds_only_what_the_visitor_opened(weekday: day.Day):
|
||||
assert len(set(products)) == len(products)
|
||||
|
||||
|
||||
def test_putting_something_in_the_cart_is_not_opening_the_cart_page(
|
||||
fortnight: list[day.Day],
|
||||
):
|
||||
"""Положил и корзину не открыл — самый массовый сюжет магазина.
|
||||
|
||||
До правки два множества совпадали в точности все 14 дней, и факт
|
||||
добавления идеально предсказывался более поздним просмотром страницы —
|
||||
такого равенства в живых данных не бывает. Сторож меряет долю, а не факт
|
||||
различия одним визитом: одно расхождение прошлую ложь не лечит.
|
||||
"""
|
||||
quiet = putting = 0
|
||||
for events in fortnight:
|
||||
put = visits_that_put(events)
|
||||
quiet += len(put - visits_with(events, Page.CART))
|
||||
putting += len(put)
|
||||
assert quiet / putting > 1 / 3
|
||||
|
||||
|
||||
def test_a_visit_that_opened_the_cart_page_has_something_in_the_cart(
|
||||
fortnight: list[day.Day],
|
||||
):
|
||||
"""Страница корзины при пустой корзине невозможна.
|
||||
|
||||
Это включение намеренное и остаётся: ломалось обратное. Проверяется на
|
||||
обеих неделях — и на буднях, и на выходных: правило про всякий визит, а
|
||||
не про удачный день.
|
||||
"""
|
||||
for events in fortnight:
|
||||
assert visits_with(events, Page.CART) <= visits_that_put(events)
|
||||
|
||||
|
||||
def test_no_demand_level_is_locked_behind_the_cart_page(fortnight: list[day.Day]):
|
||||
"""Отвязка от страницы корзины — про весь ассортимент, а не про часть.
|
||||
|
||||
Обнули вероятность целому уровню — и по его товарам вернётся ровно то
|
||||
равенство, ради которого правка делалась: «положили — значит, откроют
|
||||
корзину», без единого исключения на четверти каталога. Сторож меряет
|
||||
долю у каждого уровня, а не факт: одно событие такую примету не лечит.
|
||||
"""
|
||||
goods = catalog.catalog()
|
||||
levels = range(len(catalog.DEMAND_LEVELS))
|
||||
quiet = [0] * len(catalog.DEMAND_LEVELS)
|
||||
put = [0] * len(catalog.DEMAND_LEVELS)
|
||||
for events in fortnight:
|
||||
shopping = visits_with(events, Page.CART)
|
||||
here = events.columns["EventType"] == commerce.ADD_TO_CART
|
||||
for visit, number in zip(
|
||||
events.columns["VisitID"][here].tolist(),
|
||||
events.product[here].tolist(),
|
||||
strict=True,
|
||||
):
|
||||
put[goods.demand[number]] += 1
|
||||
quiet[goods.demand[number]] += visit not in shopping
|
||||
|
||||
for level in levels:
|
||||
assert quiet[level] / put[level] > 0.10, catalog.DEMAND_LEVELS[level]
|
||||
|
||||
|
||||
def test_the_demand_level_tells_the_cart_conversion_apart(fortnight: list[day.Day]):
|
||||
"""Привлекательность товара стала измеримой величиной, а не шумом.
|
||||
|
||||
Знаменатель — разные карточки товаров этого уровня: повторный просмотр
|
||||
внутри визита считается один раз, так же как считается позиция. До
|
||||
правки решение принималось один раз на визит, и по sku выходил чистый
|
||||
шум: внутри воронки 100%, вне её 0%.
|
||||
"""
|
||||
goods = catalog.catalog()
|
||||
levels = range(len(catalog.DEMAND_LEVELS))
|
||||
shown = [0] * len(catalog.DEMAND_LEVELS)
|
||||
put = [0] * len(catalog.DEMAND_LEVELS)
|
||||
for events in fortnight:
|
||||
for _, number in opened_cards(events):
|
||||
shown[goods.demand[number]] += 1
|
||||
for number in cart_products(events):
|
||||
put[goods.demand[number]] += 1
|
||||
|
||||
share = [put[level] / shown[level] for level in levels]
|
||||
magnet, usual, slow = share
|
||||
assert magnet > usual > slow
|
||||
assert magnet >= 2 * slow
|
||||
|
||||
|
||||
def test_the_cart_events_stay_inside_the_event_budget(fortnight: list[day.Day]):
|
||||
"""Меняется не сколько кладут, а кто и что: бюджет событий на месте.
|
||||
|
||||
Доля просмотров карточек, дошедших до корзины, — та же величина, что
|
||||
держала бюджет до правки; средний день остаётся около 50 тыс. событий
|
||||
(спека генератора, разделы 5 и 9).
|
||||
"""
|
||||
put = sum(len(cart_products(events)) for events in fortnight)
|
||||
seen = sum(card_views(events) for events in fortnight)
|
||||
assert 0.05 < put / seen < 0.09
|
||||
average = sum(len(events) for events in fortnight) / len(fortnight)
|
||||
assert 48_000 < average < 52_000
|
||||
|
||||
|
||||
def test_a_trade_event_sits_on_the_page_that_sent_it(weekday: day.Day):
|
||||
"""Корзина — на карточке того самого товара, покупка — на подтверждении.
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user