Альтернативная ссылка на видео

Всех приветствую! Меня зовут Александр Исаков, я аналитик‑разработчик в Яндекс Лавке. Занимаюсь прогнозированием с применением методов машинного обучения, чтобы помочь Лавке вовремя подготовиться к пикам спроса. Мой доклад про то, как мы прогнозируем множество параметров для расчёта необходимого числа курьеров, чтобы у нас была возможность довезти все заказы вовремя.

Зачем Яндекс Лавке прогнозирование

Яндекс Лавка — сервис быстрой доставки. Быстрой, потому что, как только заказ появляется в системе, свободный сборщик сразу начинает его собирать, чтобы потом передать его курьеру, который доставит заказ в обещанный срок с момента оформления. Чтобы развезти каждый заказ за 15 минут, нам необходимо прогнозировать нагрузку и понимать, сколько курьеров должно быть на каждой из «лавок». Так мы иногда называем дарксторы — закрытые помещения со стеллажами продуктов, как в магазине. Кстати, похожий пайплайн работы с прогнозом можно встретить при оптимизации графика работы персонала в офлайн‑магазинах или, например, при прогнозе необходимого уровня наличных в АТМ‑банкоматах.

Яндекс Лавка работает круглосуточно, у нас более 500 дарксторов в 10 регионах. Посчитав 14 дней оптимальным сроком для предсказания, мы получаем, что спрогнозировать нам в итоге нужно более 10 000 чисел (24 часа в сутках × 500 дарксторов × 14 дней). Звучит как интересный вызов, и первое, что мы сделали, — задумались над постановкой задачи.

Постановка задачи прогнозирования

Чтобы сформулировать задачу в ML‑терминах, прежде всего нужно определиться с глобальной целью. В нашем случае запрос от бизнеса был сформулирован так: «Нужно понять, сколько нам выводить курьеров на каждую лавку, чтобы довести каждый из заказов до клиентов». При более глубоком погружении в задачу мы понимаем, что сформулировать можно проще — нужно рассчитать спрос на заказы в каждый час, и тогда мы сможем понять, сколько нам нужно курьерских смен.

Зная глобальную цель, мы можем задать себе следующие вопросы:

  • Какие данные у нас есть?

  • Какой будет горизонт прогноза?

  • Какая будет дискретность прогноза?

  • Какую модель применять?

  • Какое количество входных признаков выбрать?

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

Если мы считаем, что подготовили топовую модель, то надо это ещё доказать и продемонстрировать её метрики на предполагаемых будущих данных, смещённых по времени вперёд. Поэтому следует отделить из известных нам данных выборку, позволяющую отграничить некоторую часть временного интервала, который у нас есть (OOT). Это такая выборка, которая вообще не должна участвовать в обучении, — то есть во время подбора оптимальных параметров для модели мы не будем её трогать, а пригодится она для проверки стабильности метрик качества модели.

С development‑данными (это наши наблюдения за вычетом OOT‑выборки) мы работаем по классическому сценарию: можем поделить на train и validation и искать на них оптимальные параметры модели. И если наши метрики на всех трёх выборках (train, validation, OOT) не разлетаются и всех устраивают, то можем считать модель финальной и готовиться к выводу в прод.

Для оценки горизонта прогноза мы руководствовались следующей логикой: на выходных курьеры уже видят рабочие слоты на следующую неделю и могут планировать своё время. Мы же, в свою очередь, понимаем, что суббота — крайний срок, когда нам нужны прогнозы слотов, и к этому времени нужно успеть подготовить не только прогноз заказов, но и необходимое количество курьерских слотов на этой основе. При необходимости можно внести ручные корректировки. Поэтому прогнозировать заказы начинаем в четверг, чтобы оставить пятницу на корректировки, если они будут нужны. Следовательно, если посчитать число дней от четверга, когда мы начинаем применять прогноз, до воскресенья следующей от этого четверга недели, то получаем в сумме 4 + 7 = 11 дней.

Также могут возникнуть ситуации, когда модель, которая сделала прогноз в среду, сломалась. И у нас нет сейчас достаточного количества времени, чтобы её починить до четверга. Мы заранее продумали этот момент и заложили 14 дней в горизонте прогноза. Поэтому мы можем взять какой‑нибудь запасной прогноз, который был сделан в понедельник или во вторник и, вместо того чтобы дебажить модель или исправлять её несколько дней, просто использовать его и потом в спокойном режиме работать с моделью.

