Всем привет! Меня зовут Аня Шатшнайдер, я старший BI-разработчик в команде антифрода Авито. Мы пользуемся сотнями ИИ-моделей, чтобы бороться с действиями недобросовестных пользователей. Но иногда модели работают не так эффективно, как следует. Поэтому моя команда решила найти аутсайдеров и передать их на доработку DS-инженерам. Расскажу, как мы пересматривали подход к оценке моделей.
Статья будет полезна аналитикам и DS, которые работают с несколькими ML-моделями в проде и хоть раз озадачились вопросом, как сравнивать их между собой.

Наши модели работают в разных сервисах, пишут логи в различных форматах, одни выдают скоры, другие флаги 0/1, а в конце этой цепочки стоит система санкций и обеления, если действие пользователя ошибочно оценили как недопустимое.
Изменения в антифроде мы запускаем быстро, поскольку это помогает опережать недобросовестных пользователей. В результате за всё время накопилось много моделей, из-за чего простой вопрос «какая из них вносит наибольший вклад, и нет ли дублей?» превращается в отдельное исследование и съедает заметную часть аналитического ресурса.
Мы не могли найти лучшую модель
Давайте сразу обозначим контекст. В антифроде Авито модель — это часть длинной цепочки: Предсказание модели → запрос на санкцию → обеление, то есть возможная отмена санкции → применение → прохождение санкции (пользователь сталкивается с ограничением: например, ему нужно пройти дополнительную проверку) → блокировка (или нет).
Каждое звено этой цепочки раньше было автономным, со своими таблицами, логами и дашбордами, из-за чего мы испытывали ряд трудностей.
Не получалось связать модель и её последствия. В логах не было сквозного идентификатора. Модель предсказала фрод — окей, это где-то записалось. Потом к пользователю применилась санкция — это записалось в другом месте. Само решение принималось корректно, но проанализировать работу было сложно.
Не было быстрого способа надёжно сопоставить, что вот эта конкретная санкция — следствие этого предсказания той модели. Мы проводили анализ вручную, что занимало много времени, из-за чего быстро измерять вклад каждой модели на потоке мы не могли. В некоторых сервисах логирования не хватало: часть срабатываний вообще не фиксировалась, потому что агрегация была на пользователя и логировалась только одно срабатывание за период.
Обеления ломали любую наивную метрику. Чтобы не наказывать пользователей зря, у нас работает механизм обелений: в некоторых случаях санкция не применяется, даже если модель настаивает. Это хорошо для пользователей, но плохо для аналитики — без сквозного ID связать обеление с предсказанием конкретной модели тоже было сложно.
Нет понимания, как учитывать в аналитике пересечения. Отправить пользователя на санкцию может не одна модель, а сразу несколько. В аналитических отчётах это не учитывалось, поскольку каждая оценивалась отдельно. В итоге на действия одного и того же пользователя могли среагировать две модели, и обе выглядели эффективными, хотя по факту они просто дублировали решение друг друга.
AUC не работает. Это площадь под ROC-кривой — стандартная метрика качества классификатора. Она показывает, насколько хорошо ваша модель способна отличать один класс от другого. Но классический ROC-AUC требует таргета: мы должны знать, реально ли пользователь был фродером. А после запуска модели в прод у нас этого знания нет, потому что получить таргет можно только через ручную разметку, что довольно дорого. Значит, офлайн-метрики качества классификации нам мало что скажут про реальную пользу модели.
В итоге аналитики на недели уходили в ручные расследования, у каждого был свой подход, и честно сравнить модели между собой было практически невозможно. Некоторые из них были неэффективными, но никто об этом не знал, потому что не было данных.

