Spec-Zone.ru › scikit-learn

Note

Go to the end to download the full example code or to run this example in your browser via JupyterLite or Binder.

Post-tuning the decision threshold for cost-sensitive learning

Once a classifier is trained, the output of the predict method outputs class label predictions corresponding to a thresholding of either the decision_function or the predict_proba output. For a binary classifier, the default threshold is defined as a posterior probability estimate of 0.5 or a decision score of 0.0.

However, this default strategy is most likely not optimal for the task at hand. Here, we use the “Statlog” German credit dataset [1] to illustrate a use case. In this dataset, the task is to predict whether a person has a “good” or “bad” credit. In addition, a cost-matrix is provided that specifies the cost of misclassification. Specifically, misclassifying a “bad” credit as “good” is five times more costly on average than misclassifying a “good” credit as “bad”.

We use the TunedThresholdClassifierCV to select the cut-off point of the decision function that minimizes the provided business cost.

In the second part of the example, we further extend this approach by considering the problem of fraud detection in credit card transactions: in this case, the business metric depends on the amount of each individual transaction.

References

[1] (1,2,3)

“Statlog (German Credit Data) Data Set”, UCI Machine Learning Repository, Link.

[2] (1,2,3,4,5,6)

Charles Elkan, “The Foundations of Cost-Sensitive Learning”, International joint conference on artificial intelligence. Vol. 17. No. 1. Lawrence Erlbaum Associates Ltd, 2001.

# Authors: The scikit-learn developers
# SPDX-License-Identifier: BSD-3-Clause

Cost-sensitive learning with constant gains and costs

In this first section, we illustrate the use of the TunedThresholdClassifierCV in a setting of cost-sensitive learning when the gains and costs associated to each entry of the confusion matrix are constant. We use the problematic presented in [2] using the “Statlog” German credit dataset [1].

“Statlog” German credit dataset

We fetch the German credit dataset from 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

We check the feature types available in X.

X.info()
<class 'pandas.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: 68.9 KB

Many features are categorical and usually string-encoded. We need to encode these categories when we develop our predictive model. Let’s check the targets.

y.value_counts()
class
good    700
bad     300
Name: count, dtype: int64

Another observation is that the dataset is imbalanced. We would need to be careful when evaluating our predictive model and use a family of metrics that are adapted to this setting.

In addition, we observe that the target is string-encoded. Some metrics (e.g. precision and recall) require to provide the label of interest also called the “positive label”. Here, we define that our goal is to predict whether or not a sample is a “bad” credit.

pos_label, neg_label = "bad", "good"

To carry our analysis, we split our dataset using a single stratified split.

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)

We are ready to design our predictive model and the associated evaluation strategy.

Evaluation metrics

In this section, we define a set of metrics that we use later. To see the effect of tuning the cut-off point, we evaluate the predictive model using the Receiver Operating Characteristic (ROC) curve and the Precision-Recall curve. The values reported on these plots are therefore the true positive rate (TPR), also known as the recall or the sensitivity, and the false positive rate (FPR), also known as the specificity, for the ROC curve and the precision and recall for the Precision-Recall curve.

From these four metrics, scikit-learn does not provide a scorer for the FPR. We therefore need to define a small custom function to compute it.

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

As previously stated, the “positive label” is not defined as the value “1” and calling some of the metrics with this non-standard value raise an error. We need to provide the indication of the “positive label” to the metrics.

We therefore need to define a scikit-learn scorer using make_scorer where the information is passed. We store all the custom scorers in a dictionary. To use them, we need to pass the fitted model, the data and the target on which we want to evaluate the predictive model.

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),
}

In addition, the original research [1] defines a custom business metric. We call a “business metric” any metric function that aims at quantifying how the predictions (correct or wrong) might impact the business value of deploying a given machine learning model in a specific application context. For our credit prediction task, the authors provide a custom cost-matrix which encodes that classifying a “bad” credit as “good” is 5 times more costly on average than the opposite: it is less costly for the financing institution to not grant a credit to a potential customer that will not default (and therefore miss a good customer that would have otherwise both reimbursed the credit and paid interests) than to grant a credit to a customer that will default.

We define a python function that weighs the confusion matrix and returns the overall cost. 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”),
  • a 0 gain for true positives and true negatives.

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 1/5 of the cost ratio, as stated by Eq. (2) in Elkan’s paper [2].

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])

    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
)

Vanilla predictive model

We use HistGradientBoostingClassifier as a predictive model that natively handles categorical features and missing values.

from sklearn.ensemble import HistGradientBoostingClassifier

model = HistGradientBoostingClassifier(
    categorical_features="from_dtype", random_state=0
).fit(X_train, y_train)
model
HistGradientBoostingClassifier(random_state=0)
In a Jupyter environment, please rerun this cell to show the HTML representation or trust the notebook.
On GitHub, the HTML representation is unable to render, please try loading this page with nbviewer.org.
Parameters
random_state random_state: int, RandomState instance or None, default=None

Pseudo-random number generator to control the subsampling in the
binning process, and the train/validation data split if early stopping
is enabled.
Pass an int for reproducible output across multiple function calls.
See :term:`Glossary <random_state>`.
0
loss loss: {'log_loss'}, default='log_loss'

The loss function to use in the boosting process.

For binary classification problems, 'log_loss' is also known as logistic loss,
binomial deviance or binary crossentropy. Internally, the model fits one tree
per boosting iteration and uses the logistic sigmoid function (expit) as
inverse link function to compute the predicted positive class probability.

For multiclass classification problems, 'log_loss' is also known as multinomial
deviance or categorical crossentropy. Internally, the model fits one tree per
boosting iteration and per class and uses the softmax function as inverse link
function to compute the predicted probabilities of the classes.
'log_loss'
learning_rate learning_rate: float, default=0.1

The learning rate, also known as *shrinkage*. This is used as a
multiplicative factor for the leaves values. Use ``1`` for no
shrinkage.
0.1
max_iter max_iter: int, default=100

The maximum number of iterations of the boosting process, i.e. the
maximum number of trees for binary classification. For multiclass
classification, `n_classes` trees per iteration are built.
100
max_leaf_nodes max_leaf_nodes: int or None, default=31

The maximum number of leaves for each tree. Must be strictly greater
than 1. If None, there is no maximum limit.
31
max_depth max_depth: int or None, default=None

The maximum depth of each tree. The depth of a tree is the number of
edges to go from the root to the deepest leaf.
Depth isn't constrained by default.
None
min_samples_leaf min_samples_leaf: int, default=20

The minimum number of samples per leaf. For small datasets with less
than a few hundred samples, it is recommended to lower this value
since only very shallow trees would be built.
20
l2_regularization l2_regularization: float, default=0

The L2 regularization parameter penalizing leaves with small hessians.
Use ``0`` for no regularization (default).
0.0
max_features max_features: float, default=1.0

Proportion of randomly chosen features in each and every node split.
This is a form of regularization, smaller values make the trees weaker
learners and might prevent overfitting.
If interaction constraints from `interaction_cst` are present, only allowed
features are taken into account for the subsampling.

.. versionadded:: 1.4
1.0
max_bins max_bins: int, default=255

The maximum number of bins to use for non-missing values. Before
training, each feature of the input array `X` is binned into
integer-valued bins, which allows for a much faster training stage.
Features with a small number of unique values may use less than
``max_bins`` bins. In addition to the ``max_bins`` bins, one more bin
is always reserved for missing values. Must be no larger than 255.
255
categorical_features categorical_features: array-like of {bool, int, str} of shape (n_features) or shape (n_categorical_features,), default='from_dtype'

Indicates the categorical features.

- None : no feature will be considered categorical.
- boolean array-like : boolean mask indicating categorical features.
- integer array-like : integer indices indicating categorical
features.
- str array-like: names of categorical features (assuming the training
data has feature names).
- `"from_dtype"`: dataframe columns with dtype "Categorical" and "Enum" are
considered to be categorical features. The input must be a dataframe that
is supported by narwhals (or supports it): :func:`narwhals.from_native` must
work. This is the case, for instance, for pandas and polars DataFrames.

For each categorical feature, there must be at most `max_bins` unique
categories. Negative values for categorical features encoded as numeric
dtypes are treated as missing values. All categorical values are
converted to floating point numbers. This means that categorical values
of 1.0 and 1 are treated as the same category.

Read more in the :ref:`User Guide <categorical_support_gbdt>`.

.. versionadded:: 0.24

.. versionchanged:: 1.2
Added support for feature names.

.. versionchanged:: 1.4
Added `"from_dtype"` option.

.. versionchanged:: 1.6
The default value changed from `None` to `"from_dtype"`.
'from_dtype'
monotonic_cst monotonic_cst: array-like of int of shape (n_features) or dict, default=None

Monotonic constraint to enforce on each feature are specified using the
following integer values:

- 1: monotonic increase
- 0: no constraint
- -1: monotonic decrease

If a dict with str keys, map feature to monotonic constraints by name.
If an array, the features are mapped to constraints by position. See
:ref:`monotonic_cst_features_names` for a usage example.

The constraints are only valid for binary classifications and hold
over the probability of the positive class.
Read more in the :ref:`User Guide <monotonic_cst_gbdt>`.

.. versionadded:: 0.23

.. versionchanged:: 1.2
Accept dict of constraints with feature names as keys.
None
interaction_cst interaction_cst: {"pairwise", "no_interactions"} or sequence of lists/tuples/sets of int, default=None

Specify interaction constraints, the sets of features which can
interact with each other in child node splits.

Each item specifies the set of feature indices that are allowed
to interact with each other. If there are more features than
specified in these constraints, they are treated as if they were
specified as an additional set.

The strings "pairwise" and "no_interactions" are shorthands for
allowing only pairwise or no interactions, respectively.

For instance, with 5 features in total, `interaction_cst=[{0, 1}]`
is equivalent to `interaction_cst=[{0, 1}, {2, 3, 4}]`,
and specifies that each branch of a tree will either only split
on features 0 and 1 or only split on features 2, 3 and 4.

See :ref:`this example<ice-vs-pdp>` on how to use `interaction_cst`.

.. versionadded:: 1.2
None
warm_start warm_start: bool, default=False

When set to ``True``, reuse the solution of the previous call to fit
and add more estimators to the ensemble. For results to be valid, the
estimator should be re-trained on the same data only.
See :term:`the Glossary <warm_start>`.
False
early_stopping early_stopping: 'auto' or bool, default='auto'

