Когда читаешь о том, как работают с ML в крупных компаниях, всё выглядит логично: разбили пользователей на кластеры, провели A/B-тест, модель показала +5% к метКогда читаешь о том, как работают с ML в крупных компаниях, всё выглядит логично: разбили пользователей на кластеры, провели A/B-тест, модель показала +5% к мет

3 проблемы двусторонних маркетплейсов, которые мы до сих пор не можем решить

2026/01/16 18:41
9м. чтение
Для обратной связи или замечаний по поводу данного контента, свяжитесь с нами по адресу crypto.news@mexc.com
8bafe041a13467faa78748f6367aa162.png

Когда читаешь о том, как работают с ML в крупных компаниях, всё выглядит логично: разбили пользователей на кластеры, провели A/B-тест, модель показала +5% к метрике — понесли в продакшен.

У нас в Профи.ру более сложный продукт — двусторонний маркетплейс: живые заказы, которые через час уже будут неактуальны; специалисты, которые сегодня работают, а завтра в отгуле. Как тогда быть?

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

Спойлер: до конца мы их пока не решили, но кое-что придумали.

Проблема 1. Классическим A/B-тестам нельзя верить из-за конкуренции между группами

Обнаружили так: взяли 10 000 специалистов (сантехники, электрики, мастера по ремонту), случайным образом поделили на контрольную (A) и тестовую (B) группы. Группе B показывали заказы с улучшенным алгоритмом ранжирования.

Сработало не совсем так, как мы ожидали:

  • У группы B выросла конверсия в сделку.

  • У группы А она сильно упала, как и CTR.

В чём была проблема:

  • В группе A специалисты видели заказы, отсортированные с помощью старого алгоритма.

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

В итоге специалисты из группы B быстрее получили релевантные предложения и начали массово их разбирать.

И к тому времени, когда специалисты из группы A добрались до этих же заказов в своих лентах, слоты для откликов уже были заняты. Особенно ярко это проявилось в популярных категориях услуг, где несколько специалистов одновременно конкурируют за один заказ.

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

Как мы решаем проблему

После изучения опыта Uber, Lyft и DoorDash (они столкнулись с той же проблемой) мы внедрили switchback-подход:

Шаг 1. Кластеризация на независимые мини-рынки

Сначала мы идентифицируем естественно сложившиеся кластеры в нашем маркетплейсе.

Например:

  • Сантехники Москвы.

  • Репетиторы английского в Санкт-Петербурге.

  • Трамитадоры в Казани.

Шаг 2. Switchback по расписанию

Для каждого кластера расписываем чередование алгоритмов по дням:

Кластер "Сантехники Москвы, Бутово":
Пн: Алгоритм B | Вт: Алгоритм A | Ср: Алгоритм A | Чт: Алгоритм B ...

Кластер "Репетиторы английского онлайн":
Пн: Алгоритм A | Вт: Алгоритм B | Ср: Алгоритм B | Чт: Алгоритм A ...

Из-за того что мы тестим алгоритмы в рандомные дни, снижаем влияние сезонности на результат

Шаг 3. Статистический анализ

Метрики считаем не для отдельных пользователей, а для целых кластеров и сравниваем их поведение в разные дни при разных условиях.

