Всем привет, на связи MAGNIT TECH!
Меня зовут Женя Пахалюк, я ведущий ML-разработчик в команде оптимизационных алгоритмов. Оптимизацию цен часто упрощают: учимся прогнозировать спрос, зная его при заданных ценах — оптимизируем цены, фиксируем прибыль. Вот диаграмма последовательности для лучшего понимания:

И, казалось бы, данные о продажах в прошлом найти нетрудно, готовых фреймворков с алгоритмами оптимизации — десятки, осталось только немного «поиграть в kaggle», обучив хорошую модель для предсказания спроса и всё готово! Но на практике всё намного сложнее. Почему модель, идеально прогнозирующая спрос, может предлагать цены, разоряющие бизнес? Может ли максимизация прибыли привести к потере покупателей в долгосрочной перспективе? Почему оптимизатор может считать, что яблоки должны продаваться по цене iPhone? В этой статье разберём эти и прочие «смертные грехи» ценообразования с помощью ML.
Важное уточнение: наш проект всё ещё находится в стадии R&D. В этой статье — не успех‑кейс, а честный разбор подводных камней и инженерных приемов, которые помогли нам двигаться вперёд. Надеемся, наш опыт сэкономит вам месяцы проб и тестов.

Алчность: как из-за каннибализации оптимизация одного товара уничтожает другой
Каннибализация — это не про людоедство, это когда цена на один товар влияет на продажи других товаров. На графике пример — чем ниже цена на товар A, тем чаще будут покупать его и тем реже будут покупать товар B.