If 'auto', early stopping is enabled if the sample size is larger than
10000 or if `X_val` and `y_val` are passed to `fit`. If True, early stopping
is enabled, otherwise early stopping is disabled.

.. versionadded:: 0.23
'auto'
scoring scoring: str or callable or None, default='loss'

Scoring method to use for early stopping. Only used if `early_stopping`
is enabled. Options:

- str: see :ref:`scoring_string_names` for options.
- callable: a scorer callable object (e.g., function) with signature
``scorer(estimator, X, y)``. See :ref:`scoring_callable` for details.
- `None`: :ref:`accuracy <accuracy_score>` is used.
- 'loss': early stopping is checked w.r.t the loss value.
'loss'
validation_fraction validation_fraction: int or float or None, default=0.1

Proportion (or absolute size) of training data to set aside as
validation data for early stopping. If None, early stopping is done on
the training data.
The value is ignored if either early stopping is not performed, e.g.
`early_stopping=False`, or if `X_val` and `y_val` are passed to fit.
0.1
n_iter_no_change n_iter_no_change: int, default=10

Used to determine when to "early stop". The fitting process is
stopped when none of the last ``n_iter_no_change`` scores are better
than the ``n_iter_no_change - 1`` -th-to-last one, up to some
tolerance. Only used if early stopping is performed.
10
tol tol: float, default=1e-7

The absolute tolerance to use when comparing scores. The higher the
tolerance, the more likely we are to early stop: higher tolerance
means that it will be harder for subsequent iterations to be
considered an improvement upon the reference score.
1e-07
verbose verbose: int, default=0

The verbosity level. If not zero, print some information about the
fitting process. ``1`` prints only summary info, ``2`` prints info per
iteration.
0
class_weight class_weight: dict or 'balanced', default=None

Weights associated with classes in the form `{class_label: weight}`.
If not given, all classes are supposed to have weight one.
The "balanced" mode uses the values of y to automatically adjust
weights inversely proportional to class frequencies in the input data
as `n_samples / (n_classes * np.bincount(y))`.
Note that these weights will be multiplied with sample_weight (passed
through the fit method) if `sample_weight` is specified.

.. versionadded:: 1.2
None
Fitted attributes
Name Type Value
classes_ classes_: array, shape = (n_classes,)

Class labels.
ndarray[object](2,) ['bad','good']
do_early_stopping_ do_early_stopping_: bool

Indicates whether early stopping is used during training.
bool False
feature_names_in_ feature_names_in_: ndarray of shape (`n_features_in_`,)

Names of features seen during :term:`fit`. Defined only when `X`
has feature names that are all strings.

.. versionadded:: 1.0
ndarray[object](20,) ['checking_status','duration','credit_history',...,'num_dependents', 'own_telephone','foreign_worker']
is_categorical_ is_categorical_: ndarray, shape (n_features, ) or None

Boolean mask for the categorical features. ``None`` if there are no
categorical features.
ndarray[bool](20,) [ True,False, True,...,False, True, True]
n_features_in_ n_features_in_: int

Number of features seen during :term:`fit`.

.. versionadded:: 0.24
int 20
n_iter_ n_iter_: int

The number of iterations as selected by early stopping, depending on
the `early_stopping` parameter. Otherwise it corresponds to max_iter.
int 100
n_trees_per_iteration_ n_trees_per_iteration_: int

The number of tree that are built at each iteration. This is equal to 1
for binary classification, and to ``n_classes`` for multiclass
classification.
int 1
train_score_ train_score_: ndarray, shape (n_iter_+1,)

The scores at each iteration on the training data. The first entry
is the score of the ensemble before the first iteration. Scores are
computed according to the ``scoring`` parameter. If ``scoring`` is
not 'loss', scores are computed on a subset of at most 10 000
samples. Empty if no early stopping.
ndarray[float64](0,) []
validation_score_ validation_score_: ndarray, shape (n_iter_+1,)

The scores at each iteration on the held-out validation data. The
first entry is the score of the ensemble before the first iteration.
Scores are computed according to the ``scoring`` parameter. Empty if
no early stopping or if ``validation_fraction`` is None.
ndarray[float64](0,) []


We evaluate the performance of our predictive model using the ROC and Precision-Recall curves.

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")
Evaluation of the vanilla GBDT model, Precision-Recall curve, ROC curve

We recall that these curves give insights on the statistical performance of the predictive model for different cut-off points. For the Precision-Recall curve, the reported metrics are the precision and recall and for the ROC curve, the reported metrics are the TPR (same as recall) and FPR.

Here, the different cut-off points correspond to different levels of posterior probability estimates ranging between 0 and 1. By default, model.predict uses a cut-off point at a probability estimate of 0.5. The metrics for such a cut-off point are reported with the blue dot on the curves: it corresponds to the statistical performance of the model when using model.predict.

However, we recall that the original aim was to minimize the cost (or maximize the gain) as defined by the business metric. We can compute the value of the business metric:

print(f"Business defined metric: {scoring['credit_gain'](model, X_test, y_test)}")
Business defined metric: -209

At this stage we don’t know if any other cut-off can lead to a greater gain. To find the optimal one, we need to compute the cost-gain using the business metric for all possible cut-off points and choose the best. This strategy can be quite tedious to implement by hand, but the TunedThresholdClassifierCV class is here to help us. It automatically computes the cost-gain for all possible cut-off points and optimizes for the scoring.

Tuning the cut-off point

We use TunedThresholdClassifierCV to tune the cut-off point. We need to provide the business metric to optimize as well as the positive label. Internally, the optimum cut-off point is chosen such that it maximizes the business metric via cross-validation. By default a 5-fold stratified cross-validation is used.

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.03

We plot the ROC and Precision-Recall curves for the vanilla model and the tuned model. Also we plot the cut-off points that would be used by each model. Because, we are reusing the same code later, we define a function that generates the plots.

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,
            ax=axs[0],
            name=name,
            curve_kwargs={"linestyle": linestyle, "color": color},
        )
        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,
            curve_kwargs={"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)
Comparison of the cut-off point for the vanilla and tuned GBDT model, Precision-Recall curve, ROC curve, Objective score as a function of the decision threshold

The first remark is that both classifiers have exactly the same ROC and Precision-Recall curves. It is expected because by default, the classifier is fitted on the same training data. In a later section, we discuss more in detail the available options regarding model refitting and cross-validation.

The second remark is that the cut-off points of the vanilla and tuned model are different. To understand why the tuned model has chosen this cut-off point, we can look at the right-hand side plot that plots the objective score that is our exactly the same as our business metric. We see that the optimum threshold corresponds to the maximum of the objective score. This maximum is reached for a decision threshold much lower than 0.5: the tuned model enjoys a much higher recall at the cost of of significantly lower precision: the tuned model is much more eager to predict the “bad” class label to larger fraction of individuals.

We can now check if choosing this cut-off point leads to a better score on the testing set:

print(f"Business defined metric: {scoring['credit_gain'](tuned_model, X_test, y_test)}")
Business defined metric: -143

We observe that tuning the decision threshold almost improves our business gains by factor of 2.

Consideration regarding model refitting and cross-validation

In the above experiment, we used the default setting of the TunedThresholdClassifierCV. In particular, the cut-off point is tuned using a 5-fold stratified cross-validation. Also, the underlying predictive model is refitted on the entire training data once the cut-off point is chosen.

These two strategies can be changed by providing the refit and cv parameters. For instance, one could provide a fitted estimator and set cv="prefit", in which case the cut-off point is found on the entire dataset provided at fitting time. Also, the underlying classifier is not be refitted by setting refit=False. Here, we can try to do such experiment.

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.25

Then, we evaluate our model with the same approach as before:

title = "Tuned GBDT model without refitting and using the entire dataset"
plot_roc_pr_curves(model, tuned_model, title=title)
Tuned GBDT model without refitting and using the entire dataset, Precision-Recall curve, ROC curve, Objective score as a function of the decision threshold

We observe the that the optimum cut-off point is different from the one found in the previous experiment. If we look at the right-hand side plot, we observe that the business gain has large plateau of near-optimal 0 gain for a large span of decision thresholds. This behavior is symptomatic of an overfitting. Because we disable cross-validation, we tuned the cut-off point on the same set as the model was trained on, and this is the reason for the observed overfitting.

This option should therefore be used with caution. One needs to make sure that the data provided at fitting time to the TunedThresholdClassifierCV is not the same as the data used to train the underlying classifier. This could happen sometimes when the idea is just to tune the predictive model on a completely new validation set without a costly complete refit.

When cross-validation is too costly, a potential alternative is to use a single train-test split by providing a floating number in range [0, 1] to the cv parameter. It splits the data into a training and testing set. Let’s explore this option:

tuned_model.set_params(cv=0.75).fit(X_train, y_train)
TunedThresholdClassifierCV(cv=0.75,
                           estimator=HistGradientBoostingClassifier(random_state=0),
                           refit=False,
                           scoring=make_scorer(credit_gain_score, response_method='predict', neg_label=good, pos_label=bad),
                           store_cv_results=True)
In a Jupyter environment, please rerun this cell to show the HTML representation or trust the notebook.
On GitHub, the HTML representation is unable to render, please try loading this page with nbviewer.org.
Parameters
estimator estimator: estimator instance

The classifier, fitted or not, for which we want to optimize
the decision threshold used during `predict`.
HistGradientB...andom_state=0)
scoring scoring: str or callable, default="balanced_accuracy"

The objective metric to be optimized. Can be one of:

- str: string associated to a scoring function for binary classification,
see :ref:`scoring_string_names` for options.
- callable: a scorer callable object (e.g., function) with signature
``scorer(estimator, X, y)``. See :ref:`scoring_callable` for details.
make_scorer(c...pos_label=bad)
cv cv: int, float, cross-validation generator, iterable or "prefit", default=None

Determines the cross-validation splitting strategy to train classifier.
Possible inputs for cv are:

* `None`, to use the default 5-fold stratified K-fold cross validation;
* An integer number, to specify the number of folds in a stratified k-fold;
* A float number, to specify a single shuffle split. The floating number should
be in (0, 1) and represent the size of the validation set;
* An object to be used as a cross-validation generator;
* An iterable yielding train, test splits;
* `"prefit"`, to bypass the cross-validation.

