Модель оценила риск заявок. Какие из них проверить за десять часов?
За ночь сервис выставил оценки риска всем заявкам. До вечера у специалистов есть десять часов. Одну заявку можно проверить за четыре, другая потребует шести. Если отправить на проверку всё выше порога, работа накопится. Если просто отсортировать по риску, первые дорогие заявки займут часы, которые можно было бы распределить между несколькими проверками.
Переводим вероятность в ценность решения
Пусть модель оценивает вероятность нарушения, а продукт знает, сколько потерь в среднем предотвратит проверка при подтверждённом нарушении. Перемножим эти величины. Если заявка несёт ожидаемую пользу 74 условных единиц, мы используем это число для планирования. Реальная выгода отдельной проверки зависит от исхода. Если проверка обнаруживает только половину нарушений, это должно отразиться в оценке предотвращаемого ущерба. Если час работы стоит денег, вычтите прямые расходы из ожидаемой выгоды, а часы всё равно учитывайте как отдельное физическое ограничение.
В примере выигрыш от разных заявок складывается. Это подходит, когда каждая проверка предотвращает свой ущерб. Если десять заявок принадлежат одной сети злоупотреблений и проверка любой раскрывает всю сеть, суммы будут считать один и тот же эффект несколько раз. Объедините такие заявки или явно задайте связи между решениями.
Подбираем набор, который помещается в смену
Одна заявка попадает в очередь целиком или остаётся за её пределами. Поэтому это задача о рюкзаке 0/1. При небольшом целочисленном бюджете решим её динамическим программированием. Для каждого доступного числа часов запоминаем лучший найденный набор. Обрабатывая новую заявку, сравниваем старый набор с вариантом, где заявка добавлена к лучшему набору на оставшиеся часы. Перебираем часы в обратном порядке, чтобы источник обновления ещё не содержал текущую заявку.
Ниже пять часов на каждую из двух менее рискованных заявок дают суммарно больше пользы, чем шесть часов на первую. Для простоты все заявки имеют одинаковый предотвращаемый ущерб при нарушении. Разницу создают вероятность и время. Вероятности представлены целыми процентами, чтобы результаты можно было сверить без округления.
rows = [
("A", 6, 74, 100),
("B", 5, 60, 100),
("C", 5, 55, 100),
]
budget = 10
items = [(key, hours, prob * loss // 100)
for key, hours, prob, loss in rows]
best = [(0, ()) for _ in range(budget + 1)]
for key, hours, benefit in items:
for capacity in range(budget, hours - 1, -1):
old_value, old_ids = best[capacity - hours]
candidate = old_value + benefit
if candidate > best[capacity][0]:
best[capacity] = (candidate, old_ids + (key,))
greedy = []
remaining = budget
for key, hours, prob, loss in sorted(rows, key=lambda row: -row[2]):
if hours <= remaining:
remaining -= hours
greedy.append((key, prob * loss // 100))
value, ids = best[budget]
used = sum(hours for key, hours, _ in items if key in ids)
print("По риску:", [key for key, _ in greedy], sum(v for _, v in greedy))
print("По бюджету:", ids, value, "часов:", used)
assert (ids, value, used) == (("B", "C"), 115, 10)
Сортировка по вероятности берёт A. Даже если проверять заявки по ожидаемой пользе за час, в этом наборе A снова выйдет первой, а оставшихся четырёх часов не хватит. Оптимальный набор B и C использует десять часов и даёт 115 единиц ожидаемой пользы вместо 74. Разница появляется из-за целых проверок. У сортировки по отношению пользы ко времени нет способа пересмотреть уже занятые часы.
Проверяем правило на истории
Постройте по очереди на каждый прошлый день с его бюджетом. Для прогноза и времени проверки используйте данные, известные к началу смены. На выходе сохраните выбранные ID, использованные часы и ожидаемую пользу. Если для всех заявок доступны исходы, отдельно посчитайте фактически найденные нарушения на выбранных наборах. Если результат известен только по проверенным заявкам, сравнение с новой очередью потребует дополнительного дизайна сбора меток. Иначе вы сравните наблюдавшиеся проверки с догадкой о пропущенных случаях.
Проверьте, насколько фактическая длительность отличается от плановой. Если оценки часто занижены, план на десять часов регулярно будет переливаться на завтра. Дайте смене резерв или калибруйте время на данных по типам заявок. При очень мелких единицах времени таблица динамического программирования растёт вместе с бюджетом. Для нескольких смен с разной квалификацией потребуется учесть допустимость назначения заявки конкретной смене.
Практика на Latorn
В бесплатных задачах NumPy и pandas можно отработать группировки и обработку табличных данных перед сборкой ежедневной очереди.
На платном курсе «ML Engineer / Data Scientist» вы обучите CatBoost и LightGBM и настроите пакетный инференс. Тогда ежедневная очередь сможет опираться на прогнозы из воспроизводимого расчёта. Посмотреть программу и блок по инференсу.
Продолжить на практике
- ML Engineer / Data Scientist: табличные модели в продакшн ↗
Уже обучали модели на Python? Научитесь доводить их до работы в приложении: проверять качество без утечек, сравнивать CatBoost и LightGBM, объяснять прогнозы через SHAP и запускать модель через FastAPI. Практика на табличных данных, Docker, пакетный инференс и мониторинг.