def analyze_switchback(df): agg = ( df.groupby(["cluster_id", "period_id", "assignment"]) .agg({ "impressions": "sum", "engagements": "sum", "deals": "sum", "unique_customers": "nunique", "unique_specialists": "nunique", }) .reset_index() ) def rdiv(a, b): return 0.0 if b == 0 else a / b agg["view_to_engage"] = [rdiv(e, i) for e, i in zip(agg.engagements, agg.impressions)] agg["engage_to_deal"] = [rdiv(d, e) for d, e in zip(agg.deals, agg.engagements)] agg["deals_per_customer"] = [rdiv(d, c) for d, c in zip(agg.deals, agg.unique_customers)] agg["deals_per_specialist"] = [rdiv(d, s) for d, s in zip(agg.deals, agg.unique_specialists)] metrics = ["deals", "view_to_engage", "engage_to_deal", "deals_per_customer", "deals_per_specialist"] report = {} for m in metrics: a, b = agg.loc[agg.assignment == "A", m], agg.loc[agg.assignment == "B", m] diff = b.mean() - a.mean() rel = None if a.mean() == 0 else b.mean() / a.mean() - 1 diffs = [ sub.loc[sub.assignment == "B", m].mean() - sub.loc[sub.assignment == "A", m].mean() for , sub in agg.groupby("clusterid") if sub.assignment.nunique() > 1 ] k = len(diffs) md = sum(diffs) / k if k else 0 se = (sum((x - md) 2 for x in diffs) / (k - 1)) 0.5 / math.sqrt(k) if k > 1 else 0 report[m] = dict(treat_mean=b.mean(), ctrl_mean=a.mean(), abs_diff=diff, rel_lift=rel, cluster_diff=md, se=se, clusters=k) flipped = (agg.groupby("cluster_id")["assignment"].nunique() > 1).mean() return dict(report=report, prop_clusters_switched=flipped)

Что это дало на практике

— Достоверность тестов выросла: корреляция между тестовыми и продакшен-метриками увеличилась на 17%.

— Реальный прирост метрик: когда у нас появилась возможность проводить более честные и скоррелированные с реальностью тесты, то мы смогли:

  • нарастить количество сделок,

  • увеличить конверсию из просмотра заказа.

Что пока не получается

Межкластерная конкуренция

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

Сезонные эффекты

Праздники и погода вносят погрешности, которые сложно отфильтровать, а иногда и распознать.

Проблема 2. 99% заказов живут меньше суток

Коллеги из односторонних маркетплейсов могут спросить: «Почему бы вам просто не запускать расчёт рекомендаций ночью для всех пользователей?»

И этот вопрос наглядно показывает: мы не одинаковые.

08e613e1722210d659cd1c0b1ba58bee.png

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

У нас по-другому. Среднее время жизни заказа измеряется часами, а часто — минутами. Цена и условия могут меняться в процессе диалога клиента со специалистами, которые уже откликнулись.

Но главное, 99% заказов, актуальных сегодня, не существовали вчера. А завтра их уже не будет.

Метрика

Онлайн-магазин

Профи.ру

Единиц контента

~ 10K товаров

~ 10K активных заказов

Обновление каталога

5% в сутки

95% в сутки

Время жизни единицы

Месяцы/годы

2,5 часа (медиана)

Предсказуемость спроса

Высокая

Низкая

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

Заказ создан в 15:00.

А к 18:00 он уже либо выполнен, либо потерял актуальность: пользователь успел купить новый смеситель или позвать сантехника из управляющей компании.

Именно поэтому наша задача — быстро показать заказ нужным специалистам, чтобы случилась сделка.

Почему не работают классические методы

Коллаборативная фильтрация требует истории взаимодействий. Не подходит, так как мы мало знаем о клиентах.

Матричная факторизация зависит от стабильных сущностей. А у нас состав заказов обновляется практически полностью каждые 24 часа. Как и пул клиентов.

Как мы решаем проблему

Наш подход — real-time-архитектура. Она принимает решение за 200–500 миллисекунд от момента создания заявки до выдачи ранжированного списка заказов.

Проиллюстрируем тезис кодом. Он, конечно, не из прода, но всё равно показательный.

def rank_orders_for_specialist(specialist_id, filters): # запрашиваем предпосчитанные фичи специалиста из офлайн-хранилища spec_feats = offline_feature_store.get_specialist_features(specialist_id) # забираем из онлайн-хранилища заказы, удовлетворяющие фильтрам # (e.g. в радиусе 5 км от специалиста) order_feats= online_feature_store.query_orders(filters=filters) # объединяем фичи специалиста и заказа в один датафрейм и кодируем для модели spec_df = spec_feats.repeat(len(order_feats)).reset_index(drop=True) X = pd.concat([order_feats, spec_df, axis=1) X_enc = encode_for_model(X) # предсказываем вероятность того, что специалист заинтересуется заказом p = model.predict_proba(X_enc)[:, 1] # сортируем заказы по вероятности и возвращаем топ orders["score"] = p ranked = orders.sort_values("score", ascending=False) return {"specialist_id": specialist_id, "ranked_orders": ranked[["order_id", "score"]]}