В чём же тут проблема?
Яркий пример — оптимизатор рассматривает ситуацию «скидка 30% на всё». Модель для каждого из товаров, опираясь на похожую ситуацию в истории продаж, прогнозирует рост спроса на каждый товар в 2–3 раза. И довольный оптимизатор считает такое решение оптимальным, ожидая космического роста оборота. Что могло пойти не так? В истории скидки делались на товары по очереди, и основной рост продаж был за счёт перетекания спроса с других товаров. Сделав же скидку на всё сразу, мы не получим такого огромного прироста продаж в краткосрочной перспективе.
Как с этим бороться? Во-первых, таргет оптимизатора теперь не цена на товар, а комбинация цен в каком-нибудь кластере товаров, между которыми может происходить каннибализация. Во-вторых, необходимо учитывать цены на весь ассортимент кластера товаров при обучении моделей, прогнозирующих спрос. Это дополнительно осложняется тем, что ассортимент постоянно меняется: какие-то товары уходят, какие-то появляются, какие-то переходят из одного кластера в другой. Всё это добавляет большое количество шума в данные и сильно усложняет задачу прогноза.
Чревоугодие: огромные масштабы оффлайн-ритейла
После того, как нашей задачей стала оптимизация комбинации цен, а не отдельных цен, сложность задачи оптимизации выросла с до
, так как на оптимизацию вектора размера N нужно сильно больше итераций, чем на N единичных оптимизаций. Давайте попробуем посчитать насколько вообще мы упираемся в лимиты, и насколько для нас критично время.
Тут стоит погрузиться в особенности оффлайн-ритейла. Работники магазинов не меняют ценники мгновенно, минимальная частота изменения конкретной цены — 1 неделя. В этом плане у нас есть небольшое преимущество перед онлайном — на проведение одной итерации обновления цен есть почти целая неделя. Далее преимущества заканчиваются и начинаются проблемы…
Во-первых, в отличие от e-commerce, мы не можем каждый час проводить новые A/B-тесты в онлайне и отключать неудачные запуски. Десятки тысяч офлайн-магазинов, ручное обновление ценников и юристы с регуляторами — это другие правила игры.
Во-вторых, влияя на цену, мы влияем на нее одинаково во всех магазинах сети. А учитывая, что у Магнита более 20 тысяч точек формата «у дома», при оптимизации цены на товар необходимо учитывать реакцию покупателя и метрики по всем магазинам! Что это значит? Чтобы один раз рассчитать градиент для какого‑нибудь одного товара, нам приходится строить прогноз тысячи раз.
В-третьих, при оптимизации товара необходимо учитывать влияние его цены не только на несколько похожих товаров, но и на всю группу товаров. Группа товаров — обобщение уровня «молоко пастеризованное» или «рис среднезерный». На данный момент в Магните более 1000 таких групп и даже если не жалея ресурсы оптимизировать все группы параллельно, то в самой большой группе могут быть сотни товарных единиц с уникальными ценами. Учитывая, что обычно для таких больших групп метод SLSQP находит решение не менее чем за 100–200 итераций, а во время каждой итерации необходимо посчитать градиенты для каждой из переменных, и также учитывая число магазинов, для оптимизации самых больших групп необходимо провести прогноз спроса миллиарды раз! По этой причине прогнозирование должно быть быстрым. Это накладывает определённые ограничения — либо легковесная архитектура модели, либо ждать результатов оптимизации неделями.
Также, возвращаясь к особенностям оффлайн-ритейла, нельзя не упомянуть про постоянную изменчивость ассортимента и сезонность. Даже если на всех ваших группах оптимизация укладывается в лимиты по времени, на следующей неделе в них могут появиться новые товары и все поменять.
Тщеславие: как метрики врут нам
Представьте, что ваша модель предсказывает продажи с , вы в восторге и как можно скорее ставите её в прод, но через месяц все бизнес-метрики идут на дно. Просчитался, но где? Для ответа на этот вопрос, давайте подумаем, а коррелируют ли вообще хоть сколько-нибудь классические метрики оценки моделей (MSE, MAE,
…) с бизнес-метриками?
Допустим, у нас есть товар с низкой волатильностью продаж, например молоко, который каждую неделю в течение всего обучающего периода покупают около 1000 раз. И модель принимает логичное для нее решение — в любой ситуации прогнозировать 1000 для этого товара. Оптимизатор делает уже свой вывод — неважно какая цена, покупать товар будут всегда и выставляет максимально возможную цену на это молоко… После чего продажи всех товаров магазина сильно падают и бизнес‑метрики уходят на дно. Из этого примера можно сделать следующие выводы:
Классические метрики регрессии не так важны для оценки прогноза в задаче ценообразования. Куда важнее чувствительность прогноза к цене.
Любой алгоритм ценообразования будет иметь большие сложности с товарами, у которых не было больших изменений цены в истории.
Плохое ценообразование товара может приводить к падению метрик даже не связанных с ним товаров, так как разочарованный покупатель просто будет уходить в соседний магазин, ничего не купив.
Самая большая проблема тут — это необходимость примеров с изменениями цен в прошлом для успешного ценообразования сейчас. Почти все изменения цен в оффлайн‑магазинах сейчас это либо скидка, либо плавное повышение по мере инфляции. В истории продаж для многих товаров никогда не было примера того, как отреагирует покупатель, если взять и одним днём повысить цену на 20%. Можно попробовать провести тесты с искусственными изменениями на небольших выборках магазинов, но тогда возвращаемся к проблеме каннибализации — нам нужно не просто посмотреть для каждого товара на то, как реагируют покупатели на повышение его цены, а рассмотреть это при разных комбинациях цен на другие товары. А это миллионы отсутствующих в истории комбинаций, на получение информации по которым уйдут годы тестов… И задача, опять же, усложняется постоянной изменчивостью ассортимента.
Жадность: десятки ограничений
Представьте, что вы нашли идеальную цену на товар. Круто, но теперь:
Команда CRM: «А у клиента N на этот товар персональная скидка!»
Менеджер категории: «Вы не можете поставить разные цены на одинаковые товары разных вкусов!»
Юристы: «Это же социально значимый товар, нас оштрафует государство!»
Поставщик: «У нас запланирована промо‑акция на этот товар на следующие 10 недель!»
Маркетинг: «Но ведь тогда у нас будет цена выше, чем у конкурента, это недопустимо!»
Работник магазина: «Этот товар упал с полки и помялся, придется продавать с дополнительной скидкой!»
-
Алгоритм красивых цен: «Какая‑то некрасивая цена, давайте‑ка мы её округлим!»
Какие проблемы из этого всего следуют?
Усложнение оптимизации — теперь не всегда классические алгоритмы способны закончить оптимизацию в точке с приростом таргета и соответствующей условиям. А в случаях, когда могут, ощутимо увеличивается время работы.
Уменьшение покрытия — из‑за ограничений мы не можем влиять на весь ассортимент, потому что на какие‑то товары цены строго регулируются государством, в каких‑то есть договоренность о промо с поставщиком и так далее. Поэтому мы можем показывать положительный эффект лишь на части ассортимента, и его сложнее зафиксировать в ходе A/B теста.
Решение оптимизатора ≠ цены на полках. Представим ситуацию, что по мнению оптимизатора идеальная цена 32.41 рубля и ни копейкой больше. Но по правилам красивой цены, цена должна заканчиваться на 9.90. А снизить цену до 29.90 рублей нельзя, потому что, например, закупочная цена товара 30 рублей. И алгоритм округления выставляет итоговую цену 39.90 рублей. Итог — встала не та цена, которую ожидал оптимизатор и эффекта нет.
Гордыня: когда ML считает себя умнее инфляции
Инфляция — это как тот самый друг, который постоянно в последний момент меняет планы. Модель и оптимизатор настроились сделать изменение цен, а тут вдруг из-за инфляции поменялись закупочные цены и все данные устарели. Какие именно проблемы создает инфляция?
Необходимо учитывать инфляцию при обучении моделей, чтобы разделять плавное инфляционное повышение цен от резких изменений, как раз под которые должна быть обучена модель. Это создает дополнительную зашумленность в данных и делает их еще менее обогащенными примерами изменений цен.
Дополнительная напряженность покупателей. Повышение цен оптимизатором вызовет более негативную реакцию, если за неделю до этого цены уже повысились из-за инфляции.
Пересечение эффектов. Представим ситуацию, где оптимизатор считает, что идеальная цена на товар — 100 рублей и передает информацию, что чтобы её достичь нужно снизить изначальную цену на 5%. В то же время, независимо от оптимизатора, менеджер категории видит, что закупочная цена на товар выросла на 5% и повышает цену на 5%. Изменения съедают друг друга и никто не остаётся доволен.
-
Изменение товаров. Для многих товаров инфляция может заключаться не в росте цен, а в уменьшении веса товара (шринкфляция). Это приводит к более частому изменению ассортимента и сильно усложняет задачу отделения инфляции от изменений цен.
Лень: оптимизатор отказывается работать с плохо дифференцируемой эластичностью
Во всех учебниках экономики рисуют функцию зависимости спроса от цены как монотонную кривую с идеальной экспоненциальной зависимостью продаж от цены. Она везде легко дифференцируется, то есть ее очень удобно будет скормить оптимизатору. Также под такую зависимость очень легко обучить линейную модель, если логарифмировать цену. И вообще, всё в этой зависимости прекрасно. Но действительно ли в реальности существует такая зависимость?
Начнём с очевидного несоответствия. Для товаров с низким спросом уже как минимум тот факт, что продажи — целое число, делает функцию эластичности далекой от идеальной экспоненциальной зависимости.

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