Поиск оптимальной архитектуры модели 

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

  • замена последними значениями — прокидываем на следующую неделю профиль прошлой недели;

  • среднее за N недель, например прогноз на 16:00 в четверг взять как среднее от количества заказов в каждый четверг в 16:00 за последние три недели;

  • взвешенное среднее за N недель — чем ближе неделя ко дню прогноза, тем больше её вес;

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

Чтобы сделать первичный прогноз, мы можем взять для начала количество заказов по часам за прошлую неделю и просто скопировать это на две недели вперёд. Получается неплохой baseline, который даёт адекватные метрики, но можно пойти дальше и применить статистику. Например, чтобы прикинуть будущее количество заказов для воскресенья в 15:00, можно взять среднее за 3 предыдущих воскресенья в то же время. Если пойти ещё дальше, можно взять эти данные с определённым весом, например прошлое воскресенье — с весом 0,6, позапрошлое — с весом 0,3, позапозапрошлое — с весом 0,1.

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

Мы рассмотрим четыре группы моделей. Из них самая распространённая для задач прогнозирования — это линейная регрессия. Ещё популярны некоторые семейства авторегрессионных моделей: ARIMA/SARIMA и им подобные. Далее под LSTM мы подразумеваем все нейросети, например RNN, RCNN, xLSTM, biLSTM и GRU. У каждой из этих трёх указанных групп существуют свои плюсы и минусы, но в нашем случае их объединяет необходимость работать с категориальными переменными, которые содержат информацию о лавках. И чтобы это быстро и нормально завести, нам нужно было бы преобразовать категории под более чем 500 лавок так, чтобы моделям было удобно с ними работать, или готовить 500 временных рядов под каждую лавку.

Чтобы не возиться с большим количеством преобразований, мы решили рассмотреть CatBoost. К сожалению, он не экстраполирует, зато прекрасно справляется с категориальными переменными. Мы можем просто указать, что есть некоторый store_id, обозначить его категориальной переменной, и за счёт этого CatBoost сможет с этим хорошо работать у себя под капотом.

Поэтому, не имея достаточного времени для работы с каждым временным рядом отдельно, мы остановили выбор на этом варианте. Тут также стоит упомянуть, что это возможно с любым градиентным бустингом, но считается, что модель CatBoost лучше всего работает с категориальными переменными.

Итак, мы выбрали тип модели, знаем горизонт прогнозирования. Теперь стоит вопрос, как правильно приготовить признаки для модели и как вообще их применять.

Вариант 1 — модель прогноза на следующий день, которую прогоняем 14 раз
Вариант 1 — модель прогноза на следующий день, которую прогоняем 14 раз

В первом варианте мы берём модель и делаем предсказание на следующий день. Потом мы эту модель прогоняем 14 раз и получаем 14 предсказаний по распределению наших заказов. Удобно, что при этом варианте нужно обучить всего одну модель, которая использует самые свежие данные. Но накопленная ошибка этой модели может быть достаточно большой, поэтому давайте попробуем поискать что‑нибудь получше.

Вариант 2 — модель прогноза на 7 дней, которую прогоняем 2 раза
Вариант 2 — модель прогноза на 7 дней, которую прогоняем 2 раза

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

Вариант 3 — 14 моделей, по одной для каждого дня
Вариант 3 — 14 моделей, по одной для каждого дня

И третий вариант, самый трудозатратный. Берём 14 моделей под каждый из дней прогноза и таким образом с подбором признаков под каждую модель имеем возможность получить наилучшие метрики прогноза. Но при этом нам нужно обязательно учитывать некоторый рассинхрон в соседних днях. Если у нас модель, которая прогнозирует на первый и третий день, показывает что‑то около 100 заказов в среднем, то вторая модель тоже должна показать 100 заказов. Но она может показать 50, и эти моменты важно замечать и учитывать.

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

Что хуже — перепрогноз или недопрогноз 

Итак, мы определились с моделью, выбрали горизонт прогноза и архитектуру. Далее перед нами встаёт классический вопрос: что лучше — перепрогноз или недопрогноз?