Переформулировали задачу и выделили слои работы: данные, метрики и визуализация
Изначально задача пришла к нам в таком виде: «Разработать инструмент для оценки эффективности моделей».
Сначала мы попробовали решить её в лоб и быстро упёрлись в то, что дашборд поверх поломанных логов не лечит поломанные логи. Если в данных нет связи между моделью и её последствиями, никакая визуализация не поможет.
Поэтому задачу мы переформулировали: построить сквозную систему оценки моделей от логирования до метрики, которая покажет не только количество срабатываний, но и дальнейшие шаги в цепочке.
К этой задаче мы подошли с нестандартным подходом:
1️⃣ Заранее зафиксировали измеримый результат. Ещё до старта договорились, что считаем успехом — health score дашбордов не ниже жёлтого, а оценка от пользователей — 4,5+ (минимум 5 оценок).
Health score — это метрика здоровья разных аналитических объектов — дашбордов, датасетов и витрин внутри Авито. Каждый объект находится в зелёной, жёлтой или красной зоне в зависимости от его состояния. Для дашбордов зона зависит от значения health score по шкале от 1 до 100. Чем выше скор, тем лучше:
? — выше 80, значит, это здоровый объект.
? — от 60 до 80 означает, что дашборду нужны небольшие доработки.
? — ниже 60 значит, что дашборд надо серьёзно переработать.
Сверхрезультат — зелёный health score, мы нашли важные инсайты и запланировали на их основе работы в следующем периоде. Критерий согласовали заранее, а потом честно замерили опросом стейкхолдеров прямо в Redash.
2️⃣ Вовлекли ключевого стейкхолдера в разработку. Мы добавили в контрибьюторы ключевого стейкхолдера — DS-лида и договорились, что он регулярно участвует в разработке вместе с нами. Работали по принципу: «Скажи мне — и я забуду, покажи мне — и я запомню, вовлеки меня — и я пойму». Для BI-задач это нетипичная практика, но именно она помогла попасть в реальные потребности команд.
Дальше работа распалась на три слоя в строгом порядке: сначала данные, затем метрика, и только в конце визуализация.
Слой данных из сквозных ID и новых витрин
У нас не было идентификаторов и было неясно, что и где именно не работает. Поэтому мы начали работу именно с внедрения ID.
Ввели единый идентификатор запроса на санкцию — punisher_request_id. Это UUID, который проходит через всю цепочку модель → обеление → санкция и позволяет связать между собой звенья, которые раньше находились в разных таблицах. Этот же ID мы добавили в уже существующие витрины с обелениями, санкциями и скорами. Так быстро и просто соединили всё в единое целое.
Переработанное логирование. В одном из сервисов со скоринговыми моделями заодно отработали технический долг — переделали схему логирования. Теперь всё стало аккуратнее:
Было |
Стало |
Дублирование информации |
Схему упростили, дубли убрали |
Нет чёткой связи с санкциями |
Добавили сквозной ID и полную цепочку |
Часть данных не логировалась |
Добавили недостающие поля |
Теперь в логах по каждому срабатыванию есть ID пользователя, название модели, скор, флаг тестового режима, требуемая санкция и её параметры, а также ID отправки на санкцию.
? Вывод 1: сквозной ID — обязательный элемент, если у вас есть многошаговый процесс и каждому событию нужен идентификатор, который будет связывать эти шаги. Без него в будущем будет невозможно связать причину со следствием.
Реализовали 7 новых витрин в DWH на Trino. Это каркас, на котором держится остальная аналитика и отчётность:
Витрина |
Что внутри |
af_user_model_predictions |
Все предсказания моделей по новой схеме логирования одного из сервисов |
af_user_model_actions |
Санкции от моделей одного из сервисов |
antifraud_scores |
Скоры всех моделей по пользователям, объявлениям или чатам из разных сервисов скоринга |
antifraud_model_actions |
Санкции от моделей из разных сервисов |
punisher_requests |
Запросы на наложение санкций в систему санкций и обелений (ОРР) |
punisher_requests_params |
Параметры запрошенных санкций |
punisher_requests_settings |
Настройки запрошенных санкций |
Добавили одно событие в ClickStream — «Запрос в ОРР об отправке пользователя на санкцию». Теперь каждый такой запрос фиксируется независимо от его результата.
Главное, связали все действия моделей. Теперь по одному punisher_request_id можно восстановить всю цепочку: «Модель X предсказала фрод, отправила в ОРР запрос на санкции Y и Z, пользователь был обелён для Y, а санкция Z в итоге применилась».
? Вывод 2: аналитика начинается с логов, а не с дашборда. Сколько бы красивых графиков вы ни нарисовали поверх кривых данных, они останутся графиками поверх кривых данных. Мы потратили значительную часть времени именно на слой с логами и не жалеем.
? И сразу вывод 3: В антифроде, да и не только, модель живёт в связке с тем, что происходит после предсказания: обелениями, санкциями, прохождением. Оценивать имеет смысл всю цепочку, а не только «угадала или не угадала».