Refer :ref:`User Guide <cross_validation>` for the various
cross-validation strategies that can be used here.

.. warning::
Using `cv="prefit"` and passing the same dataset for fitting `estimator`
and tuning the cut-off point is subject to undesired overfitting. You can
refer to :ref:`TunedThresholdClassifierCV_no_cv` for an example.

This option should only be used when the set used to fit `estimator` is
different from the one used to tune the cut-off point (by calling
:meth:`TunedThresholdClassifierCV.fit`).
0.75
refit refit: bool, default=True

Whether or not to refit the classifier on the entire training set once
the decision threshold has been found.
Note that forcing `refit=False` on cross-validation having more
than a single split will raise an error. Similarly, `refit=True` in
conjunction with `cv="prefit"` will raise an error.
False
store_cv_results store_cv_results: bool, default=False

Whether to store all scores and thresholds computed during the cross-validation
process.
True
response_method response_method: {"auto", "decision_function", "predict_proba"}, default="auto"

Methods by the classifier `estimator` corresponding to the
decision function for which we want to find a threshold. It can be:

* if `"auto"`, it will try to invoke, for each classifier,
`"predict_proba"` or `"decision_function"` in that order.
* otherwise, one of `"predict_proba"` or `"decision_function"`.
If the method is not implemented by the classifier, it will raise an
error.
'auto'
thresholds thresholds: int or array-like, default=100

The number of decision threshold to use when discretizing the output of the
classifier `method`. Pass an array-like to manually specify the thresholds
to use.
100
n_jobs n_jobs: int, default=None

The number of jobs to run in parallel. When `cv` represents a
cross-validation strategy, the fitting and scoring on each data split
is done in parallel. ``None`` means 1 unless in a
:obj:`joblib.parallel_backend` context. ``-1`` means using all
processors. See :term:`Glossary <n_jobs>` for more details.
None
random_state random_state: int, RandomState instance or None, default=None

Controls the randomness of cross-validation when `cv` is a float.
See :term:`Glossary <random_state>`.
None
Fitted attributes
Name Type Value
best_score_ best_score_: float or None

The optimal score of the objective metric, evaluated at `best_threshold_`.
float64 -336
best_threshold_ best_threshold_: float

The new decision threshold.
float64 0.01994
classes_ classes_: ndarray of shape (n_classes,)

The class labels.
ndarray[object](2,) ['bad','good']
cv_results_ cv_results_: dict or None

A dictionary containing the scores and thresholds computed during the
cross-validation process. Only exist if `store_cv_results=True`. The
keys are `"thresholds"` and `"scores"`.
dict {'scores': array([-394.,... -840.]), 'th...ds': array([1.40e-...01, 9.80e-01])}
estimator_ estimator_: estimator instance

The fitted classifier used when predicting.
HistGradientBoostingClassifier HistGradientB...andom_state=0)
feature_names_in_ feature_names_in_: ndarray of shape (`n_features_in_`,)

Names of features seen during :term:`fit`. Only defined if the
underlying estimator exposes such an attribute when fit.
ndarray[object](20,) ['checking_status','duration','credit_history',...,'num_dependents', 'own_telephone','foreign_worker']
n_features_in_ n_features_in_: int

Number of features seen during :term:`fit`. Only defined if the
underlying estimator exposes such an attribute when fit.
int 20
Parameters
random_state random_state: int, RandomState instance or None, default=None

Pseudo-random number generator to control the subsampling in the
binning process, and the train/validation data split if early stopping
is enabled.
Pass an int for reproducible output across multiple function calls.
See :term:`Glossary <random_state>`.
0
loss loss: {'log_loss'}, default='log_loss'

The loss function to use in the boosting process.

For binary classification problems, 'log_loss' is also known as logistic loss,
binomial deviance or binary crossentropy. Internally, the model fits one tree
per boosting iteration and uses the logistic sigmoid function (expit) as
inverse link function to compute the predicted positive class probability.

For multiclass classification problems, 'log_loss' is also known as multinomial
deviance or categorical crossentropy. Internally, the model fits one tree per
boosting iteration and per class and uses the softmax function as inverse link
function to compute the predicted probabilities of the classes.
'log_loss'
learning_rate learning_rate: float, default=0.1

The learning rate, also known as *shrinkage*. This is used as a
multiplicative factor for the leaves values. Use ``1`` for no
shrinkage.
0.1
max_iter max_iter: int, default=100

The maximum number of iterations of the boosting process, i.e. the
maximum number of trees for binary classification. For multiclass
classification, `n_classes` trees per iteration are built.
100
max_leaf_nodes max_leaf_nodes: int or None, default=31

The maximum number of leaves for each tree. Must be strictly greater
than 1. If None, there is no maximum limit.
31
max_depth max_depth: int or None, default=None

The maximum depth of each tree. The depth of a tree is the number of
edges to go from the root to the deepest leaf.
Depth isn't constrained by default.
None
min_samples_leaf min_samples_leaf: int, default=20

The minimum number of samples per leaf. For small datasets with less
than a few hundred samples, it is recommended to lower this value
since only very shallow trees would be built.
20
l2_regularization l2_regularization: float, default=0

The L2 regularization parameter penalizing leaves with small hessians.
Use ``0`` for no regularization (default).
0.0
max_features max_features: float, default=1.0

Proportion of randomly chosen features in each and every node split.
This is a form of regularization, smaller values make the trees weaker
learners and might prevent overfitting.
If interaction constraints from `interaction_cst` are present, only allowed
features are taken into account for the subsampling.

.. versionadded:: 1.4
1.0
max_bins max_bins: int, default=255

The maximum number of bins to use for non-missing values. Before
training, each feature of the input array `X` is binned into
integer-valued bins, which allows for a much faster training stage.
Features with a small number of unique values may use less than
``max_bins`` bins. In addition to the ``max_bins`` bins, one more bin
is always reserved for missing values. Must be no larger than 255.
255
categorical_features categorical_features: array-like of {bool, int, str} of shape (n_features) or shape (n_categorical_features,), default='from_dtype'

Indicates the categorical features.

- None : no feature will be considered categorical.
- boolean array-like : boolean mask indicating categorical features.
- integer array-like : integer indices indicating categorical
features.
- str array-like: names of categorical features (assuming the training
data has feature names).
- `"from_dtype"`: dataframe columns with dtype "Categorical" and "Enum" are
considered to be categorical features. The input must be a dataframe that
is supported by narwhals (or supports it): :func:`narwhals.from_native` must
work. This is the case, for instance, for pandas and polars DataFrames.

For each categorical feature, there must be at most `max_bins` unique
categories. Negative values for categorical features encoded as numeric
dtypes are treated as missing values. All categorical values are
converted to floating point numbers. This means that categorical values
of 1.0 and 1 are treated as the same category.

Read more in the :ref:`User Guide <categorical_support_gbdt>`.

.. versionadded:: 0.24

.. versionchanged:: 1.2
Added support for feature names.

.. versionchanged:: 1.4
Added `"from_dtype"` option.

.. versionchanged:: 1.6
The default value changed from `None` to `"from_dtype"`.
'from_dtype'
monotonic_cst monotonic_cst: array-like of int of shape (n_features) or dict, default=None

Monotonic constraint to enforce on each feature are specified using the
following integer values:

- 1: monotonic increase
- 0: no constraint
- -1: monotonic decrease

If a dict with str keys, map feature to monotonic constraints by name.
If an array, the features are mapped to constraints by position. See
:ref:`monotonic_cst_features_names` for a usage example.

The constraints are only valid for binary classifications and hold
over the probability of the positive class.
Read more in the :ref:`User Guide <monotonic_cst_gbdt>`.

.. versionadded:: 0.23

.. versionchanged:: 1.2
Accept dict of constraints with feature names as keys.
None
interaction_cst interaction_cst: {"pairwise", "no_interactions"} or sequence of lists/tuples/sets of int, default=None

Specify interaction constraints, the sets of features which can
interact with each other in child node splits.

Each item specifies the set of feature indices that are allowed
to interact with each other. If there are more features than
specified in these constraints, they are treated as if they were
specified as an additional set.

The strings "pairwise" and "no_interactions" are shorthands for
allowing only pairwise or no interactions, respectively.

For instance, with 5 features in total, `interaction_cst=[{0, 1}]`
is equivalent to `interaction_cst=[{0, 1}, {2, 3, 4}]`,
and specifies that each branch of a tree will either only split
on features 0 and 1 or only split on features 2, 3 and 4.

See :ref:`this example<ice-vs-pdp>` on how to use `interaction_cst`.

.. versionadded:: 1.2
None
warm_start warm_start: bool, default=False

When set to ``True``, reuse the solution of the previous call to fit
and add more estimators to the ensemble. For results to be valid, the
estimator should be re-trained on the same data only.
See :term:`the Glossary <warm_start>`.
False
early_stopping early_stopping: 'auto' or bool, default='auto'

If 'auto', early stopping is enabled if the sample size is larger than
10000 or if `X_val` and `y_val` are passed to `fit`. If True, early stopping
is enabled, otherwise early stopping is disabled.

.. versionadded:: 0.23
'auto'
scoring scoring: str or callable or None, default='loss'

Scoring method to use for early stopping. Only used if `early_stopping`
is enabled. Options:

- str: see :ref:`scoring_string_names` for options.
- callable: a scorer callable object (e.g., function) with signature
``scorer(estimator, X, y)``. See :ref:`scoring_callable` for details.
- `None`: :ref:`accuracy <accuracy_score>` is used.
- 'loss': early stopping is checked w.r.t the loss value.
'loss'
validation_fraction validation_fraction: int or float or None, default=0.1

Proportion (or absolute size) of training data to set aside as
validation data for early stopping. If None, early stopping is done on
the training data.
The value is ignored if either early stopping is not performed, e.g.
`early_stopping=False`, or if `X_val` and `y_val` are passed to fit.
0.1
n_iter_no_change n_iter_no_change: int, default=10

Used to determine when to "early stop". The fitting process is
stopped when none of the last ``n_iter_no_change`` scores are better
than the ``n_iter_no_change - 1`` -th-to-last one, up to some
tolerance. Only used if early stopping is performed.
10
tol tol: float, default=1e-7

The absolute tolerance to use when comparing scores. The higher the
tolerance, the more likely we are to early stop: higher tolerance
means that it will be harder for subsequent iterations to be
considered an improvement upon the reference score.
1e-07
verbose verbose: int, default=0