Что это дало на практике

— Пользователи получают быстрые и актуальные отклики.

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

— Метрики конверсий выросли — пользователи чаще получают специалистов, а заказы закрываются быстрее.

Что пока не получается

Не можем учесть всё: предугадать человеческий и природный факторы.

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

В новых услугах и нишах тоже тяжело, так как не хватает вводных для расчётов.

Пример: мы не знаем заранее, насколько специалисты (скажем, тренеры по хоббихорсингу) мобильны и будут ли они готовы ехать на другой конец Москвы.

Проблема 3. Мы не контролируем сделку целиком

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

Но у такого разнообразия есть и обратная сторона:

  • мы не можем досконально промоделировать каждую новую услугу и описать все детали;

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

Например, пользователю нужно погулять не просто с одной собакой, а с тремя разными питомцами. При этом у каждого своя траектория, арсенал для прогулки (игрушки, лакомства, вода) и даже время выгула.

Как в итоге может выглядеть пайплайн такой сделки:

  • Клиент формирует приблизительный запрос.

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

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

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

Есть ещё вариант, когда специалист после беседы понимает, что это не его профиль (например, нужен не просто догситтер, а ещё и кинолог), и поэтому отказывается от задачи: не срослось.

Что это значит для нас, ML-разработчиков:

  • Не всегда знаем, какая цена в итоге была согласована.

  • Не можем сказать точно, была ли она оправданной относительно сложности заказа.

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

Как мы решаем проблему

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

  • на дизайне рынка;

  • алгоритмах мэтчинга.

Что конкретно делаем:

1. Фокусируемся на том, кому и какие заказы показать:

  • кто первым увидит новый заказ;

  • какие заказы окажутся выше в ленте у специалиста;

  • как учитывать профиль специалиста, опыт, локацию и другие признаки релевантности.

2. Используем как показатель успеха только те события, которые точно можем отследить на платформе:

  • факт отклика;

  • факт начала общения в чате;

  • отказ специалиста от заказа.

3. Концентрируемся не на стоимости задач, а на качестве мэтчинга:

  • насколько задача подходит конкретному специалисту;

  • насколько высока вероятность, что после переписки они договорятся.

Что это дало на практике

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

  • отклик →

  • переписка →

  • отказ / продолжение общения.

Что пока не получается

Свести сделку к одному правильному ответу для модели

Даже если мы видим часть переписки, у нас нет понимания, что эта задача за X рублей — ок, а она же за Y — дороговато. Есть только цепочка событий и косвенные сигналы, которые пока что сложно превратить в ground truth.

Развести причины отказа на уровне данных

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


Интересно послушать о проблемах, с которыми вы сталкиваетесь в вашем продукте. Может, было что-то похожее на наши? И как, кстати, решаете задачу найма ML-специалистов, которые должны работать со специфичным продуктом?

Источник

Возможности рынка
Логотип B
B Курс (B)
$0.09478
$0.09478$0.09478
+3.21%
USD
График цены B (B) в реальном времени
Отказ от ответственности: Статьи, размещенные на этом веб-сайте, взяты из общедоступных источников и предоставляются исключительно в информационных целях. Они не обязательно отражают точку зрения MEXC. Все права принадлежат первоисточникам. Если вы считаете, что какой-либо контент нарушает права третьих лиц, пожалуйста, обратитесь по адресу crypto.news@mexc.com для его удаления. MEXC не дает никаких гарантий в отношении точности, полноты или своевременности контента и не несет ответственности за любые действия, предпринятые на основе предоставленной информации. Контент не является финансовой, юридической или иной профессиональной консультацией и не должен рассматриваться как рекомендация или одобрение со стороны MEXC.

Вам также может быть интересно

Генезис USD1: 0% + 12% APR

Генезис USD1: 0% + 12% APRГенезис USD1: 0% + 12% APR

Новые пользователи: Стейкайте и получите до 600% APR