Слой MES из метрик эффективности моделей
Когда данные, наконец, стали связными, появилась возможность нормально считать. ROC-AUC мы использовать не можем, потому что таргета нет. Значит, надо смотреть, что реально произошло с пользователем после срабатывания модели. Так появилась MES (Model Effectiveness Score) — сводная метрика из шести компонентов:
Компонент |
Что измеряет |
Вес |
Формула |
Конверсия в санкцию |
Доля предсказаний, которые привели к реальному наложению санкции |
1 |
# санкций / # предсказаний |
1 − Прохождение санкции пользователем |
Доля пользователей, которые успешно прошли санкцию |
2 |
1 − # пройденных / # наложенных |
1 − 2 × Конверсия в блокировку |
Сколько пользователей, которые прошли санкцию, в итоге заблокировали в течение недели. Умножаем на 2, поскольку конверсия в блокировку очень низкая по сравнению с другими конверсиями |
1 |
1 − # блокировок / # пройденных санкций |
1 − False Positive Rate |
Доля избыточных санкций |
1 |
1 − # избыточных / # всех санкций |
Уникальность |
Доля предсказаний, в которых никто не сработал кроме этой модели |
0.5 |
# уникальных предсказаний / # всех предсказаний модели |
1 − Степень неуникальности / n |
Среднее число других моделей, сработавших вместе с этой. Деление на n нужно для нормировки |
0.5 |
1 − (# совместных срабатываний / # срабатываний модели) / n |
MES = Σ(Компонент × Вес) / 6
Чем выше MES, тем эффективнее модель в связке с санкцией.
Каждый компонент закрывает одну проблему из первой части статьи:
Три конверсии (в санкцию, прохождение или блокировку) — реальная оценка вместо ROC-AUC. Они показывают, доводит ли модель дело до конца. Ведь есть вероятность, что она просто выдаст флаг и на этом успокоится.
Конверсия на прохождение санкции пользователем получила вес 2, потому что важно понимать, насколько она останавливает потенциальных недобросовестных пользователей.
False Positive Rate — модель не должна зарабатывать очки за счёт того, что наказывает всех подряд. Мы постоянно работаем над снижением ложных отправок пользователей на санкции.
Уникальность и степень неуникальности показывает, нет ли дублирования работы других моделей. Если модель срабатывает только там, где уже сработали пять других, её вклад невелик, даже если конверсии на бумаге красивые.
? Вывод 4: Частота срабатываний ≠ полезность. Модель, которая 1000 раз триггерится на одном пользователе, не в 1000 раз лучше модели, сработавшей один раз. Учитывайте уникальность срабатываний как по пользователю, так и между моделями.
Слой дашбордов из 10 штук
Все доски находятся в Redash — это стандартный стек для Авито из Trino и Redash. Всего получилось 10 дашбордов, объединённых в один отчёт. Я специально сделала их много, потому что один универсальный дашборд на всё обычно превращается в кашу, которую никто не читает.
Дашборд |
Зачем он нужен |
Основной — MES по всем моделям |
Это главный экран. Единая метрика эффективности с фильтрами по дате, сервису и команде. Сразу видно, какие модели можно переработать. Показываем итоговый MES и все его компоненты, чтобы было понятно, почему считаем модель хорошей или плохой |
Воронка и конверсии |
Вся цепочка: предсказание → обеление → санкция → прохождение → блокировка в течение 7 дней. Каждый шаг видно отдельно и в динамике. Есть разбивка по типам санкций, фильтры по дате, сервису, команде, модели |
4 дашборда по шагам воронки |
Детализация по этапам цепочки: флагирование, обеления, применение санкций, прохождение санкций. Нужны, когда в общей воронке видно просадку и надо посмотреть детальнее каждый шаг |
Уникальность моделей |
Для каждой модели смотрим, сработали ли другие модели в окне ±1 сутки, считаем среднее число таких соседей. Учитываем конверсии отдельно для уникальных и неуникальных предсказаний |
Покрытие Proxy Fraud 2.0 |
Обратный взгляд: берём пользователей, которых мы в итоге заблокировали за недобросовестное поведение и смотрим, как на них срабатывали модели. Это проверка на полноту. |
False Positive по моделям |
Доля избыточных наложений санкций по каждой модели. Модели с высоким FP уходят в проработку |
Бонус: Мы получили системный инструмент и единый язык, на котором команды из разных сервисов теперь могут обсуждать эффективность своих моделей. И это, пожалуй, оказалось самым ценным.
Что получилось в итоге
? Модели из разных сервисов и команд теперь можно сравнивать по одной метрике, с учётом нескольких факторов, а не только конверсии.
? 8 неэффективных моделей нашлись сразу. Одну взяли в переработку одновременно с выходом отчёта, ещё 4 уже отключили или переработали, а остальные запланированы на следующие периоды.
? Теперь MES подсвечивает проблемные модели. Именно на неё смотрят при решениях о переработке.
? 7 витрин живут отдельной жизнью. Их используют в дашбордах, а также в обычных задачах аналитиков и DS. Оказалось, что нормальные логи нужны всем.
? Ручной анализ схлопнулся, и там, где раньше уходили дни, теперь хватает одного отчёта.
? Мы достигли и перевыполнили цель. Итоговая оценка дашбордов от пользователей — 5.0, health score — зелёный, а среди моделей нашлись те, что мы отправили в переработку. Это даже выше планки, которую мы ставили себе на старте.
Если у вас есть свой опыт построения таких систем — расскажите в комментариях, особенно интересны истории, где пришлось отказаться от AUC в пользу продуктовых метрик. И задавайте вопросы, с радостью отвечу.
А если вы хотите узнать больше о работе аналитиков в Авито — подписывайтесь на канал «Коммуналка аналитиков»