The verbosity level. If not zero, print some information about the
fitting process. ``1`` prints only summary info, ``2`` prints info per
iteration.
0
class_weight class_weight: dict or 'balanced', default=None

Weights associated with classes in the form `{class_label: weight}`.
If not given, all classes are supposed to have weight one.
The "balanced" mode uses the values of y to automatically adjust
weights inversely proportional to class frequencies in the input data
as `n_samples / (n_classes * np.bincount(y))`.
Note that these weights will be multiplied with sample_weight (passed
through the fit method) if `sample_weight` is specified.

.. versionadded:: 1.2
None
Fitted attributes
Name Type Value
classes_ classes_: array, shape = (n_classes,)

Class labels.
ndarray[object](2,) ['bad','good']
do_early_stopping_ do_early_stopping_: bool

Indicates whether early stopping is used during training.
bool False
feature_names_in_ feature_names_in_: ndarray of shape (`n_features_in_`,)

Names of features seen during :term:`fit`. Defined only when `X`
has feature names that are all strings.

.. versionadded:: 1.0
ndarray[object](20,) ['checking_status','duration','credit_history',...,'num_dependents', 'own_telephone','foreign_worker']
is_categorical_ is_categorical_: ndarray, shape (n_features, ) or None

Boolean mask for the categorical features. ``None`` if there are no
categorical features.
ndarray[bool](20,) [ True,False, True,...,False, True, True]
n_features_in_ n_features_in_: int

Number of features seen during :term:`fit`.

.. versionadded:: 0.24
int 20
n_iter_ n_iter_: int

The number of iterations as selected by early stopping, depending on
the `early_stopping` parameter. Otherwise it corresponds to max_iter.
int 100
n_trees_per_iteration_ n_trees_per_iteration_: int

The number of tree that are built at each iteration. This is equal to 1
for binary classification, and to ``n_classes`` for multiclass
classification.
int 1
train_score_ train_score_: ndarray, shape (n_iter_+1,)

The scores at each iteration on the training data. The first entry
is the score of the ensemble before the first iteration. Scores are
computed according to the ``scoring`` parameter. If ``scoring`` is
not 'loss', scores are computed on a subset of at most 10 000
samples. Empty if no early stopping.
ndarray[float64](0,) []
validation_score_ validation_score_: ndarray, shape (n_iter_+1,)

The scores at each iteration on the held-out validation data. The
first entry is the score of the ensemble before the first iteration.
Scores are computed according to the ``scoring`` parameter. Empty if
no early stopping or if ``validation_fraction`` is None.
ndarray[float64](0,) []


title = "Tuned GBDT model without refitting and using the entire dataset"
plot_roc_pr_curves(model, tuned_model, title=title)
Tuned GBDT model without refitting and using the entire dataset, Precision-Recall curve, ROC curve, Objective score as a function of the decision threshold

Regarding the cut-off point, we observe that the optimum is similar to the multiple repeated cross-validation case. However, be aware that a single split does not account for the variability of the fit/predict process and thus we are unable to know if there is any variance in the cut-off point. The repeated cross-validation averages out this effect.

Another observation concerns the ROC and Precision-Recall curves of the tuned model. As expected, these curves differ from those of the vanilla model, given that we trained the underlying classifier on a subset of the data provided during fitting and reserved a validation set for tuning the cut-off point.

Cost-sensitive learning when gains and costs are not constant

As stated in [2], gains and costs are generally not constant in real-world problems. In this section, we use a similar example as in [2] for the problem of detecting fraud in credit card transaction records.

The credit card dataset

credit_card = fetch_openml(data_id=1597, as_frame=True, parser="pandas")
credit_card.frame.info()
<class 'pandas.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

The dataset contains information about credit card records from which some are fraudulent and others are legitimate. The goal is therefore to predict whether or not a credit card record is fraudulent.

columns_to_drop = ["Class"]
data = credit_card.frame.drop(columns=columns_to_drop)
target = credit_card.frame["Class"].astype(int)

First, we check the class distribution of the datasets.

target.value_counts(normalize=True)
Class
0    0.998273
1    0.001727
Name: proportion, dtype: float64

The dataset is highly imbalanced with fraudulent transaction representing only 0.17% of the data. Since we are interested in training a machine learning model, we should also make sure that we have enough samples in the minority class to train the model.

target.value_counts()
Class
0    284315
1       492
Name: count, dtype: int64

We observe that we have around 500 samples that is on the low end of the number of samples required to train a machine learning model. In addition of the target distribution, we check the distribution of the amount of the fraudulent transactions.

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 (€)")
Amount of fraud transaction

Addressing the problem with a business metric

Now, we create the business metric that depends on the amount of each transaction. We define the cost matrix similarly to [2]. Accepting a legitimate transaction provides a gain of 2% of the amount of the transaction. However, accepting a fraudulent transaction result in a loss of the amount of the transaction. As stated in [2], the gain and loss related to refusals (of fraudulent and legitimate transactions) are not trivial to define. Here, we define that a refusal of a legitimate transaction is estimated to a loss of 5€ while the refusal of a fraudulent transaction is estimated to a gain of 50€. Therefore, we define the following function to compute the total benefit of a given decision:

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

From this business metric, we create a scikit-learn scorer that given a fitted classifier and a test set compute the business metric. In this regard, we use the make_scorer factory. The variable amount is an additional metadata to be passed to the scorer and we need to use metadata routing to take into account this information.

sklearn.set_config(enable_metadata_routing=True)
business_scorer = make_scorer(business_metric).set_score_request(amount=True)

So at this stage, we observe that the amount of the transaction is used twice: once as a feature to train our predictive model and once as a metadata to compute the the business metric and thus the statistical performance of our model. When used as a feature, we are only required to have a column in data that contains the amount of each transaction. To use this information as metadata, we need to have an external variable that we can pass to the scorer or the model that internally routes this metadata to the scorer. So let’s create this variable.

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
    )
)

We first evaluate some baseline policies to serve as reference. Recall that class “0” is the legitimate class and class “1” is the fraudulent class.

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€

A policy that considers all transactions as legitimate would create a profit of around 220,000€. We make the same evaluation for a classifier that predicts all transactions as fraudulent.

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€

Such a policy would entail a catastrophic loss: around 670,000€. This is expected since the vast majority of the transactions are legitimate and the policy would refuse them at a non-trivial cost.

A predictive model that adapts the accept/reject decisions on a per transaction basis should ideally allow us to make a profit larger than the 220,000€ of the best of our constant baseline policies.

We start with a logistic regression model with the default decision threshold at 0.5. Here we tune the hyperparameter C of the logistic regression with a proper scoring rule (the log loss) to ensure that the model’s probabilistic predictions returned by its predict_proba method are as accurate as possible, irrespectively of the choice of the value of the decision threshold.

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
GridSearchCV(estimator=Pipeline(steps=[('standardscaler', StandardScaler()),
                                       ('logisticregression',
                                        LogisticRegression())]),
             param_grid={'logisticregression__C': array([1.e-06, 1.e-05, 1.e-04, 1.e-03, 1.e-02, 1.e-01, 1.e+00, 1.e+01,
       1.e+02, 1.e+03, 1.e+04, 1.e+05, 1.e+06])},
             scoring='neg_log_loss')
In a Jupyter environment, please rerun this cell to show the HTML representation or trust the notebook.
On GitHub, the HTML representation is unable to render, please try loading this page with nbviewer.org.
Parameters
estimator estimator: estimator object

This is assumed to implement the scikit-learn estimator interface.
Either estimator needs to provide a ``score`` function,
or ``scoring`` must be passed.
Pipeline(step...egression())])
param_grid param_grid: dict or list of dictionaries

Dictionary with parameters names (`str`) as keys and lists of
parameter settings to try as values, or a list of such
dictionaries, in which case the grids spanned by each dictionary
in the list are explored. This enables searching over any sequence
of parameter settings.
{'logisticregression__C': array([1.e-06...e+05, 1.e+06])}
scoring scoring: str, callable, list, tuple or dict, default=None

Strategy to evaluate the performance of the cross-validated model on
the test set.

If `scoring` represents a single score, one can use:

- a single string (see :ref:`scoring_string_names`);
- a callable (see :ref:`scoring_callable`) that returns a single value;
- `None`, the `estimator`'s
:ref:`default evaluation criterion <scoring_api_overview>` is used.

If `scoring` represents multiple scores, one can use:

- a list or tuple of unique strings;
- a callable returning a dictionary where the keys are the metric
names and the values are the metric scores;
- a dictionary with metric names as keys and callables as values.

See :ref:`multimetric_grid_search` for an example.
'neg_log_loss'
n_jobs n_jobs: int, default=None

Number of jobs to run in parallel.
``None`` means 1 unless in a :obj:`joblib.parallel_backend` context.
``-1`` means using all processors. See :term:`Glossary <n_jobs>`
for more details.

.. versionchanged:: v0.20
`n_jobs` default changed from 1 to None
None
refit refit: bool, str, or callable, default=True

Refit an estimator using the best found parameters on the whole
dataset.

For multiple metric evaluation, this needs to be a `str` denoting the
scorer that would be used to find the best parameters for refitting
the estimator at the end.

Where there are considerations other than maximum score in
choosing a best estimator, ``refit`` can be set to a function which
returns the selected ``best_index_`` given ``cv_results_``. In that
case, the ``best_estimator_`` and ``best_params_`` will be set
according to the returned ``best_index_`` while the ``best_score_``
attribute will not be available.

The refitted estimator is made available at the ``best_estimator_``
attribute and permits using ``predict`` directly on this
``GridSearchCV`` instance.

Also for multiple metric evaluation, the attributes ``best_index_``,
``best_score_`` and ``best_params_`` will only be available if
``refit`` is set and all of them will be determined w.r.t this specific
scorer.

See ``scoring`` parameter to know more about multiple metric
evaluation.

See :ref:`sphx_glr_auto_examples_model_selection_plot_grid_search_digits.py`
to see how to design a custom selection strategy using a callable
via `refit`.

See :ref:`this example
<sphx_glr_auto_examples_model_selection_plot_grid_search_refit_callable.py>`
for an example of how to use ``refit=callable`` to balance model
complexity and cross-validated score.

.. versionchanged:: 0.20
Support for callable added.
True
cv cv: int, cross-validation generator or an iterable, default=None

