Привет, Habr! На связи отдел аналитики данных X5 Tech.

Сегодня мы поговорим об очень интересном разделе прикладной математики — оптимизации.

Цели данной статьи:

  • рассказать про задачи в ритейле, которые могут решаться методами оптимизации,

  • продемонстрировать, как модельная задача ценообразования решается пакетами Pyomo и SciPy,

  • сравнить производительность солверов* Pyomo и SciPy на примере поставленной задачи.

Прим. Солвер (от англ. Solver) - программа, скомпилированная под выбранную платформу, для решения математической задачи.

Так как данная тема достаточно обширна, то помимо данной статьи (статья 1) мы планируем написать ещё две:

  • Статья 2: Обзор open-source солверов на примере задачи ритейла.

  • Статья 3: Решение модельной задачи ценообразования оптимизаторами в различных постановках.

Примеры задач

Практически каждый человек ежедневно решает оптимизационные задачи даже не задумываясь об этом. Пара примеров:

Закупка. Как правило, мы хотим минимизировать наши расходы для приобретения необходимых товаров, но при условии, чтобы эти товары были максимально полезны. Полезность здесь у каждого своя: для одних она определяется количеством растительных жиров, для других — минимальной суммарной стоимостью корзины, для третьих — наличием привычных товаров, и тд.

Отпуск. Во время ограниченного отпуска мы хотим распределить свой маршрут так, чтобы посетить всё, что запланировали с минимальными временными затратами на дорогу, и при этом не забыть позагорать на пляже.

Бизнес-требования часто приводят к задаче многокритериальной оптимизации и задаче оптимального управления. В таких постановках решения, найденные методами оптимизации, чрезвычайно полезны для принятия решений. Для иллюстрации приведём несколько задач из области ритейла, которые могут быть сформулированы в виде задачи оптимизации.

Ценообразование. Поиск оптимальной конфигурации цен с учетом ценового позиционирования, допустимых ценовых диапазонов для каждого товара и набора бизнес-ограничений. Цены должны максимизировать суммарный доход, а прибыль быть не ниже заданного уровня.

Оптимальное распределение маркетингового бюджета. Реализовать максимально эффективно выделенный на маркетинговые активности бюджет. Есть несколько каналов для рекламных акций, цель — инвестировать бюджет так, чтобы суммарный доход со всех коммуникаций был максимален. Необходимо учесть ограничения на предельную нагрузку на канал, допустимое количество коммуникаций для каждого клиента и пр.

Планирование ассортимента. Подобрать полочный ассортимент товаров так, чтобы максимизировать оборот с учетом полочного пространства и других характеристик магазина.

Закупка товаров. Задача — распределить бюджет, выделенный на закупки так, чтобы поддерживать товарооборот, заданный уровень доступности товаров и при этом достигнуть определенных финансовых показателей.

Логистика. Задача — найти оптимальный график доставки продуктов с учётом вместимости грузовиков, затрат на логистику и тд.

Общая постановка задачи и её разновидности

Прежде всего рассмотрим постановку задачи в общем виде:

x- вектор размерности nx  \in X - допустимое множество значений этих переменных.

f(x)→\min(\max),f(\cdot)- целевая функция;

g_{i}(x) \leqslant 0,\ i=1..m- ограничения вида неравенств;

h_i(x) = 0,\ j=1..k- ограничения вида равенств;

Исходя из практики можно разложить данную постановку на несколько классов в зависимости от вида целевой функции, ограничений и X:

  • Безусловная оптимизация g_i(x), h_j(x)- отсутствуют, X = \mathbb{R}^n

  • LP (linear programming) - линейное программирование. f(x), g_i(x), h_j(x)- линейные функции, X = \mathbb{R}_+^n

  • MILP (mixed integer linear programming) - смешанное целочисленное линейное программирование, это задача LP в которой только часть переменных являются целочисленными;

  • NLP (nonlinear programming) - нелинейное программирование, возникает когда хотя бы одна из функций f(x),\ g_i(x),\ h_j(x)нелинейна;

  • MINLP (mixed integer nonlinear programming) - смешанное целочисленное нелинейное программирование, возникает как и в MILP, когда часть переменных принимает целочисленные значения;