На этот вопрос нет универсального ответа, потому что у каждого бизнеса он будет своим. В нашем случае лучше держаться где‑то посередине. Если мы перепрогнозировали, то вывели курьеров больше, чем нужно, и в итоге тратим больше денег, чем могли бы. Если, наоборот, недопрогнозировали, то не смогли поддержать спрос и снова потеряли деньги. Поэтому нам нужно стараться держаться ближе к середине. Иначе говоря, попасть именно в тот факт заказов, который будет.

Здесь нам очень подходит МАЕ, потому что она одинаково штрафует и за перепрогноз, и за недопрогноз. Также можно рассмотреть MSE и RMSE, но мы решили, что не хотим переобучаться на выбросы. Для других проектов возможно подойдут остальные метрики.

Генерация признаков для временных рядов

Теперь попробуем подготовить признаки для модели. Самое очевидное — это лаги, сезонность, информация по каждому даркстору и временные данные. За счёт того, что у нас существует признак «время», мы можем выражать его разными способами: день недели, час определённого дня, время суток. Имея информацию об этих признаках, мы можем передавать шаблон спроса, который будет в этот час и в этот день недели. Например, если у нас существует гипотеза, что днём по будням люди чаще заказывают в Лавке.

Мы собрали около 360 признаков, которые можно распределить по следующим группам:

  • лаговые признаки по дням и часам (заказы 1 день назад,…);

  • скользящие средние и квантили (среднее количество заказов за последние 3 часа);

  • временные признаки (день недели, час, время суток);

  • признаки, содержащие информацию о Лавке (ID Лавки, покрытие зоны доставки);

  • дневная сезонность распределения заказов (синус/косинус от часа).

Кажется, что это достаточно много признаков с повторяющейся информацией. Поэтому давайте найдём самые важные из них, которые могут дать 99% результата.

Воронка, по которой мы отбирали признаки
Воронка, по которой мы отбирали признаки

Первым делом из всего числа признаков следует удалить те, которые не дают почти никакой информации: например, в них много пропусков или одно уникальное значение. Потом нужно посмотреть, что хорошо коррелирует с таргетом, и проредить признаки с высокой парной корреляцией. Допустим, из той пары, у которой высокая корреляция, можно убрать фичу, у которой корреляция с таргетом меньше. За счёт этого мы оставляем информативность, уменьшаем признаковое пространство и позволяем модели переобучаться меньше. Также не стоит забывать про проверку на линейную зависимость, а после неё можно даже отсортировать признаки по важности (permutation importance) и выбрать самые эффективные с точки зрения метрик.

Допустим, финально мы получили 10 признаков, и нам нужно их внимательно рассмотреть. Потому что иначе при прогнозе могут появиться ложные связи (например, в финальном списке признаков для предсказания числа фильмов с участием Николаса Кейджа может случайно оказаться число инцидентов в бассейнах).

Итого — мы прогнали наши 300+ фич через воронку, получили финальные 14 признаков и можем предположить, что закончили с обучением модели. Но это ещё не конец. Даже если мы собрали крутую модель, которая хорошо прогнозирует на 14 дней вперёд, мы можем столкнуться с проблемой выхода за рамки распределения по будням и выходным (помним ещё про неспособность CatBoost к экстраполяции). Это происходит из‑за праздничных дней, когда все уезжают из города, или, наоборот, когда больше людей заказывают доставку на праздничный стол. Тут для нас стоит задача передать информацию о спросе в эти праздники за прошлые годы, а горизонта применяемых для модели данных недостаточно.

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

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

Результаты и оценка модели

Основные результаты работы модели за год:

  1. Удалось снизить MAE на 25%.

  2. Доля дней в месяце с вылетом из доверительного интервала +-5%: 10%.

  3. Внедрён автоматический учёт праздников не только по суммарному количеству заказов в регионе, но и с поправкой на профиль праздничного дня.


В конце хочу поделиться советами, которые, как мне кажется, точно пригодятся при работе с вашими моделями:

  1. Убедитесь, что вы действительно понимаете, какую задачу бизнеса решаете.

  2. Быстрый MVP возможен даже без ML.

  3. 20% признаков могут дать 80% результата.

  4. Совершенствуйте модели итеративно, начиная с простой архитектуры.

  5. В погоне за метриками важно не забывать о логике и бизнесе.

  6. Важно проводить разбор ошибок — это потенциал для улучшения моделей.

  7. Не стоит бояться изменить постановку задачи, если это необходимо.

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

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