Обратно в блог
  • ML
  • backend

Будьте в курсе всех возможностей

Как мы увеличили Supply Hours на 7,5% с помощью системы рекомендаций смен для курьеров

Что сложнее — порекомендовать фильм на вечер или рабочую смену курьеру? На первый взгляд, задачи во многом похожи. Но есть один нюанс, который меняет всё: один и тот же фильм можно порекомендовать тысячам людей, а вот рабочую смену — только одному.

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

В какой-то момент стало понятно, что так дальше продолжаться не может и нужно уже решать эту проблему.

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

Векторизуем время, считаем скор

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

В основе MVP лежала простая, но эффективная эвристика.

Шаг 1: создаём цифровой отпечаток рабочего графика

Чтобы что-то рекомендовать, нужно понять предпочтения. Мы решили оцифровать привычки каждого курьера, представив его рабочую неделю как 168-мерный вектор, где каждая компонента — это один час недели.

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

preference = Σ (вес_недели * вектор_активности_недели)

Здесь вектор_активности — это тоже 168-мерный вектор, где на месте каждого часа стоит доля этого часа, которую курьер отработал (например, 1.0, если он был на смене весь час, или 0.5, если 30 минут). Ключевой момент — вес_недели. Мы сделали его убывающим: чем ближе неделя к текущему моменту, тем больше её вес. Это позволило системе быть чувствительной к изменениям в жизни курьера — если он, например, сменил график учёбы, система быстро адаптировалась к его новому расписанию.

Шаг 2: оцениваем совместимость смены и курьера

Смену или, как мы говорим, слот мы представили точно так же — в виде 168-мерного бинарного вектора shift, где 1 стоит на тех часах, когда смена активна, и 0 — во всех остальных.

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

score = (preference, shift)

01.jpg

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

Шаг 3: решаем, кому достанутся лучшие смены

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

  1. Идти по сменам (shift-first): брать смену и искать для неё курьера с максимальным score.
  2. Идти по курьерам (courier-first): брать курьера и подбирать для него пакет смен с лучшим score.

Мы выбрали второй путь. Это было осознанное бизнес-решение: в том числе таким способом мы хотели поощрять самых эффективных и лояльных курьеров. Алгоритм распределения выглядел так:

  1. Сортируем всех курьеров по внутреннему рейтингу — от топовых до новичков.
  2. Берём первого курьера из списка.
  3. Жадно набираем для него пакет смен с самым высоким score, пока не заполним его расписание.
  4. Удаляем эти смены из общего пула.
  5. Переходим к следующему курьеру в списке и повторяем процесс.

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

02.jpg

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

Что делать с новыми сменами и новыми курьерами?

Наш офлайн-MVP решил главную проблему — мы остановили еженедельную гонку за слотами и сэкономили курьерам массу времени и нервов. Но, как выяснилось и у этого решения тоже были свои недостатки.

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

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

Так мы поняли, чтобы система была по-настоящему полезной, она должна быть не только плановой, но и гибкой. Ей нужно было научиться работать в реальном времени.

Мы решили добавить в приложение новую точку входа — кнопку «Хочу подборку слотов». По нажатию на неё курьер мог в любой момент запросить персональную подборку из актуальных, доступных прямо сейчас смен. Однако, сложность состояла в том, что это требовало совершенно иного подхода к архитектуре, чем использовался нами ранее.

03.jpg

Асинхронная логика по запросу пользователя

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

Отказываться от офлайн-расчётов было бы недальновидно. Этот подход отлично решал задачу массового недельного планирования и позволял нам управлять распределением на уровне всего пула курьеров, обеспечивая базовую стабильность. Рантайм же был необходим для тактической гибкости — для обработки смен, появляющихся «здесь и сейчас», для новых курьеров и на тот случай, если курьер не успел в пятницу полностью заполнить своё расписание.

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

  • Балковые рекомендации по-прежнему раз в неделю проводят основное распределение смен. Этот процесс гарантирует, что большинство курьеров получат стабильный и предсказуемый график на неделю вперёд. С появлением рантайм-сервиса мы перевели и эту логику в онлайн: теперь это не офлайн-пайплайн, а отдельный процесс, который по расписанию готовит рекомендации и отправляет их напрямую в сервис планирования смен.
  • Рантайм-компонент работает по запросу и отвечает за рекомендации здесь и сейчас. Он позволяет курьерам находить удобные слоты, появившиеся вне планового расписания, и помогает новичкам быстрее интегрироваться в сервис.

Архитектура рантайм-рекомендаций

04.jpg

