Примечание
Перейти к концу для скачивания полного примера кода. Или запустить этот пример в вашем браузере через JupyterLite или Binder
Настройка порога принятия решения для обучения с учётом затрат
После обучения классификатора, метод predict выводит предсказания меток классов, соответствующие пороговому значению, либо функции decision_function, либо результату predict_proba. Для бинарного классификатора значение по умолчанию определяется как оценка апостериорной вероятности 0,5 или значение функции принятия решения 0,0.
Однако, эта стратегия по умолчанию, скорее всего, не оптимальна для задачи. Здесь мы используем набор данных «Statlog» по кредитам немцев [1] для иллюстрации использования. В этом наборе данных задача заключается в предсказании того, имеет ли человек «хороший» или «плохой» кредит. Кроме того, предоставляется матрица затрат, которая определяет затраты на неправильную классификацию. В частности, неправильная классификация «плохого» кредита как «хорошего» в среднем в пять раз дороже, чем неправильная классификация «хорошего» кредита как «плохого».
Мы используем TunedThresholdClassifierCV для выбора точки отсечения функции принятия решения, которая минимизирует заданные бизнес-затраты.
Во второй части примера мы далее расширяем этот подход, рассматривая проблему выявления мошенничества при кредитных картах: в этом случае бизнес-метрика зависит от суммы каждой отдельной транзакции.
Ссылки
# Authors: The scikit-learn developers # SPDX-License-Identifier: BSD-3-Clause
Чувствительное к затратам обучение с постоянными выигрышами и затратами
В этом первом разделе мы проиллюстрируем использование класса
TunedThresholdClassifierCV в задаче обучения, чувствительного к затратам, когда выигрыши и затраты, связанные с каждой ячейкой матрицы ошибок, являются постоянными. Мы используем проблему, представленную в [2], используя набор данных «Statlog» по немецким кредитным заявкам [1].
Набор данных «Statlog» по немецким кредитным заявкам
Мы загружаем набор данных по немецким кредитным заявкам из OpenML.
import sklearn from sklearn.datasets import fetch_openml sklearn.set_config(transform_output="pandas") german_credit = fetch_openml(data_id=31, as_frame=True, parser="pandas") X, y = german_credit.data, german_credit.target
Мы проверяем типы признаков, доступные в X.
X.info()
<class 'pandas.core.frame.DataFrame'> RangeIndex: 1000 entries, 0 to 999 Data columns (total 20 columns): # Column Non-Null Count Dtype --- ------ -------------- ----- 0 checking_status 1000 non-null category 1 duration 1000 non-null int64 2 credit_history 1000 non-null category 3 purpose 1000 non-null category 4 credit_amount 1000 non-null int64 5 savings_status 1000 non-null category 6 employment 1000 non-null category 7 installment_commitment 1000 non-null int64 8 personal_status 1000 non-null category 9 other_parties 1000 non-null category 10 residence_since 1000 non-null int64 11 property_magnitude 1000 non-null category 12 age 1000 non-null int64 13 other_payment_plans 1000 non-null category 14 housing 1000 non-null category 15 existing_credits 1000 non-null int64 16 job 1000 non-null category 17 num_dependents 1000 non-null int64 18 own_telephone 1000 non-null category 19 foreign_worker 1000 non-null category dtypes: category(13), int64(7) memory usage: 69.9 KB
Многие признаки являются категориальными и обычно закодированы строками. Нам нужно закодировать эти категории при разработке нашей предсказательной модели. Давайте проверим целевые значения.
y.value_counts()
class good 700 bad 300 Name: count, dtype: int64
Еще одной особенностью является то, что набор данных несбалансирован. При оценке нашей предсказательной модели нужно проявлять осторожность и использовать набор метрик, адаптированных к этой ситуации.
Кроме того, мы наблюдаем, что целевая переменная закодирована строками. Некоторые метрики (например, точность и полнота) требуют предоставления целевого метки, также называемой «положительной меткой». Здесь мы определяем, что наша цель — предсказать, является ли образец «плохим» кредитом.
pos_label, neg_label = "bad", "good"
Для проведения нашего анализа мы разделим наш набор данных с помощью одного стратифицированного разделения.
from sklearn.model_selection import train_test_split X_train, X_test, y_train, y_test = train_test_split(X, y, stratify=y, random_state=0)
Мы готовы разработать нашу предсказательную модель и связанную с ней стратегию оценки.
Метрики оценки
В этом разделе мы определим набор метрик, которые будем использовать позже. Чтобы увидеть эффект настройки точки отсечения, мы оценим предсказательную модель с помощью ROC-кривой и кривой точность-полнота. Значения, отображаемые на этих графиках, являются поэтому истинным положительным значением (ИПЗ), также известным как полнота или чувствительность, и ложным положительным значением (ЛПЗ), также известным как специфичность, для ROC-кривой, и точность и полнота для кривой точность-полнота.
Из этих четырёх метрик scikit-learn не предоставляет оценщик для ЛПЗ. Поэтому нам нужно определить небольшую пользовательскую функцию для его вычисления.
from sklearn.metrics import confusion_matrix
def fpr_score(y, y_pred, neg_label, pos_label):
cm = confusion_matrix(y, y_pred, labels=[neg_label, pos_label])
tn, fp, _, _ = cm.ravel()
tnr = tn / (tn + fp)
return 1 - tnr
Как уже упоминалось, «положительная метка» не определена как значение «1», и вызов некоторых метрик с этим нестандартным значением вызывает ошибку. Нам необходимо указать метки «положительной метки» для метрик.
Поэтому нам необходимо определить оценщик scikit-learn, используя make_scorer, где информация передается. Все пользовательские оценщики хранятся в словаре. Для их использования нам необходимо передать обученную модель, данные и целевую переменную, по которым мы хотим оценить предсказательную модель.
from sklearn.metrics import make_scorer, precision_score, recall_score
tpr_score = recall_score # TPR and recall are the same metric
scoring = {
"precision": make_scorer(precision_score, pos_label=pos_label),
"recall": make_scorer(recall_score, pos_label=pos_label),
"fpr": make_scorer(fpr_score, neg_label=neg_label, pos_label=pos_label),
"tpr": make_scorer(tpr_score, pos_label=pos_label),
}
Кроме того, в оригинальном исследовании [1] определена пользовательская бизнес-метрика. Мы называем «бизнес-метрикой» любую функцию метрики, которая стремится к количественному определению того, как предсказания (правильные или неправильные) могут повлиять на бизнес-ценность развертывания данной модели машинного обучения в определенном контексте приложения. Для нашей задачи прогнозирования кредитов авторы предоставляют пользовательскую матрицу затрат, которая кодирует, что классификация «плохого» кредита как «хорошего» в среднем в 5 раз дороже, чем обратное: для финансового учреждения менее затратно не предоставлять кредит потенциальному клиенту, который не просрочит (и, следовательно, потерять хорошего клиента, который в противном случае выплатил бы кредит и проценты), чем предоставить кредит клиенту, который просрочит.
Мы определяем функцию Python, которая взвешивает матрицу ошибок и возвращает общую стоимость.
import numpy as np
def credit_gain_score(y, y_pred, neg_label, pos_label):
cm = confusion_matrix(y, y_pred, labels=[neg_label, pos_label])
# The rows of the confusion matrix hold the counts of observed classes
# while the columns hold counts of predicted classes. Recall that here we
# consider "bad" as the positive class (second row and column).
# Scikit-learn model selection tools expect that we follow a convention
# that "higher" means "better", hence the following gain matrix assigns
# negative gains (costs) to the two kinds of prediction errors:
# - a gain of -1 for each false positive ("good" credit labeled as "bad"),
# - a gain of -5 for each false negative ("bad" credit labeled as "good"),
# The true positives and true negatives are assigned null gains in this
# metric.
#
# Note that theoretically, given that our model is calibrated and our data
# set representative and large enough, we do not need to tune the
# threshold, but can safely set it to the cost ration 1/5, as stated by Eq.
# (2) in Elkan paper [2]_.
gain_matrix = np.array(
[
[0, -1], # -1 gain for false positives
[-5, 0], # -5 gain for false negatives
]
)
return np.sum(cm * gain_matrix)
scoring["credit_gain"] = make_scorer(
credit_gain_score, neg_label=neg_label, pos_label=pos_label
)
Простая предсказательная модель
Мы используем HistGradientBoostingClassifier в качестве предсказательной модели, которая изначально обрабатывает категориальные признаки и пропущенные значения.
from sklearn.ensemble import HistGradientBoostingClassifier
model = HistGradientBoostingClassifier(
categorical_features="from_dtype", random_state=0
).fit(X_train, y_train)
model
Мы оцениваем производительность нашей предсказательной модели с помощью ROC- и Precision-Recall-кривых.
import matplotlib.pyplot as plt
from sklearn.metrics import PrecisionRecallDisplay, RocCurveDisplay
fig, axs = plt.subplots(nrows=1, ncols=2, figsize=(14, 6))
PrecisionRecallDisplay.from_estimator(
model, X_test, y_test, pos_label=pos_label, ax=axs[0], name="GBDT"
)
axs[0].plot(
scoring["recall"](model, X_test, y_test),
scoring["precision"](model, X_test, y_test),
marker="o",
markersize=10,
color="tab:blue",
label="Default cut-off point at a probability of 0.5",
)
axs[0].set_title("Precision-Recall curve")
axs[0].legend()
RocCurveDisplay.from_estimator(
model,
X_test,
y_test,
pos_label=pos_label,
ax=axs[1],
name="GBDT",
plot_chance_level=True,
)
axs[1].plot(
scoring["fpr"](model, X_test, y_test),
scoring["tpr"](model, X_test, y_test),
marker="o",
markersize=10,
color="tab:blue",
label="Default cut-off point at a probability of 0.5",
)
axs[1].set_title("ROC curve")
axs[1].legend()
_ = fig.suptitle("Evaluation of the vanilla GBDT model")
Мы помним, что эти кривые дают представление о статистической производительности предсказательной модели для различных точек отсечения. Для кривой точность-полнота сообщаемыми метриками являются точность и полнота, а для ROC-кривой – ИПЗ (то же, что и полнота) и ЛПЗ.
Здесь разные точки отсечения соответствуют разным уровням оценок вероятности, изменяющихся от 0 до 1. По умолчанию model.predict использует точку отсечения при оценке вероятности 0,5. Метрики для такой точки отсечения отображаются синей точкой на кривых: это соответствует статистической производительности модели при использовании model.predict.
Однако, мы помним, что первоначальной целью было минимизировать затраты (или максимизировать выигрыш), как определено бизнес-метрикой. Мы можем вычислить значение бизнес-метрики:
print(f"Business defined metric: {scoring['credit_gain'](model, X_test, y_test)}")
Business defined metric: -232
На данном этапе нам неизвестно, может ли любая другая точка отсечения привести к большему выигрышу. Чтобы найти оптимальную, нам нужно вычислить стоимость-выигрыш с помощью бизнес-метрики для всех возможных точек отсечения и выбрать лучшую. Эта стратегия может быть довольно трудоёмкой для ручной реализации, но класс TunedThresholdClassifierCV поможет нам. Он автоматически вычисляет стоимость-выигрыш для всех возможных точек отсечения и оптимизирует для scoring.
Настройка точки отсечения
Мы используем TunedThresholdClassifierCV для настройки точки отсечения. Нам необходимо указать оптимизируемую бизнес-метрику и положительную метку. Внутренне оптимальная точка отсечения выбирается таким образом, чтобы максимизировать бизнес-метрику с помощью перекрестной проверки. По умолчанию используется 5-кратная стратифицированная перекрестная проверка.
from sklearn.model_selection import TunedThresholdClassifierCV
tuned_model = TunedThresholdClassifierCV(
estimator=model,
scoring=scoring["credit_gain"],
store_cv_results=True, # necessary to inspect all results
)
tuned_model.fit(X_train, y_train)
print(f"{tuned_model.best_threshold_=:0.2f}")
tuned_model.best_threshold_=0.02
Мы строим ROC- и Precision-Recall-кривые для простой модели и настроенной модели. Также мы отображаем точки отсечения, которые будут использоваться каждой моделью. Поскольку мы повторно используем этот код позже, мы определим функцию для генерации графиков.
def plot_roc_pr_curves(vanilla_model, tuned_model, *, title):
fig, axs = plt.subplots(nrows=1, ncols=3, figsize=(21, 6))
linestyles = ("dashed", "dotted")
markerstyles = ("o", ">")
colors = ("tab:blue", "tab:orange")
names = ("Vanilla GBDT", "Tuned GBDT")
for idx, (est, linestyle, marker, color, name) in enumerate(
zip((vanilla_model, tuned_model), linestyles, markerstyles, colors, names)
):
decision_threshold = getattr(est, "best_threshold_", 0.5)
PrecisionRecallDisplay.from_estimator(
est,
X_test,
y_test,
pos_label=pos_label,
linestyle=linestyle,
color=color,
ax=axs[0],
name=name,
)
axs[0].plot(
scoring["recall"](est, X_test, y_test),
scoring["precision"](est, X_test, y_test),
marker,
markersize=10,
color=color,
label=f"Cut-off point at probability of {decision_threshold:.2f}",
)
RocCurveDisplay.from_estimator(
est,
X_test,
y_test,
pos_label=pos_label,
linestyle=linestyle,
color=color,
ax=axs[1],
name=name,
plot_chance_level=idx == 1,
)
axs[1].plot(
scoring["fpr"](est, X_test, y_test),
scoring["tpr"](est, X_test, y_test),
marker,
markersize=10,
color=color,
label=f"Cut-off point at probability of {decision_threshold:.2f}",
)
axs[0].set_title("Precision-Recall curve")
axs[0].legend()
axs[1].set_title("ROC curve")
axs[1].legend()
axs[2].plot(
tuned_model.cv_results_["thresholds"],
tuned_model.cv_results_["scores"],
color="tab:orange",
)
axs[2].plot(
tuned_model.best_threshold_,
tuned_model.best_score_,
"o",
markersize=10,
color="tab:orange",
label="Optimal cut-off point for the business metric",
)
axs[2].legend()
axs[2].set_xlabel("Decision threshold (probability)")
axs[2].set_ylabel("Objective score (using cost-matrix)")
axs[2].set_title("Objective score as a function of the decision threshold")
fig.suptitle(title)
title = "Comparison of the cut-off point for the vanilla and tuned GBDT model" plot_roc_pr_curves(model, tuned_model, title=title)
Первый вывод состоит в том, что у обоих классификаторов абсолютно одинаковые ROC- и Precision-Recall-кривые. Это ожидаемо, так как классификатор по умолчанию обучен на одних и тех же обучающих данных. В последующем разделе мы более подробно рассмотрим доступные варианты переобучения модели и перекрестной проверки.
Второй вывод состоит в том, что точки отсечения простой модели и настроенной модели отличаются. Чтобы понять, почему настроенная модель выбрала эту точку отсечения, можно посмотреть на график справа, который отображает целевую метрику, то есть нашу бизнес-метрику. Мы видим, что оптимальный порог соответствует максимуму целевой метрики. Этот максимум достигается при значении порога, намного меньшем, чем 0,5: настроенная модель обладает гораздо большей полнотой за счёт значительно меньшей точности: настроенная модель гораздо охотнее предсказывает метку класса «плохой» для большей части людей.
Теперь мы можем проверить, приводит ли выбор этой точки отсечения к лучшему результату на тестовом наборе:
print(f"Business defined metric: {scoring['credit_gain'](tuned_model, X_test, y_test)}")
Business defined metric: -134
Мы наблюдаем, что настройка порога принятия решения почти удваивает наш бизнес-выигрыш.
Рассмотрение переобучения модели и перекрестной проверки
В приведенном выше эксперименте мы использовали стандартные настройки TunedThresholdClassifierCV. В частности, точка отсечения настраивается с помощью 5-кратной стратифицированной перекрестной проверки. Кроме того, базовую предсказательную модель переобучают на всех данных обучения после выбора точки отсечения.
Эти две стратегии можно изменить, предоставив параметры refit и cv. Например, можно предоставить обученную модель estimator и установить cv="prefit", в этом случае точка отсечения находится на всех данных, предоставленных при обучении. Также базовую классификатор не переобучают, установив refit=False. Здесь мы можем попробовать провести такой эксперимент.
model.fit(X_train, y_train)
tuned_model.set_params(cv="prefit", refit=False).fit(X_train, y_train)
print(f"{tuned_model.best_threshold_=:0.2f}")
tuned_model.best_threshold_=0.28
Затем мы оцениваем нашу модель тем же способом, что и ранее:
title = "Tuned GBDT model without refitting and using the entire dataset" plot_roc_pr_curves(model, tuned_model, title=title)

