Собесов

Яндекс.Еда: Метрики утилизации курьеров и анализ простоев

Кейсы и метрикиOperations / MetricsСредняяMiddle

Условие

В службе доставки еды есть смены курьеров. Бизнес жалуется: «слишком высокий ФОТ при низкой производительности». Какие метрики утилизации курьеров вы бы предложили? Как декомпозировать «непродуктивное время»? Как искать неэффективные зоны / тайм-слоты?

Решение

Метрики утилизации

Hours-based (макроуровень):

  • utilization = active_minutes / on_shift_minutes — доля активного времени.
  • orders_per_hour — доставок в час смены.
  • idle_rate — доля простоя в зоне.
  • revenue_per_courier_hour — выручка / час оплаченного времени.

Order-lifecycle (микроуровень):

  • dispatch_time — от размещения заказа до назначения курьеру.
  • to_restaurant_time — путь курьера до ресторана.
  • wait_at_restaurant — ожидание готовности заказа.
  • to_client_time — путь к клиенту.
  • delivery_time — общее время доставки.

Декомпозиция непродуктивного времени

on_shift_time = active_time + idle_time
active_time   = travel_to_restaurant + wait_at_restaurant + travel_to_client + handover
idle_time     = waiting_for_dispatch + break + tech_issues

«Тёплые точки» неэффективности:

  1. wait_at_restaurant — рестораны медленно готовят. Решение: SLA для ресторанов, отображение «слот готовности» при приёме заказа.
  2. waiting_for_dispatch — мало заказов в зоне в это время. Решение: перебрасывать курьеров в соседнюю зону или сокращать смену.
  3. dispatch_time — алгоритм назначения медленный или зоны нарезаны слишком крупно.
  4. travel_to_restaurant — рестораны далеко от dispatch-точки → нужно перераспределить зоны.

SQL для расчёта

WITH shift_minutes AS (
    SELECT courier_id,
           shift_id,
           SUM(EXTRACT(EPOCH FROM (end_ts - start_ts)) / 60) AS shift_min
    FROM courier_shifts
    GROUP BY 1, 2
),
order_minutes AS (
    SELECT courier_id, shift_id,
           SUM(EXTRACT(EPOCH FROM (delivered_at - accepted_at)) / 60) AS active_min,
           COUNT(*) AS n_orders,
           AVG(EXTRACT(EPOCH FROM (picked_up_at - arrived_at_resto)) / 60) AS avg_wait_resto
    FROM orders
    GROUP BY 1, 2
)
SELECT
    s.courier_id, s.shift_id, s.shift_min,
    COALESCE(o.active_min, 0)                        AS active_min,
    COALESCE(o.active_min, 0) / s.shift_min          AS utilization,
    COALESCE(o.n_orders, 0)                          AS n_orders,
    s.shift_min - COALESCE(o.active_min, 0)          AS idle_min,
    o.avg_wait_resto
FROM shift_minutes s
LEFT JOIN order_minutes o USING (courier_id, shift_id);

Heatmap «зона × час» для неэффективности

SELECT zone_id,
       EXTRACT(HOUR FROM accepted_at) AS hr,
       AVG(EXTRACT(EPOCH FROM (delivered_at - placed_at)) / 60) AS avg_delivery_min,
       COUNT(*) AS n_orders,
       SUM(CASE WHEN wait_at_resto_min > 10 THEN 1 ELSE 0 END) * 1.0 / COUNT(*) AS pct_long_wait
FROM orders
WHERE accepted_at >= CURRENT_DATE - 30
GROUP BY 1, 2;

→ Видно, в каких зонах и часах рестораны тормозят и где «дыра» с курьерами.

Что НЕ делать

  • Не оптимизировать только utilization — высокая утилизация может означать пере-нагрузку и выгорание / уход курьеров.
  • Не сводить к одной метрике — нужен баланс utilization × CSAT × SLA delivery time.

Прогноз спроса для shift planning

# простой baseline: avg orders / (zone, hour, dow) с поправкой на weather/holidays
import pandas as pd
hist = pd.read_sql(...)
baseline = hist.groupby(['zone_id','hour','dow'])['orders'].mean().reset_index()
# +ridge / xgboost с фичами weather, events для уточнения

Подводные камни

  1. «Utilization 90%» — почти всегда плохо. Маленькая буферность → каждый incident ломает SLA.
  2. active_time без правильного определения: «работа с заказом» начинается с accept или с pickup? Согласовать.
  3. avg_delivery_time обманчив: одна 60-минутная доставка ломает среднее. Использовать p90 / median.
  4. Зоны нарезаны вручную — часто устарели; A/B-тесты на нарезке.
  5. «Низкая утилизация всегда плохо» — не всегда: low utilization + быстрый SLA = высокая CSAT. Зависит от стратегии.
  6. Привязка к расценкам: курьер на час vs на заказ — оптимизация разная.
  7. Smoothing нулей: курьер без заказов в смене может быть не «бесполезным», а сидеть в пустой зоне. Винить алгоритм dispatch, не курьера.

Эталонный ответ

Метрики: utilization, orders/hour, idle_rate, time-of-cycle декомпозиция (travel_to_resto, wait_at_resto, travel_to_client). Декомпозируем simple total cost = on_shift_time × hourly_rate; неэффективность — в waiting_at_resto (SLA рестораны) и idle (плохой dispatch / нарезка зон). Решения: SLA для ресторанов, перенарезка зон, динамическое управление сменами на основе прогноза спроса.

Главный принцип: utilization 90%+ часто = плохой UX и выгорание. Баланс utilization × SLA × CSAT, не максимизация одной.

Хочешь увидеть разбор?

Зарегистрируйся бесплатно — откроется развёрнутое решение этой задачи и ещё 4 на выбор.

Зарегистрироваться и увидеть разбор
Уже есть аккаунт? Войти