Determines the cross-validation splitting strategy.
Possible inputs for cv are:

- None, to use the default 5-fold cross validation,
- integer, to specify the number of folds in a `(Stratified)KFold`,
- :term:`CV splitter`,
- an iterable yielding (train, test) splits as arrays of indices.

For integer/None inputs, if the estimator is a classifier and ``y`` is
either binary or multiclass, :class:`StratifiedKFold` is used. In all
other cases, :class:`KFold` is used. These splitters are instantiated
with `shuffle=False` so the splits will be the same across calls.

Refer :ref:`User Guide <cross_validation>` for the various
cross-validation strategies that can be used here.

.. versionchanged:: 0.22
``cv`` default value if None changed from 3-fold to 5-fold.
None
verbose verbose: int, default=0

Controls the verbosity of information printed during fitting, with higher
values yielding more detailed logging.

- 0 : no messages are printed;
- >=1 : summary of the total number of fits;
- >=2 : computation time for each fold and parameter candidate;
- >=3 : fold indices and scores;
- >=10 : parameter candidate indices and START messages before each fit.
0
pre_dispatch pre_dispatch: int, or str, default='2*n_jobs'

Controls the number of jobs that get dispatched during parallel
execution. Reducing this number can be useful to avoid an
explosion of memory consumption when more jobs get dispatched
than CPUs can process. This parameter can be:

- None, in which case all the jobs are immediately created and spawned. Use
this for lightweight and fast-running jobs, to avoid delays due to on-demand
spawning of the jobs
- An int, giving the exact number of total jobs that are spawned
- A str, giving an expression as a function of n_jobs, as in '2*n_jobs'
'2*n_jobs'
error_score error_score: 'raise' or numeric, default=np.nan

Value to assign to the score if an error occurs in estimator fitting.
If set to 'raise', the error is raised. If a numeric value is given,
FitFailedWarning is raised. This parameter does not affect the refit
step, which will always raise the error.
nan
return_train_score return_train_score: bool, default=False

If ``False``, the ``cv_results_`` attribute will not include training
scores.
Computing training scores is used to get insights on how different
parameter settings impact the overfitting/underfitting trade-off.
However computing the scores on the training set can be computationally
expensive and is not strictly required to select the parameters that
yield the best generalization performance.

.. versionadded:: 0.19

.. versionchanged:: 0.21
Default value was changed from ``True`` to ``False``
False
Fitted attributes
Name Type Value
best_estimator_ best_estimator_: estimator

Estimator that was chosen by the search, i.e. estimator
which gave highest score (or smallest loss if specified)
on the left out data. Not available if ``refit=False``.

See ``refit`` parameter for more information on allowed values.
Pipeline Pipeline(step...t64(100.0)))])
best_index_ best_index_: int

The index (of the ``cv_results_`` arrays) which corresponds to the best
candidate parameter setting.

The dict at ``search.cv_results_['params'][search.best_index_]`` gives
the parameter setting for the best model, that gives the highest
mean score (``search.best_score_``).

For multi-metric evaluation, this is present only if ``refit`` is
specified.
int64 np.int64(8)
best_params_ best_params_: dict

Parameter setting that gave the best results on the hold out data.

For multi-metric evaluation, this is present only if ``refit`` is
specified.
dict {'lo..._C': np.float64(100.0)}
best_score_ best_score_: float

Mean cross-validated score of the best_estimator

For multi-metric evaluation, this is present only if ``refit`` is
specified.

This attribute is not available if ``refit`` is a function.
float64 -0.003994
classes_ classes_: ndarray of shape (n_classes,)

The classes labels. This is present only if ``refit`` is specified and
the underlying estimator is a classifier.
ndarray[int64](2,) [0,1]
cv_results_ cv_results_: dict of numpy (masked) ndarrays

A dict with keys as column headers and values as columns, that can be
imported into a pandas ``DataFrame``.

For instance the below given table

+------------+-----------+------------+-----------------+---+---------+
|param_kernel|param_gamma|param_degree|split0_test_score|...|rank_t...|
+============+===========+============+=================+===+=========+
| 'poly' | -- | 2 | 0.80 |...| 2 |
+------------+-----------+------------+-----------------+---+---------+
| 'poly' | -- | 3 | 0.70 |...| 4 |
+------------+-----------+------------+-----------------+---+---------+
| 'rbf' | 0.1 | -- | 0.80 |...| 3 |
+------------+-----------+------------+-----------------+---+---------+
| 'rbf' | 0.2 | -- | 0.93 |...| 1 |
+------------+-----------+------------+-----------------+---+---------+

will be represented by a ``cv_results_`` dict of::

{
'param_kernel': masked_array(data = ['poly', 'poly', 'rbf', 'rbf'],
mask = [False False False False]...)
'param_gamma': masked_array(data = [-- -- 0.1 0.2],
mask = [ True True False False]...),
'param_degree': masked_array(data = [2.0 3.0 -- --],
mask = [False False True True]...),
'split0_test_score' : [0.80, 0.70, 0.80, 0.93],
'split1_test_score' : [0.82, 0.50, 0.70, 0.78],
'mean_test_score' : [0.81, 0.60, 0.75, 0.85],
'std_test_score' : [0.01, 0.10, 0.05, 0.08],
'rank_test_score' : [2, 4, 3, 1],
'split0_train_score' : [0.80, 0.92, 0.70, 0.93],
'split1_train_score' : [0.82, 0.55, 0.70, 0.87],
'mean_train_score' : [0.81, 0.74, 0.70, 0.90],
'std_train_score' : [0.01, 0.19, 0.00, 0.03],
'mean_fit_time' : [0.73, 0.63, 0.43, 0.49],
'std_fit_time' : [0.01, 0.02, 0.01, 0.01],
'mean_score_time' : [0.01, 0.06, 0.04, 0.04],
'std_score_time' : [0.00, 0.00, 0.00, 0.01],
'params' : [{'kernel': 'poly', 'degree': 2}, ...],
}

For an example of visualization and interpretation of GridSearch results,
see :ref:`sphx_glr_auto_examples_model_selection_plot_grid_search_stats.py`.

NOTE

The key ``'params'`` is used to store a list of parameter
settings dicts for all the parameter candidates.

The ``mean_fit_time``, ``std_fit_time``, ``mean_score_time`` and
``std_score_time`` are all in seconds.

For multi-metric evaluation, the scores for all the scorers are
available in the ``cv_results_`` dict at the keys ending with that
scorer's name (``'_<scorer_name>'``) instead of ``'_score'`` shown
above. ('split0_test_precision', 'mean_train_precision' etc.)
dict {'me...me': array([0.2 , ... 0.24, 0.24]), 'me...me': array([0.02, ... 0.02, 0.02]), 'me...re': array([-0.01,...-0. , -0. ]), 'pa..._C': masked_array(...l_value=1e+20), ...}
feature_names_in_ feature_names_in_: ndarray of shape (`n_features_in_`,)

Names of features seen during :term:`fit`. Only defined if
`best_estimator_` is defined (see the documentation for the `refit`
parameter for more details) and that `best_estimator_` exposes
`feature_names_in_` when fit.

.. versionadded:: 1.0
ndarray[object](29,) ['V1','V2','V3',...,'V27','V28','Amount']
multimetric_ multimetric_: bool

Whether or not the scorers compute several metrics.
bool False
n_features_in_ n_features_in_: int

Number of features seen during :term:`fit`. Only defined if
`best_estimator_` is defined (see the documentation for the `refit`
parameter for more details) and that `best_estimator_` exposes
`n_features_in_` when fit.

.. versionadded:: 0.24
int 29
n_splits_ n_splits_: int

The number of cross-validation splits (folds/iterations).
int 5
refit_time_ refit_time_: float

Seconds used for refitting the best model on the whole dataset.

This is present only if ``refit`` is not False.

.. versionadded:: 0.20
float 0.3153
scorer_ scorer_: function or a dict

Scorer function used on the held out data to choose the best
parameters for the model.