Мы наблюдаем, что оптимальная точка отсечения отличается от той, что была найдена в предыдущем эксперименте. Если посмотреть на график справа, мы видим, что коммерческая выгода имеет большой плато близкой к оптимальной выгоды 0 для большого диапазона пороговых значений решений. Такое поведение является признаком переобучения. Поскольку мы отключили перекрестную проверку, мы настраивали точку отсечения на том же наборе данных, что и модель была обучена, и это причина наблюдаемого переобучения.
Поэтому этот вариант следует использовать с осторожностью. Необходимо убедиться, что данные, предоставленные при обучении TunedThresholdClassifierCV, не совпадают с данными, используемыми для обучения базового классификатора. Это иногда может произойти, когда цель — настроить предсказательную модель на полностью новом наборе проверки без дорогостоящей полной переобучения.
Если перекрестная проверка слишком затратна, потенциальной альтернативой является использование одного разделения на обучающую и тестовую выборки, предоставив число с плавающей точкой в диапазоне [0, 1] параметру cv. Оно разбивает данные на обучающую и тестовую выборки. Давайте исследуем этот вариант:
tuned_model.set_params(cv=0.75).fit(X_train, y_train)
title = "Tuned GBDT model without refitting and using the entire dataset" plot_roc_pr_curves(model, tuned_model, title=title)