NLP в свою очередь можно подробить еще на множество разных классов в зависимости от вида нелинейности и выпуклости.

Подробнее о том, как получаются различные виды задачи ценообразования исходя из бизнес-формулировки, мы будем говорить в статье 3.

Оптимизация модельной задачи ценообразования

Рассмотрев основные классы оптимизационных задач, перейдём к построению модели ценообразования.

Предположим, что для товара iизвестно значение эластичности E_i, а спрос задаётся следующей зависимостью:

\begin{equation} Q_{i}(P_{i}) = Q_{0, i} \exp\bigg(E_{i} \cdot \bigg(\frac{P_{i}}{P_{0, i}} - 1\bigg)\bigg). \end{equation}

Введём обозначения:

  • n- количество товаров,

  • C_i- себестоимость i-го товара,

  • Q_{i}, Q_{0, i}- спрос по новой P_iи текущей P_{0, i}ценам, соответственно,

  • x_{i}=P_{i}\mathbin{/}P_{0, i}.

В качестве иллюстративного примера рассмотрим задачу с одним ограничением сложного вида и ограничения на переменные.

Задача — найти такой набор новых цен, чтобы:

  • максимизировать оборот со всех товаров,

  • общая прибыль осталась на прежнем уровне,

  • цены лежали в заданных границах (индекс l и u - для нижней и верхней, соответственно).

Тогда оптимизационную задачу можно записать следующей системой:

\begin{cases} \sum_{i=1}^{n} P_i(x_i) \cdot Q_i(x_i) \to \max_{x},\\ \\ \sum_{i=1}^{n} (P_i(x_i) - C_i) \cdot Q_i(x_i) \geqslant \sum_{i=1}^{n} (P_{0, i} - C_i) \cdot Q_ {0, i},\\ \\ x_i \in [x_i^l, x_i^u], \ i=1..n\\ \end{cases}

Данная модель принадлежит к классу NLP, как нетрудно заметить из данных выше определений.

Реализация модели в Pyomo и SciPy

Для демонстрации решения необходимы данные.

Реальные данные мы использовать не можем из-за NDA, поэтому будем генерировать их из случайных распределений (функция generate_data), исходя из наших представлений о возможных значениях величин в фуд-ритейле.

 Пример данных для NLP постановки: 

SciPy и Pyomo имеют разные интерфейсы, чтобы как-то унифицировать работу с ними, будем наследоваться от базового класса, код класса.

Методы, которые необходимо реализовать для каждого солвера:

  • init_objective - задание целевой функции,

  • init_constraints - добавление ограничений,

  • solve - поиск оптимального решения.

Опишем далее отличия в реализации этих методов в SciPy и Pyomo.

Другие примеры из официальной документации можно найти здесь, для Pyomo и здесь, для SciPy.

Задание целевой функции

В SciPy оптимизатор работает в режиме минимизации, поэтому суммарный оборот берём со знаком "-".

В Pyomo функцию пересчёта суммарного оборота необходимо передать в переменную expr объекта pyo.Objective.

# SciPy
def objective(x):
    return -sum(self.P * x * self.Q * self._el(self.E, x))

# Pyomo
objective = sum(self.P[i] * self.model.x[i] * self.Q[i] * self._el(i) for i in range(self.N))
self.model.obj = pyo.Objective(expr=objective, sense=pyo.maximize)

Задание ограничений

Ограничение на суммарную прибыль задаётся в методе init_constraints.

Для SciPy ограничения передаются через NonlinearConstraint или LinearConstraint().

Для Pyomo ограничения передаются через pyo.Constraint().