Двуличие: нетривиальный таргет
В завершающем пункте вопрос, с которого нужно было начать. А что мы вообще должны оптимизировать, чтобы всем стало хорошо? Представьте, что бизнес хочет от вас, чтобы маржа выросла на 3%, при этом чтобы не упал оборот и покупатели оставались лояльны, и ещё и чтобы цена не слишком сильно отличалась от конкурентов. Так не работает. В реальности любое ценообразование — это тонкая балансировка между разными целями.
Хочешь увеличить маржу? Придется поднять цены, рискуя потерять покупателей.
Хочешь нарастить оборот? Придется переманивать клиентов у конкурента, понижая цены и рискуя опустить маржу.
Хочешь увеличить количество чеков? Придется добавлять персональные скидки и скидки на популярные товары, опуская маржу.
Отсюда возникают сложности с тем, чтобы объяснить оптимизатору, что ему нужно не просто максимизировать таргет, как он привык, а попытаться сделать лучше по какому-нибудь из критериев, при этом не сделав сильно хуже по другим критериям и, если это невозможно, то оставить как есть. Сложности добавляет также то, что нет ответа на вопросы: «Сколько маржи стоит 1% оборота» и наоборот.
Можно было отступить. Открыть Excel и забыть про ML. Но мы пошли дальше — и искупили всё
Итак, мы прошли через все 7 грехов ML ценообразования, какой из этого можно сделать вывод? Хорошо прогнозировать спрос — важно, но это уже давно существующая и понятная ML задача. Куда сложнее вторая стадия этой задачи — оптимизация. Именно тут результатом работы становится не просто красивое, пушистое число с понятными метриками ‘MAE = 1.5, R2 = 0.85’, а сложное действие, вызывающее цепную реакцию событий и эффект, который не так просто измерить. Далее мы покажем как мы боролись с этими грехами и разберем “добродетели” нашего решения, которые помогли существенно снизить остроту некоторых из перечисленных выше проблем.
Дисциплина: ограничения на прогноз
Одной из главных проблем является то, что модель не идеально реагирует на изменения цен, так как далеко не всегда похожие изменения присутствовали в истории обучения. И заметно снизить эффект этой проблемы можно простым способом — добавить в оптимизатор простых условий того, как прогнозируемый спрос не должен меняться при изменениях цен. Лучший результат нам принесли следующие ограничения:
Базовое — прогноз не может быть больше, чем 1.5 * максимальный спрос за последний год. Очевидное и простое, но, тем не менее, позволяет защищаться от случаев, когда модель думает: «Если скидка 5% увеличивает продажи в n раз, то скидка 50% увеличит продажи в 10n раз!».
Каннибализационное — суммарный спрос группы товаров не может вырасти более, чем на 10% от прогнозируемого при стартовых ценах. Если суммарный прогноз оказывается больше, то для всех товаров прогнозируемый спрос равномерно понижается пока ограничение не выполнится. Тут тоже всё логично — если вы сделаете большую скидку сразу на все товары из группы орехи, то люди не превратятся в хомяков и не начнут покупать орехи в 10 раз чаще.
На маржу — если при полученном прогнозе спроса ожидаемая маржа повышается более чем на K% относительно стартовых цен, где K — константа, подобранная на основе исторических изменений маржи по группе, то прогноз спроса уменьшается так, чтобы это ограничение выполнялось.
Что дают эти ограничения? На метрики модели при валидации на тестовой выборке они почти никак не влияют. Но благодаря ограничениям эти метрики меньше проседают в боевых ситуациях, когда требуется сделать прогноз уже в ходе эксперимента с существенными изменениями цен.
Целомудрие: один тест – один таргет
Возвращаясь к проблеме нетривиального таргета — как понять что лучше оптимизировать, если наша цель – всё и сразу? Кажется, что можно попробовать максимизировать следующую сумму:, где
— прогнозируемая маржа,
— прогнозируемый оборот,
и
— коэффициенты приоритетов. Но тогда мы встретим следующие проблемы:
Теряем гибкость. Например, если у группы товаров N проседает оборот и бизнес просит попробовать оптимизировать именно его, мы теряем такую возможность.
Эффект смешивается между группами — где-то прирастает маржа, где-то оборот, где-то ничего не прирастает, а где-то сильно растёт маржа, хотя цены не менялись… В итоге сложнее оценить что из метрик мы умеем повышать, а что нет, потому что у всех групп разный таргет.
Непонятно что присвоить в
и
— для всех групп товаров баланс будет очень разным и неочевидным. Что важнее 1% оборота или 1% маржи? А если при росте оборота на 300% маржа падает на 150% и становится отрицательной?

