В сервисе выросло число нулей: как отличить модель от аварийного fallback

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

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

Ноль имеет несколько причин

Модель может обоснованно предсказать нулевой спрос. Постобработка может обрезать отрицательный сырой ответ до нуля. Наконец, fallback может подставить ноль после таймаута. Клиент во всех случаях видит одинаковое число, однако исправления различаются: проверить модель, изменить правило ограничения или восстановить источник. Одного поля prediction для такого разбора мало.

Добавьте origin со стабильными значениями model, clipped и fallback, а для резервного пути — reason. Версия модели полезна и при fallback: она покажет, какой релиз пытались вызвать. Отдельно сохраните версию резервного правила, потому что оно тоже меняется. Не кодируйте все сочетания в одном свободном текстовом сообщении: группировать структурированные поля проще и надёжнее.

Посчитаем два разных показателя

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

from collections import Counter
rows = [
    ("model", None, 0), ("model", None, 4),
    ("model", None, 0), ("model", None, 7),
    ("model", None, 2), ("fallback", "feature_timeout", 0),
    ("fallback", "feature_timeout", 0), ("fallback", "model_timeout", 0),
]
counts = Counter(origin for origin, reason, value in rows)
normal = [v for origin, reason, v in rows if origin == "model"]
reasons = Counter(reason for origin, reason, value in rows if origin == "fallback")
all_zero = sum(value == 0 for _, _, value in rows) / len(rows)
model_zero = sum(value == 0 for value in normal) / len(normal)
fallback_share = counts["fallback"] / len(rows)
print("all_zero", all_zero, "model_zero", model_zero)
print("fallback_share", fallback_share, "reasons", dict(reasons))
assert all_zero == 0.625 and model_zero == 0.4
assert fallback_share == 0.375

Общий график показывает 62,5% нулей. Среди ответов модели их 40%, а доля резервных ответов составляет 37,5%. Это три разных показателя. По причинам видно, что два случая связаны с признаками, один — с вычислением модели. Теперь расследование начинается с задержки соответствующих этапов, а не с перебора гиперпараметров.

Что сравнивать с вчерашним днём

Сначала сравните долю fallback в одинаковых временных окнах и сегментах трафика. Затем посмотрите распределение настоящих модельных прогнозов. Учитывайте, что при аварии до модели доходит другая выборка: например, остаются только пользователи с быстрым кэшем признаков. Стабильность распределения на уцелевших запросах сама по себе не доказывает стабильность качества на всём потоке.

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

Алерты привяжите к причине и последствиям. Рост feature_timeout направляет инженера к источнику признаков и бюджету ожидания. Рост model_timeout — к времени выполнения и нагрузке. Рост доли clipped при стабильной доступности требует проверки сырых предсказаний. Порог выбирайте по допустимому объёму затронутых решений, а не по красоте графика. Для малых окон показывайте также количество запросов, чтобы один случай не выглядел массовой аварией.

Проверьте журнал на сценариях обычного нуля, резервного нуля и ненулевого резерва. Если fallback однажды станет возвращать медиану, поиск по prediction == 0 перестанет замечать аварию. Поле происхождения продолжит работать. Его нужно устанавливать там, где выбирается путь ответа, и передавать через все последующие этапы без догадок по числу.

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

В курсе вы пройдёте мониторинг после релиза, data drift, prediction drift и эксплуатацию ML-сервиса. Следующая практическая задача — связать происхождение ответа с API-контрактом и отчётом качества, чтобы отличать изменение модели от сбоя её окружения. Откройте программу платного курса «Data Scientist: от данных до деплоя моделей».

Что наблюдать до появления истинного ответа.

Продолжить на практике

  • Data Scientist: от данных до деплоя моделей ↗

    Практический курс по Data Science: от постановки ML-задачи и работы с данными до inference API, CI/CD и мониторинга моделей в продакшне. Вы пройдёте EDA, feature engineering, обучение и оценку моделей, experiment tracking, error analysis и deployment — чтобы собирать полноценный рабочий контур вокруг ML-решения.