From 60fbf2294ef819933db71f61f0759780593b5b42 Mon Sep 17 00:00:00 2001 From: Hassan Maissoro Date: Thu, 20 Nov 2025 11:35:59 +0100 Subject: [PATCH 01/55] init check_is_fitted mapie function --- mapie/utils.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/mapie/utils.py b/mapie/utils.py index 860bc5066..4d857f1be 100644 --- a/mapie/utils.py +++ b/mapie/utils.py @@ -1637,3 +1637,14 @@ def _raise_error_if_fit_called_in_prefit_mode( "The fit method must be skipped when the prefit parameter is set to True. " "Use the conformalize method directly after instanciation." ) + +class NotFittedError(ValueError): + pass + + +def check_is_fitted_mapie(obj): + if not getattr(obj, "_is_fitted", False): + raise NotFittedError( + f"{obj.__class__.__name__} is not fitted yet. " + "Call .fit() before using this method." + ) From aed8170b4bbb8d0c4098e2388ef0c87184bce8f8 Mon Sep 17 00:00:00 2001 From: Hassan Maissoro Date: Thu, 20 Nov 2025 11:36:30 +0100 Subject: [PATCH 02/55] add alias to sklearn check_is_fitted --- mapie/utils.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/mapie/utils.py b/mapie/utils.py index 4d857f1be..3c6cdde12 100644 --- a/mapie/utils.py +++ b/mapie/utils.py @@ -20,7 +20,7 @@ from sklearn.utils.validation import ( _check_sample_weight, _num_features, - check_is_fitted, + check_is_fitted as sk_check_is_fitted, column_or_1d, ) @@ -287,12 +287,12 @@ def _fit_estimator( -------- >>> import numpy as np >>> from sklearn.linear_model import LinearRegression - >>> from sklearn.utils.validation import check_is_fitted + >>> from sklearn.utils.validation import check_is_fitted as sk_check_is_fitted >>> X = np.array([[0], [1], [2], [3], [4], [5]]) >>> y = np.array([5, 7, 9, 11, 13, 15]) >>> estimator = LinearRegression() >>> estimator = _fit_estimator(estimator, X, y) - >>> check_is_fitted(estimator) + >>> sk_check_is_fitted(estimator) """ fit_parameters = signature(estimator.fit).parameters supports_sw = "sample_weight" in fit_parameters @@ -1012,7 +1012,7 @@ def _check_estimator_classification( "predict, and predict_proba methods." ) if cv == "prefit": - check_is_fitted(est) + sk_check_is_fitted(est) if not hasattr(est, "classes_"): raise AttributeError( "Invalid classifier. " @@ -1642,7 +1642,7 @@ class NotFittedError(ValueError): pass -def check_is_fitted_mapie(obj): +def check_is_fitted(obj): if not getattr(obj, "_is_fitted", False): raise NotFittedError( f"{obj.__class__.__name__} is not fitted yet. " From ade95471272d37cbe3bc9b44d5ddd861d39b20a6 Mon Sep 17 00:00:00 2001 From: Hassan Maissoro Date: Thu, 20 Nov 2025 17:50:38 +0100 Subject: [PATCH 03/55] remore sklearn check_is_fitted func --- mapie/estimator/regressor.py | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/mapie/estimator/regressor.py b/mapie/estimator/regressor.py index f872e4ede..33af502a9 100644 --- a/mapie/estimator/regressor.py +++ b/mapie/estimator/regressor.py @@ -4,17 +4,18 @@ import numpy as np from joblib import Parallel, delayed +from numpy.typing import ArrayLike, NDArray from sklearn.base import RegressorMixin, clone from sklearn.model_selection import BaseCrossValidator from sklearn.utils import _safe_indexing -from sklearn.utils.validation import _num_samples, check_is_fitted +from sklearn.utils.validation import _num_samples -from numpy.typing import ArrayLike, NDArray from mapie.aggregation_functions import aggregate_all, phi2D from mapie.utils import ( _check_nan_in_aposteriori_prediction, _check_no_agg_cv, _fit_estimator, + check_is_fitted, ) @@ -169,6 +170,11 @@ def __init__( self.n_jobs = n_jobs self.test_size = test_size self.verbose = verbose + self._is_fitted = False + + @property + def is_fitted(self): + return self._is_fitted @staticmethod def _fit_oof_estimator( @@ -353,7 +359,7 @@ def predict_calib( NDArray of shape (n_samples_test, 1) The predictions. """ - check_is_fitted(self, self.fit_attributes) + check_is_fitted(self) if self.cv == "prefit": y_pred = self.single_estimator_.predict(X) @@ -400,7 +406,7 @@ def fit( """ Note to developer: this fit method has been broken down into fit_single_estimator and fit_multi_estimators, - but we kept it so that EnsembleRegressor passes sklearn.check_is_fitted. + but we kept it for consistency with the public fit() API. Prefer using fit_single_estimator and fit_multi_estimators. Fit the base estimator under the ``single_estimator_`` attribute. @@ -440,6 +446,8 @@ def fit( self.fit_multi_estimators(X, y, sample_weight, groups, **fit_params) + self._is_fitted = True + return self def fit_multi_estimators( @@ -560,7 +568,7 @@ def predict( - The multiple predictions for the lower bound of the intervals. - The multiple predictions for the upper bound of the intervals. """ - check_is_fitted(self, self.fit_attributes) + check_is_fitted(self) y_pred = self.single_estimator_.predict(X, **predict_params) if not return_multi_pred and not ensemble: From ff7489c5ea4c079b420676c4e73b604144144a8f Mon Sep 17 00:00:00 2001 From: Hassan Maissoro Date: Thu, 20 Nov 2025 18:03:00 +0100 Subject: [PATCH 04/55] remore sklearn check_is_fitted --- mapie/estimator/classifier.py | 22 +++++++++++++++++----- 1 file changed, 17 insertions(+), 5 deletions(-) diff --git a/mapie/estimator/classifier.py b/mapie/estimator/classifier.py index 20abfdd69..6f95aa1be 100644 --- a/mapie/estimator/classifier.py +++ b/mapie/estimator/classifier.py @@ -4,13 +4,18 @@ import numpy as np from joblib import Parallel, delayed +from numpy.typing import ArrayLike, NDArray from sklearn.base import ClassifierMixin, clone from sklearn.model_selection import BaseCrossValidator, BaseShuffleSplit from sklearn.utils import _safe_indexing -from sklearn.utils.validation import _num_samples, check_is_fitted +from sklearn.utils.validation import _num_samples -from numpy.typing import ArrayLike, NDArray -from mapie.utils import _check_no_agg_cv, _fit_estimator, _fix_number_of_classes +from mapie.utils import ( + _check_no_agg_cv, + _fit_estimator, + _fix_number_of_classes, + check_is_fitted, +) class EnsembleClassifier: @@ -121,6 +126,11 @@ def __init__( self.n_jobs = n_jobs self.test_size = test_size self.verbose = verbose + self._is_fitted = False + + @property + def is_fitted(self): + return self._is_fitted @staticmethod def _fit_oof_estimator( @@ -344,6 +354,8 @@ def fit( self.estimators_ = estimators_ self.k_ = k_ + self._is_fitted = True + return self def predict_proba_calib( @@ -381,7 +393,7 @@ def predict_proba_calib( NDArray of shape (n_samples_test, 1) The predictions. """ - check_is_fitted(self, self.fit_attributes) + check_is_fitted(self) if self.cv == "prefit": y_pred_proba = self.single_estimator_.predict_proba(X, **predict_params) @@ -445,7 +457,7 @@ def predict_agg_proba( Predictions of shape (n_samples, n_classes) """ - check_is_fitted(self, self.fit_attributes) + check_is_fitted(self) y_pred_proba_k = np.asarray( Parallel(n_jobs=self.n_jobs, verbose=self.verbose)( From c670d220433b682cb8ac30d79a1a6b412b89d277 Mon Sep 17 00:00:00 2001 From: Hassan Maissoro Date: Thu, 20 Nov 2025 19:47:42 +0100 Subject: [PATCH 05/55] replace sk_leatn's check_is_fitted --- mapie/regression/time_series_regression.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/mapie/regression/time_series_regression.py b/mapie/regression/time_series_regression.py index 1e7a2ad76..c5ac26b14 100644 --- a/mapie/regression/time_series_regression.py +++ b/mapie/regression/time_series_regression.py @@ -6,7 +6,6 @@ from numpy.typing import ArrayLike, NDArray from sklearn.base import RegressorMixin from sklearn.model_selection import BaseCrossValidator -from sklearn.utils.validation import check_is_fitted from mapie.conformity_scores import BaseRegressionScore from mapie.regression.regression import _MapieRegressor @@ -14,6 +13,7 @@ _check_alpha, _check_gamma, _transform_confidence_level_to_alpha_list, + check_is_fitted, ) @@ -161,7 +161,7 @@ def _update_conformity_scores_with_ensemble( If the length of ``y`` is greater than the length of the training set. """ - check_is_fitted(self, self.fit_attributes) + check_is_fitted(self) X, y = cast(NDArray, X), cast(NDArray, y) m, n = len(X), len(self.conformity_scores_) if m > n: @@ -282,7 +282,7 @@ def adapt_conformal_inference( f"not with '{self.method}'." ) - check_is_fitted(self, self.fit_attributes) + check_is_fitted(self) _check_gamma(gamma) X, y = cast(NDArray, X), cast(NDArray, y) From b60f15e099081a423f0cbf4f9916fe8f4d16abc5 Mon Sep 17 00:00:00 2001 From: Hassan Maissoro Date: Thu, 20 Nov 2025 19:48:07 +0100 Subject: [PATCH 06/55] remove sk_leatn's check_is_fitted --- mapie/regression/quantile_regression.py | 33 +++++++++++++++---------- 1 file changed, 20 insertions(+), 13 deletions(-) diff --git a/mapie/regression/quantile_regression.py b/mapie/regression/quantile_regression.py index 1e2170810..2f86dbc70 100644 --- a/mapie/regression/quantile_regression.py +++ b/mapie/regression/quantile_regression.py @@ -1,36 +1,36 @@ from __future__ import annotations -from typing import Iterable, List, Optional, Tuple, Union, cast, Any +from typing import Any, Iterable, List, Optional, Tuple, Union, cast import numpy as np +from numpy.typing import ArrayLike, NDArray from sklearn.base import RegressorMixin, clone from sklearn.linear_model import QuantileRegressor from sklearn.model_selection import train_test_split from sklearn.pipeline import Pipeline from sklearn.utils import check_random_state -from sklearn.utils.validation import _check_y, _num_samples, check_is_fitted, indexable +from sklearn.utils.validation import _check_y, _num_samples, indexable +from sklearn.utils.validation import check_is_fitted as sk_check_is_fitted -from numpy.typing import ArrayLike, NDArray from mapie.utils import ( + _cast_predictions_to_ndarray_tuple, _check_alpha_and_n_samples, _check_defined_variables_predict_cqr, _check_estimator_fit_predict, _check_lower_upper_bounds, _check_null_weight, _fit_estimator, -) - -from .regression import _MapieRegressor -from mapie.utils import ( - _cast_predictions_to_ndarray_tuple, - _prepare_params, _prepare_fit_params_and_sample_weight, - _raise_error_if_previous_method_not_called, - _raise_error_if_method_already_called, + _prepare_params, _raise_error_if_fit_called_in_prefit_mode, + _raise_error_if_method_already_called, + _raise_error_if_previous_method_not_called, _transform_confidence_level_to_alpha, + check_is_fitted, ) +from .regression import _MapieRegressor + class ConformalizedQuantileRegressor: """ @@ -428,6 +428,11 @@ def __init__( ) self.cv = cv self.alpha = alpha + self._is_fitted = False + + @property + def is_fitted(self): + return self._is_fitted def _check_alpha( self, @@ -668,7 +673,7 @@ def _check_prefit_params( if len(estimator) == 3: for est in estimator: _check_estimator_fit_predict(est) - check_is_fitted(est) + sk_check_is_fitted(est) else: raise ValueError( "You need to have provided 3 different estimators, they" @@ -789,6 +794,8 @@ def fit( self.conformalize(X_calib, y_calib) + self._is_fitted = True + return self def _initialize_fit_conformalize(self) -> None: @@ -961,7 +968,7 @@ def predict( - [:, 0, :]: Lower bound of the prediction interval. - [:, 1, :]: Upper bound of the prediction interval. """ - check_is_fitted(self, self.fit_attributes) + check_is_fitted(self) _check_defined_variables_predict_cqr(ensemble, alpha) alpha = self.alpha if symmetry else self.alpha / 2 _check_alpha_and_n_samples(alpha, self.n_calib_samples) From 92100cc894a49f9b81d5ef9ad2dc816519e0fd0a Mon Sep 17 00:00:00 2001 From: Hassan Maissoro Date: Thu, 20 Nov 2025 19:48:40 +0100 Subject: [PATCH 07/55] replace sk_leatn's check_is_fitted --- mapie/regression/regression.py | 41 ++++++++++++++++++++-------------- 1 file changed, 24 insertions(+), 17 deletions(-) diff --git a/mapie/regression/regression.py b/mapie/regression/regression.py index 60b3d4464..02c8da115 100644 --- a/mapie/regression/regression.py +++ b/mapie/regression/regression.py @@ -3,44 +3,44 @@ from typing import Any, Iterable, Optional, Tuple, Union, cast import numpy as np +from numpy.typing import ArrayLike, NDArray from sklearn.base import BaseEstimator, RegressorMixin, clone from sklearn.linear_model import LinearRegression from sklearn.model_selection import BaseCrossValidator from sklearn.pipeline import Pipeline from sklearn.utils import check_random_state -from sklearn.utils.validation import _check_y, check_is_fitted, indexable +from sklearn.utils.validation import _check_y, indexable +from sklearn.utils.validation import check_is_fitted as sk_check_is_fitted -from numpy.typing import ArrayLike, NDArray from mapie.conformity_scores import BaseRegressionScore, ResidualNormalisedScore from mapie.conformity_scores.utils import ( - check_regression_conformity_score, check_and_select_conformity_score, + check_regression_conformity_score, ) from mapie.estimator.regressor import EnsembleRegressor from mapie.subsample import Subsample from mapie.utils import ( + _cast_point_predictions_to_ndarray, + _cast_predictions_to_ndarray_tuple, _check_alpha, _check_alpha_and_n_samples, _check_cv, + _check_cv_not_string, _check_estimator_fit_predict, + _check_if_param_in_allowed_values, _check_n_features_in, _check_n_jobs, _check_null_weight, + _check_predict_params, _check_verbose, _get_effective_calibration_samples, - _check_predict_params, -) -from mapie.utils import ( - _transform_confidence_level_to_alpha_list, - _check_if_param_in_allowed_values, - _check_cv_not_string, - _cast_point_predictions_to_ndarray, - _cast_predictions_to_ndarray_tuple, - _prepare_params, _prepare_fit_params_and_sample_weight, - _raise_error_if_previous_method_not_called, - _raise_error_if_method_already_called, + _prepare_params, _raise_error_if_fit_called_in_prefit_mode, + _raise_error_if_method_already_called, + _raise_error_if_previous_method_not_called, + _transform_confidence_level_to_alpha_list, + check_is_fitted, ) @@ -1127,6 +1127,11 @@ def __init__( self.verbose = verbose self.conformity_score = conformity_score self.random_state = random_state + self._is_fitted = False + + @property + def is_fitted(self): + return self._is_fitted def _check_parameters(self) -> None: """ @@ -1238,9 +1243,9 @@ def _check_estimator( _check_estimator_fit_predict(estimator) if self.cv == "prefit": if isinstance(estimator, Pipeline): - check_is_fitted(estimator[-1]) + sk_check_is_fitted(estimator[-1]) else: - check_is_fitted(estimator) + sk_check_is_fitted(estimator) return estimator def _check_ensemble( @@ -1400,6 +1405,8 @@ def fit( self.fit_estimator(X, y, sample_weight, groups) self.conformalize(X, y, sample_weight, groups, **kwargs) + self._is_fitted = True + return self def init_fit( @@ -1546,7 +1553,7 @@ def predict( # Checks if hasattr(self, "_predict_params"): _check_predict_params(self._predict_params, predict_params, self.cv) - check_is_fitted(self, self.fit_attributes) + check_is_fitted(self) self._check_ensemble(ensemble) alpha = cast(Optional[NDArray], _check_alpha(alpha)) From 675b95b59ceda2c19a71c77b87f99b225dfa3d51 Mon Sep 17 00:00:00 2001 From: Hassan Maissoro Date: Thu, 20 Nov 2025 19:55:47 +0100 Subject: [PATCH 08/55] remove sklearn dependence --- mapie/classification.py | 37 +++++++++++++++++++------------------ 1 file changed, 19 insertions(+), 18 deletions(-) diff --git a/mapie/classification.py b/mapie/classification.py index b29127a23..9c121e8a3 100644 --- a/mapie/classification.py +++ b/mapie/classification.py @@ -4,49 +4,44 @@ from typing import Any, Iterable, Optional, Tuple, Union, cast import numpy as np +from numpy.typing import ArrayLike, NDArray from sklearn import clone from sklearn.base import BaseEstimator, ClassifierMixin from sklearn.linear_model import LogisticRegression -from sklearn.model_selection import ( - BaseCrossValidator, - BaseShuffleSplit, -) +from sklearn.model_selection import BaseCrossValidator, BaseShuffleSplit from sklearn.preprocessing import LabelEncoder from sklearn.utils import check_random_state -from sklearn.utils.validation import _check_y, check_is_fitted, indexable - -from numpy.typing import ArrayLike, NDArray +from sklearn.utils.validation import _check_y, indexable from mapie.conformity_scores import BaseClassificationScore from mapie.conformity_scores.sets.raps import RAPSConformityScore from mapie.conformity_scores.utils import ( + check_and_select_conformity_score, check_classification_conformity_score, check_target, - check_and_select_conformity_score, ) from mapie.estimator.classifier import EnsembleClassifier from mapie.utils import ( + _cast_point_predictions_to_ndarray, + _cast_predictions_to_ndarray_tuple, _check_alpha, _check_alpha_and_n_samples, _check_cv, + _check_cv_not_string, _check_estimator_classification, _check_n_features_in, _check_n_jobs, _check_null_weight, _check_predict_params, _check_verbose, - check_proba_normalized, -) -from mapie.utils import ( - _transform_confidence_level_to_alpha_list, + _prepare_fit_params_and_sample_weight, + _prepare_params, _raise_error_if_fit_called_in_prefit_mode, _raise_error_if_method_already_called, - _prepare_params, _raise_error_if_previous_method_not_called, - _cast_predictions_to_ndarray_tuple, - _cast_point_predictions_to_ndarray, - _check_cv_not_string, - _prepare_fit_params_and_sample_weight, + _transform_confidence_level_to_alpha_list, + check_is_fitted, + check_proba_normalized, ) @@ -734,6 +729,11 @@ def __init__( self.conformity_score = conformity_score self.random_state = random_state self.verbose = verbose + self._is_fitted = False + + @property + def is_fitted(self): + return self._is_fitted def _check_parameters(self) -> None: """ @@ -967,6 +967,7 @@ def fit( groups=groups, predict_params=predict_params, ) + self._is_fitted = True return self def predict( @@ -1049,7 +1050,7 @@ def predict( if hasattr(self, "_predict_params"): _check_predict_params(self._predict_params, predict_params, self.cv) - check_is_fitted(self, self.fit_attributes) + check_is_fitted(self) alpha = cast(Optional[NDArray], _check_alpha(alpha)) # Estimate predictions From 29fae7a7d94d2a8af919254dd4fa3a268c00176d Mon Sep 17 00:00:00 2001 From: Hassan Maissoro Date: Thu, 20 Nov 2025 19:56:08 +0100 Subject: [PATCH 09/55] use mapie check_is_fitted function --- mapie/tests/test_classification.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/mapie/tests/test_classification.py b/mapie/tests/test_classification.py index 5a29b064d..61555d817 100644 --- a/mapie/tests/test_classification.py +++ b/mapie/tests/test_classification.py @@ -23,7 +23,6 @@ ) from sklearn.pipeline import Pipeline, make_pipeline from sklearn.preprocessing import OneHotEncoder -from sklearn.utils.validation import check_is_fitted from typing_extensions import TypedDict from mapie.classification import _MapieClassifier @@ -36,7 +35,7 @@ TopKConformityScore, ) from mapie.metrics.classification import classification_coverage_score -from mapie.utils import check_proba_normalized +from mapie.utils import check_is_fitted, check_proba_normalized random_state = 42 @@ -856,7 +855,7 @@ def test_valid_conformity_score(conformity_score: BaseClassificationScore) -> No conformity_score=conformity_score, cv="prefit", random_state=random_state ) mapie_clf.fit(X, y) - check_is_fitted(mapie_clf, mapie_clf.fit_attributes) + check_is_fitted(mapie_clf) @pytest.mark.parametrize( From ea484b411a4cb28a0a7d06be92b86c809b643c78 Mon Sep 17 00:00:00 2001 From: Hassan Maissoro Date: Thu, 20 Nov 2025 19:58:46 +0100 Subject: [PATCH 10/55] use mapie function --- mapie/tests/test_quantile_regression.py | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/mapie/tests/test_quantile_regression.py b/mapie/tests/test_quantile_regression.py index d2ea6f534..8d933ccab 100644 --- a/mapie/tests/test_quantile_regression.py +++ b/mapie/tests/test_quantile_regression.py @@ -6,6 +6,7 @@ import numpy as np import pandas as pd import pytest +from numpy.typing import NDArray from sklearn.base import BaseEstimator, RegressorMixin, clone from sklearn.compose import ColumnTransformer from sklearn.datasets import make_regression @@ -15,14 +16,11 @@ from sklearn.model_selection import KFold, LeaveOneOut, train_test_split from sklearn.pipeline import Pipeline, make_pipeline from sklearn.preprocessing import OneHotEncoder -from sklearn.utils.validation import check_is_fitted from typing_extensions import TypedDict -from numpy.typing import NDArray -from mapie.metrics.regression import ( - regression_coverage_score, -) +from mapie.metrics.regression import regression_coverage_score from mapie.regression.quantile_regression import _MapieQuantileRegressor +from mapie.utils import check_is_fitted X_toy = np.array( [0, 1, 2, 3, 4, 5, 0, 1, 2, 3, 4, 5, 0, 1, 2, 3, 4, 5, 0, 1, 2, 3, 4, 5] @@ -183,7 +181,7 @@ def test_valid_method(strategy: str, estimator: RegressorMixin) -> None: """Test that valid strategies and estimators raise no error""" mapie_reg = _MapieQuantileRegressor(estimator=estimator, **STRATEGIES[strategy]) mapie_reg.fit(X_train_toy, y_train_toy, X_calib=X_calib_toy, y_calib=y_calib_toy) - check_is_fitted(mapie_reg, mapie_reg.fit_attributes) + check_is_fitted(mapie_reg) assert mapie_reg.__dict__["method"] == "quantile" From a908c70ccd4c640a5d0e91f68902d75e55a80105 Mon Sep 17 00:00:00 2001 From: Hassan Maissoro Date: Thu, 20 Nov 2025 20:02:41 +0100 Subject: [PATCH 11/55] replace by mapie function --- mapie/tests/test_regression.py | 17 +++++++---------- 1 file changed, 7 insertions(+), 10 deletions(-) diff --git a/mapie/tests/test_regression.py b/mapie/tests/test_regression.py index 708800bdc..aba143c10 100644 --- a/mapie/tests/test_regression.py +++ b/mapie/tests/test_regression.py @@ -6,8 +6,8 @@ import numpy as np import pandas as pd import pytest +from numpy.typing import NDArray from scipy.stats import ttest_1samp - from sklearn.compose import ColumnTransformer from sklearn.datasets import make_regression from sklearn.dummy import DummyRegressor @@ -17,19 +17,17 @@ from sklearn.model_selection import ( GroupKFold, KFold, + LeaveOneGroupOut, LeaveOneOut, + LeavePGroupsOut, PredefinedSplit, ShuffleSplit, train_test_split, - LeaveOneGroupOut, - LeavePGroupsOut, ) from sklearn.pipeline import Pipeline, make_pipeline from sklearn.preprocessing import OneHotEncoder -from sklearn.utils.validation import check_is_fitted from typing_extensions import TypedDict -from numpy.typing import NDArray from mapie.aggregation_functions import aggregate_all from mapie.conformity_scores import ( AbsoluteConformityScore, @@ -38,14 +36,13 @@ ResidualNormalisedScore, ) from mapie.estimator.regressor import EnsembleRegressor -from mapie.metrics.regression import ( - regression_coverage_score, -) +from mapie.metrics.regression import regression_coverage_score from mapie.regression.regression import ( - _MapieRegressor, JackknifeAfterBootstrapRegressor, + _MapieRegressor, ) from mapie.subsample import Subsample +from mapie.utils import check_is_fitted class TestCheckAndConvertResamplingToCv: @@ -254,7 +251,7 @@ def test_valid_method(method: str) -> None: """Test that valid methods raise no errors.""" mapie_reg = _MapieRegressor(method=method) mapie_reg.fit(X_toy, y_toy) - check_is_fitted(mapie_reg, mapie_reg.fit_attributes) + check_is_fitted(mapie_reg) @pytest.mark.parametrize("agg_function", ["dummy", 0, 1, 2.5, [1, 2]]) From e12a55a52f263a1e7d73f6966467073a9b4206a6 Mon Sep 17 00:00:00 2001 From: Hassan Maissoro Date: Thu, 20 Nov 2025 20:47:14 +0100 Subject: [PATCH 12/55] reduce comment --- mapie/utils.py | 24 ++++++++---------------- 1 file changed, 8 insertions(+), 16 deletions(-) diff --git a/mapie/utils.py b/mapie/utils.py index 3c6cdde12..f29de2a65 100644 --- a/mapie/utils.py +++ b/mapie/utils.py @@ -1,9 +1,14 @@ +import copy import logging import warnings +from collections.abc import Iterable as IterableType +from decimal import Decimal from inspect import signature +from math import isclose from typing import Any, Iterable, Optional, Tuple, Union, cast import numpy as np +from numpy.typing import ArrayLike, NDArray from sklearn.base import ClassifierMixin, RegressorMixin from sklearn.linear_model import LogisticRegression from sklearn.model_selection import ( @@ -17,18 +22,8 @@ from sklearn.pipeline import Pipeline from sklearn.utils import _safe_indexing from sklearn.utils.multiclass import type_of_target -from sklearn.utils.validation import ( - _check_sample_weight, - _num_features, - check_is_fitted as sk_check_is_fitted, - column_or_1d, -) - -from numpy.typing import ArrayLike, NDArray -import copy -from collections.abc import Iterable as IterableType -from decimal import Decimal -from math import isclose +from sklearn.utils.validation import _check_sample_weight, _num_features, column_or_1d +from sklearn.utils.validation import check_is_fitted as sk_check_is_fitted # This function is the only public utility of MAPIE as of v1 release @@ -1644,7 +1639,4 @@ class NotFittedError(ValueError): def check_is_fitted(obj): if not getattr(obj, "_is_fitted", False): - raise NotFittedError( - f"{obj.__class__.__name__} is not fitted yet. " - "Call .fit() before using this method." - ) + raise NotFittedError(f"{obj.__class__.__name__} is not fitted yet. ") From b873640ff151f3fa661c634ba6a0ce36f2f915f9 Mon Sep 17 00:00:00 2001 From: Hassan Maissoro Date: Thu, 20 Nov 2025 20:52:14 +0100 Subject: [PATCH 13/55] add test --- mapie/tests/test_utils.py | 46 +++++++++++++++++++++++++++------------ 1 file changed, 32 insertions(+), 14 deletions(-) diff --git a/mapie/tests/test_utils.py b/mapie/tests/test_utils.py index cb33da518..012bbd6d2 100644 --- a/mapie/tests/test_utils.py +++ b/mapie/tests/test_utils.py @@ -8,15 +8,17 @@ import numpy as np import pytest from numpy.random import RandomState +from numpy.typing import ArrayLike, NDArray from sklearn.datasets import make_regression from sklearn.linear_model import LinearRegression from sklearn.model_selection import BaseCrossValidator, KFold, LeaveOneOut, ShuffleSplit -from sklearn.utils.validation import check_is_fitted - -from numpy.typing import ArrayLike, NDArray +from sklearn.utils.validation import check_is_fitted as sk_check_is_fitted from mapie.regression.quantile_regression import _MapieQuantileRegressor from mapie.utils import ( + NotFittedError, + _cast_point_predictions_to_ndarray, + _cast_predictions_to_ndarray_tuple, _check_alpha, _check_alpha_and_n_samples, _check_array_inf, @@ -24,7 +26,9 @@ _check_arrays_length, _check_binary_zero_one, _check_cv, + _check_cv_not_string, _check_gamma, + _check_if_param_in_allowed_values, _check_lower_upper_bounds, _check_n_features_in, _check_n_jobs, @@ -37,18 +41,15 @@ _compute_quantiles, _fit_estimator, _get_binning_groups, - train_conformalize_test_split, - _transform_confidence_level_to_alpha, - _transform_confidence_level_to_alpha_list, - _check_if_param_in_allowed_values, - _check_cv_not_string, - _cast_point_predictions_to_ndarray, - _cast_predictions_to_ndarray_tuple, - _prepare_params, _prepare_fit_params_and_sample_weight, - _raise_error_if_previous_method_not_called, - _raise_error_if_method_already_called, + _prepare_params, _raise_error_if_fit_called_in_prefit_mode, + _raise_error_if_method_already_called, + _raise_error_if_previous_method_not_called, + _transform_confidence_level_to_alpha, + _transform_confidence_level_to_alpha_list, + check_is_fitted, + train_conformalize_test_split, ) @@ -454,7 +455,7 @@ def test_check_null_weight_with_zeros() -> None: def test_fit_estimator(estimator: Any, sample_weight: Optional[NDArray]) -> None: """Test that the returned estimator is always fitted.""" estimator = _fit_estimator(estimator, X_toy, y_toy, sample_weight) - check_is_fitted(estimator) + sk_check_is_fitted(estimator) def test_fit_estimator_sample_weight() -> None: @@ -879,3 +880,20 @@ def test_invalid_n_samples_float(n_samples: float) -> None: ), ): _check_n_samples(X=X, n_samples=n_samples, indices=indices) + + +class DummyModel: + pass + + +def test_check_is_fitted_raises_before_fit(): + model = DummyModel() + with pytest.raises(NotFittedError) as excinfo: + check_is_fitted(model) + assert "DummyModel is not fitted yet" in str(excinfo.value) + + +def test_check_is_fitted_passes_after_fit(): + model = DummyModel() + model._is_fitted = True + check_is_fitted(model) From 545809bc8736b83f27c1926a1a6087b40d84b3ae Mon Sep 17 00:00:00 2001 From: Hassan Maissoro Date: Thu, 20 Nov 2025 20:58:30 +0100 Subject: [PATCH 14/55] remove sklearn's check_is_fitted dependence --- mapie/calibration.py | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/mapie/calibration.py b/mapie/calibration.py index ded5106e7..b6e3cad5b 100644 --- a/mapie/calibration.py +++ b/mapie/calibration.py @@ -4,14 +4,14 @@ from typing import Dict, Optional, Tuple, Union, cast import numpy as np +from numpy.typing import ArrayLike, NDArray from sklearn.base import BaseEstimator, ClassifierMixin, RegressorMixin, clone from sklearn.calibration import _SigmoidCalibration from sklearn.isotonic import IsotonicRegression from sklearn.utils import check_random_state from sklearn.utils.multiclass import type_of_target -from sklearn.utils.validation import _check_y, _num_samples, check_is_fitted, indexable +from sklearn.utils.validation import _check_y, _num_samples, indexable -from numpy.typing import ArrayLike, NDArray from .utils import ( _check_estimator_classification, _check_estimator_fit_predict, @@ -19,6 +19,7 @@ _check_null_weight, _fit_estimator, _get_calib_set, + check_is_fitted, ) @@ -123,6 +124,11 @@ def __init__( self.estimator = estimator self.calibrator = calibrator self.cv = cv + self._is_fitted = False + + @property + def is_fitted(self): + return self._is_fitted def _check_cv( self, @@ -480,6 +486,9 @@ def fit( self.calibrators = self._fit_calibrators( X_calib, y_calib, sw_calib, calibrator ) + + self._is_fitted = True + return self def predict_proba( @@ -501,7 +510,7 @@ def predict_proba( The calibrated score for each max score and zeros at every other position in that line. """ - check_is_fitted(self, self.fit_attributes) + check_is_fitted(self) self.uncalib_pred = self.single_estimator_.predict_proba(X=X) max_prob, y_pred = self._get_labels(X) @@ -537,5 +546,5 @@ def predict( NDArray of shape (n_samples,) The class from the scores. """ - check_is_fitted(self, self.fit_attributes) + check_is_fitted(self) return self.single_estimator_.predict(X) From 33c548e8d86e5d654ab54c42d0462d708c1030c8 Mon Sep 17 00:00:00 2001 From: Hassan Maissoro Date: Thu, 20 Nov 2025 20:59:02 +0100 Subject: [PATCH 15/55] update --- mapie/utils.py | 1 + 1 file changed, 1 insertion(+) diff --git a/mapie/utils.py b/mapie/utils.py index f29de2a65..ff6b3da23 100644 --- a/mapie/utils.py +++ b/mapie/utils.py @@ -1633,6 +1633,7 @@ def _raise_error_if_fit_called_in_prefit_mode( "Use the conformalize method directly after instanciation." ) + class NotFittedError(ValueError): pass From 0c31be17bf4c7de594f011e7fb7c44444d1cda62 Mon Sep 17 00:00:00 2001 From: Hassan Maissoro Date: Fri, 21 Nov 2025 14:06:05 +0100 Subject: [PATCH 16/55] more self._is_fitted bool --- mapie/estimator/regressor.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/mapie/estimator/regressor.py b/mapie/estimator/regressor.py index 33af502a9..54b0072b3 100644 --- a/mapie/estimator/regressor.py +++ b/mapie/estimator/regressor.py @@ -446,8 +446,6 @@ def fit( self.fit_multi_estimators(X, y, sample_weight, groups, **fit_params) - self._is_fitted = True - return self def fit_multi_estimators( @@ -490,6 +488,8 @@ def fit_multi_estimators( self.estimators_ = estimators + self._is_fitted = True + return self def fit_single_estimator( @@ -523,6 +523,9 @@ def fit_single_estimator( ) self.single_estimator_ = single_estimator_ + + self._is_fitted = True + return self def predict( From 9d7431e7faa6a92bb25380aefd7f6267dbe5cdf1 Mon Sep 17 00:00:00 2001 From: Hassan Maissoro Date: Fri, 21 Nov 2025 14:07:43 +0100 Subject: [PATCH 17/55] add is_fitted checking --- mapie/regression/quantile_regression.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/mapie/regression/quantile_regression.py b/mapie/regression/quantile_regression.py index 2f86dbc70..422862467 100644 --- a/mapie/regression/quantile_regression.py +++ b/mapie/regression/quantile_regression.py @@ -809,6 +809,8 @@ def _initialize_and_check_prefit_estimators(self) -> None: self.estimators_ = list(estimator) self.single_estimator_ = self.estimators_[2] + self._is_fitted = True + def _prepare_train_calib( self, X: ArrayLike, @@ -883,6 +885,8 @@ def _fit_estimators( ) ) + self._is_fitted = True + self.single_estimator_ = self.estimators_[2] def conformalize( From f59d422dbdf1f2bf2f955d54a2bc3b5745ad684e Mon Sep 17 00:00:00 2001 From: Hassan Maissoro Date: Fri, 21 Nov 2025 14:08:14 +0100 Subject: [PATCH 18/55] import mapie NotFittedError --- mapie/tests/test_common.py | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/mapie/tests/test_common.py b/mapie/tests/test_common.py index 8f70a39b6..3393df407 100644 --- a/mapie/tests/test_common.py +++ b/mapie/tests/test_common.py @@ -4,29 +4,29 @@ import numpy as np import pytest from sklearn.base import BaseEstimator -from sklearn.datasets import make_regression, make_classification -from sklearn.dummy import DummyRegressor, DummyClassifier -from sklearn.exceptions import NotFittedError +from sklearn.datasets import make_classification, make_regression +from sklearn.dummy import DummyClassifier, DummyRegressor from sklearn.linear_model import LinearRegression, LogisticRegression, QuantileRegressor from sklearn.model_selection import KFold, train_test_split from sklearn.pipeline import make_pipeline from sklearn.utils.validation import check_is_fitted from mapie.classification import ( - _MapieClassifier, - SplitConformalClassifier, CrossConformalClassifier, + SplitConformalClassifier, + _MapieClassifier, +) +from mapie.regression.quantile_regression import ( + ConformalizedQuantileRegressor, + _MapieQuantileRegressor, ) from mapie.regression.regression import ( - _MapieRegressor, - SplitConformalRegressor, CrossConformalRegressor, JackknifeAfterBootstrapRegressor, + SplitConformalRegressor, + _MapieRegressor, ) -from mapie.regression.quantile_regression import ( - _MapieQuantileRegressor, - ConformalizedQuantileRegressor, -) +from mapie.utils import NotFittedError RANDOM_STATE = 1 From cedfd6e0d6ee1fa14a844eb4fb95a310fed0a211 Mon Sep 17 00:00:00 2001 From: Hassan Maissoro Date: Fri, 21 Nov 2025 14:11:01 +0100 Subject: [PATCH 19/55] import sklearn's NotFittedError as sk_NotFittedError --- mapie/tests/test_common.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/mapie/tests/test_common.py b/mapie/tests/test_common.py index 3393df407..4c4c6755c 100644 --- a/mapie/tests/test_common.py +++ b/mapie/tests/test_common.py @@ -26,6 +26,7 @@ SplitConformalRegressor, _MapieRegressor, ) +from sklearn.exceptions import NotFittedError as sk_NotFittedError from mapie.utils import NotFittedError RANDOM_STATE = 1 @@ -320,7 +321,7 @@ def test_invalid_prefit_estimator(pack: Tuple[BaseEstimator, BaseEstimator]) -> """Test that non-fitted estimator with prefit cv raise errors.""" MapieEstimator, estimator = pack mapie_estimator = MapieEstimator(estimator=estimator, cv="prefit") - with pytest.raises(NotFittedError): + with pytest.raises(sk_NotFittedError): mapie_estimator.fit(X_toy, y_toy) From ee5183486cecb9a4b64e58c60526a1b880d03213 Mon Sep 17 00:00:00 2001 From: Hassan Maissoro Date: Fri, 21 Nov 2025 14:21:46 +0100 Subject: [PATCH 20/55] Update HISTORY.rst --- HISTORY.rst | 1 + 1 file changed, 1 insertion(+) diff --git a/HISTORY.rst b/HISTORY.rst index 60193661b..acb6eace2 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -4,6 +4,7 @@ History 1.x.x (2025-xx-xx) ------------------ +* Remove dependency of internal classes on sklearn's check_is_fitted 1.2.0 (2025-11-17) ------------------ From c91d5323b9cab91ab16a1339bb060ab3dda4e311 Mon Sep 17 00:00:00 2001 From: Hassan Maissoro Date: Fri, 21 Nov 2025 14:42:45 +0100 Subject: [PATCH 21/55] add docstring --- mapie/utils.py | 1 + 1 file changed, 1 insertion(+) diff --git a/mapie/utils.py b/mapie/utils.py index ff6b3da23..6dc6f6493 100644 --- a/mapie/utils.py +++ b/mapie/utils.py @@ -1639,5 +1639,6 @@ class NotFittedError(ValueError): def check_is_fitted(obj): + """Check that _is_fitted attribute is True""" if not getattr(obj, "_is_fitted", False): raise NotFittedError(f"{obj.__class__.__name__} is not fitted yet. ") From 6135b076cd17e821656163a878a97cc9c3daf9a2 Mon Sep 17 00:00:00 2001 From: Hassan Maissoro Date: Fri, 21 Nov 2025 14:52:47 +0100 Subject: [PATCH 22/55] add docstring --- mapie/calibration.py | 1 + mapie/estimator/classifier.py | 1 + mapie/estimator/regressor.py | 1 + mapie/regression/quantile_regression.py | 1 + mapie/regression/regression.py | 1 + 5 files changed, 5 insertions(+) diff --git a/mapie/calibration.py b/mapie/calibration.py index b6e3cad5b..02586486a 100644 --- a/mapie/calibration.py +++ b/mapie/calibration.py @@ -128,6 +128,7 @@ def __init__( @property def is_fitted(self): + """Returns True if the estimator is fitted""" return self._is_fitted def _check_cv( diff --git a/mapie/estimator/classifier.py b/mapie/estimator/classifier.py index 6f95aa1be..81d7e4303 100644 --- a/mapie/estimator/classifier.py +++ b/mapie/estimator/classifier.py @@ -130,6 +130,7 @@ def __init__( @property def is_fitted(self): + """Returns True if the estimator is fitted""" return self._is_fitted @staticmethod diff --git a/mapie/estimator/regressor.py b/mapie/estimator/regressor.py index 54b0072b3..9b2d5d535 100644 --- a/mapie/estimator/regressor.py +++ b/mapie/estimator/regressor.py @@ -174,6 +174,7 @@ def __init__( @property def is_fitted(self): + """Returns True if the estimator is fitted""" return self._is_fitted @staticmethod diff --git a/mapie/regression/quantile_regression.py b/mapie/regression/quantile_regression.py index 422862467..52b595519 100644 --- a/mapie/regression/quantile_regression.py +++ b/mapie/regression/quantile_regression.py @@ -432,6 +432,7 @@ def __init__( @property def is_fitted(self): + """Returns True if the estimator is fitted""" return self._is_fitted def _check_alpha( diff --git a/mapie/regression/regression.py b/mapie/regression/regression.py index 02c8da115..d1013c3ea 100644 --- a/mapie/regression/regression.py +++ b/mapie/regression/regression.py @@ -1131,6 +1131,7 @@ def __init__( @property def is_fitted(self): + """Returns True if the estimator is fitted""" return self._is_fitted def _check_parameters(self) -> None: From d434c437a80f84419c158e671b11e482c3e7ce07 Mon Sep 17 00:00:00 2001 From: Hassan Maissoro Date: Fri, 21 Nov 2025 14:57:04 +0100 Subject: [PATCH 23/55] add docstring --- mapie/classification.py | 1 + 1 file changed, 1 insertion(+) diff --git a/mapie/classification.py b/mapie/classification.py index 9c121e8a3..3d5075c4d 100644 --- a/mapie/classification.py +++ b/mapie/classification.py @@ -733,6 +733,7 @@ def __init__( @property def is_fitted(self): + """Returns True if the estimator is fitted""" return self._is_fitted def _check_parameters(self) -> None: From 08f183f88ceade8e3774871998621e91e35e75bf Mon Sep 17 00:00:00 2001 From: Hassan Maissoro Date: Fri, 21 Nov 2025 16:03:15 +0100 Subject: [PATCH 24/55] no cover --- mapie/calibration.py | 2 +- mapie/classification.py | 2 +- mapie/estimator/classifier.py | 2 +- mapie/estimator/regressor.py | 2 +- mapie/regression/quantile_regression.py | 2 +- mapie/regression/regression.py | 2 +- 6 files changed, 6 insertions(+), 6 deletions(-) diff --git a/mapie/calibration.py b/mapie/calibration.py index 02586486a..f46afeac9 100644 --- a/mapie/calibration.py +++ b/mapie/calibration.py @@ -127,7 +127,7 @@ def __init__( self._is_fitted = False @property - def is_fitted(self): + def is_fitted(self): # pragma: no cover """Returns True if the estimator is fitted""" return self._is_fitted diff --git a/mapie/classification.py b/mapie/classification.py index 3d5075c4d..9c80064d9 100644 --- a/mapie/classification.py +++ b/mapie/classification.py @@ -732,7 +732,7 @@ def __init__( self._is_fitted = False @property - def is_fitted(self): + def is_fitted(self): # pragma: no cover """Returns True if the estimator is fitted""" return self._is_fitted diff --git a/mapie/estimator/classifier.py b/mapie/estimator/classifier.py index 81d7e4303..268ac84b5 100644 --- a/mapie/estimator/classifier.py +++ b/mapie/estimator/classifier.py @@ -129,7 +129,7 @@ def __init__( self._is_fitted = False @property - def is_fitted(self): + def is_fitted(self): # pragma: no cover """Returns True if the estimator is fitted""" return self._is_fitted diff --git a/mapie/estimator/regressor.py b/mapie/estimator/regressor.py index 9b2d5d535..bdaf0a863 100644 --- a/mapie/estimator/regressor.py +++ b/mapie/estimator/regressor.py @@ -173,7 +173,7 @@ def __init__( self._is_fitted = False @property - def is_fitted(self): + def is_fitted(self): # pragma: no cover """Returns True if the estimator is fitted""" return self._is_fitted diff --git a/mapie/regression/quantile_regression.py b/mapie/regression/quantile_regression.py index 52b595519..313e219ac 100644 --- a/mapie/regression/quantile_regression.py +++ b/mapie/regression/quantile_regression.py @@ -431,7 +431,7 @@ def __init__( self._is_fitted = False @property - def is_fitted(self): + def is_fitted(self): # pragma: no cover """Returns True if the estimator is fitted""" return self._is_fitted diff --git a/mapie/regression/regression.py b/mapie/regression/regression.py index d1013c3ea..3f81ea101 100644 --- a/mapie/regression/regression.py +++ b/mapie/regression/regression.py @@ -1130,7 +1130,7 @@ def __init__( self._is_fitted = False @property - def is_fitted(self): + def is_fitted(self): # pragma: no cover """Returns True if the estimator is fitted""" return self._is_fitted From 0026e075e8000f860af5e07124a06e24b5709cea Mon Sep 17 00:00:00 2001 From: Hassan Maissoro Date: Tue, 25 Nov 2025 11:00:25 +0100 Subject: [PATCH 25/55] test is_fitted parameter directly --- mapie/utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mapie/utils.py b/mapie/utils.py index 6dc6f6493..ce9e0290c 100644 --- a/mapie/utils.py +++ b/mapie/utils.py @@ -1640,5 +1640,5 @@ class NotFittedError(ValueError): def check_is_fitted(obj): """Check that _is_fitted attribute is True""" - if not getattr(obj, "_is_fitted", False): + if not getattr(obj, "is_fitted", False): raise NotFittedError(f"{obj.__class__.__name__} is not fitted yet. ") From b24a45abe2558cb8711eebc7984f0bdf57ccaa68 Mon Sep 17 00:00:00 2001 From: Hassan Maissoro Date: Tue, 25 Nov 2025 11:16:58 +0100 Subject: [PATCH 26/55] is_fitted is now covered --- mapie/calibration.py | 2 +- mapie/classification.py | 2 +- mapie/estimator/classifier.py | 2 +- mapie/estimator/regressor.py | 2 +- mapie/regression/quantile_regression.py | 2 +- mapie/regression/regression.py | 2 +- 6 files changed, 6 insertions(+), 6 deletions(-) diff --git a/mapie/calibration.py b/mapie/calibration.py index f46afeac9..02586486a 100644 --- a/mapie/calibration.py +++ b/mapie/calibration.py @@ -127,7 +127,7 @@ def __init__( self._is_fitted = False @property - def is_fitted(self): # pragma: no cover + def is_fitted(self): """Returns True if the estimator is fitted""" return self._is_fitted diff --git a/mapie/classification.py b/mapie/classification.py index 9c80064d9..3d5075c4d 100644 --- a/mapie/classification.py +++ b/mapie/classification.py @@ -732,7 +732,7 @@ def __init__( self._is_fitted = False @property - def is_fitted(self): # pragma: no cover + def is_fitted(self): """Returns True if the estimator is fitted""" return self._is_fitted diff --git a/mapie/estimator/classifier.py b/mapie/estimator/classifier.py index 268ac84b5..81d7e4303 100644 --- a/mapie/estimator/classifier.py +++ b/mapie/estimator/classifier.py @@ -129,7 +129,7 @@ def __init__( self._is_fitted = False @property - def is_fitted(self): # pragma: no cover + def is_fitted(self): """Returns True if the estimator is fitted""" return self._is_fitted diff --git a/mapie/estimator/regressor.py b/mapie/estimator/regressor.py index bdaf0a863..9b2d5d535 100644 --- a/mapie/estimator/regressor.py +++ b/mapie/estimator/regressor.py @@ -173,7 +173,7 @@ def __init__( self._is_fitted = False @property - def is_fitted(self): # pragma: no cover + def is_fitted(self): """Returns True if the estimator is fitted""" return self._is_fitted diff --git a/mapie/regression/quantile_regression.py b/mapie/regression/quantile_regression.py index 313e219ac..52b595519 100644 --- a/mapie/regression/quantile_regression.py +++ b/mapie/regression/quantile_regression.py @@ -431,7 +431,7 @@ def __init__( self._is_fitted = False @property - def is_fitted(self): # pragma: no cover + def is_fitted(self): """Returns True if the estimator is fitted""" return self._is_fitted diff --git a/mapie/regression/regression.py b/mapie/regression/regression.py index 3f81ea101..d1013c3ea 100644 --- a/mapie/regression/regression.py +++ b/mapie/regression/regression.py @@ -1130,7 +1130,7 @@ def __init__( self._is_fitted = False @property - def is_fitted(self): # pragma: no cover + def is_fitted(self): """Returns True if the estimator is fitted""" return self._is_fitted From aa4f42311f946a8c74a9b719d1bb58fa0e25be4b Mon Sep 17 00:00:00 2001 From: Hassan Maissoro Date: Tue, 25 Nov 2025 14:08:08 +0100 Subject: [PATCH 27/55] correct test --- mapie/tests/test_utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mapie/tests/test_utils.py b/mapie/tests/test_utils.py index 012bbd6d2..28055a878 100644 --- a/mapie/tests/test_utils.py +++ b/mapie/tests/test_utils.py @@ -895,5 +895,5 @@ def test_check_is_fitted_raises_before_fit(): def test_check_is_fitted_passes_after_fit(): model = DummyModel() - model._is_fitted = True + model.is_fitted = True check_is_fitted(model) From 05b23be98f6568667db851958a458d2dadc3f29b Mon Sep 17 00:00:00 2001 From: Hassan Maissoro Date: Tue, 25 Nov 2025 16:58:19 +0100 Subject: [PATCH 28/55] user_model_check : add check function --- mapie/utils.py | 46 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 46 insertions(+) diff --git a/mapie/utils.py b/mapie/utils.py index ce9e0290c..232a2a0d4 100644 --- a/mapie/utils.py +++ b/mapie/utils.py @@ -1642,3 +1642,49 @@ def check_is_fitted(obj): """Check that _is_fitted attribute is True""" if not getattr(obj, "is_fitted", False): raise NotFittedError(f"{obj.__class__.__name__} is not fitted yet. ") + + +FIT_INDICATORS = [ + "n_features_in_", + "classes_", + "coef_", + "feature_names_in_", + "tree_", + "estimators_", +] + + +def check_user_model_is_fitted(estimator): + """ + Check whether a user-provided estimator is fitted. + + Logic: + 1. Raise error if no typical fit-related attributes are present. + 2. If `n_features_in_` exists try a minimal predict-probe. Else, assume fitted but warn. + """ + present_attrs = [attr for attr in FIT_INDICATORS if hasattr(estimator, attr)] + + if not present_attrs: + raise NotFittedError( + "Estimator does not appear fitted. " + f"Missing expected attributes: {FIT_INDICATORS}." + ) + + if hasattr(estimator, "n_features_in_"): + try: + estimator.predict(np.zeros((1, estimator.n_features_in_))) + return True + except Exception as err: + warnings.warn( + f"Estimator has `n_features_in_` but failed a minimal prediction test " + f"(shape={(1, estimator.n_features_in_)}). Error: {err}", + UserWarning, + ) + return True + + warnings.warn( + f"Estimator exposes fitted-like attributes {present_attrs} but lacks " + "`n_features_in_`.", + UserWarning, + ) + return True From 5857eb18e7c34101cee928049af3caa30f7ea6c6 Mon Sep 17 00:00:00 2001 From: Hassan Maissoro Date: Tue, 25 Nov 2025 18:27:01 +0100 Subject: [PATCH 29/55] user_model_check : add tests --- mapie/tests/test_utils.py | 59 ++++++++++++++++++++++++++++++++++++++- 1 file changed, 58 insertions(+), 1 deletion(-) diff --git a/mapie/tests/test_utils.py b/mapie/tests/test_utils.py index 28055a878..cd97f6cbc 100644 --- a/mapie/tests/test_utils.py +++ b/mapie/tests/test_utils.py @@ -10,7 +10,7 @@ from numpy.random import RandomState from numpy.typing import ArrayLike, NDArray from sklearn.datasets import make_regression -from sklearn.linear_model import LinearRegression +from sklearn.linear_model import LinearRegression, LogisticRegression from sklearn.model_selection import BaseCrossValidator, KFold, LeaveOneOut, ShuffleSplit from sklearn.utils.validation import check_is_fitted as sk_check_is_fitted @@ -49,6 +49,7 @@ _transform_confidence_level_to_alpha, _transform_confidence_level_to_alpha_list, check_is_fitted, + check_user_model_is_fitted, train_conformalize_test_split, ) @@ -897,3 +898,59 @@ def test_check_is_fitted_passes_after_fit(): model = DummyModel() model.is_fitted = True check_is_fitted(model) + + +def test_check_user_model_is_fitted_unfitted(): + model = DummyModel() + with pytest.raises(NotFittedError): + check_user_model_is_fitted(model) + + +def test_check_user_model_is_fitted_raises_for_unfitted_model(): + model = LinearRegression() + with pytest.raises(NotFittedError): + check_user_model_is_fitted(model) + + +@pytest.mark.parametrize("Model", [LinearRegression, LogisticRegression]) +def test_check_user_model_is_fitted_sklearn_models(Model): + """Check that sklearn classifiers and regressors pass.""" + X = np.random.randn(20, 4) + y = ( + (np.random.randn(20) > 0).astype(int) + if Model is LogisticRegression + else np.random.randn(20) + ) + model = Model().fit(X, y) + assert check_user_model_is_fitted(model) is True + + +class DummyFittedNoFeatures: + """A fake estimator that mimics a fitted model but without n_features_in_.""" + + def __init__(self): + self.coef_ = np.array([1.0]) + + def predict(self, X): + return np.array([0.0]) + + +def test_check_user_model_fitted_no_n_features_in(): + model = DummyFittedNoFeatures() + with pytest.warns(UserWarning): + assert check_user_model_is_fitted(model) is True + + +class PartiallyFitted: + def __init__(self): + self.coef_ = np.array([1, 2, 3]) + + +def test_check_user_model_is_fitted_partial_fit_warning(): + """ + Test that a partially fitted user model triggers a UserWarning + but still returns True from check_user_model_is_fitted. + """ + model = PartiallyFitted() + with pytest.warns(UserWarning): + assert check_user_model_is_fitted(model) is True \ No newline at end of file From ed1cfb37aed9a6d38278fac595d419022892cafb Mon Sep 17 00:00:00 2001 From: Hassan Maissoro Date: Tue, 25 Nov 2025 18:32:27 +0100 Subject: [PATCH 30/55] replace sklearn's check_is_fitted --- mapie/regression/quantile_regression.py | 4 ++-- mapie/tests/test_quantile_regression.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/mapie/regression/quantile_regression.py b/mapie/regression/quantile_regression.py index 52b595519..07c12c232 100644 --- a/mapie/regression/quantile_regression.py +++ b/mapie/regression/quantile_regression.py @@ -10,7 +10,6 @@ from sklearn.pipeline import Pipeline from sklearn.utils import check_random_state from sklearn.utils.validation import _check_y, _num_samples, indexable -from sklearn.utils.validation import check_is_fitted as sk_check_is_fitted from mapie.utils import ( _cast_predictions_to_ndarray_tuple, @@ -27,6 +26,7 @@ _raise_error_if_previous_method_not_called, _transform_confidence_level_to_alpha, check_is_fitted, + check_user_model_is_fitted, ) from .regression import _MapieRegressor @@ -674,7 +674,7 @@ def _check_prefit_params( if len(estimator) == 3: for est in estimator: _check_estimator_fit_predict(est) - sk_check_is_fitted(est) + check_user_model_is_fitted(est) else: raise ValueError( "You need to have provided 3 different estimators, they" diff --git a/mapie/tests/test_quantile_regression.py b/mapie/tests/test_quantile_regression.py index 8d933ccab..9d795d6b0 100644 --- a/mapie/tests/test_quantile_regression.py +++ b/mapie/tests/test_quantile_regression.py @@ -467,7 +467,7 @@ def test_non_trained_estimator() -> None: """ with pytest.raises( ValueError, - match=r".*instance is not fitted yet. Call 'fit' with appropriate*", + match=r".*Missing expected attributes.*", ): gb_trained1, gb_trained2, gb_trained3 = clone(gb), clone(gb), clone(gb) gb_trained1.fit(X_train, y_train) From 6b443eb37d49bfc9c8c133f898183e9f07163e0b Mon Sep 17 00:00:00 2001 From: Hassan Maissoro Date: Tue, 25 Nov 2025 18:35:35 +0100 Subject: [PATCH 31/55] replace sklearn's check_is_fitted --- mapie/regression/regression.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/mapie/regression/regression.py b/mapie/regression/regression.py index d1013c3ea..80b3553a8 100644 --- a/mapie/regression/regression.py +++ b/mapie/regression/regression.py @@ -10,7 +10,6 @@ from sklearn.pipeline import Pipeline from sklearn.utils import check_random_state from sklearn.utils.validation import _check_y, indexable -from sklearn.utils.validation import check_is_fitted as sk_check_is_fitted from mapie.conformity_scores import BaseRegressionScore, ResidualNormalisedScore from mapie.conformity_scores.utils import ( @@ -41,6 +40,7 @@ _raise_error_if_previous_method_not_called, _transform_confidence_level_to_alpha_list, check_is_fitted, + check_user_model_is_fitted, ) @@ -1244,9 +1244,9 @@ def _check_estimator( _check_estimator_fit_predict(estimator) if self.cv == "prefit": if isinstance(estimator, Pipeline): - sk_check_is_fitted(estimator[-1]) + check_user_model_is_fitted(estimator[-1]) else: - sk_check_is_fitted(estimator) + check_user_model_is_fitted(estimator) return estimator def _check_ensemble( From 49acf1a39b07980358438fe16ba882e72d4ad340 Mon Sep 17 00:00:00 2001 From: Hassan Maissoro Date: Tue, 25 Nov 2025 18:45:50 +0100 Subject: [PATCH 32/55] replace sklearn's check_is_fitted --- mapie/tests/test_utils.py | 6 ++++-- mapie/utils.py | 1 + 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/mapie/tests/test_utils.py b/mapie/tests/test_utils.py index cd97f6cbc..7f55bff73 100644 --- a/mapie/tests/test_utils.py +++ b/mapie/tests/test_utils.py @@ -12,7 +12,6 @@ from sklearn.datasets import make_regression from sklearn.linear_model import LinearRegression, LogisticRegression from sklearn.model_selection import BaseCrossValidator, KFold, LeaveOneOut, ShuffleSplit -from sklearn.utils.validation import check_is_fitted as sk_check_is_fitted from mapie.regression.quantile_regression import _MapieQuantileRegressor from mapie.utils import ( @@ -451,12 +450,15 @@ def test_check_null_weight_with_zeros() -> None: np.testing.assert_almost_equal(np.array(y_out), np.array([7, 9, 11, 13, 15])) +@pytest.mark.filterwarnings( + "ignore:Estimator exposes fitted-like attributes.*:UserWarning" +) @pytest.mark.parametrize("estimator", [LinearRegression(), DumbEstimator()]) @pytest.mark.parametrize("sample_weight", [None, np.ones_like(y_toy)]) def test_fit_estimator(estimator: Any, sample_weight: Optional[NDArray]) -> None: """Test that the returned estimator is always fitted.""" estimator = _fit_estimator(estimator, X_toy, y_toy, sample_weight) - sk_check_is_fitted(estimator) + check_user_model_is_fitted(estimator) def test_fit_estimator_sample_weight() -> None: diff --git a/mapie/utils.py b/mapie/utils.py index 232a2a0d4..8e978dbed 100644 --- a/mapie/utils.py +++ b/mapie/utils.py @@ -1651,6 +1651,7 @@ def check_is_fitted(obj): "feature_names_in_", "tree_", "estimators_", + "fitted_", ] From 5d17fec41b3824dad225bc887abe935166b1f8f9 Mon Sep 17 00:00:00 2001 From: Hassan Maissoro Date: Tue, 25 Nov 2025 18:48:16 +0100 Subject: [PATCH 33/55] replace sklearn's check_is_fitted --- mapie/tests/test_common.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/mapie/tests/test_common.py b/mapie/tests/test_common.py index 4c4c6755c..805027df0 100644 --- a/mapie/tests/test_common.py +++ b/mapie/tests/test_common.py @@ -6,10 +6,10 @@ from sklearn.base import BaseEstimator from sklearn.datasets import make_classification, make_regression from sklearn.dummy import DummyClassifier, DummyRegressor +from sklearn.exceptions import NotFittedError as sk_NotFittedError from sklearn.linear_model import LinearRegression, LogisticRegression, QuantileRegressor from sklearn.model_selection import KFold, train_test_split from sklearn.pipeline import make_pipeline -from sklearn.utils.validation import check_is_fitted from mapie.classification import ( CrossConformalClassifier, @@ -26,8 +26,7 @@ SplitConformalRegressor, _MapieRegressor, ) -from sklearn.exceptions import NotFittedError as sk_NotFittedError -from mapie.utils import NotFittedError +from mapie.utils import NotFittedError, check_user_model_is_fitted RANDOM_STATE = 1 @@ -332,7 +331,7 @@ def test_valid_prefit_estimator(pack: Tuple[BaseEstimator, BaseEstimator]) -> No estimator.fit(X_toy, y_toy) mapie_estimator = MapieEstimator(estimator=estimator, cv="prefit") mapie_estimator.fit(X_toy, y_toy) - check_is_fitted(mapie_estimator, mapie_estimator.fit_attributes) + check_user_model_is_fitted(mapie_estimator) assert mapie_estimator.n_features_in_ == 1 From cb19dada49ec2c689a04afd886ea659ae4398725 Mon Sep 17 00:00:00 2001 From: Hassan Maissoro Date: Wed, 26 Nov 2025 09:51:22 +0100 Subject: [PATCH 34/55] add newline at end of file --- mapie/tests/test_utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mapie/tests/test_utils.py b/mapie/tests/test_utils.py index 7f55bff73..0c6929d4e 100644 --- a/mapie/tests/test_utils.py +++ b/mapie/tests/test_utils.py @@ -955,4 +955,4 @@ def test_check_user_model_is_fitted_partial_fit_warning(): """ model = PartiallyFitted() with pytest.warns(UserWarning): - assert check_user_model_is_fitted(model) is True \ No newline at end of file + assert check_user_model_is_fitted(model) is True From e8fc5d08da34b28eee00076eb8ff14fc268d76d9 Mon Sep 17 00:00:00 2001 From: Hassan Maissoro Date: Wed, 26 Nov 2025 10:11:55 +0100 Subject: [PATCH 35/55] accept two expectations --- mapie/tests/test_common.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/mapie/tests/test_common.py b/mapie/tests/test_common.py index 805027df0..09a308d96 100644 --- a/mapie/tests/test_common.py +++ b/mapie/tests/test_common.py @@ -5,8 +5,8 @@ import pytest from sklearn.base import BaseEstimator from sklearn.datasets import make_classification, make_regression +from sklearn.exceptions import NotFittedError as Sk_NotFittedError from sklearn.dummy import DummyClassifier, DummyRegressor -from sklearn.exceptions import NotFittedError as sk_NotFittedError from sklearn.linear_model import LinearRegression, LogisticRegression, QuantileRegressor from sklearn.model_selection import KFold, train_test_split from sklearn.pipeline import make_pipeline @@ -320,7 +320,7 @@ def test_invalid_prefit_estimator(pack: Tuple[BaseEstimator, BaseEstimator]) -> """Test that non-fitted estimator with prefit cv raise errors.""" MapieEstimator, estimator = pack mapie_estimator = MapieEstimator(estimator=estimator, cv="prefit") - with pytest.raises(sk_NotFittedError): + with pytest.raises((NotFittedError, Sk_NotFittedError)): mapie_estimator.fit(X_toy, y_toy) From f9488bd80169f415c4c53a4368ee40ecc3badb0f Mon Sep 17 00:00:00 2001 From: Hassan Maissoro Date: Wed, 26 Nov 2025 15:03:16 +0100 Subject: [PATCH 36/55] filter the warning since it is expected --- mapie/tests/test_quantile_regression.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/mapie/tests/test_quantile_regression.py b/mapie/tests/test_quantile_regression.py index 9d795d6b0..d50a1ea9e 100644 --- a/mapie/tests/test_quantile_regression.py +++ b/mapie/tests/test_quantile_regression.py @@ -461,13 +461,14 @@ def test_prefit_no_fit_predict() -> None: mapie_reg.fit(X_calib, y_calib) +@pytest.mark.filterwarnings("ignore:Estimator does not appear fitted.*:UserWarning") def test_non_trained_estimator() -> None: """ Check that the estimators are all already trained when used in prefit. """ with pytest.raises( ValueError, - match=r".*Missing expected attributes.*", + match=r".*instance is not fitted yet. Call 'fit' with appropriate*", ): gb_trained1, gb_trained2, gb_trained3 = clone(gb), clone(gb), clone(gb) gb_trained1.fit(X_train, y_train) From d42ad5bb368acafbec5a761b29650d9c415566e8 Mon Sep 17 00:00:00 2001 From: Hassan Maissoro Date: Wed, 26 Nov 2025 15:03:53 +0100 Subject: [PATCH 37/55] change implementation --- mapie/utils.py | 21 ++++++++------------- 1 file changed, 8 insertions(+), 13 deletions(-) diff --git a/mapie/utils.py b/mapie/utils.py index 8e978dbed..de3ba8dec 100644 --- a/mapie/utils.py +++ b/mapie/utils.py @@ -1660,32 +1660,27 @@ def check_user_model_is_fitted(estimator): Check whether a user-provided estimator is fitted. Logic: - 1. Raise error if no typical fit-related attributes are present. - 2. If `n_features_in_` exists try a minimal predict-probe. Else, assume fitted but warn. + 1. Raise warning if no typical fit-related attributes are present. + 2. If `n_features_in_` exists try a minimal predict-probe and raise error if it fails. """ present_attrs = [attr for attr in FIT_INDICATORS if hasattr(estimator, attr)] if not present_attrs: - raise NotFittedError( + warnings.warn( "Estimator does not appear fitted. " - f"Missing expected attributes: {FIT_INDICATORS}." + f"At least one of the expected attributes is missing in : {FIT_INDICATORS}.", + UserWarning, ) if hasattr(estimator, "n_features_in_"): try: + if "Pipeline" in str(type(estimator)): + estimator = list(estimator.named_steps.values())[-1] estimator.predict(np.zeros((1, estimator.n_features_in_))) return True except Exception as err: - warnings.warn( + raise NotFittedError( f"Estimator has `n_features_in_` but failed a minimal prediction test " f"(shape={(1, estimator.n_features_in_)}). Error: {err}", - UserWarning, ) - return True - - warnings.warn( - f"Estimator exposes fitted-like attributes {present_attrs} but lacks " - "`n_features_in_`.", - UserWarning, - ) return True From bfb623e514341cb61a29758e7554a98e1890dc09 Mon Sep 17 00:00:00 2001 From: Hassan Maissoro Date: Wed, 26 Nov 2025 15:04:40 +0100 Subject: [PATCH 38/55] update check_use_model_is_fitted's function is fitter --- mapie/tests/test_utils.py | 49 +++++++++++---------------------------- 1 file changed, 13 insertions(+), 36 deletions(-) diff --git a/mapie/tests/test_utils.py b/mapie/tests/test_utils.py index 0c6929d4e..6d0564556 100644 --- a/mapie/tests/test_utils.py +++ b/mapie/tests/test_utils.py @@ -12,6 +12,7 @@ from sklearn.datasets import make_regression from sklearn.linear_model import LinearRegression, LogisticRegression from sklearn.model_selection import BaseCrossValidator, KFold, LeaveOneOut, ShuffleSplit +from sklearn.pipeline import Pipeline from mapie.regression.quantile_regression import _MapieQuantileRegressor from mapie.utils import ( @@ -904,55 +905,31 @@ def test_check_is_fitted_passes_after_fit(): def test_check_user_model_is_fitted_unfitted(): model = DummyModel() - with pytest.raises(NotFittedError): + with pytest.warns(UserWarning, match=r".*Estimator does not appear fitted.*"): check_user_model_is_fitted(model) def test_check_user_model_is_fitted_raises_for_unfitted_model(): model = LinearRegression() - with pytest.raises(NotFittedError): + with pytest.warns(UserWarning, match=r".*Estimator does not appear fitted.*"): check_user_model_is_fitted(model) -@pytest.mark.parametrize("Model", [LinearRegression, LogisticRegression]) +@pytest.mark.parametrize( + "Model", + [ + LinearRegression(), + LogisticRegression(), + Pipeline([("LinearRegression", LinearRegression())]), + ], +) def test_check_user_model_is_fitted_sklearn_models(Model): """Check that sklearn classifiers and regressors pass.""" X = np.random.randn(20, 4) y = ( (np.random.randn(20) > 0).astype(int) - if Model is LogisticRegression + if isinstance(Model, LogisticRegression) else np.random.randn(20) ) - model = Model().fit(X, y) + model = Model.fit(X, y) assert check_user_model_is_fitted(model) is True - - -class DummyFittedNoFeatures: - """A fake estimator that mimics a fitted model but without n_features_in_.""" - - def __init__(self): - self.coef_ = np.array([1.0]) - - def predict(self, X): - return np.array([0.0]) - - -def test_check_user_model_fitted_no_n_features_in(): - model = DummyFittedNoFeatures() - with pytest.warns(UserWarning): - assert check_user_model_is_fitted(model) is True - - -class PartiallyFitted: - def __init__(self): - self.coef_ = np.array([1, 2, 3]) - - -def test_check_user_model_is_fitted_partial_fit_warning(): - """ - Test that a partially fitted user model triggers a UserWarning - but still returns True from check_user_model_is_fitted. - """ - model = PartiallyFitted() - with pytest.warns(UserWarning): - assert check_user_model_is_fitted(model) is True From 88d2bc8cda9402e890c4e5226198f8ec92e0ee29 Mon Sep 17 00:00:00 2001 From: Hassan Maissoro Date: Wed, 26 Nov 2025 15:12:19 +0100 Subject: [PATCH 39/55] filter expected warning and remove sklearn's NotFittedError dependence --- mapie/tests/test_common.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/mapie/tests/test_common.py b/mapie/tests/test_common.py index 09a308d96..e8d6035ab 100644 --- a/mapie/tests/test_common.py +++ b/mapie/tests/test_common.py @@ -5,7 +5,6 @@ import pytest from sklearn.base import BaseEstimator from sklearn.datasets import make_classification, make_regression -from sklearn.exceptions import NotFittedError as Sk_NotFittedError from sklearn.dummy import DummyClassifier, DummyRegressor from sklearn.linear_model import LinearRegression, LogisticRegression, QuantileRegressor from sklearn.model_selection import KFold, train_test_split @@ -315,12 +314,16 @@ def test_invalid_estimator(MapieEstimator: BaseEstimator, estimator: Any) -> Non mapie_estimator.fit(X_toy, y_toy) +@pytest.mark.filterwarnings("ignore:Estimator does not appear fitted.*:UserWarning") @pytest.mark.parametrize("pack", MapieTestEstimators()) def test_invalid_prefit_estimator(pack: Tuple[BaseEstimator, BaseEstimator]) -> None: """Test that non-fitted estimator with prefit cv raise errors.""" MapieEstimator, estimator = pack mapie_estimator = MapieEstimator(estimator=estimator, cv="prefit") - with pytest.raises((NotFittedError, Sk_NotFittedError)): + with pytest.raises( + ValueError, + match=r".*instance is not fitted yet. Call 'fit' with appropriate*", + ): mapie_estimator.fit(X_toy, y_toy) From 24f7c5392f6a80805fe51c034bd2f16f0e57e5de Mon Sep 17 00:00:00 2001 From: Hassan Maissoro Date: Wed, 26 Nov 2025 15:38:17 +0100 Subject: [PATCH 40/55] remove unnecessary check_is_fitted --- mapie/classification.py | 2 -- mapie/regression/regression.py | 2 -- 2 files changed, 4 deletions(-) diff --git a/mapie/classification.py b/mapie/classification.py index 3d5075c4d..720b14fa4 100644 --- a/mapie/classification.py +++ b/mapie/classification.py @@ -40,7 +40,6 @@ _raise_error_if_method_already_called, _raise_error_if_previous_method_not_called, _transform_confidence_level_to_alpha_list, - check_is_fitted, check_proba_normalized, ) @@ -1051,7 +1050,6 @@ def predict( if hasattr(self, "_predict_params"): _check_predict_params(self._predict_params, predict_params, self.cv) - check_is_fitted(self) alpha = cast(Optional[NDArray], _check_alpha(alpha)) # Estimate predictions diff --git a/mapie/regression/regression.py b/mapie/regression/regression.py index 80b3553a8..0d05fe20e 100644 --- a/mapie/regression/regression.py +++ b/mapie/regression/regression.py @@ -39,7 +39,6 @@ _raise_error_if_method_already_called, _raise_error_if_previous_method_not_called, _transform_confidence_level_to_alpha_list, - check_is_fitted, check_user_model_is_fitted, ) @@ -1554,7 +1553,6 @@ def predict( # Checks if hasattr(self, "_predict_params"): _check_predict_params(self._predict_params, predict_params, self.cv) - check_is_fitted(self) self._check_ensemble(ensemble) alpha = cast(Optional[NDArray], _check_alpha(alpha)) From ac478f93fab6fb28e36c75f697a32b6584a229c1 Mon Sep 17 00:00:00 2001 From: Hassan Maissoro Date: Wed, 26 Nov 2025 17:44:44 +0100 Subject: [PATCH 41/55] leave check_is_fitted in .predict methods because of tests --- mapie/classification.py | 2 ++ mapie/regression/regression.py | 2 ++ 2 files changed, 4 insertions(+) diff --git a/mapie/classification.py b/mapie/classification.py index 720b14fa4..3d5075c4d 100644 --- a/mapie/classification.py +++ b/mapie/classification.py @@ -40,6 +40,7 @@ _raise_error_if_method_already_called, _raise_error_if_previous_method_not_called, _transform_confidence_level_to_alpha_list, + check_is_fitted, check_proba_normalized, ) @@ -1050,6 +1051,7 @@ def predict( if hasattr(self, "_predict_params"): _check_predict_params(self._predict_params, predict_params, self.cv) + check_is_fitted(self) alpha = cast(Optional[NDArray], _check_alpha(alpha)) # Estimate predictions diff --git a/mapie/regression/regression.py b/mapie/regression/regression.py index 0d05fe20e..80b3553a8 100644 --- a/mapie/regression/regression.py +++ b/mapie/regression/regression.py @@ -39,6 +39,7 @@ _raise_error_if_method_already_called, _raise_error_if_previous_method_not_called, _transform_confidence_level_to_alpha_list, + check_is_fitted, check_user_model_is_fitted, ) @@ -1553,6 +1554,7 @@ def predict( # Checks if hasattr(self, "_predict_params"): _check_predict_params(self._predict_params, predict_params, self.cv) + check_is_fitted(self) self._check_ensemble(ensemble) alpha = cast(Optional[NDArray], _check_alpha(alpha)) From aa28ede7025174bbcf7d3892e7443f057d2426f5 Mon Sep 17 00:00:00 2001 From: Hassan Maissoro Date: Wed, 26 Nov 2025 18:55:35 +0100 Subject: [PATCH 42/55] change regex --- mapie/tests/test_common.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mapie/tests/test_common.py b/mapie/tests/test_common.py index e8d6035ab..8653abfa6 100644 --- a/mapie/tests/test_common.py +++ b/mapie/tests/test_common.py @@ -322,7 +322,7 @@ def test_invalid_prefit_estimator(pack: Tuple[BaseEstimator, BaseEstimator]) -> mapie_estimator = MapieEstimator(estimator=estimator, cv="prefit") with pytest.raises( ValueError, - match=r".*instance is not fitted yet. Call 'fit' with appropriate*", + match=r".*not fitted.*", ): mapie_estimator.fit(X_toy, y_toy) From 31527e6b124fc42e87f62f607c52e1dda51c6082 Mon Sep 17 00:00:00 2001 From: Hassan Maissoro Date: Wed, 26 Nov 2025 19:35:47 +0100 Subject: [PATCH 43/55] add test for coverage --- mapie/tests/test_utils.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/mapie/tests/test_utils.py b/mapie/tests/test_utils.py index 6d0564556..edbea8c21 100644 --- a/mapie/tests/test_utils.py +++ b/mapie/tests/test_utils.py @@ -933,3 +933,15 @@ def test_check_user_model_is_fitted_sklearn_models(Model): ) model = Model.fit(X, y) assert check_user_model_is_fitted(model) is True + +class BrokenPredictModel: + """Model with n_features_in_ but predict always fails""" + n_features_in_ = 3 + def predict(self, X): + raise RuntimeError("Predict failure") + + +def test_check_user_model_is_fitted_predict_fails(): + model = BrokenPredictModel() + with pytest.raises(NotFittedError, match=r"Estimator has `n_features_in_` but failed"): + check_user_model_is_fitted(model) From afa79043218c07dad67592beef11fbaa3f4c1f25 Mon Sep 17 00:00:00 2001 From: Hassan Maissoro Date: Wed, 26 Nov 2025 19:40:07 +0100 Subject: [PATCH 44/55] formatting --- mapie/tests/test_utils.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/mapie/tests/test_utils.py b/mapie/tests/test_utils.py index edbea8c21..f68768ca5 100644 --- a/mapie/tests/test_utils.py +++ b/mapie/tests/test_utils.py @@ -934,14 +934,19 @@ def test_check_user_model_is_fitted_sklearn_models(Model): model = Model.fit(X, y) assert check_user_model_is_fitted(model) is True + class BrokenPredictModel: """Model with n_features_in_ but predict always fails""" + n_features_in_ = 3 + def predict(self, X): raise RuntimeError("Predict failure") def test_check_user_model_is_fitted_predict_fails(): model = BrokenPredictModel() - with pytest.raises(NotFittedError, match=r"Estimator has `n_features_in_` but failed"): + with pytest.raises( + NotFittedError, match=r"Estimator has `n_features_in_` but failed" + ): check_user_model_is_fitted(model) From 82f851671bd9287f9dcf0daf98643ef206c5836f Mon Sep 17 00:00:00 2001 From: Hassan Maissoro Date: Thu, 27 Nov 2025 13:13:10 +0100 Subject: [PATCH 45/55] remove remaining sklearn's check_is_fitted dependence --- mapie/tests/test_common.py | 4 ++-- mapie/utils.py | 29 ++++++++++++++++------------- 2 files changed, 18 insertions(+), 15 deletions(-) diff --git a/mapie/tests/test_common.py b/mapie/tests/test_common.py index 8653abfa6..fcc9e18c4 100644 --- a/mapie/tests/test_common.py +++ b/mapie/tests/test_common.py @@ -321,8 +321,8 @@ def test_invalid_prefit_estimator(pack: Tuple[BaseEstimator, BaseEstimator]) -> MapieEstimator, estimator = pack mapie_estimator = MapieEstimator(estimator=estimator, cv="prefit") with pytest.raises( - ValueError, - match=r".*not fitted.*", + (AttributeError, ValueError), + match=r".*(does not contain 'classes_'|is not fitted).*", ): mapie_estimator.fit(X_toy, y_toy) diff --git a/mapie/utils.py b/mapie/utils.py index de3ba8dec..f10c3ea14 100644 --- a/mapie/utils.py +++ b/mapie/utils.py @@ -23,7 +23,6 @@ from sklearn.utils import _safe_indexing from sklearn.utils.multiclass import type_of_target from sklearn.utils.validation import _check_sample_weight, _num_features, column_or_1d -from sklearn.utils.validation import check_is_fitted as sk_check_is_fitted # This function is the only public utility of MAPIE as of v1 release @@ -282,12 +281,13 @@ def _fit_estimator( -------- >>> import numpy as np >>> from sklearn.linear_model import LinearRegression - >>> from sklearn.utils.validation import check_is_fitted as sk_check_is_fitted + >>> from mapie.utils import check_user_model_is_fitted >>> X = np.array([[0], [1], [2], [3], [4], [5]]) >>> y = np.array([5, 7, 9, 11, 13, 15]) >>> estimator = LinearRegression() >>> estimator = _fit_estimator(estimator, X, y) - >>> sk_check_is_fitted(estimator) + >>> check_user_model_is_fitted(estimator) + True """ fit_parameters = signature(estimator.fit).parameters supports_sw = "sample_weight" in fit_parameters @@ -1007,13 +1007,8 @@ def _check_estimator_classification( "predict, and predict_proba methods." ) if cv == "prefit": - sk_check_is_fitted(est) - if not hasattr(est, "classes_"): - raise AttributeError( - "Invalid classifier. " - "Fitted classifier does not contain " - "'classes_' attribute." - ) + check_user_model_is_fitted(est) + return estimator @@ -1660,9 +1655,17 @@ def check_user_model_is_fitted(estimator): Check whether a user-provided estimator is fitted. Logic: - 1. Raise warning if no typical fit-related attributes are present. - 2. If `n_features_in_` exists try a minimal predict-probe and raise error if it fails. - """ + 1. Raise AttributeError for classifiers missing 'classes_'. + 2. Raise warning if no typical fit-related attributes are present. + 3. If `n_features_in_` exists, try a minimal predict-probe. + """ + if isinstance(estimator, ClassifierMixin) and not hasattr(estimator, "classes_"): + raise AttributeError( + "Invalid classifier. " + "Fitted classifier does not contain " + "'classes_' attribute." + ) + present_attrs = [attr for attr in FIT_INDICATORS if hasattr(estimator, attr)] if not present_attrs: From a4176d4b730a0bd83fbb4a2dfa292bdff4e5a187 Mon Sep 17 00:00:00 2001 From: Hassan Maissoro Date: Thu, 27 Nov 2025 17:42:51 +0100 Subject: [PATCH 46/55] remove sklearn's check_is_fitted --- .../multi_label_classification.py | 24 +++++++++++++++---- 1 file changed, 19 insertions(+), 5 deletions(-) diff --git a/mapie/risk_control/multi_label_classification.py b/mapie/risk_control/multi_label_classification.py index 84e8ba534..64a74fbc8 100644 --- a/mapie/risk_control/multi_label_classification.py +++ b/mapie/risk_control/multi_label_classification.py @@ -12,9 +12,15 @@ from sklearn.multioutput import MultiOutputClassifier from sklearn.pipeline import Pipeline from sklearn.utils import check_random_state -from sklearn.utils.validation import _check_y, _num_samples, check_is_fitted, indexable - -from mapie.utils import _check_alpha, _check_n_jobs, _check_verbose +from sklearn.utils.validation import _check_y, _num_samples, indexable + +from mapie.utils import ( + _check_alpha, + _check_n_jobs, + _check_verbose, + check_is_fitted, + check_user_model_is_fitted, +) from .methods import ( find_lambda_star, @@ -182,6 +188,12 @@ def __init__( self.n_jobs = n_jobs self.random_state = random_state self.verbose = verbose + self._is_fitted = False + + @property + def is_fitted(self): + """Returns True if the estimator is fitted""" + return self._is_fitted def _check_parameters(self) -> None: """ @@ -375,7 +387,7 @@ def _check_estimator( "Please provide a classifier with fit," "predict, and predict_proba methods." ) - check_is_fitted(est) + check_user_model_is_fitted(est) return estimator, X, y def _check_partial_fit_first_call(self) -> bool: @@ -526,6 +538,8 @@ def partial_fit( ) self.risks = np.concatenate([self.risks, partial_risk], axis=0) + self._is_fitted = True + return self def fit( @@ -608,7 +622,7 @@ def predict( self._check_delta(delta) self._check_bound(bound) alpha = cast(Optional[NDArray], _check_alpha(alpha)) - check_is_fitted(self, self.fit_attributes) + check_is_fitted(self) # Estimate prediction sets y_pred = self.single_estimator_.predict(X) From 167661cd35eac22bc18ca4c48b0e68be0e203e65 Mon Sep 17 00:00:00 2001 From: Hassan Maissoro Date: Thu, 27 Nov 2025 17:43:30 +0100 Subject: [PATCH 47/55] remove sklearn's check_is_fitted in tests --- mapie/tests/risk_control/test_precision_recall_control.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/mapie/tests/risk_control/test_precision_recall_control.py b/mapie/tests/risk_control/test_precision_recall_control.py index 61c31cb2f..2f44787d7 100644 --- a/mapie/tests/risk_control/test_precision_recall_control.py +++ b/mapie/tests/risk_control/test_precision_recall_control.py @@ -11,10 +11,10 @@ from sklearn.multioutput import MultiOutputClassifier from sklearn.pipeline import Pipeline, make_pipeline from sklearn.preprocessing import OneHotEncoder -from sklearn.utils.validation import check_is_fitted from typing_extensions import TypedDict from mapie.risk_control import PrecisionRecallController +from mapie.utils import check_is_fitted Params = TypedDict( "Params", @@ -208,7 +208,7 @@ def test_valid_method() -> None: """Test that valid methods raise no errors.""" mapie_clf = PrecisionRecallController(random_state=random_state) mapie_clf.fit(X_toy, y_toy) - check_is_fitted(mapie_clf, mapie_clf.fit_attributes) + check_is_fitted(mapie_clf) @pytest.mark.parametrize("strategy", [*STRATEGIES]) @@ -219,7 +219,7 @@ def test_valid_metric_method(strategy: str) -> None: random_state=random_state, metric_control=args["metric_control"] ) mapie_clf.fit(X_toy, y_toy) - check_is_fitted(mapie_clf, mapie_clf.fit_attributes) + check_is_fitted(mapie_clf) @pytest.mark.parametrize("bound", BOUNDS) @@ -228,7 +228,7 @@ def test_valid_bound(bound: str) -> None: mapie_clf = PrecisionRecallController(random_state=random_state, method="rcps") mapie_clf.fit(X_toy, y_toy) mapie_clf.predict(X_toy, bound=bound, delta=0.1) - check_is_fitted(mapie_clf, mapie_clf.fit_attributes) + check_is_fitted(mapie_clf) @pytest.mark.parametrize("strategy", [*STRATEGIES]) From f32e3f69ba160a4f2f8e7197ef4882bbe60da1a5 Mon Sep 17 00:00:00 2001 From: Hassan Maissoro Date: Thu, 27 Nov 2025 17:43:39 +0100 Subject: [PATCH 48/55] remove sklearn's check_is_fitted --- mapie/conformity_scores/bounds/residuals.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/mapie/conformity_scores/bounds/residuals.py b/mapie/conformity_scores/bounds/residuals.py index 0b5d9ba27..c7ea9607e 100644 --- a/mapie/conformity_scores/bounds/residuals.py +++ b/mapie/conformity_scores/bounds/residuals.py @@ -2,14 +2,15 @@ from typing import Optional, Tuple, Union, cast import numpy as np +from numpy.typing import ArrayLike, NDArray from sklearn.base import RegressorMixin, clone from sklearn.linear_model import LinearRegression from sklearn.model_selection import train_test_split from sklearn.pipeline import Pipeline -from sklearn.utils.validation import check_is_fitted, check_random_state, indexable +from sklearn.utils.validation import check_random_state, indexable -from numpy.typing import ArrayLike, NDArray from mapie.conformity_scores import BaseRegressionScore +from mapie.utils import check_user_model_is_fitted class ResidualNormalisedScore(BaseRegressionScore): @@ -112,9 +113,9 @@ def _check_estimator( ) if self.prefit: if isinstance(estimator, Pipeline): - check_is_fitted(estimator[-1]) + check_user_model_is_fitted(estimator[-1]) else: - check_is_fitted(estimator) + check_user_model_is_fitted(estimator) return estimator def _check_parameters( From e187b964fc4832ab2de71cafceb5e387158893d2 Mon Sep 17 00:00:00 2001 From: Hassan Maissoro Date: Thu, 27 Nov 2025 17:53:27 +0100 Subject: [PATCH 49/55] update the docstring --- mapie/utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mapie/utils.py b/mapie/utils.py index f10c3ea14..4f5a5e430 100644 --- a/mapie/utils.py +++ b/mapie/utils.py @@ -1634,7 +1634,7 @@ class NotFittedError(ValueError): def check_is_fitted(obj): - """Check that _is_fitted attribute is True""" + """Check that .is_fitted property is True""" if not getattr(obj, "is_fitted", False): raise NotFittedError(f"{obj.__class__.__name__} is not fitted yet. ") From 433c83d044cb72f21d62f1baad563803a75fcc38 Mon Sep 17 00:00:00 2001 From: Hassan Maissoro Date: Thu, 27 Nov 2025 18:11:21 +0100 Subject: [PATCH 50/55] add sklearn in the name of check_user_model_is_fitted --- mapie/conformity_scores/bounds/residuals.py | 6 +++--- mapie/regression/quantile_regression.py | 4 ++-- mapie/regression/regression.py | 6 +++--- mapie/risk_control/multi_label_classification.py | 4 ++-- mapie/tests/test_common.py | 4 ++-- mapie/tests/test_utils.py | 12 ++++++------ mapie/utils.py | 4 ++-- 7 files changed, 20 insertions(+), 20 deletions(-) diff --git a/mapie/conformity_scores/bounds/residuals.py b/mapie/conformity_scores/bounds/residuals.py index c7ea9607e..20930bdb2 100644 --- a/mapie/conformity_scores/bounds/residuals.py +++ b/mapie/conformity_scores/bounds/residuals.py @@ -10,7 +10,7 @@ from sklearn.utils.validation import check_random_state, indexable from mapie.conformity_scores import BaseRegressionScore -from mapie.utils import check_user_model_is_fitted +from mapie.utils import check_sklearn_user_model_is_fitted class ResidualNormalisedScore(BaseRegressionScore): @@ -113,9 +113,9 @@ def _check_estimator( ) if self.prefit: if isinstance(estimator, Pipeline): - check_user_model_is_fitted(estimator[-1]) + check_sklearn_user_model_is_fitted(estimator[-1]) else: - check_user_model_is_fitted(estimator) + check_sklearn_user_model_is_fitted(estimator) return estimator def _check_parameters( diff --git a/mapie/regression/quantile_regression.py b/mapie/regression/quantile_regression.py index 07c12c232..a2c784a6a 100644 --- a/mapie/regression/quantile_regression.py +++ b/mapie/regression/quantile_regression.py @@ -26,7 +26,7 @@ _raise_error_if_previous_method_not_called, _transform_confidence_level_to_alpha, check_is_fitted, - check_user_model_is_fitted, + check_sklearn_user_model_is_fitted, ) from .regression import _MapieRegressor @@ -674,7 +674,7 @@ def _check_prefit_params( if len(estimator) == 3: for est in estimator: _check_estimator_fit_predict(est) - check_user_model_is_fitted(est) + check_sklearn_user_model_is_fitted(est) else: raise ValueError( "You need to have provided 3 different estimators, they" diff --git a/mapie/regression/regression.py b/mapie/regression/regression.py index 80b3553a8..b4091cc3a 100644 --- a/mapie/regression/regression.py +++ b/mapie/regression/regression.py @@ -40,7 +40,7 @@ _raise_error_if_previous_method_not_called, _transform_confidence_level_to_alpha_list, check_is_fitted, - check_user_model_is_fitted, + check_sklearn_user_model_is_fitted, ) @@ -1244,9 +1244,9 @@ def _check_estimator( _check_estimator_fit_predict(estimator) if self.cv == "prefit": if isinstance(estimator, Pipeline): - check_user_model_is_fitted(estimator[-1]) + check_sklearn_user_model_is_fitted(estimator[-1]) else: - check_user_model_is_fitted(estimator) + check_sklearn_user_model_is_fitted(estimator) return estimator def _check_ensemble( diff --git a/mapie/risk_control/multi_label_classification.py b/mapie/risk_control/multi_label_classification.py index 64a74fbc8..059fb861f 100644 --- a/mapie/risk_control/multi_label_classification.py +++ b/mapie/risk_control/multi_label_classification.py @@ -19,7 +19,7 @@ _check_n_jobs, _check_verbose, check_is_fitted, - check_user_model_is_fitted, + check_sklearn_user_model_is_fitted, ) from .methods import ( @@ -387,7 +387,7 @@ def _check_estimator( "Please provide a classifier with fit," "predict, and predict_proba methods." ) - check_user_model_is_fitted(est) + check_sklearn_user_model_is_fitted(est) return estimator, X, y def _check_partial_fit_first_call(self) -> bool: diff --git a/mapie/tests/test_common.py b/mapie/tests/test_common.py index fcc9e18c4..f24cc6f72 100644 --- a/mapie/tests/test_common.py +++ b/mapie/tests/test_common.py @@ -25,7 +25,7 @@ SplitConformalRegressor, _MapieRegressor, ) -from mapie.utils import NotFittedError, check_user_model_is_fitted +from mapie.utils import NotFittedError, check_sklearn_user_model_is_fitted RANDOM_STATE = 1 @@ -334,7 +334,7 @@ def test_valid_prefit_estimator(pack: Tuple[BaseEstimator, BaseEstimator]) -> No estimator.fit(X_toy, y_toy) mapie_estimator = MapieEstimator(estimator=estimator, cv="prefit") mapie_estimator.fit(X_toy, y_toy) - check_user_model_is_fitted(mapie_estimator) + check_sklearn_user_model_is_fitted(mapie_estimator) assert mapie_estimator.n_features_in_ == 1 diff --git a/mapie/tests/test_utils.py b/mapie/tests/test_utils.py index f68768ca5..5beedcc66 100644 --- a/mapie/tests/test_utils.py +++ b/mapie/tests/test_utils.py @@ -49,7 +49,7 @@ _transform_confidence_level_to_alpha, _transform_confidence_level_to_alpha_list, check_is_fitted, - check_user_model_is_fitted, + check_sklearn_user_model_is_fitted, train_conformalize_test_split, ) @@ -459,7 +459,7 @@ def test_check_null_weight_with_zeros() -> None: def test_fit_estimator(estimator: Any, sample_weight: Optional[NDArray]) -> None: """Test that the returned estimator is always fitted.""" estimator = _fit_estimator(estimator, X_toy, y_toy, sample_weight) - check_user_model_is_fitted(estimator) + check_sklearn_user_model_is_fitted(estimator) def test_fit_estimator_sample_weight() -> None: @@ -906,13 +906,13 @@ def test_check_is_fitted_passes_after_fit(): def test_check_user_model_is_fitted_unfitted(): model = DummyModel() with pytest.warns(UserWarning, match=r".*Estimator does not appear fitted.*"): - check_user_model_is_fitted(model) + check_sklearn_user_model_is_fitted(model) def test_check_user_model_is_fitted_raises_for_unfitted_model(): model = LinearRegression() with pytest.warns(UserWarning, match=r".*Estimator does not appear fitted.*"): - check_user_model_is_fitted(model) + check_sklearn_user_model_is_fitted(model) @pytest.mark.parametrize( @@ -932,7 +932,7 @@ def test_check_user_model_is_fitted_sklearn_models(Model): else np.random.randn(20) ) model = Model.fit(X, y) - assert check_user_model_is_fitted(model) is True + assert check_sklearn_user_model_is_fitted(model) is True class BrokenPredictModel: @@ -949,4 +949,4 @@ def test_check_user_model_is_fitted_predict_fails(): with pytest.raises( NotFittedError, match=r"Estimator has `n_features_in_` but failed" ): - check_user_model_is_fitted(model) + check_sklearn_user_model_is_fitted(model) diff --git a/mapie/utils.py b/mapie/utils.py index 4f5a5e430..67829c437 100644 --- a/mapie/utils.py +++ b/mapie/utils.py @@ -1007,7 +1007,7 @@ def _check_estimator_classification( "predict, and predict_proba methods." ) if cv == "prefit": - check_user_model_is_fitted(est) + check_sklearn_user_model_is_fitted(est) return estimator @@ -1650,7 +1650,7 @@ def check_is_fitted(obj): ] -def check_user_model_is_fitted(estimator): +def check_sklearn_user_model_is_fitted(estimator): """ Check whether a user-provided estimator is fitted. From e266f9cd2da45a55fb140ad80dabf4142c5c79a2 Mon Sep 17 00:00:00 2001 From: Hassan Maissoro Date: Thu, 27 Nov 2025 18:46:15 +0100 Subject: [PATCH 51/55] update name --- mapie/utils.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/mapie/utils.py b/mapie/utils.py index 67829c437..78ba78d01 100644 --- a/mapie/utils.py +++ b/mapie/utils.py @@ -281,12 +281,12 @@ def _fit_estimator( -------- >>> import numpy as np >>> from sklearn.linear_model import LinearRegression - >>> from mapie.utils import check_user_model_is_fitted + >>> from mapie.utils import check_sklearn_user_model_is_fitted >>> X = np.array([[0], [1], [2], [3], [4], [5]]) >>> y = np.array([5, 7, 9, 11, 13, 15]) >>> estimator = LinearRegression() >>> estimator = _fit_estimator(estimator, X, y) - >>> check_user_model_is_fitted(estimator) + >>> check_sklearn_user_model_is_fitted(estimator) True """ fit_parameters = signature(estimator.fit).parameters From 7cc232590979e715891116e16ed787439f039133 Mon Sep 17 00:00:00 2001 From: Hassan Maissoro Date: Thu, 27 Nov 2025 19:05:33 +0100 Subject: [PATCH 52/55] remove not covered ligne --- mapie/tests/risk_control/test_precision_recall_control.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/mapie/tests/risk_control/test_precision_recall_control.py b/mapie/tests/risk_control/test_precision_recall_control.py index 2f44787d7..08307dfe2 100644 --- a/mapie/tests/risk_control/test_precision_recall_control.py +++ b/mapie/tests/risk_control/test_precision_recall_control.py @@ -163,9 +163,6 @@ def predict_proba(self, X: NDArray, *args: Any) -> NDArray: def predict(self, X: NDArray, *args: Any) -> NDArray: return self.predict_proba(X) >= 0.3 - def __sklearn_is_fitted__(self): - return True - X_toy = np.arange(9).reshape(-1, 1) y_toy = np.stack( From a6e914c119ce8643793cf494667a0003c3d08315 Mon Sep 17 00:00:00 2001 From: Hassan Maissoro Date: Fri, 28 Nov 2025 16:20:59 +0100 Subject: [PATCH 53/55] remove unnecessary flag --- mapie/estimator/regressor.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/mapie/estimator/regressor.py b/mapie/estimator/regressor.py index 9b2d5d535..3fba10bf2 100644 --- a/mapie/estimator/regressor.py +++ b/mapie/estimator/regressor.py @@ -489,8 +489,6 @@ def fit_multi_estimators( self.estimators_ = estimators - self._is_fitted = True - return self def fit_single_estimator( From 47dc18791f0bd692b62bc484585b0a63cf59b908 Mon Sep 17 00:00:00 2001 From: Hassan Maissoro Date: Fri, 28 Nov 2025 16:51:33 +0100 Subject: [PATCH 54/55] Raise warning instead of error --- mapie/tests/test_utils.py | 3 ++- mapie/utils.py | 6 ++++-- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/mapie/tests/test_utils.py b/mapie/tests/test_utils.py index 5beedcc66..109fb31fc 100644 --- a/mapie/tests/test_utils.py +++ b/mapie/tests/test_utils.py @@ -947,6 +947,7 @@ def predict(self, X): def test_check_user_model_is_fitted_predict_fails(): model = BrokenPredictModel() with pytest.raises( - NotFittedError, match=r"Estimator has `n_features_in_` but failed" + UserWarning, + match=r".*has `n_features_in_` but failed a minimal prediction test.*", ): check_sklearn_user_model_is_fitted(model) diff --git a/mapie/utils.py b/mapie/utils.py index 78ba78d01..5182a06c8 100644 --- a/mapie/utils.py +++ b/mapie/utils.py @@ -1682,8 +1682,10 @@ def check_sklearn_user_model_is_fitted(estimator): estimator.predict(np.zeros((1, estimator.n_features_in_))) return True except Exception as err: - raise NotFittedError( - f"Estimator has `n_features_in_` but failed a minimal prediction test " + raise UserWarning( + "Estimator does not appear fitted. " + "It has `n_features_in_` but failed a minimal prediction test " f"(shape={(1, estimator.n_features_in_)}). Error: {err}", + UserWarning, ) return True From 6fabe857e4587ccd0ec6ef6015263caf3b2ad97d Mon Sep 17 00:00:00 2001 From: Hassan Maissoro Date: Fri, 28 Nov 2025 17:20:55 +0100 Subject: [PATCH 55/55] adjust flag usage --- mapie/regression/quantile_regression.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/mapie/regression/quantile_regression.py b/mapie/regression/quantile_regression.py index a2c784a6a..e80e30af1 100644 --- a/mapie/regression/quantile_regression.py +++ b/mapie/regression/quantile_regression.py @@ -429,6 +429,7 @@ def __init__( self.cv = cv self.alpha = alpha self._is_fitted = False + self._is_fitted = True if self.cv == "prefit" else False @property def is_fitted(self): @@ -795,8 +796,6 @@ def fit( self.conformalize(X_calib, y_calib) - self._is_fitted = True - return self def _initialize_fit_conformalize(self) -> None: @@ -810,8 +809,6 @@ def _initialize_and_check_prefit_estimators(self) -> None: self.estimators_ = list(estimator) self.single_estimator_ = self.estimators_[2] - self._is_fitted = True - def _prepare_train_calib( self, X: ArrayLike,