И что же тогда делать? Наш ответ — вместо того, чтобы делать оптимизатор ребенком, который хочет все конфеты сразу, сделать для него «игровые режимы».
Режим «Маржа» — можно играться с ценами, пытаясь поднять прибыль, но если оборот при этом падает ниже допустимого порога для данной группы, оптимизатор получает штраф и такое решение становится неоптимальным.
Режим «Оборот» — оптимизатор пытается увеличить оборот, но если маржа ниже того, что ожидает бизнес, то за такое решение вновь штраф.
Для оценки эффекта запускаем параллельные тесты в разных режимах, на разных группах магазинов и смотрим: насколько отличается реальный прирост таргета от ожидаемого, и удалось ли не просадить противоположную метрику, или прогнозная модель наврала оптимизатору. Далее можно уже проводить аналитику и распределять группы в зависимости от того, где и что мы научились повышать.
Порядок: кластеризация магазинов
Итак, в проблеме масштаба мы выяснили, что на очень больших группах товаров мы можем оптимизироваться очень долго. Тем не менее, хочется экспериментировать над оптимизацией и иметь запас по времени при боевых расчётах. Чтобы понять как можно ускориться, давайте лучше разберемся с причинами того, почему на некоторых группах оптимизация работает слишком долго.
Время работы оптимизатора хоть и не на 100%, но очень сильно коррелирует с количеством переменных оптимизации.
Количество переменных оптимизации = количество подбираемых цен.
Между разными магазинами сети иногда бывают разные цены из-за сторонних коэффициентов, не подвластных проекту, но при оптимизации мы не можем менять цену в разных магазинах на разный процент. Так что от магазинов количество подбираемых цен не зависит. Оно зависит только от числа товаров в группе.
Вывод — долго от рабатывают те группы, в которых большое количество товаров. Давайте посмотрим на следующие две группы:

