predict_proba в scikit-learn: как выбрать вероятность нужного класса
Сервис проверяет подозрительные операции. В обучающей таблице классы назывались fraud и legit, а в API разработчик вернул predict_proba(X)[:, 1] в поле fraud_probability. Числа лежат между нулём и единицей, сумма вероятностей правильная, сервис отвечает быстро. Однако он сообщает вероятность легитимной операции под именем риска мошенничества.
У массива вероятностей нет встроенного бизнес-смысла «первый — плохой, второй — хороший». Столбцы соответствуют порядку меток в classes_ обученного классификатора. Для выбранной модели нужно связать название события с конкретной меткой и только затем извлекать столбец.
Почему привычный срез перестаёт работать
На датасете с метками 0 и 1 второй столбец часто действительно соответствует событию, закодированному единицей. Но после смены источника целевая переменная может стать строковой или получить другую кодировку. Например, событие «клиент уйдёт» иногда хранится как 0, а «останется» — как 1. Технически модель исправна, но жёсткий индекс разворачивает смысл ответа.
Перед деплоем выведите classes_, проверьте описание целевой переменной и сохраните имя положительного события рядом с моделью. Если использовали LabelEncoder или собственную таблицу перекодировки, сохраните и обратное соответствие. Нельзя восстановить бизнес-значение класса только по его позиции в массиве.
Извлеките столбец по метке
Ниже обучается настоящая LogisticRegression на синтетических числах. Содержание признака нужно только для получения двух разных прогнозов; учебная задача — проверить отображение столбцов на классы. Низкие значения связаны с fraud, высокие — с legit. Функция возвращает вероятность указанной метки и явно отказывает, если модель её не знает.
import numpy as np
from sklearn.linear_model import LogisticRegression
X = np.array([[0], [1], [2], [8], [9], [10]], dtype=float)
y = np.array(["fraud", "fraud", "fraud", "legit", "legit", "legit"])
model = LogisticRegression().fit(X, y)
queries = np.array([[1], [9]], dtype=float)
def probability_for(estimator, rows, label):
classes = np.asarray(estimator.classes_)
if classes.ndim != 1:
raise ValueError("Expected a single-output classifier")
indices = np.flatnonzero(classes == label)
if len(indices) != 1:
raise ValueError(f"Unknown class: {label}")
proba = estimator.predict_proba(rows)
if proba.ndim != 2 or proba.shape[1] != len(classes):
raise ValueError("Probability columns do not match classes")
return proba[:, int(indices[0])]
all_proba = model.predict_proba(queries)
fraud = probability_for(model, queries, "fraud")
legit = probability_for(model, queries, "legit")
assert model.classes_.tolist() == ["fraud", "legit"]
assert np.allclose(fraud + legit, 1)
assert fraud[0] > fraud[1]
assert all_proba[0, 1] < 0.5 < fraud[0]
try:
probability_for(model, queries, "chargeback")
except ValueError:
pass
else:
raise AssertionError("Missing class accepted")
print("classes:", model.classes_.tolist())
for row, p in zip(queries, fraud):
print("feature:", row[0], "fraud:", round(float(p), 3))
Скрипт напечатает список ['fraud', 'legit'] и рассчитанные вероятности fraud для двух объектов. Для первого объекта второй столбец окажется меньше 0,5, хотя вероятность fraud больше 0,5. Это и есть воспроизведение ошибки: срез возвращает корректное число для другого события.
Функция рассчитана на обычную классификацию с одним выходом. У multi-output моделей predict_proba может возвращать список массивов, поэтому такой формат нужно обрабатывать отдельным контрактом. Не применяйте срез к любому объекту, лишь потому что у него есть метод с нужным названием.
Как оформить ответ API
Возвращайте именованное поле, например fraud_probability, и формируйте его через явно заданную метку fraud. Полезно также возвращать версию модели. Для многоклассовой задачи можно построить объект «метка → вероятность» из classes_ и строки predict_proba. Потребителю тогда не нужно догадываться, как изменился порядок столбцов после добавления класса.
При старте сервиса проверьте наличие требуемой метки. Если она исчезла, откажитесь загружать релиз. Подстановка нулевой вероятности или выбор первого столбца замаскирует несовместимость артефакта с API. В batch-задаче такую же проверку выполните до обработки большого файла.
Сравнивайте API с прямым вызовом модели на нескольких сохранённых входах. Тест должен проверять конкретное именованное поле, а не только сумму вероятностей и HTTP 200. Для границы API добавьте случай с неизвестной меткой и случай, когда артефакт содержит неожиданный набор классов.
После исправления смысла проверьте решение
Правильный столбец ещё не определяет, когда блокировать операцию или передавать её человеку. Порог выбирают отдельно с учётом цены ошибок и доступного объёма ручной проверки. До этой настройки убедитесь, что метрики и графики считают одно и то же событие. ROC-AUC для легитимности и порог риска мошенничества нельзя случайно соединить в одном отчёте.
Сама оценка вероятности также требует проверки на отложенных данных. Модель может уверенно ошибаться или нуждаться в калибровке. Сначала восстановите соответствие классов и выходных полей; затем качество вероятностей можно оценивать без путаницы в том, какое событие вообще измеряется.
В платном курсе ML Engineer / Data Scientist: табличные модели в продакшн вы обучите и сравните CatBoost и LightGBM, разберёте ошибки модели, соберёте inference pipeline и REST API. Откройте программу курса, чтобы проверять качество вместе с тем, как сервис передаёт результат потребителю. Следующий практический шаг — выбор порога по стоимости ошибок.