Когда курьер нажимает кнопку «Хочу подборку слотов», запускается следующий процесс:

  1. Первичный запрос и проверка статуса. Мобильное приложение сначала обращается к нашему recommendation-service со специальным запросом: «Можно ли для этого курьера сейчас отрисовать кнопку?» Этот шаг важен для проведения A/B-тестов. Одновременно с этим приложение может опрашивать статус генерации, если она уже была запущена ранее.
  2. Запуск генерации. Получив id курьера, наш сервис инициирует процесс варки рекомендаций. Мы не блокируем пользователя экраном загрузки, процесс асинхронный. Приложение просто показывает анимацию и периодически опрашивает ручку статуса, чтобы узнать, когда рекомендации будут готовы.
  3. Сбор данных в реальном времени. В отличие от балковых рекомендаций, рантайм-сервис собирает все необходимые данные на лету. Он обращается к нашим внутренним хранилищам за списком всех актуальных свободных смен, а также за всей необходимой информацией о сменах и профиле самого курьера, чтобы проверить его рейтинг, историю работы и так дале.
  4. Расчёт и сохранение. Собрав данные, сервис выполняет те же вычисления, что и в балковых рекомендациях — построение векторов, скоринг, а затем формирует список рекомендованных смен и загружает их в shift-scheduling-service.
  5. Отображение результата. Как только ручка статуса возвращает ответ «готово», мобильное приложение делает финальный запрос, но уже не к нам, а напрямую в shift-scheduling-service, и забирает оттуда готовый, персонализированный список смен для отображения пользователю.

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

Как рекомендации повлияли на бизнес

Любая инженерная инициатива должна доказывать свою состоятельность на языке метрик. И наша система рекомендаций — не исключение. После внедрения и стабилизации мы проанализировали результаты и увидели, что нам удалось не просто улучшить UX, но и повлиять на ключевые бизнес-показатели сервиса.

Вот основные метрики, на которые мы смотрели:

  • Occupancy — наполняемость смен. Это относительный показатель, который мы считаем на уровне отдельных дарксторов. Он показывает, какая доля от всех созданных слотов в конкретной локации была в итоге занята курьерами.
  • Supply Hours (SH) — общее количество отработанных часов. Это абсолютная метрика, которую мы измеряем уже на уровне каждого курьера. Она напрямую говорит об объёме предложения, которое генерируют исполнители в нашей системе.

После запуска гибридной модели мы зафиксировали следующие изменения:

  • Наполняемость смен выросла на 1,5%. На первый взгляд, это может показаться достаточно скромным результатом. Но стоит помнить, что в масштабах Лавки даже полтора процента — это тысячи дополнительных часов работы, которые раньше могли оставаться невостребованными. Система стала эффективнее сводить доступные слоты с заинтересованными исполнителями.
  • Supply Hours на курьера в топовой волне выросли на 7%.
  • Supply Hours на курьера в золотой волне выросли на 7,5%.
05.jpg

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

Что это значит на практике?

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

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

От скалярного произведения к градиентному бустингу

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

Мы уже находимся в стадии активного эксперимента в этом направлении. Сейчас у системы есть два ключевых ограничения:

  • Слабое определение вероятности. Наш скоринг на основе скалярного произведения учитывает только исторический график работы, но игнорирует множество других факторов: город, конкретный даркстор, время суток, историю отказов и многое другое.
  • Жадное распределение. Наш алгоритм, отдавая лучшие смены топовым курьерам, не пытается найти глобальный оптимум. Возможно, существует такое распределение смен, при котором общая удовлетворённость всех курьеров была бы выше. Эту задачу мы планируем решать отдельно, возможно, с помощью специализированных алгоритмов, но первый шаг — это научиться лучше предсказывать индивидуальную вероятность.

Анатомия будущей ML-модели

Для обучения ранжирующей модели мы собрали датасет на основе исторических данных о показах и принятиях смен в наших балковых рекомендациях. Мы используем временной сплит: 90% данных для обучения, 10% — для теста.

Модель оперирует тремя группами фичей:

  • User features: всё, что характеризует курьера. Это не только его рейтинг и история отработанных часов/дней, но и то, как давно он в сервисе, доля отказов от ранее принятых слотов и флаг «новичок», важный для отдельной бизнес-логики.
  • Item features: характеристики самой смены. Сюда входят категориальные фичи (город, даркстор), информация о том, как давно открылась эта Лавка, и исторические данные о том, как часто слоты в это время остаются неразобранными.
  • User-Item features: признаки, описывающие взаимодействие конкретного курьера и конкретного типа слота. Мы используем как прямые счётчики, вроде сколько раз курьер бронировал или отказывался от слота в этот час недели, так и эмбеддинги, полученные с помощью iALS-разложения матрицы взаимодействий. Также учитываются общие конверсии из показа в принятие и из принятия в фактический выход на смену.

Таргет и метрики

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

При оценке качества модели мы смотрим на стандартный набор метрик бинарной классификации (ROC-AUC, Precision, F1), но особый акцент делаем на Recall. Для нас важнее порекомендовать курьеру все потенциально интересные ему смены, пусть даже среди них окажется несколько менее релевантныx, чем упустить хороший вариант. Наша задача — предоставить выбор, а не сделать его за пользователя.

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

Заключение

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

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

  • ML
  • backend

Будьте в курсе всех возможностей