注意
转到末尾 下载完整的示例代码。或通过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曲线的真阳性率(TPR)(也称为召回率或灵敏度)和假阳性率(FPR)(也称为特异性),以及精确率-召回率曲线的精确率和召回率。
在这四个指标中,scikit-learn没有提供FPR的评分器。因此,我们需要定义一个小的自定义函数来计算它。
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”,并且使用此非标准值调用某些指标会引发错误。我们需要向指标提供“正标签”的指示。
因此,我们需要使用make_scorer
定义一个scikit-learn评分器,其中传递了信息。我们将所有自定义评分器存储在一个字典中。要使用它们,我们需要传递拟合的模型、数据和我们想要评估预测模型的目标。
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曲线和精确率-召回率曲线来评估预测模型的性能。
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曲线,报告的指标是TPR(与召回率相同)和FPR。
这里,不同的截断点对应于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曲线和精确率-召回率曲线。我们还绘制了每个模型将使用的截断点。因为我们稍后会重用相同的代码,所以我们定义一个生成图表的函数。
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曲线和精确率-召回率曲线完全相同。这是预期的,因为默认情况下,分类器是在相同的训练数据上拟合的。在后面的部分中,我们将更详细地讨论关于模型重拟合和交叉验证的可用选项。
第二个结论是,基础模型和调整模型的截断点不同。为了理解为什么调整后的模型选择了这个截断点,我们可以查看右侧的图表,该图表绘制了目标分数,这与我们的业务指标完全相同。我们可以看到,最佳阈值对应于目标分数的最大值。这个最大值是在远低于0.5的决策阈值下达到的:调整后的模型在显著降低精确率的代价下获得了更高的召回率:调整后的模型更倾向于将“坏”类标签预测给更大比例的个体。
我们现在可以检查选择此截断点是否会导致测试集上的分数更好
print(f"Business defined metric: {scoring['credit_gain'](tuned_model, X_test, y_test)}")
Business defined metric: -134
我们观察到,调整决策阈值几乎使我们的业务收益提高了2倍。
关于模型重拟合和交叉验证的考虑#
在上一个实验中,我们使用了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
的数据与用于训练底层分类器的數據不同。当想法只是在全新的验证集上调整预测模型而无需代价高昂的完整重拟合时,有时可能会发生这种情况。
当交叉验证代价过高时,一个可行的替代方案是使用单次训练-测试分割。通过向cv
参数提供[0, 1]
范围内的浮点数来实现数据分割,将其分成训练集和测试集。让我们探索一下这个选项。
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 分钟 28.508 秒)
相关示例