# SciPy
A = np.eye(self.N, self.N, dtype=float)
bounds = LinearConstraint(A, self.x_lower, self.x_upper)
self.constraints.append(bounds)

def con_mrg(x):
    m = sum((self.P * x - self.C) * self.Q * self._el(self.E, x))
    return m
constr = NonlinearConstraint(con_mrg, self.m_min, np.inf)
self.constraints.append(constr)

# Pyomo
self.model.x = pyo.Var(range(self.N), domain=pyo.Reals, bounds=bounds_fun, initialize=init_fun)
con_mrg_expr = sum((self.P[i] * self.model.x[i] - self.C[i]) * self.Q[i] * self._el(i)
                   for i in range(self.N)) >= self.m_min
self.model.con_mrg = pyo.Constraint(rule=con_mrg_expr)

Поиск решения

В SciPy задача оптимизации запускается через метод minimize, а в Pyomo через проинициализированный объект SolverFactory методом solve.

# SciPy
result = minimize(self.obj, self.x_init,  method='cobyla', constraints=self.constraints)

# Pyomo
solver = pyo.SolverFactory(solver, tee=False)
result = solver.solve(self.model)

Расчёт и результаты

Так как оптимизаторы устанавливаются отдельно от python, то для получения аналогичных результатов можно воспользоваться Dockerfile, настроив контейнер, как описано в README проекта.

Для сравнения расчётов достаточно выполнить команду (осторожно, в режиме compare расчёт на макбуке 2019 года занимает > 3 часов, для ускорения можно уменьшить GRID_SIZE):

python runner.py -m compare --GRID_SIZE 255

Зависимость длительности поиска оптимального решения от количества переменных представлена на графике ниже.

Из графика можно сделать вывод, что Pyomo с солвером ipopt значительно превосходит SciPy с cobyla при N > 20.

Отметим, что Pyomo затрачивает больше времени на подготовку данных во внутренний формат, что при небольших значениях N занимает больше времени, чем поиск решения. Более детально об этом поговорим в следующих статьях. Также затронем вопрос зависимости времени поиска решения от числа ограничений.

Заключение

В данной статье мы:

  • Привели типичные примеры постановок задач оптимизации в области ритейла.

    • Рассчитываем, что читателю станет легче распознавать подобные задачи в своей области.

  • Показали, как можно решать задачу ценообразования с помощью Pyomo (код для статьи).

    • Будем рады, если новичкам в моделировании данный материал упростит ознакомительный этап.

  • Сравнили производительность солверов Pyomo.ipopt и Scipy.cobyla.

    • Можно заметить существенную разницу во времени работы солверов и в результатах, о чем не следует забывать на практике.

В следующих статьях более детально обсудим другие open-source солверы для python, их различия в реализации и производительности.

Над статьей работали Антон Денисов, Михаил Будылин.

