CoolFace
Apppublic

Aluode/PerceptionLabPortable

sourceHugging Faceupdated 9mo agoView on Hugging Face
0likes
_classification_threshold.py890 linesDownload Raw Back to model_selection
1# Authors: The scikit-learn developers
2# SPDX-License-Identifier: BSD-3-Clause
3
4from collections.abc import MutableMapping
5from numbers import Integral, Real
6
7import numpy as np
8
9from ..base import (
10    BaseEstimator,
11    ClassifierMixin,
12    MetaEstimatorMixin,
13    _fit_context,
14    clone,
15)
16from ..exceptions import NotFittedError
17from ..metrics import (
18    check_scoring,
19    get_scorer_names,
20)
21from ..metrics._scorer import (
22    _CurveScorer,
23    _threshold_scores_to_class_labels,
24)
25from ..utils import _safe_indexing, get_tags
26from ..utils._param_validation import HasMethods, Interval, RealNotInt, StrOptions
27from ..utils._response import _get_response_values_binary
28from ..utils.metadata_routing import (
29    MetadataRouter,
30    MethodMapping,
31    _raise_for_params,
32    process_routing,
33)
34from ..utils.metaestimators import available_if
35from ..utils.multiclass import type_of_target
36from ..utils.parallel import Parallel, delayed
37from ..utils.validation import (
38    _check_method_params,
39    _estimator_has,
40    _num_samples,
41    check_is_fitted,
42    indexable,
43)
44from ._split import StratifiedShuffleSplit, check_cv
45
46
47def _check_is_fitted(estimator):
48    try:
49        check_is_fitted(estimator.estimator)
50    except NotFittedError:
51        check_is_fitted(estimator, "estimator_")
52
53
54class BaseThresholdClassifier(ClassifierMixin, MetaEstimatorMixin, BaseEstimator):
55    """Base class for binary classifiers that set a non-default decision threshold.
56
57    In this base class, we define the following interface:
58
59    - the validation of common parameters in `fit`;
60    - the different prediction methods that can be used with the classifier.
61
62    .. versionadded:: 1.5
63
64    Parameters
65    ----------
66    estimator : estimator instance
67        The binary classifier, fitted or not, for which we want to optimize
68        the decision threshold used during `predict`.
69
70    response_method : {"auto", "decision_function", "predict_proba"}, default="auto"
71        Methods by the classifier `estimator` corresponding to the
72        decision function for which we want to find a threshold. It can be:
73
74        * if `"auto"`, it will try to invoke, for each classifier,
75          `"predict_proba"` or `"decision_function"` in that order.
76        * otherwise, one of `"predict_proba"` or `"decision_function"`.
77          If the method is not implemented by the classifier, it will raise an
78          error.
79    """
80
81    _parameter_constraints: dict = {
82        "estimator": [
83            HasMethods(["fit", "predict_proba"]),
84            HasMethods(["fit", "decision_function"]),
85        ],
86        "response_method": [StrOptions({"auto", "predict_proba", "decision_function"})],
87    }
88
89    def __init__(self, estimator, *, response_method="auto"):
90        self.estimator = estimator
91        self.response_method = response_method
92
93    def _get_response_method(self):
94        """Define the response method."""
95        if self.response_method == "auto":
96            response_method = ["predict_proba", "decision_function"]
97        else:
98            response_method = self.response_method
99        return response_method
100
101    @_fit_context(
102        # *ThresholdClassifier*.estimator is not validated yet
103        prefer_skip_nested_validation=False
104    )
105    def fit(self, X, y, **params):
106        """Fit the classifier.
107
108        Parameters
109        ----------
110        X : {array-like, sparse matrix} of shape (n_samples, n_features)
111            Training data.
112
113        y : array-like of shape (n_samples,)
114            Target values.
115
116        **params : dict
117            Parameters to pass to the `fit` method of the underlying
118            classifier.
119
120        Returns
121        -------
122        self : object
123            Returns an instance of self.
124        """
125        _raise_for_params(params, self, None)
126
127        X, y = indexable(X, y)
128
129        y_type = type_of_target(y, input_name="y")
130        if y_type != "binary":
131            raise ValueError(
132                f"Only binary classification is supported. Unknown label type: {y_type}"
133            )
134
135        self._fit(X, y, **params)
136
137        if hasattr(self.estimator_, "n_features_in_"):
138            self.n_features_in_ = self.estimator_.n_features_in_
139        if hasattr(self.estimator_, "feature_names_in_"):
140            self.feature_names_in_ = self.estimator_.feature_names_in_
141
142        return self
143
144    @property
145    def classes_(self):
146        """Classes labels."""
147        return self.estimator_.classes_
148
149    @available_if(_estimator_has("predict_proba"))
150    def predict_proba(self, X):
151        """Predict class probabilities for `X` using the fitted estimator.
152
153        Parameters
154        ----------
155        X : {array-like, sparse matrix} of shape (n_samples, n_features)
156            Training vectors, where `n_samples` is the number of samples and
157            `n_features` is the number of features.
158
159        Returns
160        -------
161        probabilities : ndarray of shape (n_samples, n_classes)
162            The class probabilities of the input samples.
163        """
164        _check_is_fitted(self)
165        estimator = getattr(self, "estimator_", self.estimator)
166        return estimator.predict_proba(X)
167
168    @available_if(_estimator_has("predict_log_proba"))
169    def predict_log_proba(self, X):
170        """Predict logarithm class probabilities for `X` using the fitted estimator.
171
172        Parameters
173        ----------
174        X : {array-like, sparse matrix} of shape (n_samples, n_features)
175            Training vectors, where `n_samples` is the number of samples and
176            `n_features` is the number of features.
177
178        Returns
179        -------
180        log_probabilities : ndarray of shape (n_samples, n_classes)
181            The logarithm class probabilities of the input samples.
182        """
183        _check_is_fitted(self)
184        estimator = getattr(self, "estimator_", self.estimator)
185        return estimator.predict_log_proba(X)
186
187    @available_if(_estimator_has("decision_function"))
188    def decision_function(self, X):
189        """Decision function for samples in `X` using the fitted estimator.
190
191        Parameters
192        ----------
193        X : {array-like, sparse matrix} of shape (n_samples, n_features)
194            Training vectors, where `n_samples` is the number of samples and
195            `n_features` is the number of features.
196
197        Returns
198        -------
199        decisions : ndarray of shape (n_samples,)
200            The decision function computed the fitted estimator.
201        """
202        _check_is_fitted(self)
203        estimator = getattr(self, "estimator_", self.estimator)
204        return estimator.decision_function(X)
205
206    def __sklearn_tags__(self):
207        tags = super().__sklearn_tags__()
208        tags.classifier_tags.multi_class = False
209        tags.input_tags.sparse = get_tags(self.estimator).input_tags.sparse
210        return tags
211
212
213class FixedThresholdClassifier(BaseThresholdClassifier):
214    """Binary classifier that manually sets the decision threshold.
215
216    This classifier allows to change the default decision threshold used for
217    converting posterior probability estimates (i.e. output of `predict_proba`) or
218    decision scores (i.e. output of `decision_function`) into a class label.
219
220    Here, the threshold is not optimized and is set to a constant value.
221
222    Read more in the :ref:`User Guide <FixedThresholdClassifier>`.
223
224    .. versionadded:: 1.5
225
226    Parameters
227    ----------
228    estimator : estimator instance
229        The binary classifier, fitted or not, for which we want to optimize
230        the decision threshold used during `predict`.
231
232    threshold : {"auto"} or float, default="auto"
233        The decision threshold to use when converting posterior probability estimates
234        (i.e. output of `predict_proba`) or decision scores (i.e. output of
235        `decision_function`) into a class label. When `"auto"`, the threshold is set
236        to 0.5 if `predict_proba` is used as `response_method`, otherwise it is set to
237        0 (i.e. the default threshold for `decision_function`).
238
239    pos_label : int, float, bool or str, default=None
240        The label of the positive class. Used to process the output of the
241        `response_method` method. When `pos_label=None`, if `y_true` is in `{-1, 1}` or
242        `{0, 1}`, `pos_label` is set to 1, otherwise an error will be raised.
243
244    response_method : {"auto", "decision_function", "predict_proba"}, default="auto"
245        Methods by the classifier `estimator` corresponding to the
246        decision function for which we want to find a threshold. It can be:
247
248        * if `"auto"`, it will try to invoke `"predict_proba"` or `"decision_function"`
249          in that order.
250        * otherwise, one of `"predict_proba"` or `"decision_function"`.
251          If the method is not implemented by the classifier, it will raise an
252          error.
253
254    Attributes
255    ----------
256    estimator_ : estimator instance
257        The fitted classifier used when predicting.
258
259    classes_ : ndarray of shape (n_classes,)
260        The class labels.
261
262    n_features_in_ : int
263        Number of features seen during :term:`fit`. Only defined if the
264        underlying estimator exposes such an attribute when fit.
265
266    feature_names_in_ : ndarray of shape (`n_features_in_`,)
267        Names of features seen during :term:`fit`. Only defined if the
268        underlying estimator exposes such an attribute when fit.
269
270    See Also
271    --------
272    sklearn.model_selection.TunedThresholdClassifierCV : Classifier that post-tunes
273        the decision threshold based on some metrics and using cross-validation.
274    sklearn.calibration.CalibratedClassifierCV : Estimator that calibrates
275        probabilities.
276
277    Examples
278    --------
279    >>> from sklearn.datasets import make_classification
280    >>> from sklearn.linear_model import LogisticRegression
281    >>> from sklearn.metrics import confusion_matrix
282    >>> from sklearn.model_selection import FixedThresholdClassifier, train_test_split
283    >>> X, y = make_classification(
284    ...     n_samples=1_000, weights=[0.9, 0.1], class_sep=0.8, random_state=42
285    ... )
286    >>> X_train, X_test, y_train, y_test = train_test_split(
287    ...     X, y, stratify=y, random_state=42
288    ... )
289    >>> classifier = LogisticRegression(random_state=0).fit(X_train, y_train)
290    >>> print(confusion_matrix(y_test, classifier.predict(X_test)))
291    [[217   7]
292     [ 19   7]]
293    >>> classifier_other_threshold = FixedThresholdClassifier(
294    ...     classifier, threshold=0.1, response_method="predict_proba"
295    ... ).fit(X_train, y_train)
296    >>> print(confusion_matrix(y_test, classifier_other_threshold.predict(X_test)))
297    [[184  40]
298     [  6  20]]
299    """
300
301    _parameter_constraints: dict = {
302        **BaseThresholdClassifier._parameter_constraints,
303        "threshold": [StrOptions({"auto"}), Real],
304        "pos_label": [Real, str, "boolean", None],
305    }
306
307    def __init__(
308        self,
309        estimator,
310        *,
311        threshold="auto",
312        pos_label=None,
313        response_method="auto",
314    ):
315        super().__init__(estimator=estimator, response_method=response_method)
316        self.pos_label = pos_label
317        self.threshold = threshold
318
319    @property
320    def classes_(self):
321        if estimator := getattr(self, "estimator_", None):
322            return estimator.classes_
323        try:
324            check_is_fitted(self.estimator)
325            return self.estimator.classes_
326        except NotFittedError:
327            raise AttributeError(
328                "The underlying estimator is not fitted yet."
329            ) from NotFittedError
330
331    def _fit(self, X, y, **params):
332        """Fit the classifier.
333
334        Parameters
335        ----------
336        X : {array-like, sparse matrix} of shape (n_samples, n_features)
337            Training data.
338
339        y : array-like of shape (n_samples,)
340            Target values.
341
342        **params : dict
343            Parameters to pass to the `fit` method of the underlying
344            classifier.
345
346        Returns
347        -------
348        self : object
349            Returns an instance of self.
350        """
351        routed_params = process_routing(self, "fit", **params)
352        self.estimator_ = clone(self.estimator).fit(X, y, **routed_params.estimator.fit)
353        return self
354
355    def predict(self, X):
356        """Predict the target of new samples.
357
358        Parameters
359        ----------
360        X : {array-like, sparse matrix} of shape (n_samples, n_features)
361            The samples, as accepted by `estimator.predict`.
362
363        Returns
364        -------
365        class_labels : ndarray of shape (n_samples,)
366            The predicted class.
367        """
368        _check_is_fitted(self)
369
370        estimator = getattr(self, "estimator_", self.estimator)
371
372        y_score, _, response_method_used = _get_response_values_binary(
373            estimator,
374            X,
375            self._get_response_method(),
376            pos_label=self.pos_label,
377            return_response_method_used=True,
378        )
379
380        if self.threshold == "auto":
381            decision_threshold = 0.5 if response_method_used == "predict_proba" else 0.0
382        else:
383            decision_threshold = self.threshold
384
385        return _threshold_scores_to_class_labels(
386            y_score, decision_threshold, self.classes_, self.pos_label
387        )
388
389    def get_metadata_routing(self):
390        """Get metadata routing of this object.
391
392        Please check :ref:`User Guide <metadata_routing>` on how the routing
393        mechanism works.
394
395        Returns
396        -------
397        routing : MetadataRouter
398            A :class:`~sklearn.utils.metadata_routing.MetadataRouter` encapsulating
399            routing information.
400        """
401        router = MetadataRouter(owner=self.__class__.__name__).add(
402            estimator=self.estimator,
403            method_mapping=MethodMapping().add(callee="fit", caller="fit"),
404        )
405        return router
406
407
408def _fit_and_score_over_thresholds(
409    classifier,
410    X,
411    y,
412    *,
413    fit_params,
414    train_idx,
415    val_idx,
416    curve_scorer,
417    score_params,
418):
419    """Fit a classifier and compute the scores for different decision thresholds.
420
421    Parameters
422    ----------
423    classifier : estimator instance
424        The classifier to fit and use for scoring. If `classifier` is already fitted,
425        it will be used as is.
426
427    X : {array-like, sparse matrix} of shape (n_samples, n_features)
428        The entire dataset.
429
430    y : array-like of shape (n_samples,)
431        The entire target vector.
432
433    fit_params : dict
434        Parameters to pass to the `fit` method of the underlying classifier.
435
436    train_idx : ndarray of shape (n_train_samples,) or None
437        The indices of the training set. If `None`, `classifier` is expected to be
438        already fitted.
439
440    val_idx : ndarray of shape (n_val_samples,)
441        The indices of the validation set used to score `classifier`. If `train_idx`,
442        the entire set will be used.
443
444    curve_scorer : scorer instance
445        The scorer taking `classifier` and the validation set as input and outputting
446        decision thresholds and scores as a curve. Note that this is different from
447        the usual scorer that outputs a single score value as `curve_scorer`
448        outputs a single score value for each threshold.
449
450    score_params : dict
451        Parameters to pass to the `score` method of the underlying scorer.
452
453    Returns
454    -------
455    scores : ndarray of shape (thresholds,) or tuple of such arrays
456        The scores computed for each decision threshold. When TPR/TNR or precision/
457        recall are computed, `scores` is a tuple of two arrays.
458
459    potential_thresholds : ndarray of shape (thresholds,)
460        The decision thresholds used to compute the scores. They are returned in
461        ascending order.
462    """
463
464    if train_idx is not None:
465        X_train, X_val = _safe_indexing(X, train_idx), _safe_indexing(X, val_idx)
466        y_train, y_val = _safe_indexing(y, train_idx), _safe_indexing(y, val_idx)
467        fit_params_train = _check_method_params(X, fit_params, indices=train_idx)
468        score_params_val = _check_method_params(X, score_params, indices=val_idx)
469        classifier.fit(X_train, y_train, **fit_params_train)
470    else:  # prefit estimator, only a validation set is provided
471        X_val, y_val, score_params_val = X, y, score_params
472
473    return curve_scorer(classifier, X_val, y_val, **score_params_val)
474
475
476def _mean_interpolated_score(target_thresholds, cv_thresholds, cv_scores):
477    """Compute the mean interpolated score across folds by defining common thresholds.
478
479    Parameters
480    ----------
481    target_thresholds : ndarray of shape (thresholds,)
482        The thresholds to use to compute the mean score.
483
484    cv_thresholds : ndarray of shape (n_folds, thresholds_fold)
485        The thresholds used to compute the scores for each fold.
486
487    cv_scores : ndarray of shape (n_folds, thresholds_fold)
488        The scores computed for each threshold for each fold.
489
490    Returns
491    -------
492    mean_score : ndarray of shape (thresholds,)
493        The mean score across all folds for each target threshold.
494    """
495    return np.mean(
496        [
497            np.interp(target_thresholds, split_thresholds, split_score)
498            for split_thresholds, split_score in zip(cv_thresholds, cv_scores)
499        ],
500        axis=0,
501    )
502
503
504class TunedThresholdClassifierCV(BaseThresholdClassifier):
505    """Classifier that post-tunes the decision threshold using cross-validation.
506
507    This estimator post-tunes the decision threshold (cut-off point) that is
508    used for converting posterior probability estimates (i.e. output of
509    `predict_proba`) or decision scores (i.e. output of `decision_function`)
510    into a class label. The tuning is done by optimizing a binary metric,
511    potentially constrained by a another metric.
512
513    Read more in the :ref:`User Guide <TunedThresholdClassifierCV>`.
514
515    .. versionadded:: 1.5
516
517    Parameters
518    ----------
519    estimator : estimator instance
520        The classifier, fitted or not, for which we want to optimize
521        the decision threshold used during `predict`.
522
523    scoring : str or callable, default="balanced_accuracy"
524        The objective metric to be optimized. Can be one of:
525
526        - str: string associated to a scoring function for binary classification,
527          see :ref:`scoring_string_names` for options.
528        - callable: a scorer callable object (e.g., function) with signature
529          ``scorer(estimator, X, y)``. See :ref:`scoring_callable` for details.
530
531    response_method : {"auto", "decision_function", "predict_proba"}, default="auto"
532        Methods by the classifier `estimator` corresponding to the
533        decision function for which we want to find a threshold. It can be:
534
535        * if `"auto"`, it will try to invoke, for each classifier,
536          `"predict_proba"` or `"decision_function"` in that order.
537        * otherwise, one of `"predict_proba"` or `"decision_function"`.
538          If the method is not implemented by the classifier, it will raise an
539          error.
540
541    thresholds : int or array-like, default=100
542        The number of decision threshold to use when discretizing the output of the
543        classifier `method`. Pass an array-like to manually specify the thresholds
544        to use.
545
546    cv : int, float, cross-validation generator, iterable or "prefit", default=None
547        Determines the cross-validation splitting strategy to train classifier.
548        Possible inputs for cv are:
549
550        * `None`, to use the default 5-fold stratified K-fold cross validation;
551        * An integer number, to specify the number of folds in a stratified k-fold;
552        * A float number, to specify a single shuffle split. The floating number should
553          be in (0, 1) and represent the size of the validation set;
554        * An object to be used as a cross-validation generator;
555        * An iterable yielding train, test splits;
556        * `"prefit"`, to bypass the cross-validation.
557
558        Refer :ref:`User Guide <cross_validation>` for the various
559        cross-validation strategies that can be used here.
560
561        .. warning::
562            Using `cv="prefit"` and passing the same dataset for fitting `estimator`
563            and tuning the cut-off point is subject to undesired overfitting. You can
564            refer to :ref:`TunedThresholdClassifierCV_no_cv` for an example.
565
566            This option should only be used when the set used to fit `estimator` is
567            different from the one used to tune the cut-off point (by calling
568            :meth:`TunedThresholdClassifierCV.fit`).
569
570    refit : bool, default=True
571        Whether or not to refit the classifier on the entire training set once
572        the decision threshold has been found.
573        Note that forcing `refit=False` on cross-validation having more
574        than a single split will raise an error. Similarly, `refit=True` in
575        conjunction with `cv="prefit"` will raise an error.
576
577    n_jobs : int, default=None
578        The number of jobs to run in parallel. When `cv` represents a
579        cross-validation strategy, the fitting and scoring on each data split
580        is done in parallel. ``None`` means 1 unless in a
581        :obj:`joblib.parallel_backend` context. ``-1`` means using all
582        processors. See :term:`Glossary <n_jobs>` for more details.
583
584    random_state : int, RandomState instance or None, default=None
585        Controls the randomness of cross-validation when `cv` is a float.
586        See :term:`Glossary <random_state>`.
587
588    store_cv_results : bool, default=False
589        Whether to store all scores and thresholds computed during the cross-validation
590        process.
591
592    Attributes
593    ----------
594    estimator_ : estimator instance
595        The fitted classifier used when predicting.
596
597    best_threshold_ : float
598        The new decision threshold.
599
600    best_score_ : float or None
601        The optimal score of the objective metric, evaluated at `best_threshold_`.
602
603    cv_results_ : dict or None
604        A dictionary containing the scores and thresholds computed during the
605        cross-validation process. Only exist if `store_cv_results=True`. The
606        keys are `"thresholds"` and `"scores"`.
607
608    classes_ : ndarray of shape (n_classes,)
609        The class labels.
610
611    n_features_in_ : int
612        Number of features seen during :term:`fit`. Only defined if the
613        underlying estimator exposes such an attribute when fit.
614
615    feature_names_in_ : ndarray of shape (`n_features_in_`,)
616        Names of features seen during :term:`fit`. Only defined if the
617        underlying estimator exposes such an attribute when fit.
618
619    See Also
620    --------
621    sklearn.model_selection.FixedThresholdClassifier : Classifier that uses a
622        constant threshold.
623    sklearn.calibration.CalibratedClassifierCV : Estimator that calibrates
624        probabilities.
625
626    Examples
627    --------
628    >>> from sklearn.datasets import make_classification
629    >>> from sklearn.ensemble import RandomForestClassifier
630    >>> from sklearn.metrics import classification_report
631    >>> from sklearn.model_selection import TunedThresholdClassifierCV, train_test_split
632    >>> X, y = make_classification(
633    ...     n_samples=1_000, weights=[0.9, 0.1], class_sep=0.8, random_state=42
634    ... )
635    >>> X_train, X_test, y_train, y_test = train_test_split(
636    ...     X, y, stratify=y, random_state=42
637    ... )
638    >>> classifier = RandomForestClassifier(random_state=0).fit(X_train, y_train)
639    >>> print(classification_report(y_test, classifier.predict(X_test)))
640                  precision    recall  f1-score   support
641    <BLANKLINE>
642               0       0.94      0.99      0.96       224
643               1       0.80      0.46      0.59        26
644    <BLANKLINE>
645        accuracy                           0.93       250
646       macro avg       0.87      0.72      0.77       250
647    weighted avg       0.93      0.93      0.92       250
648    <BLANKLINE>
649    >>> classifier_tuned = TunedThresholdClassifierCV(
650    ...     classifier, scoring="balanced_accuracy"
651    ... ).fit(X_train, y_train)
652    >>> print(
653    ...     f"Cut-off point found at {classifier_tuned.best_threshold_:.3f}"
654    ... )
655    Cut-off point found at 0.342
656    >>> print(classification_report(y_test, classifier_tuned.predict(X_test)))
657                  precision    recall  f1-score   support
658    <BLANKLINE>
659               0       0.96      0.95      0.96       224
660               1       0.61      0.65      0.63        26
661    <BLANKLINE>
662        accuracy                           0.92       250
663       macro avg       0.78      0.80      0.79       250
664    weighted avg       0.92      0.92      0.92       250
665    <BLANKLINE>
666    """
667
668    _parameter_constraints: dict = {
669        **BaseThresholdClassifier._parameter_constraints,
670        "scoring": [
671            StrOptions(set(get_scorer_names())),
672            callable,
673            MutableMapping,
674        ],
675        "thresholds": [Interval(Integral, 1, None, closed="left"), "array-like"],
676        "cv": [
677            "cv_object",
678            StrOptions({"prefit"}),
679            Interval(RealNotInt, 0.0, 1.0, closed="neither"),
680        ],
681        "refit": ["boolean"],
682        "n_jobs": [Integral, None],
683        "random_state": ["random_state"],
684        "store_cv_results": ["boolean"],
685    }
686
687    def __init__(
688        self,
689        estimator,
690        *,
691        scoring="balanced_accuracy",
692        response_method="auto",
693        thresholds=100,
694        cv=None,
695        refit=True,
696        n_jobs=None,
697        random_state=None,
698        store_cv_results=False,
699    ):
700        super().__init__(estimator=estimator, response_method=response_method)
701        self.scoring = scoring
702        self.thresholds = thresholds
703        self.cv = cv
704        self.refit = refit
705        self.n_jobs = n_jobs
706        self.random_state = random_state
707        self.store_cv_results = store_cv_results
708
709    def _fit(self, X, y, **params):
710        """Fit the classifier and post-tune the decision threshold.
711
712        Parameters
713        ----------
714        X : {array-like, sparse matrix} of shape (n_samples, n_features)
715            Training data.
716
717        y : array-like of shape (n_samples,)
718            Target values.
719
720        **params : dict
721            Parameters to pass to the `fit` method of the underlying
722            classifier and to the `scoring` scorer.
723
724        Returns
725        -------
726        self : object
727            Returns an instance of self.
728        """
729        if isinstance(self.cv, Real) and 0 < self.cv < 1:
730            cv = StratifiedShuffleSplit(
731                n_splits=1, test_size=self.cv, random_state=self.random_state
732            )
733        elif self.cv == "prefit":
734            if self.refit is True:
735                raise ValueError("When cv='prefit', refit cannot be True.")
736            try:
737                check_is_fitted(self.estimator, "classes_")
738            except NotFittedError as exc:
739                raise NotFittedError(
740                    """When cv='prefit', `estimator` must be fitted."""
741                ) from exc
742            cv = self.cv
743        else:
744            cv = check_cv(self.cv, y=y, classifier=True)
745            if self.refit is False and cv.get_n_splits() > 1:
746                raise ValueError("When cv has several folds, refit cannot be False.")
747
748        routed_params = process_routing(self, "fit", **params)
749        self._curve_scorer = self._get_curve_scorer()
750
751        # in the following block, we:
752        # - define the final classifier `self.estimator_` and train it if necessary
753        # - define `classifier` to be used to post-tune the decision threshold
754        # - define `split` to be used to fit/score `classifier`
755        if cv == "prefit":
756            self.estimator_ = self.estimator
757            classifier = self.estimator_
758            splits = [(None, range(_num_samples(X)))]
759        else:
760            self.estimator_ = clone(self.estimator)
761            classifier = clone(self.estimator)
762            splits = cv.split(X, y, **routed_params.splitter.split)
763
764            if self.refit:
765                # train on the whole dataset
766                X_train, y_train, fit_params_train = X, y, routed_params.estimator.fit
767            else:
768                # single split cross-validation
769                train_idx, _ = next(cv.split(X, y, **routed_params.splitter.split))
770                X_train = _safe_indexing(X, train_idx)
771                y_train = _safe_indexing(y, train_idx)
772                fit_params_train = _check_method_params(
773                    X, routed_params.estimator.fit, indices=train_idx
774                )
775
776            self.estimator_.fit(X_train, y_train, **fit_params_train)
777
778        cv_scores, cv_thresholds = zip(
779            *Parallel(n_jobs=self.n_jobs)(
780                delayed(_fit_and_score_over_thresholds)(
781                    clone(classifier) if cv != "prefit" else classifier,
782                    X,
783                    y,
784                    fit_params=routed_params.estimator.fit,
785                    train_idx=train_idx,
786                    val_idx=val_idx,
787                    curve_scorer=self._curve_scorer,
788                    score_params=routed_params.scorer.score,
789                )
790                for train_idx, val_idx in splits
791            )
792        )
793
794        if any(np.isclose(th[0], th[-1]) for th in cv_thresholds):
795            raise ValueError(
796                "The provided estimator makes constant predictions. Therefore, it is "
797                "impossible to optimize the decision threshold."
798            )
799
800        # find the global min and max thresholds across all folds
801        min_threshold = min(
802            split_thresholds.min() for split_thresholds in cv_thresholds
803        )
804        max_threshold = max(
805            split_thresholds.max() for split_thresholds in cv_thresholds
806        )
807        if isinstance(self.thresholds, Integral):
808            decision_thresholds = np.linspace(
809                min_threshold, max_threshold, num=self.thresholds
810            )
811        else:
812            decision_thresholds = np.asarray(self.thresholds)
813
814        objective_scores = _mean_interpolated_score(
815            decision_thresholds, cv_thresholds, cv_scores
816        )
817        best_idx = objective_scores.argmax()
818        self.best_score_ = objective_scores[best_idx]
819        self.best_threshold_ = decision_thresholds[best_idx]
820        if self.store_cv_results:
821            self.cv_results_ = {
822                "thresholds": decision_thresholds,
823                "scores": objective_scores,
824            }
825
826        return self
827
828    def predict(self, X):
829        """Predict the target of new samples.
830
831        Parameters
832        ----------
833        X : {array-like, sparse matrix} of shape (n_samples, n_features)
834            The samples, as accepted by `estimator.predict`.
835
836        Returns
837        -------
838        class_labels : ndarray of shape (n_samples,)
839            The predicted class.
840        """
841        check_is_fitted(self, "estimator_")
842        pos_label = self._curve_scorer._get_pos_label()
843        y_score, _ = _get_response_values_binary(
844            self.estimator_,
845            X,
846            self._get_response_method(),
847            pos_label=pos_label,
848        )
849
850        return _threshold_scores_to_class_labels(
851            y_score, self.best_threshold_, self.classes_, pos_label
852        )
853
854    def get_metadata_routing(self):
855        """Get metadata routing of this object.
856
857        Please check :ref:`User Guide <metadata_routing>` on how the routing
858        mechanism works.
859
860        Returns
861        -------
862        routing : MetadataRouter
863            A :class:`~sklearn.utils.metadata_routing.MetadataRouter` encapsulating
864            routing information.
865        """
866        router = (
867            MetadataRouter(owner=self.__class__.__name__)
868            .add(
869                estimator=self.estimator,
870                method_mapping=MethodMapping().add(callee="fit", caller="fit"),
871            )
872            .add(
873                splitter=self.cv,
874                method_mapping=MethodMapping().add(callee="split", caller="fit"),
875            )
876            .add(
877                scorer=self._get_curve_scorer(),
878                method_mapping=MethodMapping().add(callee="score", caller="fit"),
879            )
880        )
881        return router
882
883    def _get_curve_scorer(self):
884        """Get the curve scorer based on the objective metric used."""
885        scoring = check_scoring(self.estimator, scoring=self.scoring)
886        curve_scorer = _CurveScorer.from_scorer(
887            scoring, self._get_response_method(), self.thresholds
888        )
889        return curve_scorer
890 
Aluode/PerceptionLabPortable · CoolFace