@@ -157,8 +157,8 @@ def fpr_score(y, y_pred, neg_label, pos_label):
157157# - a gain of `-5` for each false negative ("bad" credit labeled as "good"),
158158# - a `0` gain for true positives and true negatives.
159159#
160- # Note that theoretically, given that our model is calibrated and our data
161- # set representative and large enough, we do not need to tune the
160+ # Note that theoretically, given that our model is calibrated, our dataset
161+ # is representative and large enough, we do not need to tune the
162162# threshold, but can safely set it to 1/5 of the cost ratio, as stated by
163163# Eq. (2) in Elkan's paper [2]_.
164164import numpy as np
@@ -372,20 +372,20 @@ def plot_roc_pr_curves(vanilla_model, tuned_model, *, title):
372372#
373373# The second remark is that the cut-off points of the vanilla and tuned model are
374374# different. To understand why the tuned model has chosen this cut-off point, we can
375- # look at the right-hand side plot that plots the objective score that is our exactly
375+ # look at the right-hand side plot that plots the objective score that is exactly
376376# the same as our business metric. We see that the optimum threshold corresponds to the
377377# maximum of the objective score. This maximum is reached for a decision threshold
378378# much lower than 0.5: the tuned model enjoys a much higher recall at the cost of
379- # of significantly lower precision: the tuned model is much more eager to
380- # predict the "bad" class label to larger fraction of individuals.
379+ # significantly lower precision: the tuned model is much more eager to
380+ # predict the "bad" class label for a larger fraction of individuals.
381381#
382382# We can now check if choosing this cut-off point leads to a better score on the testing
383383# set:
384384print (f"Business defined metric: { scoring ['credit_gain' ](tuned_model , X_test , y_test )} " )
385385
386386# %%
387- # We observe that tuning the decision threshold almost improves our business gains
388- # by factor of 2.
387+ # We observe that tuning the decision threshold improves our business gains
388+ # by almost a factor of 2.
389389#
390390# .. _TunedThresholdClassifierCV_no_cv:
391391#
@@ -401,8 +401,8 @@ def plot_roc_pr_curves(vanilla_model, tuned_model, *, title):
401401# These two strategies can be changed by providing the `refit` and `cv` parameters.
402402# For instance, one could provide a fitted `estimator` and set `cv="prefit"`, in which
403403# case the cut-off point is found on the entire dataset provided at fitting time.
404- # Also, the underlying classifier is not be refitted by setting `refit=False`. Here, we
405- # can try to do such experiment.
404+ # This also requires to set `refit=False`, so that the underlying classifier will not be
405+ # refitted. Here, we can try to do such an experiment:
406406model .fit (X_train , y_train )
407407tuned_model .set_params (cv = "prefit" , refit = False ).fit (X_train , y_train )
408408print (f"{ tuned_model .best_threshold_ = :0.2f} " )
@@ -414,29 +414,31 @@ def plot_roc_pr_curves(vanilla_model, tuned_model, *, title):
414414plot_roc_pr_curves (model , tuned_model , title = title )
415415
416416# %%
417- # We observe the that the optimum cut-off point is different from the one found
417+ # We observe that the optimum cut-off point is different from the one found
418418# in the previous experiment. If we look at the right-hand side plot, we
419- # observe that the business gain has large plateau of near-optimal 0 gain for a
420- # large span of decision thresholds. This behavior is symptomatic of an
421- # overfitting. Because we disable cross-validation, we tuned the cut-off point
419+ # can see that the business gain has a large plateau of near-optimal 0 gain for a
420+ # large span of decision thresholds. This behavior is symptomatic of
421+ # overfitting. Because we disabled cross-validation, we tuned the cut-off point
422422# on the same set as the model was trained on, and this is the reason for the
423423# observed overfitting.
424424#
425425# This option should therefore be used with caution. One needs to make sure that the
426426# data provided at fitting time to the
427427# :class:`~sklearn.model_selection.TunedThresholdClassifierCV` is not the same as the
428- # data used to train the underlying classifier. This could happen sometimes when the
428+ # data used to train the underlying classifier. This can be the case when the
429429# idea is just to tune the predictive model on a completely new validation set without a
430430# costly complete refit.
431431#
432432# When cross-validation is too costly, a potential alternative is to use a
433433# single train-test split by providing a floating number in range `[0, 1]` to the `cv`
434- # parameter. It splits the data into a training and testing set. Let's explore this
435- # option:
434+ # parameter. It splits the data into a training and testing set. (Note that
435+ # `refit=False` is still set, which means the model will not be refitted on the entire
436+ # training set once the threshold has been found.)
437+ # Let's explore this option:
436438tuned_model .set_params (cv = 0.75 ).fit (X_train , y_train )
437439
438440# %%
439- title = "Tuned GBDT model without refitting and using the entire dataset "
441+ title = "Tuned GBDT model using a single train-test split "
440442plot_roc_pr_curves (model , tuned_model , title = title )
441443
442444# %%
@@ -467,8 +469,7 @@ def plot_roc_pr_curves(vanilla_model, tuned_model, *, title):
467469# The dataset contains information about credit card records from which some are
468470# fraudulent and others are legitimate. The goal is therefore to predict whether or
469471# not a credit card record is fraudulent.
470- columns_to_drop = ["Class" ]
471- data = credit_card .frame .drop (columns = columns_to_drop )
472+ data = credit_card .frame .drop (columns = ["Class" ])
472473target = credit_card .frame ["Class" ].astype (int )
473474
474475# %%
@@ -488,19 +489,18 @@ def plot_roc_pr_curves(vanilla_model, tuned_model, *, title):
488489# fraudulent transactions.
489490fraud = target == 1
490491amount_fraud = data ["Amount" ][fraud ]
491- _ , ax = plt .subplots ()
492- ax .hist (amount_fraud , bins = 30 )
493- ax .set_title ("Amount of fraud transaction" )
494- _ = ax .set_xlabel ("Amount (€)" )
492+ _ = amount_fraud .plot .hist (
493+ bins = 30 , title = "Amount of fraud transaction" , xlabel = "Amount (€)"
494+ )
495495
496496# %%
497497# Addressing the problem with a business metric
498498# ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
499499#
500- # Now, we create the business metric that depends on the amount of each transaction. We
500+ # Now, we create a business metric that depends on the amount of each transaction. We
501501# define the cost matrix similarly to [2]_. Accepting a legitimate transaction provides
502502# a gain of 2% of the amount of the transaction. However, accepting a fraudulent
503- # transaction result in a loss of the amount of the transaction. As stated in [2]_, the
503+ # transaction results in a loss of the amount of the transaction. As stated in [2]_, the
504504# gain and loss related to refusals (of fraudulent and legitimate transactions) are not
505505# trivial to define. Here, we define that a refusal of a legitimate transaction
506506# is estimated to a loss of 5€ while the refusal of a fraudulent transaction is
@@ -521,17 +521,17 @@ def business_metric(y_true, y_pred, amount):
521521
522522
523523# %%
524- # From this business metric, we create a scikit-learn scorer that given a fitted
525- # classifier and a test set compute the business metric. In this regard, we use
526- # the :func:`~sklearn.metrics.make_scorer` factory. The variable `amount` is an
527- # additional metadata to be passed to the scorer and we need to use
528- # :ref:`metadata routing <metadata_routing>` to take into account this information.
524+ # From this business metric, we create a scikit-learn scorer that, given a fitted
525+ # classifier and a test set, computes the business metric. We use the
526+ # :func:`~sklearn.metrics.make_scorer` factory again here . The variable `amount` is an
527+ # additional metadata to be passed to the scorer and we need to use :ref:`metadata
528+ # routing <metadata_routing>` to take this information into account .
529529sklearn .set_config (enable_metadata_routing = True )
530530business_scorer = make_scorer (business_metric ).set_score_request (amount = True )
531531
532532# %%
533533# So at this stage, we observe that the amount of the transaction is used twice: once
534- # as a feature to train our predictive model and once as a metadata to compute the
534+ # as a feature to train our predictive model and once as metadata to compute the
535535# the business metric and thus the statistical performance of our model. When used as a
536536# feature, we are only required to have a column in `data` that contains the amount of
537537# each transaction. To use this information as metadata, we need to have an external
@@ -573,7 +573,7 @@ def business_metric(y_true, y_pred, amount):
573573
574574
575575# %%
576- # Such a policy would entail a catastrophic loss: around 670 ,000€. This is
576+ # Such a policy would entail a catastrophic loss: almost 700 ,000€. This is
577577# expected since the vast majority of the transactions are legitimate and the
578578# policy would refuse them at a non-trivial cost.
579579#
@@ -614,7 +614,7 @@ def business_metric(y_true, y_pred, amount):
614614# Tuning the decision threshold
615615# ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
616616#
617- # Now the question is: is our model optimum for the type of decision that we want to do?
617+ # Now the question is: is our model optimal for the type of decision that we want to do?
618618# Up to now, we did not optimize the decision threshold. We use the
619619# :class:`~sklearn.model_selection.TunedThresholdClassifierCV` to optimize the decision
620620# given our business scorer. To avoid a nested cross-validation, we will use the
@@ -689,5 +689,5 @@ def business_metric(y_true, y_pred, amount):
689689# on live data (online evaluation). Note however that A/B testing models is
690690# beyond the scope of the scikit-learn library itself.
691691#
692- # At the end, we disable the configuration flag for metadata routing::
692+ # At the end, we disable the configuration flag for metadata routing:
693693sklearn .set_config (enable_metadata_routing = False )
0 commit comments