Комментарии (8)


  1. SamXYZ
    31.08.2022 23:10
    +2

    Статья очень интересная. Задача актуальная. Интересно, а как проводили тестирования? чтобы узнать как повлияет изменения цены на спрос. Или программа автоматически регулирует цену в зависимости от оборотов?


    1. X5RetailGroup Автор
      01.09.2022 17:57

      Спасибо за отзыв и интересный вопрос! Если вы применяете подобный подход (а бывают разные), то необходим контролирующий процесс. Можете смотреть разницу прогноза спроса и факта, и далее развивать дополнительную логику корректировки модели.


  1. ANazarov
    01.09.2022 12:15

    Поддерживаю, статья интересная и задача актуальная. Я бы еще поинтересовался - параметры модели, по сути, являются случайными величинами, то есть здесь имеет место задача стохастического программирования. Вероятностный фактор каким-то образом учитываете?


    1. X5RetailGroup Автор
      01.09.2022 17:57

      Спасибо за отзыв!
      В конкретном примере приводятся фиксированные значения параметров.
      Если мы правильно поняли вопрос, то товары с сильно случайным поведением параметров нуждаются в уточнении через набор статистики, либо для них лучше использовать стандартные процессы ценообразования.


  1. akhkmed
    01.09.2022 16:37

    Из графика можно сделать вывод, что Pyomo с солвером ipopt значительно превосходит SciPy с cobyla при N > 20.

    По-моему не очень корректное сравнение: ipopt использует 1 и 2 производные (градиент и гессиан), а cobyla - нет. Последний подходит и для случая, когда производные невычислимы. В первом явное определение производных должно сильно ускорить.

    Кстати, а эта задача не имеет ли аналитического решения? Или её можно свести к СНАУ?


    1. X5RetailGroup Автор
      01.09.2022 17:59

      Да, верно, данные солверы по-разному подходят к решению оптимизационной задачи, именно поэтому они были выбраны для сравнения, чтобы показать разницу в производительности, в будущих статьях мы обратим больше внимания на эти моменты.
      По поводу аналитического решения - такой задачи мы себе не ставили, т.к. на практике система уравнений сложнее.


  1. ANazarov
    01.09.2022 21:14

    А как вы группируете ассортимент? Ведь товарная номенклатура интернет-магазина включает в себя десятки тысяч наименований товаров, которые можно объединить в классы, подклассы, группы, подгруппы и т.д. Для каждого индивидуального товара коэффициенты эластичности оценить невозможно, значит нужно укрупнять...

    Мне это напоминает задачу из моей практики работы инженером ПТО в строительной организации, молодым специалистом после института. У нас был завод железобетонных конструкций (ЖБК), на продукцию которого нужно было разработать прейскурант. Калькуляция на ЖБК составляется на 1 м3, а отпускная цена определяется путем умножения цены в руб./м3 на объем 1 изделия (м3/шт.). Продукция завода - это реально тысячи видов конструкций, которые были разбиты на разделы - например, плиты перекрытий, сваи, фундаментные блоки, перемычки и т.д. Внутри каждой группы - свои подгруппы, например, сваи подразделяются по классу нагрузки (из-за различного армирования цена на 1 м3 получается разной). На каждое изделие калькуляцию не составишь - прейскурант получится гигантский, а чересчур укрупнять тоже нельзя - будут искажения в отпускной цене, особенно для ходовых изделий, которые пользуются спросом на рынке, типа фундаментных блоков и плит перекрытий. Нужно было искать золотую середину. Получалось нечто вроде задачи классификации... Ну, тогда про возможности кластерного анализа в python мы еще знать не знали ))

    В вашем случае тоже, вероятно, как-то нужно искать эту золотую середину.

    И еще один момент, который мне бросился в глаза. В вашей модели не учтены перекрестные связи между различными группами товаров. У вас не получается так, что модель просто выберет группы товаров с наибольшей торговой наценкой, а малорентабельные группы просто обнулит?

    Вообще, эти перекрестные эффекты как-то исследовались? Ведь если же товары-заменители. Если вы поднимете цены на один вид штанов, то граждане начнут массово покупать штаны другого вида, то есть эластичность спроса на один товар зависит от цен на другой.


    1. X5RetailGroup Автор
      02.09.2022 18:13

      Cпасибо за интересный пример из вашего опыта,
      да, товары имеют классификацию, как правило, это древовидная иерархия категорий и т.д.
      В рамках этих групп можно решать задачу ценообразования.
      Там, где невозможно определить коэффициенты — нет достаточной статистики или возможности найти похожие товары для восстановления коэффициентов, подобные модели не используются. В таких случаях используются стандартные подходы ценообразования (по наценкам, рынку и т.д.).
      По поводу кросс-эффектов и товаров заменителей – как правило данные эффекты можно наблюдать при значительных изменениях цены, в этом случае можно добавлять дополнительные ограничения в систему, штрафные функции и т.д. Собственно, этими и подобными вопросами занимаются в ценообразовании.