Для студентов Data Science это одна из самых недооцененных тем. Часто кажется, что если модель уже обучена и умеет выдавать вероятность, то осталось лишь вызвать predict() и идти дальше. Но в реальной задаче классификатор почти никогда не живет в вакууме. Его решение запускает бизнес-процесс: блокирует транзакцию, отправляет лид менеджеру, поднимает тревогу, включает ручную проверку, рекомендует лечение, скрывает контент или открывает кредитный лимит. А значит, рабочий порог — это не техническая мелочь, а часть самой ML-системы.
Метрики здесь действительно важны, но сами по себе они не отвечают на главный прикладной вопрос: при каком score система уже должна действовать, а при каком ещё нет. Поэтому threshold selection почти всегда оказывается шагом между красивой модельной оценкой и реальной операционной политикой.
В 2026 году это особенно заметно, потому что современные production-модели все чаще встроены в контуры с ограниченными ресурсами: ограничена команда верификации, ограничено количество звонков, ограничен риск ложной блокировки, ограничен бюджет на ручную модерацию. Поэтому выбор порога — это уже не вопрос “что любит sklearn”, а вопрос того, как система будет вести себя в реальной среде.
Почему порог 0.5 почти никогда не является магическим ответом
Порог 0.5 часто воспринимается как естественный: если вероятность выше половины, берем положительный класс, если ниже — отрицательный. Но это всего лишь условность, а не закон. Она имеет смысл только в очень узкой ситуации, где классы примерно симметричны по частоте, модель калибрована, а цена ложноположительной и ложноотрицательной ошибки близка. В прикладном ML это встречается гораздо реже, чем хотелось бы.
Если мы ищем фрод, пропустить мошенника может быть куда дороже, чем случайно проверить честную транзакцию. Если мы делаем медицинский скрининг, пропуск больного объекта часто опаснее, чем лишний раз отправить пациента на дообследование. Если мы работаем с лидогенерацией, наоборот, слишком много ложноположительных срабатываний может парализовать отдел продаж. Во всех этих случаях порог должен двигаться вместе с ценой ошибки.
Поэтому рядом особенно полезно смотреть и на то, насколько вообще можно доверять числу, которое модель называет вероятностью. Без калибровки выбор порога может выглядеть точным, хотя сам score интерпретируется слишком оптимистично.
Что именно делает порог
Модель бинарной классификации обычно выдает число от (0) до (1). Это не готовый класс, а score уверенности или оценка вероятности положительного события. Порог просто говорит, где провести границу между “считать положительным” и “считать отрицательным”.
(\hat{y}=\mathbb{I}(p \ge t))
Раздел математики: теория вероятностей и математическая статистика.
Что означает каждый символ:
(\hat{y}) — итоговое бинарное решение модели.
(\mathbb{I}(\cdot)) — индикаторная функция, которая возвращает (1), если условие выполнено, и (0) в противном случае.
(p) — score или вероятность положительного класса.
(t) — выбранный порог.
Формула появляется из самой механики decision rule: мы не меняем модель, мы меняем границу, по которой ее score превращается в действие.
Численный пример: если модель выдала (p=0.73), а рабочий порог равен (t=0.6), тогда (p \ge t), и решение будет положительным. Если тот же объект проверять при пороге (t=0.8), решение станет отрицательным. Модель не изменилась, изменилась политика действия.
Интуиция: порог — это ручка, которая двигает систему вдоль кривой ошибок
Когда мы снижаем порог, система становится смелее. Она чаще говорит “да”, поэтому растет recall: больше положительных объектов удается поднять. Но вместе с этим растет и число ложных срабатываний, поэтому precision часто падает. Когда мы повышаем порог, система становится осторожнее. Precision обычно растет, но recall снижается, потому что часть настоящих положительных объектов теперь отсеивается.
Именно поэтому threshold selection нельзя отделить от ROC-кривой, PR-кривой, confusion matrix и стоимости ошибки. Порог — это просто координата на уже существующей кривой поведения модели. Он не делает модель умнее. Он выбирает, как именно существующий ranking переводится в действие.
Если задача несбалансирована, здесь особенно полезно отдельно посмотреть, что именно показывает precision-recall curve при редких положительных классах. Тогда видно, почему один и тот же threshold может выглядеть приемлемо по ROC и при этом оказаться слабым в реальном потоке с редким событием.
Почему выбор порога — это задача оптимизации, а не вкуса
Как только у нас появляются разные цены ошибок, задача сразу становится оптимизационной. Мы хотим выбрать такое значение (t), при котором целевая метрика, бизнес-прибыль или полезность системы максимальны.
(t^{*}=\arg\max_{t \in [0,1]} U(t))
Раздел математики: оптимизация.
Что означает каждый символ:
(t^{*}) — оптимальный порог.
(\arg\max) — операция поиска аргумента, на котором функция максимальна.
(U(t)) — полезность или quality-метрика системы при пороге (t).
Эта формула появляется естественно: у нас есть набор допустимых порогов и есть функция качества, которую мы хотим максимизировать.
Численный пример: пусть при пороге (0.3) бизнес-полезность равна 120, при пороге (0.5) — 150, а при пороге (0.7) — 130. Тогда оптимальным будет (t^{*}=0.5), потому что именно там функция полезности максимальна.
Ключевая мысль здесь такая: правильный порог не “угадывают”, а выбирают под objective function. И эта функция может быть разной в зависимости от бизнеса. В задачах с сильным дисбалансом это часто идёт в паре с вопросом о том, нужно ли дополнительно менять саму механику обучения через class weights и rebalancing, а не только двигать границу решения.
Когда оптимизировать F1, а когда этого недостаточно
Часто в учебных задачах порог выбирают по максимальному F1-score. Это разумно, если precision и recall примерно одинаково важны. Но в прикладных задачах такое равновесие встречается не всегда. Если пропуск события значительно дороже ложной тревоги, полезнее смотреть на (F_{\beta}) при условии (\beta \gt 1). Если важнее чистота положительного потока, нужно смещать акцент в сторону precision.
(F_{\beta}=(1+\beta^2)\frac{\mathrm{Precision}\cdot\mathrm{Recall}}{\beta^2\cdot\mathrm{Precision}+\mathrm{Recall}})
Раздел математики: математическая статистика и прикладная оптимизация.
Что означает каждый символ:
(\beta) задает, чему мы придаем больший вес.
Если (\beta=1), получаем баланс precision и recall.
Если (\beta=2), recall становится важнее.
Формула нужна не сама по себе, а как инструмент выбора порога под конкретную цену ошибок.
Численный пример: пусть precision равен (0.45), recall — (0.75), а нам важнее recall, поэтому берем (\beta=2). Тогда (F_2=5\cdot\frac{0.45\cdot0.75}{4\cdot0.45+0.75}=5\cdot\frac{0.3375}{2.55}\approx0.662). Это число уже учитывает, что пропуск положительного объекта для нас дороже.
Геометрический смысл выбора порога
Геометрически порог — это не просто число. Это точка на ROC-кривой или PR-кривой. Когда мы меняем порог, мы двигаемся по траектории режимов одной и той же модели. То есть threshold selection — это навигация по кривой качества, а не обучение новой модели.
Если думать совсем интуитивно, модель выстраивает объекты вдоль шкалы уверенности, а порог режет эту шкалу в некотором месте. Чем ниже разрез, тем больше объектов мы называем положительными. Чем выше — тем строже фильтр. В этом смысле порог — это геометрическая граница между действием и бездействием.
Почему порог нельзя выбирать на тесте один раз и забывать
Рабочий порог зависит не только от качества модели, но и от среды, в которой она живет. Если меняется base rate положительного класса, если меняется цена ошибки, если меняется capacity downstream-команды, если появляются новые правила риска, порог тоже может потребовать пересмотра. Это особенно важно в проде, где модель включена в поток решений, а не просто демонстрирует красивую метрику.
Именно поэтому сильная production-практика не заканчивается словами “мы подобрали threshold”. Нужно понимать, при каких предпосылках он был выбран и что именно должно случиться, чтобы этот threshold перестал быть рабочим.
На проде это особенно чувствительно в связке с дрейфом признаков и данных: если меняется входной поток, прежний threshold быстро перестаёт соответствовать старой логике риска. Поэтому рядом полезно держать в голове и материал о том, как drift ломает рабочие решения даже у вчера ещё стабильной модели.
Python: как выбирать порог на практике
from sklearn.datasets import make_classification # Генерируем синтетическую задачу бинарной классификации.
from sklearn.model_selection import train_test_split # Подключаем разделение на train и validation.
from sklearn.linear_model import LogisticRegression # Берем логистическую регрессию как базовый классификатор.
from sklearn.metrics import precision_recall_curve # Импортируем PR-кривую для анализа порогов.
import numpy as np # Numpy нужен для перебора и выбора лучшего порога.
X, y = make_classification( # Создаем выборку с дисбалансом классов.
n_samples=5000, # Генерируем пять тысяч объектов.
n_features=20, # Используем двадцать признаков.
n_informative=6, # Шесть признаков действительно несут сигнал.
n_redundant=2, # Два признака коррелируют с полезными.
weights=[0.95, 0.05], # Положительный класс редкий: только 5 процентов.
random_state=42, # Фиксируем случайность.
) # Получаем признаки и target.
X_train, X_valid, y_train, y_valid = train_test_split( # Делим данные на train и validation.
X, y, test_size=0.2, stratify=y, random_state=42
) # Сохраняем долю положительного класса в обеих частях.
model = LogisticRegression(max_iter=1000) # Создаем модель с запасом по итерациям.
model.fit(X_train, y_train) # Обучаем классификатор.
scores = model.predict_proba(X_valid)[:, 1] # Получаем вероятности положительного класса.
precision, recall, thresholds = precision_recall_curve(y_valid, scores) # Строим PR-кривую.
f1_values = 2 * precision[:-1] * recall[:-1] / (precision[:-1] + recall[:-1] + 1e-12) # Считаем F1 для каждого порога.
best_idx = np.argmax(f1_values) # Находим индекс лучшего значения F1.
best_threshold = thresholds[best_idx] # Достаем соответствующий порог.
print('Best threshold:', round(best_threshold, 4)) # Печатаем лучший найденный порог.
print('Precision:', round(precision[best_idx], 4)) # Показываем precision в этой точке.
print('Recall:', round(recall[best_idx], 4)) # Показываем recall в этой точке.
print('F1:', round(f1_values[best_idx], 4)) # Показываем F1 для выбранного порога.Этот код хорош тем, что показывает правильный workflow. Сначала модель обучается. Потом мы не доверяем порогу 0.5 по умолчанию, а строим кривую, считаем метрику для множества порогов и выбираем тот, который соответствует нашей цели. То есть решение принимается уже на уровне политики действия, а не только на уровне качества score.
Когда оптимальный порог по метрике не оптимален по бизнесу
Это очень важный и часто болезненный момент. Порог, который максимизирует F1 или даже PR-AUC-похожий proxy, не всегда оптимален для бизнеса. Допустим, он требует, чтобы команда проверяла 50 тысяч тревог в день, а у нее capacity только на 3 тысячи. Или наоборот: математически лучший порог оказывается слишком консервативным и пропускает слишком много дорогих случаев. Поэтому последний шаг всегда должен быть связан с операционной реальностью.
В хороших командах threshold selection обсуждается не только внутри ML, но и с продуктом, риском, операциями, медициной, безопасностью или любым другим downstream-контуром. Потому что порог — это граница не только модели, но и рабочего процесса.
Что важно вынести из темы
Threshold selection — это этап, на котором вероятностная модель превращается в систему решения. Порог определяет, сколько положительных объектов система поднимет, насколько чистым будет поток сигналов и как именно распределятся ложные тревоги и пропуски. Поэтому он не выбирается по привычке и не наследуется автоматически из 0.5.
Если сказать совсем коротко, хороший порог — это не тот, который красиво смотрится в ноутбуке, а тот, который делает модель рабочей именно в вашей задаче. В этом смысле threshold selection — это мост между математикой классификации и реальной жизнью ML-системы.
Kaggle notebook по теме:
https://www.kaggle.com/code/taniarv/digital-health-eda-ml-threshold-tuning