Слева товары из группы «рис среднезерный», справа товары группы «молоко пастеризованное». Видим, что у риса всего 51 уникальный товар, в то время как у молока безумные 4898. Откуда такая разница? Все просто, рис — федеральный товар, в разных городах продаются одинаковые бренды этого товара. Молоко — локальный товар, во многих городах есть десятки брендов, продающихся только в этом городе. Все группы, которые имеют большое число товаров и долго оптимизируются — локальные, и большое количество товаров в них как раз из-за широкого ассортимента локальных брендов. За счет этого момента можно ускориться — локальные товары из разных городов, которые никогда не продаются вместе можно разделить и оптимизировать по отдельности. Разделение (кластеризация) объектов оптимизации происходит по следующему алгоритму:
Векторизуем магазины для каждой группы отдельно, как tf-idf вектора встречаемости разных товаров группы.
Для групп товаров с большим числом товаров подбираем K с лучшей метрикой и с помощью KMeans разбиваем магазины на кластера.
Для каждого кластера магазинов проводим оптимизацию их ассортимента независимо.
-
Для тех товаров, которые все-таки попали сразу в несколько кластеров, усредняем подобранную цену по кластерам, но такое приходится делать редко.
Как итог — ускорение в десятки раз на тех группах, где это необходимо, без потери качества.
Терпение: штраф за большие изменения цен
Мы не смогли никуда деться от того, что нужно соблюдать 1001 ограничение на то, какой цена не может являться. Какие-то из ограничений получается учесть при помощи выставления границ на переменные оптимизации. Для каких-то нужно в явном виде прописывать ограничения функцией, благо есть алгоритмы оптимизации, которые это поддерживают. Какие-то ограничения приходится вставлять в логику прогноза, оптимизации. Фактор большого количества бизнес-ограничений не является сложной проблемой, а просто увеличивает объем работы и кода проекта. Но, тем не менее, одно из ограничений прибавило нам проблем. Оно звучит следующим образом: «Для каждой группы товаров ценовой индекс продаж, рассчитываемый по формуле
, где
— множество оптимизируемых объектов;
— цена на товар подобранная в результате оптимизации;
— начальная цена на товар;
— средние продажи товара за последние 3 месяца,
должен лежать в диапазоне ». По простому говоря, средневзвешенное изменение цен не должно превышать 5%.
В чём проблема выставить это ограничение напрямую в SLSQP? Логичный шаг, давайте представим, что мы так и сделали и представим такую группу товаров:
Товар |
Стартовая цена |
Закупочная цена |
% оборота группы |
|
A |
120 |
220 |
85 |
~84% |
B |
160 |
12 |
140 |
~6% |
C |
135 |
15 |
100 |
~6% |
D |
150 |
8 |
120 |
~4% |
Итак, вы – оптимизатор. Вы уверены на 100%, что чтобы улучшить бизнес-метрики, нужно поднимать цену на товар A как можно сильнее, а что происходит с остальными товарами не особо важно. Но вы сталкиваетесь с проблемой — вы можете поднять цену на товар A максимум на 6%, до 127, а далее упретесь в ограничение ценового индекса. Но ваша цель — максимальная прибыль, и вы идёте хитрым путем. Чтобы максимально задрать цену на товар A и соблюсти ограничение, вы снижаете цену на все остальные товары до минимума и получаете:
Товар |
Стартовая цена |
Закупочная цена |
Подобранная цена |
A |
120 |
85 |
130 |
B |
160 |
140 |
135 |
C |
135 |
100 |
115 |
D |
150 |
120 |
125 |
PI = 1.05, и цену на товар A удалось поднять не на 6%, а на 10%, успех! Но цены на остальные товары снижены в минимум, а товар B вообще продается в минус…
Фиксируем наблюдение — если пытаться соблюдать ограничение на ценовой индекс в лоб, то иногда оптимизатор начинает жульничать и менять цены на товары не потому что считает это выгодным, а ради того, чтобы сильнее поменять цену на других товарах. И действительно доходило до абсурдных ситуаций, когда товар X продавался в минус только ради того, чтобы сильнее поменять цену на соседний товар Y.
Чтобы ограничение ценового индекса выполняло свою изначальную функцию – снизить количество изменений цен, мы добавили в оптимизацию следующую логику:
1. К таргету оптимизации добавляется штраф, равный
где— подбираемый для группы вектор цен.
2. Для каждой группы товаров подбирается коэффициент штрафатак, чтобы в результате оптимизации ценовой индекс не превышал порог, но при это был максимально далек от 1. Коэффициент подбирается с помощью метода Ньютона.
В итоге, ограничение соблюдается на 100%, его работа интерпретируема, и при этом оптимизатор не слишком ограничен, у него остается возможность менять цены.
Бдительность: заморозки
Среди оптимизируемых объектов всегда будет какой‑то процент таких, которые будут пытаться вставить определенные палки в колеса оптимизатору. Необходимо избежать их негативного влияния, но при этом нельзя вычеркивать их из оптимизации, так как они участвуют в каннибализации, общегрупповых ограничениях и финальной оценке эффекта. Для этого мы используем мягкую «заморозку» нездоровых объектов. Как это работает? Цены на «замороженные» объекты не являются переменными оптимизации, но при расчете спроса на остальные объекты и суммарных метрик группы вносят свой вклад, подставляя в качестве себя цену с прошлого периода.
Давайте разберем основные виды объектов, которые лучше замораживать и проблемы, которые эта механика помогает избежать.
Противоречие в ограничениях. Иногда встречаются товары, на которые среди десятков разных бизнес-ограничений встречаются явно противоречащие друг другу и из-за этого область решений для всей группы товаров будет пустым множеством. Даже если конфликт ограничений встретился у какого-то незначительного товара с долей оборота менее 1% от всей группы, для SLSQP эта группа будет нерешаемой задачей, с которой он либо уйдет в бесконечный поиск, либо вернет статус с ошибкой.
Неверная чувствительность к цене. Всегда будет какой-то процент товаров, для которых модель прогноза обучится неверно и будет ожидать повышения спроса при повышении цен или наоборот. Такие товары могут ослепить оптимизатор своим неверным прогнозом и заставить его сделать ставку не на тот товар, поломав оптимизацию всей группы целиком.
Слишком важные товары. Эксперименты и тесты на ключевых товарах под запретом, для них ценообразование происходит по особым правилам, стремящимся минимизировать отличие цены от конкурентов. Но тем не менее, нельзя просто не оптимизировать их по причине каннибализации.
Трезвость: оценка прогноза
Давайте вернемся к вопросу — как после того, как ваши идеальные ML алгоритмы реализованы, не попасть в ловушку «все работает, но бизнес умирает». Мы уже разобрались в причинах того, почему в нашем случае условный MSE модели практически ни о чем нам не говорит, но, все же, как отличить модель‑пророка от модели‑шарлатана? Мы используем следующие методы для того, чтобы оценить качество нашего прогноза:
Стресс-тест моделей — для того, чтобы верно оценить чувствительность модели к цене и понять, насколько хорошо она умеет редактировать прогноз при обновлении цены, мы смотрим на то, как сильно будут отличаться метрики обычной валидации на исторической выборке и метрики в ходе теста с неклассическими изменениями цен. Разберем простой пример: модель A для построения прогноза тупо копирует объем продаж с прошлой недели, а модель B пытается найти сложную зависимость с учетом фактора текущей цены. WAPE и BIAS, подсчитанные на исторической выборке, без резких изменений цен у модели A будут, скорее всего, лучше. Но если посмотреть те же метрики на данных, полученных в результате теста с искусственными неклассическими изменениями цен, у модели A они сильно просядут, а у модели B будут совсем чуть-чуть отличаться от валидации. Вывод: хороша не та модель, которая имеет хорошие метрики, а та, которая имеет хорошие метрики в боевых условиях.
Адекватная эластичность — мы уже говорили, что «замораживаем» оптимизацию объектов, для которых модель прогнозирует странную эластичность (например, повышение спроса при повышении цены). Поэтому очевидно, что еще одним критерием качества для прогнозных моделей является то, насколько много объектов из‑за нее приходится «замораживать». Более того, если идти еще дальше, то можно попробовать пойти дальше и для каждого объекта не просто бинарно определять адекватная у него получилась эластичность или нет, а считать отклонение его кривой эластичности от образцовой кривой эластичности, вычисленной экспертно для каждой группы товаров. Как итог — получаем еще один критерий оценки, который будет оценивать в модели то, что нам нужно.
Ограничения — то, насколько часто модель не попадает в ограничения, разобранные ранее, тоже является показателем. Если модель — это ученик, сдающий экзамен, то ограничения оптимизатора — как шпаргалки: они помогают получить хорошую оценку, но выдают слабую подготовку. И, конечно, мы всегда будем ценить того студента, которому требуется меньше подсказок для идеального ответа. Поэтому еще одним верным критерием для модели является то, насколько сильно ограничения из оптимизатора редактируют результат прогноза.
Эти три простых критерия помогают нам не тратить месяцы на проведение A/B-теста для проверки каждой новой гипотезы по улучшению моделей, а получать результат на ходу.
Трудолюбие: борьба с инфляцией
Хоть мы и не центробанк, но нам тоже приходится с ней бороться, чтобы она не создавала описанные выше проблемы при прогнозировании и оптимизации. Итак, что же мы можем сделать?
Способ номер один — нормирован ие цены на инфляционные индексы при обучении. Позволяет модели во время обучения работать только с реальными изменениями цены, а не плавными инфляционными повышениями.