For multi-metric evaluation, this attribute holds the validated
``scoring`` dict which maps the scorer key to the scorer callable.
_Scorer make_scorer(l...redict_proba')
Parameters
copy copy: bool, default=True

If False, try to avoid a copy and do inplace scaling instead.
This is not guaranteed to always work inplace; e.g. if the data is
not a NumPy array or scipy.sparse CSR matrix, a copy may still be
returned.
True
with_mean with_mean: bool, default=True

If True, center the data before scaling.
This does not work (and will raise an exception) when attempted on
sparse matrices, because centering them entails building a dense
matrix which in common use cases is likely to be too large to fit in
memory.
True
with_std with_std: bool, default=True

If True, scale the data to unit variance (or equivalently,
unit standard deviation).
True
Fitted attributes
Name Type Value
feature_names_in_ feature_names_in_: ndarray of shape (`n_features_in_`,)

Names of features seen during :term:`fit`. Defined only when `X`
has feature names that are all strings.

.. versionadded:: 1.0
ndarray[object](29,) ['V1','V2','V3',...,'V27','V28','Amount']
mean_ mean_: ndarray of shape (n_features,) or None

The mean value for each feature in the training set.
Equal to ``None`` when ``with_mean=False`` and ``with_std=False``.
ndarray[float64](29,) [ 0. ,-0. ,-0.01,..., 0. , 0. ,88.35]
n_features_in_ n_features_in_: int

Number of features seen during :term:`fit`.

.. versionadded:: 0.24
int 29
n_samples_seen_ n_samples_seen_: int or ndarray of shape (n_features,)

The number of samples processed by the estimator for each feature.
If there are no missing samples, the ``n_samples_seen`` will be an
integer, otherwise it will be an array of dtype int. If
`sample_weights` are used it will be a float (if no missing data)
or an array of dtype float that sums the weights seen so far.
Will be reset on new calls to fit, but increments across
``partial_fit`` calls.
float64 1.424e+05
scale_ scale_: ndarray of shape (n_features,) or None

Per feature relative scaling of the data to achieve zero mean and unit
variance. Generally this is calculated using `np.sqrt(var_)`. If a
variance is zero, we can't achieve unit variance, and the data is left
as-is, giving a scaling factor of 1. `scale_` is equal to `None`
when `with_std=False`.

.. versionadded:: 0.17
*scale_*
ndarray[float64](29,) [ 1.97, 1.66, 1.53,..., 0.41, 0.31,256.68]
var_ var_: ndarray of shape (n_features,) or None

The variance for each feature in the training set. Used to compute
`scale_`. Equal to ``None`` when ``with_mean=False`` and
``with_std=False``.
ndarray[float64](29,) [ 3.86, 2.75, 2.34,..., 0.17, 0.1 ,65885.14]
29 features
V1
V2
V3
V4
V5
V6
V7
V8
V9
V10
V11
V12
V13
V14
V15
V16
V17
V18
V19
V20
V21
V22
V23
V24
V25
V26
V27
V28
Amount
Parameters
C C: float, default=1.0

Inverse of regularization strength; must be a positive float.
Like in support vector machines, smaller values specify stronger
regularization. `C=np.inf` results in unpenalized logistic regression.
For a visual example on the effect of tuning the `C` parameter
with an L1 penalty, see:
:ref:`sphx_glr_auto_examples_linear_model_plot_logistic_path.py`.
np.float64(100.0)
penalty penalty: {'l1', 'l2', 'elasticnet', None}, default='l2'

Specify the norm of the penalty:

- `None`: no penalty is added;
- `'l2'`: add an L2 penalty term and it is the default choice;
- `'l1'`: add an L1 penalty term;
- `'elasticnet'`: both L1 and L2 penalty terms are added.

.. warning::
Some penalties may not work with some solvers. See the parameter
`solver` below, to know the compatibility between the penalty and
solver.

.. versionadded:: 0.19
l1 penalty with SAGA solver (allowing 'multinomial' + L1)

.. deprecated:: 1.8
`penalty` was deprecated in version 1.8 and will be removed in 1.10.
Use `l1_ratio` and `C` instead. `l1_ratio=0` for `penalty='l2'`,
`l1_ratio=1` for `penalty='l1'`, `l1_ratio` set to any float between 0 and 1
for `penalty='elasticnet'`, and `C=np.inf` for `penalty=None`.
'deprecated'
l1_ratio l1_ratio: float, default=0.0

The Elastic-Net mixing parameter, with `0 <= l1_ratio <= 1`. Setting
`l1_ratio=1` gives a pure L1-penalty, setting `l1_ratio=0` a pure L2-penalty.
Any value between 0 and 1 gives an Elastic-Net penalty of the form
`l1_ratio * L1 + (1 - l1_ratio) * L2`.

.. warning::
Certain values of `l1_ratio`, i.e. some penalties, may not work with some
solvers. See the parameter `solver` below, to know the compatibility between
the penalty and solver.

.. versionchanged:: 1.8
Default value changed from None to 0.0.

.. deprecated:: 1.8
`None` is deprecated and will be removed in version 1.10. Always use
`l1_ratio` to specify the penalty type.
0.0
dual dual: bool, default=False

Dual (constrained) or primal (regularized, see also
:ref:`this equation <regularized-logistic-loss>`) formulation. Dual formulation
is only implemented for l2 penalty with liblinear solver. Prefer `dual=False`
when n_samples > n_features.
False
tol tol: float, default=1e-4

Tolerance for stopping criteria.
0.0001
fit_intercept fit_intercept: bool, default=True

Specifies if a constant (a.k.a. bias or intercept) should be
added to the decision function.
True
intercept_scaling intercept_scaling: float, default=1

Useful only when the solver `liblinear` is used
and `self.fit_intercept` is set to `True`. In this case, `x` becomes
`[x, self.intercept_scaling]`,
i.e. a "synthetic" feature with constant value equal to
`intercept_scaling` is appended to the instance vector.
The intercept becomes
``intercept_scaling * synthetic_feature_weight``.

.. note::
The synthetic feature weight is subject to L1 or L2
regularization as all other features.
To lessen the effect of regularization on synthetic feature weight
(and therefore on the intercept) `intercept_scaling` has to be increased.
1
class_weight class_weight: dict or 'balanced', default=None

Weights associated with classes in the form ``{class_label: weight}``.
If not given, all classes are supposed to have weight one.

The "balanced" mode uses the values of y to automatically adjust
weights inversely proportional to class frequencies in the input data
as ``n_samples / (n_classes * np.bincount(y))``.

Note that these weights will be multiplied with sample_weight (passed
through the fit method) if sample_weight is specified.

.. versionadded:: 0.17
*class_weight='balanced'*
None
random_state random_state: int, RandomState instance, default=None

Used when ``solver`` == 'sag', 'saga' or 'liblinear' to shuffle the
data. See :term:`Glossary <random_state>` for details.
None
solver solver: {'lbfgs', 'liblinear', 'newton-cg', 'newton-cholesky', 'sag', 'saga'}, default='lbfgs'

Algorithm to use in the optimization problem. Default is 'lbfgs'.
To choose a solver, you might want to consider the following aspects:

- 'lbfgs' is a good default solver because it works reasonably well for a wide
class of problems.
- For :term:`multiclass` problems (`n_classes >= 3`), all solvers except
'liblinear' minimize the full multinomial loss, 'liblinear' will raise an
error.
- 'newton-cholesky' is a good choice for
`n_samples` >> `n_features * n_classes`, especially with one-hot encoded
categorical features with rare categories. Be aware that the memory usage
of this solver has a quadratic dependency on `n_features * n_classes`
because it explicitly computes the full Hessian matrix.
- For small datasets, 'liblinear' is a good choice, whereas 'sag'
and 'saga' are faster for large ones;
- 'liblinear' can only handle binary classification by default. To apply a
one-versus-rest scheme for the multiclass setting one can wrap it with the
:class:`~sklearn.multiclass.OneVsRestClassifier`.

.. warning::
The choice of the algorithm depends on the penalty chosen (`l1_ratio=0`
for L2-penalty, `l1_ratio=1` for L1-penalty and `0 < l1_ratio < 1` for
Elastic-Net) and on (multinomial) multiclass support:

================= ======================== ======================
solver l1_ratio multinomial multiclass
================= ======================== ======================
'lbfgs' l1_ratio=0 yes
'liblinear' l1_ratio=1 or l1_ratio=0 no
'newton-cg' l1_ratio=0 yes
'newton-cholesky' l1_ratio=0 yes
'sag' l1_ratio=0 yes
'saga' 0<=l1_ratio<=1 yes
================= ======================== ======================

.. note::
'sag' and 'saga' fast convergence is only guaranteed on features
with approximately the same scale. You can preprocess the data with
a scaler from :mod:`sklearn.preprocessing`.

.. seealso::
Refer to the :ref:`User Guide <Logistic_regression>` for more
information regarding :class:`LogisticRegression` and more specifically the
:ref:`Table <logistic_regression_solvers>`
summarizing solver/penalty supports.

.. versionadded:: 0.17
Stochastic Average Gradient (SAG) descent solver. Multinomial support in
version 0.18.
.. versionadded:: 0.19
SAGA solver.
.. versionchanged:: 0.22
The default solver changed from 'liblinear' to 'lbfgs' in 0.22.
.. versionadded:: 1.2
newton-cholesky solver. Multinomial support in version 1.6.
'lbfgs'
max_iter max_iter: int, default=100

Maximum number of iterations taken for the solvers to converge.
100
verbose verbose: int, default=0

For the liblinear and lbfgs solvers set verbose to any positive
number for verbosity.
0
warm_start warm_start: bool, default=False

When set to True, reuse the solution of the previous call to fit as
initialization, otherwise, just erase the previous solution.
Useless for liblinear solver. See :term:`the Glossary <warm_start>`.

.. versionadded:: 0.17
*warm_start* to support *lbfgs*, *newton-cg*, *sag*, *saga* solvers.
False
n_jobs n_jobs: int, default=None

Does not have any effect.

.. deprecated:: 1.8
`n_jobs` is deprecated in version 1.8 and will be removed in 1.10.
None
Fitted attributes
Name Type Value
classes_ classes_: ndarray of shape (n_classes, )

A list of class labels known to the classifier.
ndarray[int64](2,) [0,1]
coef_ coef_: ndarray or CSR matrix of shape (1, n_features) or (n_classes, n_features)

Coefficients of the features in the decision function.

`coef_` is of shape (1, n_features) when the given problem is binary.

By default, it will be created as a dense array, but can be turned to
sparse (CSR format) through :meth:`sparsify` (which can be beneficial
under L1 regularization when many coefficients are zero), and back to
dense through :meth:`densify`.
ndarray[float64](1, 29) [[ 0.1 , 0.02, 0.1 ,...,-0.3 ,-0.05, 0.18]]
feature_names_in_ feature_names_in_: ndarray of shape (`n_features_in_`,)

Names of features seen during :term:`fit`. Defined only when `X`
has feature names that are all strings.

.. versionadded:: 1.0
ndarray[object](29,) ['V1','V2','V3',...,'V27','V28','Amount']
intercept_ intercept_: ndarray of shape (1,) or (n_classes,)

Intercept (a.k.a. bias) added to the decision function.

If `fit_intercept` is set to False, the intercept is set to zero.
`intercept_` is of shape (1,) when the given problem is binary.
ndarray[float64](1,) [-8.96]
n_features_in_ n_features_in_: int

Number of features seen during :term:`fit`.

.. versionadded:: 0.24
int 29
n_iter_ n_iter_: ndarray of shape (1, )

Actual number of iterations for all classes.

.. versionchanged:: 0.20

In SciPy <= 1.0.0 the number of lbfgs iterations may exceed
``max_iter``. ``n_iter_`` will now report at most ``max_iter``.
ndarray[int32](1,) [25]


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€

The business metric shows that our predictive model with a default decision threshold is already winning over the baseline in terms of profit and it would be already beneficial to use it to accept or reject transactions instead of accepting all transactions.

Tuning the decision threshold

Now the question is: is our model optimum for the type of decision that we want to do? Up to now, we did not optimize the decision threshold. We use the TunedThresholdClassifierCV to optimize the decision given our business scorer. To avoid a nested cross-validation, we will use the best estimator found during the previous grid-search.

tuned_model = TunedThresholdClassifierCV(
    estimator=model.best_estimator_,
    scoring=business_scorer,
    thresholds=100,
    n_jobs=2,
)

Since our business scorer requires the amount of each transaction, we need to pass this information in the fit method. The TunedThresholdClassifierCV is in charge of automatically dispatching this metadata to the underlying scorer.

tuned_model.fit(data_train, target_train, amount=amount_train)
TunedThresholdClassifierCV(estimator=Pipeline(steps=[('standardscaler',
                                                      StandardScaler()),
                                                     ('logisticregression',
                                                      LogisticRegression(C=np.float64(100.0)))]),
                           n_jobs=2,
                           scoring=make_scorer(business_metric, response_method='predict'))
In a Jupyter environment, please rerun this cell to show the HTML representation or trust the notebook.
On GitHub, the HTML representation is unable to render, please try loading this page with nbviewer.org.
Parameters
estimator estimator: estimator instance

The classifier, fitted or not, for which we want to optimize
the decision threshold used during `predict`.
Pipeline(step...t64(100.0)))])
scoring scoring: str or callable, default="balanced_accuracy"