Что касается точки отсечения, мы наблюдаем, что оптимальное значение аналогично случаю с многократной перекрестной проверкой. Однако следует помнить, что одно разделение не учитывает изменчивость процесса подбора/предсказания, и поэтому мы не можем знать, есть ли вариация в точке отсечения. Многократная перекрестная проверка усредняет этот эффект.
Другое наблюдение касается кривых ROC и точность-полнота настроенной модели. Как ожидалось, эти кривые отличаются от кривых обычной модели, поскольку мы обучили базовый классификатор на подмножестве данных, предоставленных при обучении, и зарезервировали набор проверки для настройки точки отсечения.
Чувствительное к стоимости обучение, когда выгоды и затраты не являются постоянными
Как указано в [2], выгоды и затраты в реальных задачах обычно не являются постоянными. В этом разделе мы используем аналогичный пример, как в [2], для задачи обнаружения мошенничества в данных о транзакциях по кредитным картам.
Набор данных по кредитным картам
credit_card = fetch_openml(data_id=1597, as_frame=True, parser="pandas") credit_card.frame.info()
<class 'pandas.core.frame.DataFrame'> RangeIndex: 284807 entries, 0 to 284806 Data columns (total 30 columns): # Column Non-Null Count Dtype --- ------ -------------- ----- 0 V1 284807 non-null float64 1 V2 284807 non-null float64 2 V3 284807 non-null float64 3 V4 284807 non-null float64 4 V5 284807 non-null float64 5 V6 284807 non-null float64 6 V7 284807 non-null float64 7 V8 284807 non-null float64 8 V9 284807 non-null float64 9 V10 284807 non-null float64 10 V11 284807 non-null float64 11 V12 284807 non-null float64 12 V13 284807 non-null float64 13 V14 284807 non-null float64 14 V15 284807 non-null float64 15 V16 284807 non-null float64 16 V17 284807 non-null float64 17 V18 284807 non-null float64 18 V19 284807 non-null float64 19 V20 284807 non-null float64 20 V21 284807 non-null float64 21 V22 284807 non-null float64 22 V23 284807 non-null float64 23 V24 284807 non-null float64 24 V25 284807 non-null float64 25 V26 284807 non-null float64 26 V27 284807 non-null float64 27 V28 284807 non-null float64 28 Amount 284807 non-null float64 29 Class 284807 non-null category dtypes: category(1), float64(29) memory usage: 63.3 MB
Набор данных содержит информацию о записях по кредитным картам, некоторые из которых являются мошенническими, а другие — законными. Цель состоит в том, чтобы предсказать, является ли запись по кредитной карте мошеннической или нет.
columns_to_drop = ["Class"] data = credit_card.frame.drop(columns=columns_to_drop) target = credit_card.frame["Class"].astype(int)
Сначала мы проверяем распределение классов в наборе данных.
target.value_counts(normalize=True)
Class 0 0.998273 1 0.001727 Name: proportion, dtype: float64
Набор данных сильно несбалансирован, при этом мошеннические транзакции составляют всего 0,17% данных. Поскольку мы заинтересованы в обучении модели машинного обучения, мы также должны убедиться, что в классе меньшинства достаточно образцов для обучения модели.
target.value_counts()
Class 0 284315 1 492 Name: count, dtype: int64
Мы наблюдаем, что у нас около 500 образцов, что находится на нижнем пределе числа образцов, необходимого для обучения модели машинного обучения. В дополнение к распределению целевой переменной, мы проверяем распределение суммы мошеннических транзакций.
fraud = target == 1
amount_fraud = data["Amount"][fraud]
_, ax = plt.subplots()
ax.hist(amount_fraud, bins=30)
ax.set_title("Amount of fraud transaction")
_ = ax.set_xlabel("Amount (€)")