На графике показан пример нормализации цены на инфляцию. Далее можем увидеть, что коррелировать с продажами будет лучше нормированная цена.

Способ номер два — логарифмирование цены. Так как инфляция это мультипликативный шум, то логарифм преобразует его в аддитивный, что уменьшает дисперсию данных. После логарифмирования хорошо выделяются реальные изменения цен, не связанные с инфляцией.
Способ номер три — ручная очистка данных от аномальных скачков инфляции, связанных с особыми событиями. Например, полностью вырезаем период ковида из обучения.
Подводя итоги
Итак, давайте резюмируем основные проблемы и то, как мы с ними справлялись.
Проблема |
Что не так |
Наше решение |
1. Каннибализация |
Скидка на один товар влияет на спрос других. При скидке «на все» модели переоценивают эффект. |
Оптимизация комбинаций цен в кластере + учёт каннибализации при прогнозе. |
2. Масштаб |
O(n^3) сложность + 18 тыс. магазинов = миллиарды прогнозов. |
Кластеризация ассортимента по локальности. Лёгкие модели. Асинхронная оптимизация. |
3. MSE врёт |
Высокий R² не гарантирует рост бизнеса. Модели могут не учитывать реакцию на цену. |
Ограничения на прогноз, стресс‑тестирование, отказ от «классических» метрик. |
4. Ограничения |
Бизнес, CRM, закупки, юристы, округления — мешают внедрить идеальные цены. |
Встроенные хард‑ограничения, штрафы, «мягкие» округления, частичное покрытие. |
5. Инфляция |
Путается с реакцией на цену, создаёт шум, искажает выводы. |
Нормализация по индексам, логарифмирование цен, очистка от «аномалий». |
6. Эластичность |
Реальные зависимости ступенчатые, а не гладкие. Градиенты плохо считаются. |
Ограничения + «заморозка» неадекватных товаров при оптимизации. |
7. Нетривиальный таргет |
Баланс оборота, маржи, лояльности — оптимизатор не понимает «что важнее». |
«Игровые режимы» оптимизации с порогами по падению вторичных метрик. |
Заключение
Когда мы начинали работу над проектом, первые мысли были просты и самоуверенны: «Ну что, модель есть, сейчас допишем optimize — и все взлетит». Не взлетело... Но несмотря на то, что мы столкнулись со множеством проблем, нашей команде удалось разработать MVP и сейчас мы находимся в стадии тестирования. В этой статье постарались вспомнить для вас самые интересные проблемы, с которыми мы столкнулись и самые эффективные приемы, которые помогали улучшать продукт. Если у вас возникли вопросы, то задавайте их в комментариях, мы обязательно ответим!