The objective metric to be optimized. Can be one of:

- str: string associated to a scoring function for binary classification,
see :ref:`scoring_string_names` for options.
- callable: a scorer callable object (e.g., function) with signature
``scorer(estimator, X, y)``. See :ref:`scoring_callable` for details.
make_scorer(b...hod='predict')
n_jobs n_jobs: int, default=None

The number of jobs to run in parallel. When `cv` represents a
cross-validation strategy, the fitting and scoring on each data split
is done in parallel. ``None`` means 1 unless in a
:obj:`joblib.parallel_backend` context. ``-1`` means using all
processors. See :term:`Glossary <n_jobs>` for more details.
2
response_method response_method: {"auto", "decision_function", "predict_proba"}, default="auto"

Methods by the classifier `estimator` corresponding to the
decision function for which we want to find a threshold. It can be:

* if `"auto"`, it will try to invoke, for each classifier,
`"predict_proba"` or `"decision_function"` in that order.
* otherwise, one of `"predict_proba"` or `"decision_function"`.
If the method is not implemented by the classifier, it will raise an
error.
'auto'
thresholds thresholds: int or array-like, default=100

The number of decision threshold to use when discretizing the output of the
classifier `method`. Pass an array-like to manually specify the thresholds
to use.
100
cv cv: int, float, cross-validation generator, iterable or "prefit", default=None

Determines the cross-validation splitting strategy to train classifier.
Possible inputs for cv are:

* `None`, to use the default 5-fold stratified K-fold cross validation;
* An integer number, to specify the number of folds in a stratified k-fold;
* A float number, to specify a single shuffle split. The floating number should
be in (0, 1) and represent the size of the validation set;
* An object to be used as a cross-validation generator;
* An iterable yielding train, test splits;
* `"prefit"`, to bypass the cross-validation.

Refer :ref:`User Guide <cross_validation>` for the various
cross-validation strategies that can be used here.

.. warning::
Using `cv="prefit"` and passing the same dataset for fitting `estimator`
and tuning the cut-off point is subject to undesired overfitting. You can
refer to :ref:`TunedThresholdClassifierCV_no_cv` for an example.

This option should only be used when the set used to fit `estimator` is
different from the one used to tune the cut-off point (by calling
:meth:`TunedThresholdClassifierCV.fit`).
None
refit refit: bool, default=True

Whether or not to refit the classifier on the entire training set once
the decision threshold has been found.
Note that forcing `refit=False` on cross-validation having more
than a single split will raise an error. Similarly, `refit=True` in
conjunction with `cv="prefit"` will raise an error.
True
random_state random_state: int, RandomState instance or None, default=None

Controls the randomness of cross-validation when `cv` is a float.
See :term:`Glossary <random_state>`.
None
store_cv_results store_cv_results: bool, default=False

Whether to store all scores and thresholds computed during the cross-validation
process.
False
Fitted attributes
Name Type Value
best_score_ best_score_: float or None

The optimal score of the objective metric, evaluated at `best_threshold_`.
float64 5.057e+04
best_threshold_ best_threshold_: float

The new decision threshold.
float64 0.0303
classes_ classes_: ndarray of shape (n_classes,)

The class labels.
ndarray[int64](2,) [0,1]
estimator_ estimator_: estimator instance

The fitted classifier used when predicting.
Pipeline Pipeline(step...t64(100.0)))])
feature_names_in_ feature_names_in_: ndarray of shape (`n_features_in_`,)

Names of features seen during :term:`fit`. Only defined if the
underlying estimator exposes such an attribute when fit.
ndarray[object](29,) ['V1','V2','V3',...,'V27','V28','Amount']
n_features_in_ n_features_in_: int

Number of features seen during :term:`fit`. Only defined if the
underlying estimator exposes such an attribute when fit.
int 29
Parameters
steps steps: list of tuples

List of (name of step, estimator) tuples that are to be chained in
sequential order. To be compatible with the scikit-learn API, all steps
must define `fit`. All non-last steps must also define `transform`. See
:ref:`Combining Estimators <combining_estimators>` for more details.
[('standardscaler', ...), ('logisticregression', ...)]
transform_input transform_input: list of str, default=None

The names of the :term:`metadata` parameters that should be transformed by the
pipeline before passing it to the step consuming it.

This enables transforming some input arguments to ``fit`` (other than ``X``)
to be transformed by the steps of the pipeline up to the step which requires
them. Requirement is defined via :ref:`metadata routing <metadata_routing>`.
For instance, this can be used to pass a validation set through the pipeline.

You can only set this if metadata routing is enabled, which you
can enable using ``sklearn.set_config(enable_metadata_routing=True)``.

.. versionadded:: 1.6
None
memory memory: str or object with the joblib.Memory interface, default=None

Used to cache the fitted transformers of the pipeline. The last step
will never be cached, even if it is a transformer. By default, no
caching is performed. If a string is given, it is the path to the
caching directory. Enabling caching triggers a clone of the transformers
before fitting. Therefore, the transformer instance given to the
pipeline cannot be inspected directly. Use the attribute ``named_steps``
or ``steps`` to inspect estimators within the pipeline. Caching the
transformers is advantageous when fitting is time consuming. See
:ref:`sphx_glr_auto_examples_neighbors_plot_caching_nearest_neighbors.py`
for an example on how to enable caching.
None
verbose verbose: bool, default=False

If True, the time elapsed while fitting each step will be printed as it
is completed.
False
Fitted attributes
Name Type Value
classes_ classes_: ndarray of shape (n_classes,)

The classes labels. Only exist if the last step of the pipeline is a
classifier.
ndarray[int64](2,) [0,1]
feature_names_in_ feature_names_in_: ndarray of shape (`n_features_in_`,)

Names of features seen during :term:`fit`. Only defined if the
underlying estimator exposes such an attribute when fit.

.. versionadded:: 1.0
ndarray[object](29,) ['V1','V2','V3',...,'V27','V28','Amount']
n_features_in_ n_features_in_: int

Number of features seen during :term:`fit`. Only defined if the
underlying first estimator in `steps` exposes such an attribute
when fit.

.. versionadded:: 0.24
int 29
Parameters
copy copy: bool, default=True

If False, try to avoid a copy and do inplace scaling instead.
This is not guaranteed to always work inplace; e.g. if the data is
not a NumPy array or scipy.sparse CSR matrix, a copy may still be
returned.
True
with_mean with_mean: bool, default=True

If True, center the data before scaling.
This does not work (and will raise an exception) when attempted on
sparse matrices, because centering them entails building a dense
matrix which in common use cases is likely to be too large to fit in
memory.
True
with_std with_std: bool, default=True

If True, scale the data to unit variance (or equivalently,
unit standard deviation).
True
Fitted attributes
Name Type Value
feature_names_in_ feature_names_in_: ndarray of shape (`n_features_in_`,)

Names of features seen during :term:`fit`. Defined only when `X`
has feature names that are all strings.

.. versionadded:: 1.0
ndarray[object](29,) ['V1','V2','V3',...,'V27','V28','Amount']
mean_ mean_: ndarray of shape (n_features,) or None

The mean value for each feature in the training set.
Equal to ``None`` when ``with_mean=False`` and ``with_std=False``.
ndarray[float64](29,) [ 0. ,-0. ,-0.01,..., 0. , 0. ,88.35]
n_features_in_ n_features_in_: int

Number of features seen during :term:`fit`.

.. versionadded:: 0.24
int 29
n_samples_seen_ n_samples_seen_: int or ndarray of shape (n_features,)

The number of samples processed by the estimator for each feature.
If there are no missing samples, the ``n_samples_seen`` will be an
integer, otherwise it will be an array of dtype int. If
`sample_weights` are used it will be a float (if no missing data)
or an array of dtype float that sums the weights seen so far.
Will be reset on new calls to fit, but increments across
``partial_fit`` calls.
float64 1.424e+05
scale_ scale_: ndarray of shape (n_features,) or None

Per feature relative scaling of the data to achieve zero mean and unit
variance. Generally this is calculated using `np.sqrt(var_)`. If a
variance is zero, we can't achieve unit variance, and the data is left
as-is, giving a scaling factor of 1. `scale_` is equal to `None`
when `with_std=False`.

.. versionadded:: 0.17
*scale_*
ndarray[float64](29,) [ 1.97, 1.66, 1.53,..., 0.41, 0.31,256.68]
var_ var_: ndarray of shape (n_features,) or None

The variance for each feature in the training set. Used to compute
`scale_`. Equal to ``None`` when ``with_mean=False`` and
``with_std=False``.
ndarray[float64](29,) [ 3.86, 2.75, 2.34,..., 0.17, 0.1 ,65885.14]
29 features
V1
V2
V3
V4
V5
V6
V7
V8
V9
V10
V11
V12
V13
V14
V15
V16
V17
V18
V19
V20
V21
V22
V23
V24
V25
V26
V27
V28
Amount
Parameters
C C: float, default=1.0

Inverse of regularization strength; must be a positive float.
Like in support vector machines, smaller values specify stronger
regularization. `C=np.inf` results in unpenalized logistic regression.
For a visual example on the effect of tuning the `C` parameter
with an L1 penalty, see:
:ref:`sphx_glr_auto_examples_linear_model_plot_logistic_path.py`.
np.float64(100.0)
penalty penalty: {'l1', 'l2', 'elasticnet', None}, default='l2'

Specify the norm of the penalty:

- `None`: no penalty is added;
- `'l2'`: add an L2 penalty term and it is the default choice;
- `'l1'`: add an L1 penalty term;
- `'elasticnet'`: both L1 and L2 penalty terms are added.

