Привет! Я Эдуард, в ecom.tech руковожу группой прогноза спроса для Мегамаркета. В этой статье хочу рассказать, что меняется в работе с алгоритмами машинного обучения, когда начинаешь учитывать ограничения и нюансы бизнес-задачи. Расскажу на примере одного исследования – как мы искали способы увеличить выручку маркетплейса.
В статье будет математика, псевдокод, но главное – постараюсь рассказать, как вся эта техническая часть опирается на бизнес-контекст. Поехали!
Описание бизнес-процесса: карточка товара
Мегамаркет — стремительно развивающийся маркетплейс. Одна из его особенностей состоит в том, что товарные предложения от разных продавцов по конкретному товару представлены внутри одной карточки.
В примере мы видим описание товара, его цену, сроки доставки, кешбэк, а также кнопку «Купить». Справа внизу есть кнопка «Еще n предложений от m рублей», нажав на которую мы увидим все товарные предложения и условия по данному товару от различных продавцов.
Напротив каждого товарного предложения есть своя кнопка «Купить». Видно, что товарные предложения отсортированы определенным образом и на их позицию влияет ряд факторов. Назовем их факторами «привлекательности». Чем выше по нашим расчётам «привлекательность» предложения для клиента, тем выше оно в списке.
А ещё обратите внимание, что верхнее предложение идентично тому, что отображается у фиолетовой кнопки «Купить». Это не совпадение. Товарное предложение на верхнем месте становится выбором по умолчанию при нажатии вышеупомянутой кнопки. Эти товарные предложения мы называем «избранными».
Постановка задачи: увеличить выручку
Факторы, которые могут влиять на привлекательность товарного предложения, могут быть разными – навскидку, это цена или кешбэк, сроки доставки или ее стоимость.
Управляя этими факторами, можно влиять на сортировку товарных предложений.
История ниже – о том, как мы в группе прогноза спроса исследовали, какие товары выгоднее всего выводить в избранные, с точки зрения максимизации выручки маркетплейса.
Выручка формируется из двух ключевых составляющих:
цена проданного товара;
количество проданных товаров.
С одной стороны, если предложение с самой высокой ценой попадает в «избранные», оборот от каждой продажи будет большим. Однако, вероятно, такой товар будет покупаться реже из-за своей высокой стоимости.
С другой стороны, товар с более доступной ценой будет покупаться чаще, но каждая продажа будет приносить меньше выручки. Чаще всего цена и объем продаж действуют в противоположных направлениях. Мы хотим найти ту золотую середину, которая максимизирует выручку.
В постановке задачи нам необходимо было учитывать пожелания бизнеса — полученная модель должна быть хорошо интерпретируемой.
Правила сортировки товарных предложений были разработаны специалистами, с учётом их опыта и глубокого понимания рынка. Любые изменения в логике сортировки, опирающиеся на математику, эксперименты или что бы то ни было – должны быть прозрачными для бизнеса.
Поскольку важна интерпретируемость, то модели типа Black-Box отпадают автоматически. Так получаем оптимизационную задачу, где целью является поиск наилучших коэффициентов для увеличения выручки.
Давайте расскажу, как мы исследовали эту задачу.
Формализация задачи
Требуется найти вектор параметров каждый из которых лежит в интервалеи используется в функции
Эта функция определяет продавца товара.
Все продавцы маркетплейса индексированы , все товары индексированы ; стоимость товара у продавца . Параметр для цены не подбирается, а назначается равным единице (максимальное значение интервала для остальных параметров); четыре численных характеристики продавцатовара .
Если продавец не продает товар , то полагаем
Положим- значение функции для пары
Пусть параметры векторазаданы, а товарзафиксирован (то есть определено значение его индекса ). Тогда для товара основным является тот продавец, для которого значение функции достигает максимума. Если максимум достигается в нескольких точках, то основного продавца выбираем случайным образом.
Параметры вектора должны быть определены таким образом, чтобы максимизировать выручку маркетплейса.
Матрица разметки
Рассмотрим матрицу, в которой по осям продавец-товар однозначно показано, какой продавец для какого товара является оптимальным.
Пусть параметрыизвестны, тогда определенаматрица где если для товара при заданном основной продавец - , в противном случае
В таком случае для произвольной пары продавец-товар
Отметим, что должно выполняться равенстводля любого товара Поэтому если для некоторогосуществуют несколько значений таких, что , то случайным образом отбираем только один из таких кандидатов, а для остальных индексов значение функции для этого зануляем.
Матрица объемов продаж R
Для каждого товара по историческим данным определяем объем продаж (в количественном эквиваленте) этого товара за единицу времени (за предыдущий день) каждым продавцом . Если продавец не реализует товар , то полагаем, что .
Матрица цен C
Для каждого товара определяем цену у продавцаБерется последняя известная цена (за предыдущий день).
Функция ожидаемой выручки
По определению эта функция равна
для всех
Функция не является непрерывной в рассматриваемом гиперкубе. Поэтому оптимизационные методы, основанные на дифференциальном исчислении, могут давать неудовлетворительный результат. Вероятно, для решения предпочтительней будет использовать какой-либо derivative-free optimization solver.
Псевдокод описанной модели:
import numpy as np
import pandas as pd
def construct_model(x: list[float], features: list[str], df: pd.DataFrame) -> float:
"""Модель оптимизации выручки
:param x: список оптимизируемых параметров (инициализация)
:param features: список с названиями факторов привлекательности (price,
cashback, delivery_time, delivery_price)
:param df: датафрейм с данными о факторах привлекательности и объемах продаж
(quantity с индексом (продавец, товар)
:return: ожидаемая выручка при заданных значениях x"""
dims = размер тензора (i, j, k)
missed_vals = вектор длиной k заполненной - Inf и 0 (для цены)
coords = сохраняем индексы ненулевых элементов из датафрейм df
B = создаем тензор размерностью dims и заполняем missed_vals
B[coords] = заменяем missed_vals на значения (где они имеются)
R = создаем матрицу из нулей размером (i, j)
R[coords] = по аналогии, заменяем нулевые значения на фактические
C = матрица цен размером (i, j)
phi = считаем привлекательность как скалярное произведение B и x
max_phi = выбираем индекс самого привлекательного продавца
L = матрицу max_phi пребразумем {0, 1} матрицу
LRC = L * R * C поэлементно перемножаем матрицы
f = - sum(LRC) суммируем по i, j
return f
Ограничения и допущения
Наша оптимизационная модель практически готова, но существует одно концептуальное ограничение, препятствующее прямому решению задачи. Оно связано с матрицей объемов продаж . Как я упоминал выше, особенностью маркетплейса является то, что внутри карточки товара есть разные товарные предложения.
Избранное товарное предложение будет иметь больший объем продаж по сравнению с остальными из-за его заметного местоположения — прямо под главной фиолетовой кнопкой «Купить». Этот высокий объем продаж может зависеть от UX-дизайна карточки товара.
Действительно, если где-то внизу, на 20-м месте, будет размещено более привлекательное товарное предложение, его объем продаж, вероятно, будет ниже, чем у избранного. А выручка избранных предложений будет выше по сравнению с остальными. Если учитывать, что:
а) объем продаж напрямую влияет на выручку;
б) мы базируемся на сырых данных, полученных из исторической статистики за день.
Ситуация усложняется тем, что объемы продаж варьируются в гораздо более широком диапазоне, чем цена — второй множитель в формуле выручки.
Так, если в матрице R будут представлены реальные значения, то множество товарных предложений, максимизирующих выручку, совпадет с текущим множеством избранных предложений. Оптимальные параметры x будут такими, при которых сохраняется первоначальная сортировка в карточке товара, и, следовательно, оптимизации не произойдет.
Чтобы обойти данное ограничение, необходимо каким-то образом скорректировать объемы продаж и использовать их в матрице . Скорректированные значения можно будет интерпретировать следующим образом: какой бы был объем продаж у товарного предложения, если бы оно было избранным.
Перед корректировкой отметим два момента:
объемы продаж избранных товарных предложений не зависят от сортировки, поэтому их значения не требуют корректировки;
главный фактор, от которого зависит объем продаж, будем считать цену.
Поэтому, для каждого товара (не товарного предложения, а товара), по историческим данным о продажах за последний месяц, строится линейная модель зависимости объема продаж от цены (точнее от ), которая имеет вид:
цена, по которой был куплен товар;
цена избранного товарного предложения в момент покупки товара;
объем продаж при разнице цены покупки товара и цены избранного предложения.
Цель построения данных моделей – получение для каждого товара значения коэффициента , характеризующего степень зависимости объемов продаж от цен. Его, с определенными допущениями, можно интерпретировать как эластичность спроса по цене.
В результате построения были получены следующие взаимосвязи (пример для нескольких товаров).
В качестве моделей использовалась робастная линейная модель с Huber Loss функцией.
В случае, если данных для конкретного товара недостаточно (товар редко покупают), в качестве коэффициента бралось среднее значение по товарной категории, которой принадлежит данный товар.
Видно, что у каждого товара своя зависимость объема продаж от цены и это стоит использовать: для каких-то товаров спрос при изменении цены падает сильнее, для других – в меньшей степени.
Следующим шагом является использование коэффициентов для корректировки объемов продаж товарных предложений матрицы .
скорректированный объем продаж товара у продавца;
объем продаж избранного товарного предложения товараза единицу времени (за предыдущий день);
коэффициент эластичности для товара(всегда меньше нуля)
цена товара у продавца ;
- цена избранного товарного предложения товара.
Данная формула является хорошо интерпретируемой и имеет концептуальный смысл. Если
В итоге модель имеет следующий вид
Оптимизация: имитация отжига
Из-за негладкости функции применение градиентных оптимизационных методов ограничено (хоть и не полностью). Учитывая это, в качестве оптимизационного алгоритма предлагается использовать simulated annealing (имитация отжига). Используем его реализацию Dual Annealing из пакета scipy.optimize.
Первым аргументом передадим приведенную выше функцию . В аргументе зададим границы для каждого коэффициента.
Анализ результатов
Анализ результатов начался с проверки на исторических данных. Мы сравнивали ожидаемую выручку двух множеств: исторически избранных товарных предложений и предложений, которые стали избранными в результате новой сортировки, выполненной с использованием коэффициентов, определенных при оптимизации.
ожидаемая выручка увеличилась на 2.7%;
средняя цена уменьшилась на 1.3%;
средний кэшбэк увеличился на 1.7%;
средние сроки доставки уменьшились на 4.4%;
средняя стоимость доставки увеличилась на 2.7%;
Следующим шагом мы хотели убедиться, что ситуация будет аналогичной не только на исторических данных, но и в реальности. Для этого был проведен А/B-тест.
A/B-тест
В эксперименте участвовали товары из 1 категории и 3 городов.
В остальном все по классике:
выбираем правило разделения на пилотную и контрольную группы;
выбираем ключевую и контрольные метрики;
проводим А/А-тесты для проверки ключевой метрики;
выбираем величину минимального детектируемого эффекта;
считаем необходимый размер выборки;
проводим A/B-тест.
Сплитование – по клиентам. Одной группе будет показан новый функционал, другой — старый. В качестве ключевой метрики берем среднюю выручку. В качестве контрольных метрик используем: количество клиентов в группах, среднее количество купленных товаров в корзине, средний размер примененного кешбэка. После этого проведем А/А-тест для ключевой метрики с целью проверки равномерности p-value.
Следующим шагом является выбор минимального детектируемого эффекта. В качестве отправной точки будем использовать значение ожидаемой выручки в 2.7 %. Исходя из этого, рассчитаем необходимый размер выборки, который даст статистически значимый результат, принимая уровни ошибок 1 и 2 рода равными 0.05 и 0.2 соответственно.
Таким образом, с помощью этого эксперимента мы получили подтверждение о наличии эффекта с заданной статистической значимостью.
Как найти оптимальное решение
Впереди – много всего интересного.
Первое, что хочется попробовать — переформулировать текущую модель с целью преобразования оптимизируемой функции в выпуклую. Это позволит применять солверы, обеспечивающие сходимость к глобальному оптимуму.
Также мы планируем провести исследования для других категорий товаров.
Для учета большего количества критериев, влияющих на популярность товара, в модель предстоит добавить другие факторы, такие как рейтинг продавца, размер скидки и клиентские характеристики.
Это исследование дало нам не только математические и экспериментальные результаты. Помимо потенциальных точек роста для бизнеса, подтвержденных статистически, ещё мы получили опыт. Лишний раз увидели на практике, в какой области могут лежать ответы на вопрос “Как найти оптимальное решение прикладной оптимизационной задачи?”.
Ключевое здесь на мой взгляд – помнить, что решение не всегда опирается на наиболее популярные инструменты. Важный принцип – учитывать специфику бизнеса, принимать во внимание допущения и ограничения конкретного бизнес-контекста.
Нужно помнить, что задача в первую очередь прикладная и только потом оптимизационная. Остальное – дело техники.