Решение проблемы с бизнес-метрикой
Теперь мы создаем бизнес-метрику, зависящую от суммы каждой транзакции. Мы определяем матрицу затрат аналогично [2]. Принятие законной транзакции обеспечивает выгоду в 2% от суммы транзакции. Однако принятие мошеннической транзакции приводит к убыткам в размере суммы транзакции. Как указано в [2], выгоды и потери, связанные с отклонениями (мошеннических и законных транзакций), нетривиально определить. Здесь мы определяем, что отклонение законной транзакции оценивается в 5€ убытка, а отклонение мошеннической транзакции оценивается в 50€ выгоды. Таким образом, мы определяем следующую функцию для вычисления общей выгоды от данного решения:
def business_metric(y_true, y_pred, amount):
mask_true_positive = (y_true == 1) & (y_pred == 1)
mask_true_negative = (y_true == 0) & (y_pred == 0)
mask_false_positive = (y_true == 0) & (y_pred == 1)
mask_false_negative = (y_true == 1) & (y_pred == 0)
fraudulent_refuse = mask_true_positive.sum() * 50
fraudulent_accept = -amount[mask_false_negative].sum()
legitimate_refuse = mask_false_positive.sum() * -5
legitimate_accept = (amount[mask_true_negative] * 0.02).sum()
return fraudulent_refuse + fraudulent_accept + legitimate_refuse + legitimate_accept
Из этой бизнес-метрики мы создаем инструмент оценки scikit-learn, который, принимая обученный классификатор и тестовый набор, вычисляет бизнес-метрику. В этом отношении мы используем фабрику make_scorer. Переменная amount — это дополнительная метаданные, передаваемые инструменту оценки, и нам необходимо использовать маршрутизацию метаданных, чтобы учесть эту информацию.
sklearn.set_config(enable_metadata_routing=True) business_scorer = make_scorer(business_metric).set_score_request(amount=True)
Итак, на этом этапе мы видим, что сумма транзакции используется дважды: как признак для обучения нашей предсказательной модели и как метаданные для вычисления бизнес-метрики и, следовательно, статистической эффективности нашей модели. При использовании в качестве признака нам требуется только столбец в data, содержащий сумму каждой транзакции. Чтобы использовать эту информацию как метаданные, нам нужна внешняя переменная, которую мы можем передать в инструмент оценки или модель, которая внутренне передает эти метаданные в инструмент оценки. Поэтому давайте создадим эту переменную.
amount = credit_card.frame["Amount"].to_numpy()
from sklearn.model_selection import train_test_split
data_train, data_test, target_train, target_test, amount_train, amount_test = (
train_test_split(
data, target, amount, stratify=target, test_size=0.5, random_state=42
)
)
Сначала мы оценим некоторые базовые политики, которые послужат эталоном. Напоминаем, что класс «0» — это класс законных транзакций, а класс «1» — класс мошеннических транзакций.
from sklearn.dummy import DummyClassifier
always_accept_policy = DummyClassifier(strategy="constant", constant=0)
always_accept_policy.fit(data_train, target_train)
benefit = business_scorer(
always_accept_policy, data_test, target_test, amount=amount_test
)
print(f"Benefit of the 'always accept' policy: {benefit:,.2f}€")
Benefit of the 'always accept' policy: 221,445.07€
Политика, которая рассматривает все транзакции как законные, создаст прибыль около 220 000€. Мы проводим аналогичную оценку для классификатора, который предсказывает все транзакции как мошеннические.
always_reject_policy = DummyClassifier(strategy="constant", constant=1)
always_reject_policy.fit(data_train, target_train)
benefit = business_scorer(
always_reject_policy, data_test, target_test, amount=amount_test
)
print(f"Benefit of the 'always reject' policy: {benefit:,.2f}€")
Benefit of the 'always reject' policy: -698,490.00€
Такая политика повлечёт катастрофические убытки: около 670 000€. Это ожидаемо, поскольку подавляющее большинство транзакций — законные, а политика будет отклонять их с ненулевой стоимостью.
Предсказательная модель, которая адаптирует решения о принятии/отклонении для каждой транзакции, в идеале должна позволить нам получить прибыль, большую, чем 220 000€ — наилучший из наших постоянных базовых политик.
Мы начинаем с модели логистической регрессии с порогом принятия решений по умолчанию 0,5. Здесь мы настраиваем гиперпараметр C логистической регрессии с правилом оценки (логарифмическая потеря), чтобы гарантировать, что вероятностные предсказания модели, возвращаемые ее методом predict_proba, максимально точны, независимо от выбора значения порога принятия решений.
from sklearn.linear_model import LogisticRegression
from sklearn.model_selection import GridSearchCV
from sklearn.pipeline import make_pipeline
from sklearn.preprocessing import StandardScaler
logistic_regression = make_pipeline(StandardScaler(), LogisticRegression())
param_grid = {"logisticregression__C": np.logspace(-6, 6, 13)}
model = GridSearchCV(logistic_regression, param_grid, scoring="neg_log_loss").fit(
data_train, target_train
)
model
print(
"Benefit of logistic regression with default threshold: "
f"{business_scorer(model, data_test, target_test, amount=amount_test):,.2f}€"
)
Benefit of logistic regression with default threshold: 244,919.87€
Бизнес-метрика показывает, что наша предсказательная модель с порогом принятия решений по умолчанию уже превосходит базовые политики с точки зрения прибыли, и её использование для принятия или отклонения транзакций уже будет выгодным, вместо принятия всех транзакций.
Настройка порога принятия решения
Теперь вопрос в том: оптимальна ли наша модель для типа решения, которое мы хотим принять? До сих пор мы не оптимизировали порог принятия решения. Мы используем TunedThresholdClassifierCV, чтобы оптимизировать решение, учитывая наш бизнес-оценщик. Чтобы избежать вложенной кросс-валидации, мы будем использовать лучший оценочный модуль, найденный во время предыдущего поиска по сетке.
tuned_model = TunedThresholdClassifierCV(
estimator=model.best_estimator_,
scoring=business_scorer,
thresholds=100,
n_jobs=2,
)
Поскольку наш бизнес-оценщик требует суммы каждой транзакции, нам необходимо передать эту информацию в метод fit. Класс TunedThresholdClassifierCV отвечает за автоматическую передачу этих метаданных в базовый оценщик.
tuned_model.fit(data_train, target_train, amount=amount_train)
Заметим, что настроенный порог принятия решения сильно отличается от значения по умолчанию 0,5:
print(f"Tuned decision threshold: {tuned_model.best_threshold_:.2f}")
Tuned decision threshold: 0.03
print(
"Benefit of logistic regression with a tuned threshold: "
f"{business_scorer(tuned_model, data_test, target_test, amount=amount_test):,.2f}€"
)
Benefit of logistic regression with a tuned threshold: 249,433.39€
Мы наблюдаем, что настройка порога принятия решения увеличивает ожидаемую прибыль при развертывании нашей модели, как показано по бизнес-метрик. Поэтому, когда это возможно, целесообразно оптимизировать порог принятия решения относительно бизнес-метрики.
Ручное задание порога принятия решения вместо его настройки
В предыдущем примере мы использовали TunedThresholdClassifierCV для поиска оптимального порога принятия решения. Однако в некоторых случаях у нас может быть предварительное знание о рассматриваемой проблеме, и мы можем быть удовлетворены ручным заданием порога принятия решения.
Класс FixedThresholdClassifier позволяет нам вручную задать порог принятия решения. При прогнозировании он ведет себя как предыдущая настроенная модель, но поиск не выполняется во время процесса обучения. Обратите внимание, что здесь мы используем FrozenEstimator, чтобы обернуть предсказательную модель, чтобы избежать повторного обучения.
Здесь мы повторно используем порог принятия решения, найденный в предыдущем разделе, чтобы создать новую модель и проверить, что она дает те же результаты.
from sklearn.frozen import FrozenEstimator
from sklearn.model_selection import FixedThresholdClassifier
model_fixed_threshold = FixedThresholdClassifier(
estimator=FrozenEstimator(model), threshold=tuned_model.best_threshold_
)
business_score = business_scorer(
model_fixed_threshold, data_test, target_test, amount=amount_test
)
print(f"Benefit of logistic regression with a tuned threshold: {business_score:,.2f}€")
Benefit of logistic regression with a tuned threshold: 249,433.39€
Мы наблюдаем, что получили точно такие же результаты, но процесс обучения был намного быстрее, так как мы не выполняли поиск по гиперпараметрам.
Наконец, оценка (средней) бизнес-метрики сама по себе может быть ненадежной, особенно когда количество точек данных в классе меньшинства очень мало. Любой бизнес-влияние, оцениваемый с помощью кросс-валидации бизнес-метрики на исторических данных (офлайн-оценка), в идеале должен быть подтвержден A/B-тестированием на реальных данных (онлайн-оценка). Тем не менее, моделирование A/B-тестирования выходит за рамки самой библиотеки scikit-learn.
Общее время выполнения скрипта: (0 минут 31.200 секунд)
Похожие примеры
© 2007–2025 The scikit-learn developers
Licensed under the 3-clause BSD License.
https://scikit-learn.org/1.6/auto_examples/model_selection/plot_cost_sensitive_learning.html