Условие
Модель fraud-детекции с предсказанной вероятностью. Bool FN = пропущенный фрод = 5000 ₽ убытка. FP = ложный блок = 50 ₽ убытка (поддержка + злой клиент). На каком пороге запускать?
Решение
Подход
Оптимизируем expected cost, не F1 / AUC. Cost-sensitive threshold:
Cost(t) = C_FN · FN(t) + C_FP · FP(t)
В пересчёте на одного позитива:
Cost = C_FN · (1 − recall(t)) · P + C_FP · (1 − precision(t)) · positives_predicted(t)
Или эквивалентно через cost-aware decision:
Predict positive iff p(x) > C_FP / (C_FN + C_FP)
В нашем случае: t* = 50 / (5000 + 50) ≈ 0.0099. То есть блокируем при p > ~1%.
Реализация
import numpy as np
def optimal_threshold(y_true, y_prob, c_fn=5000, c_fp=50):
thresholds = np.linspace(0.001, 0.999, 999)
costs = []
for t in thresholds:
pred = (y_prob >= t).astype(int)
fn = ((pred == 0) & (y_true == 1)).sum()
fp = ((pred == 1) & (y_true == 0)).sum()
costs.append(c_fn*fn + c_fp*fp)
best_t = thresholds[np.argmin(costs)]
return best_t, min(costs)
# Алгебраический shortcut (для калиброванной модели):
t_star = c_fp / (c_fn + c_fp)Когда shortcut работает
- Модель калибрована (предсказывает истинные вероятности).
- Costs линейны (не зависят от величины ошибки).
- Не учитываем opportunity cost (например, удерживаем клиента).
Если модель не калибрована — используйте grid search.
Метрики для отчёта
- Precision@t, Recall@t.
- Confusion matrix в денежных единицах.
- Expected cost per transaction.
- Saved fraud / cost of investigation.
def business_report(y_true, y_prob, t, c_fn, c_fp):
pred = (y_prob >= t).astype(int)
fn = ((pred == 0) & (y_true == 1)).sum()
fp = ((pred == 1) & (y_true == 0)).sum()
tp = ((pred == 1) & (y_true == 1)).sum()
return {
'threshold': t,
'precision': tp / (tp + fp),
'recall': tp / (tp + fn),
'expected_cost_per_tx': (c_fn*fn + c_fp*fp) / len(y_true),
'fraud_saved_rub': c_fn * tp,
}Подводные камни
- F1 / accuracy игнорируют асимметрию costs — не используйте на cost-sensitive задачах.
- Калибровка обязательна для shortcut формулы. Без неё — grid search.
- Stationarity: cost_FN может меняться сезонно (праздники → выше) — пересчитывайте threshold.
- Multi-class: формула расширяется до cost matrix, выбираем класс с минимальным expected cost.
- Если threshold очень малый (1%), precision крошечная — большая нагрузка на support. Это бизнес-trade-off.
Эталонный ответ
Threshold = argmin expected_cost = C_FP / (C_FN + C_FP) для калиброванной модели. Иначе grid search. F1 не подходит для асимметричных cost. Здесь t ≈ 1%, что даёт максимум сохранённого фрода — но большой precision tradeoff.*