.. warning::
Some penalties may not work with some solvers. See the parameter
`solver` below, to know the compatibility between the penalty and
solver.

.. versionadded:: 0.19
l1 penalty with SAGA solver (allowing 'multinomial' + L1)

.. deprecated:: 1.8
`penalty` was deprecated in version 1.8 and will be removed in 1.10.
Use `l1_ratio` and `C` instead. `l1_ratio=0` for `penalty='l2'`,
`l1_ratio=1` for `penalty='l1'`, `l1_ratio` set to any float between 0 and 1
for `penalty='elasticnet'`, and `C=np.inf` for `penalty=None`.
'deprecated'
l1_ratio l1_ratio: float, default=0.0

The Elastic-Net mixing parameter, with `0 <= l1_ratio <= 1`. Setting
`l1_ratio=1` gives a pure L1-penalty, setting `l1_ratio=0` a pure L2-penalty.
Any value between 0 and 1 gives an Elastic-Net penalty of the form
`l1_ratio * L1 + (1 - l1_ratio) * L2`.

.. warning::
Certain values of `l1_ratio`, i.e. some penalties, may not work with some
solvers. See the parameter `solver` below, to know the compatibility between
the penalty and solver.

.. versionchanged:: 1.8
Default value changed from None to 0.0.

.. deprecated:: 1.8
`None` is deprecated and will be removed in version 1.10. Always use
`l1_ratio` to specify the penalty type.
0.0
dual dual: bool, default=False

Dual (constrained) or primal (regularized, see also
:ref:`this equation <regularized-logistic-loss>`) formulation. Dual formulation
is only implemented for l2 penalty with liblinear solver. Prefer `dual=False`
when n_samples > n_features.
False
tol tol: float, default=1e-4

Tolerance for stopping criteria.
0.0001
fit_intercept fit_intercept: bool, default=True

Specifies if a constant (a.k.a. bias or intercept) should be
added to the decision function.
True
intercept_scaling intercept_scaling: float, default=1

Useful only when the solver `liblinear` is used
and `self.fit_intercept` is set to `True`. In this case, `x` becomes
`[x, self.intercept_scaling]`,
i.e. a "synthetic" feature with constant value equal to
`intercept_scaling` is appended to the instance vector.
The intercept becomes
``intercept_scaling * synthetic_feature_weight``.

.. note::
The synthetic feature weight is subject to L1 or L2
regularization as all other features.
To lessen the effect of regularization on synthetic feature weight
(and therefore on the intercept) `intercept_scaling` has to be increased.
1
class_weight class_weight: dict or 'balanced', default=None

Weights associated with classes in the form ``{class_label: weight}``.
If not given, all classes are supposed to have weight one.

The "balanced" mode uses the values of y to automatically adjust
weights inversely proportional to class frequencies in the input data
as ``n_samples / (n_classes * np.bincount(y))``.

Note that these weights will be multiplied with sample_weight (passed
through the fit method) if sample_weight is specified.

.. versionadded:: 0.17
*class_weight='balanced'*
None
random_state random_state: int, RandomState instance, default=None

Used when ``solver`` == 'sag', 'saga' or 'liblinear' to shuffle the
data. See :term:`Glossary <random_state>` for details.
None
solver solver: {'lbfgs', 'liblinear', 'newton-cg', 'newton-cholesky', 'sag', 'saga'}, default='lbfgs'

Algorithm to use in the optimization problem. Default is 'lbfgs'.
To choose a solver, you might want to consider the following aspects:

- 'lbfgs' is a good default solver because it works reasonably well for a wide
class of problems.
- For :term:`multiclass` problems (`n_classes >= 3`), all solvers except
'liblinear' minimize the full multinomial loss, 'liblinear' will raise an
error.
- 'newton-cholesky' is a good choice for
`n_samples` >> `n_features * n_classes`, especially with one-hot encoded
categorical features with rare categories. Be aware that the memory usage
of this solver has a quadratic dependency on `n_features * n_classes`
because it explicitly computes the full Hessian matrix.
- For small datasets, 'liblinear' is a good choice, whereas 'sag'
and 'saga' are faster for large ones;
- 'liblinear' can only handle binary classification by default. To apply a
one-versus-rest scheme for the multiclass setting one can wrap it with the
:class:`~sklearn.multiclass.OneVsRestClassifier`.

.. warning::
The choice of the algorithm depends on the penalty chosen (`l1_ratio=0`
for L2-penalty, `l1_ratio=1` for L1-penalty and `0 < l1_ratio < 1` for
Elastic-Net) and on (multinomial) multiclass support:

================= ======================== ======================
solver l1_ratio multinomial multiclass
================= ======================== ======================
'lbfgs' l1_ratio=0 yes
'liblinear' l1_ratio=1 or l1_ratio=0 no
'newton-cg' l1_ratio=0 yes
'newton-cholesky' l1_ratio=0 yes
'sag' l1_ratio=0 yes
'saga' 0<=l1_ratio<=1 yes
================= ======================== ======================

.. note::
'sag' and 'saga' fast convergence is only guaranteed on features
with approximately the same scale. You can preprocess the data with
a scaler from :mod:`sklearn.preprocessing`.

.. seealso::
Refer to the :ref:`User Guide <Logistic_regression>` for more
information regarding :class:`LogisticRegression` and more specifically the
:ref:`Table <logistic_regression_solvers>`
summarizing solver/penalty supports.

.. versionadded:: 0.17
Stochastic Average Gradient (SAG) descent solver. Multinomial support in
version 0.18.
.. versionadded:: 0.19
SAGA solver.
.. versionchanged:: 0.22
The default solver changed from 'liblinear' to 'lbfgs' in 0.22.
.. versionadded:: 1.2
newton-cholesky solver. Multinomial support in version 1.6.
'lbfgs'
max_iter max_iter: int, default=100

Maximum number of iterations taken for the solvers to converge.
100
verbose verbose: int, default=0

For the liblinear and lbfgs solvers set verbose to any positive
number for verbosity.
0
warm_start warm_start: bool, default=False

When set to True, reuse the solution of the previous call to fit as
initialization, otherwise, just erase the previous solution.
Useless for liblinear solver. See :term:`the Glossary <warm_start>`.

.. versionadded:: 0.17
*warm_start* to support *lbfgs*, *newton-cg*, *sag*, *saga* solvers.
False
n_jobs n_jobs: int, default=None

Does not have any effect.

.. deprecated:: 1.8
`n_jobs` is deprecated in version 1.8 and will be removed in 1.10.
None
Fitted attributes
Name Type Value
classes_ classes_: ndarray of shape (n_classes, )

A list of class labels known to the classifier.
ndarray[int64](2,) [0,1]
coef_ coef_: ndarray or CSR matrix of shape (1, n_features) or (n_classes, n_features)

Coefficients of the features in the decision function.

`coef_` is of shape (1, n_features) when the given problem is binary.

By default, it will be created as a dense array, but can be turned to
sparse (CSR format) through :meth:`sparsify` (which can be beneficial
under L1 regularization when many coefficients are zero), and back to
dense through :meth:`densify`.
ndarray[float64](1, 29) [[ 0.1 , 0.02, 0.1 ,...,-0.3 ,-0.05, 0.18]]
feature_names_in_ feature_names_in_: ndarray of shape (`n_features_in_`,)

Names of features seen during :term:`fit`. Defined only when `X`
has feature names that are all strings.

.. versionadded:: 1.0
ndarray[object](29,) ['V1','V2','V3',...,'V27','V28','Amount']
intercept_ intercept_: ndarray of shape (1,) or (n_classes,)

Intercept (a.k.a. bias) added to the decision function.

If `fit_intercept` is set to False, the intercept is set to zero.
`intercept_` is of shape (1,) when the given problem is binary.
ndarray[float64](1,) [-8.96]
n_features_in_ n_features_in_: int

Number of features seen during :term:`fit`.

.. versionadded:: 0.24
int 29
n_iter_ n_iter_: ndarray of shape (1, )

Actual number of iterations for all classes.

.. versionchanged:: 0.20

In SciPy <= 1.0.0 the number of lbfgs iterations may exceed
``max_iter``. ``n_iter_`` will now report at most ``max_iter``.
ndarray[int32](1,) [25]


We observe that the tuned decision threshold is far away from the default 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€

We observe that tuning the decision threshold increases the expected profit when deploying our model - as indicated by the business metric. It is therefore valuable, whenever possible, to optimize the decision threshold with respect to the business metric.

Manually setting the decision threshold instead of tuning it

In the previous example, we used the TunedThresholdClassifierCV to find the optimal decision threshold. However, in some cases, we might have some prior knowledge about the problem at hand and we might be happy to set the decision threshold manually.

The class FixedThresholdClassifier allows us to manually set the decision threshold. At prediction time, it behave as the previous tuned model but no search is performed during the fitting process. Note that here we use FrozenEstimator to wrap the predictive model to avoid any refitting.

Here, we will reuse the decision threshold found in the previous section to create a new model and check that it gives the same results.

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€

We observe that we obtained the exact same results but the fitting process was much faster since we did not perform any hyper-parameter search.

Finally, the estimate of the (average) business metric itself can be unreliable, in particular when the number of data points in the minority class is very small. Any business impact estimated by cross-validation of a business metric on historical data (offline evaluation) should ideally be confirmed by A/B testing on live data (online evaluation). Note however that A/B testing models is beyond the scope of the scikit-learn library itself.

At the end, we disable the configuration flag for metadata routing:

.. GENERATED FROM PYTHON SOURCE LINES 693-694
sklearn.set_config(enable_metadata_routing=False)

Total running time of the script: (0 minutes 28.847 seconds)

Launch binder
Launch JupyterLite

Download Jupyter notebook: plot_cost_sensitive_learning.ipynb

Download Python source code: plot_cost_sensitive_learning.py

Download zipped: plot_cost_sensitive_learning.zip

Related examples

Post-hoc tuning the cut-off point of decision function

Release Highlights for scikit-learn 1.5

Precision-Recall

Custom refit strategy of a grid search with cross-validation

© 2007–2026 The scikit-learn developers
Licensed under the 3-clause BSD License.
https://scikit-learn.org/1.9/auto_examples/model_selection/plot_cost_sensitive_learning.html

Spec-Zone.ru

Настройки Оффлайн Что нового Помощь О нас
Spec-Zone .ru
спецификации, руководства, описания, API