From 690112666078452afe715cd65d0cba0b669525f1 Mon Sep 17 00:00:00 2001 From: Tamara Date: Mon, 4 Nov 2024 13:31:06 +0100 Subject: [PATCH 01/43] Fix _sgd imports --- skops/io/_sklearn.py | 106 +++++++++++++++++++++++++------------------ 1 file changed, 61 insertions(+), 45 deletions(-) diff --git a/skops/io/_sklearn.py b/skops/io/_sklearn.py index 9f6058a1..730f0a45 100644 --- a/skops/io/_sklearn.py +++ b/skops/io/_sklearn.py @@ -12,17 +12,34 @@ from sklearn.covariance._graph_lasso import _DictWithDeprecatedKeys except ImportError: _DictWithDeprecatedKeys = None -from sklearn.linear_model._sgd_fast import ( - EpsilonInsensitive, - Hinge, - Huber, - Log, - LossFunction, - ModifiedHuber, - SquaredEpsilonInsensitive, - SquaredHinge, - SquaredLoss, -) +try: + # TODO: remove once support for sklearn<1.6 is dropped. + from sklearn.linear_model._sgd_fast import ( + EpsilonInsensitive, + Hinge, + Huber, + Log, + LossFunction, + ModifiedHuber, + SquaredEpsilonInsensitive, + SquaredHinge, + SquaredLoss, + ) + + ALLOWED_SGD_LOSSES = { + ModifiedHuber, + Hinge, + SquaredHinge, + Log, + SquaredLoss, + Huber, + EpsilonInsensitive, + SquaredEpsilonInsensitive, + } + SKLEARN_GT_15 = False +except ImportError: + SKLEARN_GT_15 = True + from sklearn.tree._tree import Tree from ._audit import Node, get_tree @@ -30,17 +47,6 @@ from ._utils import LoadContext, SaveContext, get_module, get_state, gettype from .exceptions import UnsupportedTypeException -ALLOWED_SGD_LOSSES = { - ModifiedHuber, - Hinge, - SquaredHinge, - Log, - SquaredLoss, - Huber, - EpsilonInsensitive, - SquaredEpsilonInsensitive, -} - UNSUPPORTED_TYPES = {Birch} @@ -52,7 +58,7 @@ def reduce_get_state(obj: Any, save_context: SaveContext) -> dict[str, Any]: "__module__": get_module(type(obj)), } - # We get the output of __reduce__ and use it to reconstruct the object. + # We get the oPutput of __reduce__ and use it to reconstruct the object. # For security reasons, we don't save the constructor object returned by # __reduce__, and instead use the pre-defined constructor for the object # that we know. This avoids having a function such as `eval()` as the @@ -169,23 +175,25 @@ def sgd_loss_get_state(obj: Any, save_context: SaveContext) -> dict[str, Any]: return state -class SGDNode(ReduceNode): - def __init__( - self, - state: dict[str, Any], - load_context: LoadContext, - trusted: Optional[Sequence[str]] = None, - ) -> None: - # TODO: make sure trusted here makes sense and used. - self.trusted = self._get_trusted( - trusted, [get_module(x) + "." + x.__name__ for x in ALLOWED_SGD_LOSSES] - ) - super().__init__( - state, - load_context, - constructor=gettype(state["__module__"], state["__class__"]), - trusted=self.trusted, - ) +if not SKLEARN_GT_15: + + class SGDNode(ReduceNode): + def __init__( + self, + state: dict[str, Any], + load_context: LoadContext, + trusted: Optional[Sequence[str]] = None, + ) -> None: + # TODO: make sure trusted here makes sense and used. + self.trusted = self._get_trusted( + trusted, [get_module(x) + "." + x.__name__ for x in ALLOWED_SGD_LOSSES] + ) + super().__init__( + state, + load_context, + constructor=gettype(state["__module__"], state["__class__"]), + trusted=self.trusted, + ) # TODO: remove once support for sklearn<1.2 is dropped. @@ -240,17 +248,25 @@ def _construct(self): # tuples of type and function that gets the state of that type GET_STATE_DISPATCH_FUNCTIONS = [ - (LossFunction, sgd_loss_get_state), (Tree, tree_get_state), ] +if not SKLEARN_GT_15: + GET_STATE_DISPATCH_FUNCTIONS.append((LossFunction, sgd_loss_get_state)) + for type_ in UNSUPPORTED_TYPES: GET_STATE_DISPATCH_FUNCTIONS.append((type_, unsupported_get_state)) # tuples of type and function that creates the instance of that type -NODE_TYPE_MAPPING = { - ("SGDNode", PROTOCOL): SGDNode, - ("TreeNode", PROTOCOL): TreeNode, -} +if not SKLEARN_GT_15: + NODE_TYPE_MAPPING = { + ("TreeNode", PROTOCOL): TreeNode, + ("SGDNode", PROTOCOL): SGDNode, + } +else: + NODE_TYPE_MAPPING = { + ("TreeNode", PROTOCOL): TreeNode, + } + # TODO: remove once support for sklearn<1.2 is dropped. # Starting from sklearn 1.2, _DictWithDeprecatedKeys is removed as it's no From 19b71c5cbd7b5b4a193fcc2d0d15ce1e9a434c60 Mon Sep 17 00:00:00 2001 From: Tamara Date: Mon, 4 Nov 2024 13:34:25 +0100 Subject: [PATCH 02/43] Fix _safe_tags import issue --- skops/io/tests/test_persist.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/skops/io/tests/test_persist.py b/skops/io/tests/test_persist.py index 34b4dae6..a9830f2e 100644 --- a/skops/io/tests/test_persist.py +++ b/skops/io/tests/test_persist.py @@ -42,7 +42,7 @@ StandardScaler, ) from sklearn.utils import all_estimators, check_random_state -from sklearn.utils._tags import _safe_tags +from sklearn.utils._tags import get_tags from sklearn.utils._testing import SkipTest, set_random_state from sklearn.utils.estimator_checks import ( _construct_instance, @@ -311,7 +311,7 @@ def get_input(estimator): n_samples=N_SAMPLES, n_features=N_FEATURES, random_state=0 ) y = _enforce_estimator_tags_y(estimator, y) - tags = _safe_tags(estimator) + tags = get_tags(estimator) if tags["pairwise"] is True: return np.random.rand(N_FEATURES, N_FEATURES), None @@ -364,7 +364,7 @@ def test_can_persist_fitted(estimator): set_random_state(estimator, random_state=0) X, y = get_input(estimator) - tags = _safe_tags(estimator) + tags = get_tags(estimator) if tags.get("requires_fit", True): with warnings.catch_warnings(): warnings.filterwarnings("ignore", module="sklearn") @@ -417,7 +417,7 @@ def test_unsupported_type_raises(estimator): set_random_state(estimator, random_state=0) X, y = get_input(estimator) - tags = _safe_tags(estimator) + tags = get_tags(estimator) if tags.get("requires_fit", True): with warnings.catch_warnings(): warnings.filterwarnings("ignore", module="sklearn") From e718fced3ecf6dcb387dced05be5e6dd76954e48 Mon Sep 17 00:00:00 2001 From: Tamara Date: Mon, 4 Nov 2024 13:59:06 +0100 Subject: [PATCH 03/43] Change _construct_instance import --- fig2.png | Bin 0 -> 30148 bytes skops/io/tests/test_persist.py | 12 ++++++++---- 2 files changed, 8 insertions(+), 4 deletions(-) create mode 100644 fig2.png diff --git a/fig2.png b/fig2.png new file mode 100644 index 0000000000000000000000000000000000000000..a0cc7da3da38940ab07034113b2ffa497164367b GIT binary patch literal 30148 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 literal 0 HcmV?d00001 diff --git a/skops/io/tests/test_persist.py b/skops/io/tests/test_persist.py index a9830f2e..b31c3bed 100644 --- a/skops/io/tests/test_persist.py +++ b/skops/io/tests/test_persist.py @@ -43,9 +43,9 @@ ) from sklearn.utils import all_estimators, check_random_state from sklearn.utils._tags import get_tags +from sklearn.utils._test_common.instance_generator import _construct_instances from sklearn.utils._testing import SkipTest, set_random_state from sklearn.utils.estimator_checks import ( - _construct_instance, _enforce_estimator_tags_y, _get_check_estimator_ids, ) @@ -145,9 +145,12 @@ def _tested_estimators(type_filter=None): # scikit-learn < 1.4.0) is not available in scipy >= 1.11.0. The # default solver will be "highs" from scikit-learn >= 1.4.0. # https://scikit-learn.org/stable/modules/generated/sklearn.linear_model.QuantileRegressor.html - estimator = _construct_instance(partial(Estimator, solver="highs")) + estimator = next( + _construct_instances(partial(Estimator, solver="highs")) + ) else: - estimator = _construct_instance(Estimator) + estimator = next(_construct_instances(Estimator)) + # with the kind of data we pass, it needs to be 1 for the few # estimators which have this. if "n_components" in estimator.get_params(): @@ -267,7 +270,8 @@ def _unsupported_estimators(type_filter=None): category=SkipTestWarning, message="Can't instantiate estimator", ) - estimator = _construct_instance(Estimator) + # Get the first instance directly from the generator + estimator = next(_construct_instances(Estimator)) # with the kind of data we pass, it needs to be 1 for the few # estimators which have this. if "n_components" in estimator.get_params(): From b3f64010252a2af10eba6b28186e36763a9c31df Mon Sep 17 00:00:00 2001 From: Tamara Date: Mon, 4 Nov 2024 15:20:38 +0100 Subject: [PATCH 04/43] Change get_tags syntax --- skops/io/tests/test_persist.py | 28 ++++++++++++++-------------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/skops/io/tests/test_persist.py b/skops/io/tests/test_persist.py index b31c3bed..cfffc2f2 100644 --- a/skops/io/tests/test_persist.py +++ b/skops/io/tests/test_persist.py @@ -317,35 +317,35 @@ def get_input(estimator): y = _enforce_estimator_tags_y(estimator, y) tags = get_tags(estimator) - if tags["pairwise"] is True: + if tags.input_tags.pairwise: return np.random.rand(N_FEATURES, N_FEATURES), None - if "2darray" in tags["X_types"]: + if tags.input_tags.two_d_array: # Some models require positive X return np.abs(X), y - if "1darray" in tags["X_types"]: + if tags.input_tags.one_d_array: return X[:, 0], y - if "3darray" in tags["X_types"]: + if tags.input_tags.three_d_array: return load_sample_images().images[1], None - if "1dlabels" in tags["X_types"]: + if tags.target_tags.one_d_labels: # model only expects y return y, None - if "2dlabels" in tags["X_types"]: + if tags.target_tags.two_d_labels: return [(1, 2), (3,)], None - if "categorical" in tags["X_types"]: + if tags.input_tags.categorical: X = [["Male", 1], ["Female", 3], ["Female", 2]] - y = y[: len(X)] if tags["requires_y"] else None + y = y[: len(X)] if tags.target_tags.required else None return X, y - if "dict" in tags["X_types"]: + if tags.input_tags.dict: return [{"foo": 1, "bar": 2}, {"foo": 3, "baz": 1}], None - if "string" in tags["X_types"]: + if tags.input_tags.string: return [ "This is the first document.", "This document is the second document.", @@ -353,11 +353,11 @@ def get_input(estimator): "Is this the first document?", ], None - if tags["X_types"] == "sparse": + if tags.input_tags.sparse: # TfidfTransformer in sklearn 0.24 needs this return sparse.csr_matrix(X), y - raise ValueError(f"Unsupported X type for estimator: {tags['X_types']}") + raise ValueError(f"Unsupported X type for estimator: {tags.input_tags}") @pytest.mark.parametrize( @@ -369,7 +369,7 @@ def test_can_persist_fitted(estimator): X, y = get_input(estimator) tags = get_tags(estimator) - if tags.get("requires_fit", True): + if tags.requires_fit: with warnings.catch_warnings(): warnings.filterwarnings("ignore", module="sklearn") if y is not None: @@ -422,7 +422,7 @@ def test_unsupported_type_raises(estimator): X, y = get_input(estimator) tags = get_tags(estimator) - if tags.get("requires_fit", True): + if tags.requires_fit: with warnings.catch_warnings(): warnings.filterwarnings("ignore", module="sklearn") if y is not None: From dadf4e39ff0dde521fc988ba2e57553b456f3824 Mon Sep 17 00:00:00 2001 From: Tamara Date: Mon, 4 Nov 2024 15:40:18 +0100 Subject: [PATCH 05/43] Ignore FutureWarning in sklearn --- fig2.png | Bin 30148 -> 0 bytes pyproject.toml | 2 ++ 2 files changed, 2 insertions(+) delete mode 100644 fig2.png diff --git a/fig2.png b/fig2.png deleted file mode 100644 index a0cc7da3da38940ab07034113b2ffa497164367b..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 30148 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 diff --git a/pyproject.toml b/pyproject.toml index f697b327..36e7c998 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -25,6 +25,8 @@ filterwarnings = [ "ignore:DataFrameGroupBy.apply operated on the grouping columns.:DeprecationWarning", # Ignore Pandas 2.2 warning on PyArrow. It might be reverted in a later release. "ignore:\\s*Pyarrow will become a required dependency of pandas.*:DeprecationWarning", + # sklearn 1.6 deprecation warning, currently raised in the library itself + "ignore:'force_all_finite' was renamed to 'ensure_all_finite' in 1.6 and will be removed in 1.8.:FutureWarning", ] markers = [ "network: marks tests as requiring internet (deselect with '-m \"not network\"')", From 45af8a075f09be2eadb16a71b5bd0c90b1920fb7 Mon Sep 17 00:00:00 2001 From: Tamara Date: Mon, 11 Nov 2024 12:20:19 +0100 Subject: [PATCH 06/43] Update skops/io/_sklearn.py Co-authored-by: Adrin Jalali --- skops/io/_sklearn.py | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/skops/io/_sklearn.py b/skops/io/_sklearn.py index 730f0a45..da8d99c3 100644 --- a/skops/io/_sklearn.py +++ b/skops/io/_sklearn.py @@ -257,15 +257,13 @@ def _construct(self): GET_STATE_DISPATCH_FUNCTIONS.append((type_, unsupported_get_state)) # tuples of type and function that creates the instance of that type -if not SKLEARN_GT_15: - NODE_TYPE_MAPPING = { - ("TreeNode", PROTOCOL): TreeNode, +NODE_TYPE_MAPPING = { + ("TreeNode", PROTOCOL): TreeNode, +} +if SKLEARN_SGD_LOSS: + NODE_TYPE_MAPPING.update({ ("SGDNode", PROTOCOL): SGDNode, - } -else: - NODE_TYPE_MAPPING = { - ("TreeNode", PROTOCOL): TreeNode, - } + }) # TODO: remove once support for sklearn<1.2 is dropped. From 7abf51ed81be9d79927e7336cc16de9366278d99 Mon Sep 17 00:00:00 2001 From: Tamara Date: Mon, 11 Nov 2024 12:20:41 +0100 Subject: [PATCH 07/43] Update skops/io/_sklearn.py Co-authored-by: Adrin Jalali --- skops/io/_sklearn.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/skops/io/_sklearn.py b/skops/io/_sklearn.py index da8d99c3..e31d0327 100644 --- a/skops/io/_sklearn.py +++ b/skops/io/_sklearn.py @@ -36,7 +36,7 @@ EpsilonInsensitive, SquaredEpsilonInsensitive, } - SKLEARN_GT_15 = False + SKLEARN_SGD_LOSS = True except ImportError: SKLEARN_GT_15 = True From 633247063202c1b691d87fe5c094a6cf13a18d92 Mon Sep 17 00:00:00 2001 From: Tamara Date: Mon, 11 Nov 2024 12:23:37 +0100 Subject: [PATCH 08/43] fix typo --- skops/io/_sklearn.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/skops/io/_sklearn.py b/skops/io/_sklearn.py index e31d0327..57829264 100644 --- a/skops/io/_sklearn.py +++ b/skops/io/_sklearn.py @@ -58,7 +58,7 @@ def reduce_get_state(obj: Any, save_context: SaveContext) -> dict[str, Any]: "__module__": get_module(type(obj)), } - # We get the oPutput of __reduce__ and use it to reconstruct the object. + # We get the output of __reduce__ and use it to reconstruct the object. # For security reasons, we don't save the constructor object returned by # __reduce__, and instead use the pre-defined constructor for the object # that we know. This avoids having a function such as `eval()` as the From d8da963c808c06efd3204c61afdd45710209280c Mon Sep 17 00:00:00 2001 From: Tamara Date: Mon, 11 Nov 2024 13:42:44 +0100 Subject: [PATCH 09/43] Fix variable name inconsitency --- skops/io/_sklearn.py | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/skops/io/_sklearn.py b/skops/io/_sklearn.py index 57829264..506861a1 100644 --- a/skops/io/_sklearn.py +++ b/skops/io/_sklearn.py @@ -38,7 +38,7 @@ } SKLEARN_SGD_LOSS = True except ImportError: - SKLEARN_GT_15 = True + SKLEARN_SGD_LOSS = False from sklearn.tree._tree import Tree @@ -175,7 +175,7 @@ def sgd_loss_get_state(obj: Any, save_context: SaveContext) -> dict[str, Any]: return state -if not SKLEARN_GT_15: +if SKLEARN_SGD_LOSS: class SGDNode(ReduceNode): def __init__( @@ -250,21 +250,22 @@ def _construct(self): GET_STATE_DISPATCH_FUNCTIONS = [ (Tree, tree_get_state), ] -if not SKLEARN_GT_15: +if SKLEARN_SGD_LOSS: GET_STATE_DISPATCH_FUNCTIONS.append((LossFunction, sgd_loss_get_state)) for type_ in UNSUPPORTED_TYPES: GET_STATE_DISPATCH_FUNCTIONS.append((type_, unsupported_get_state)) # tuples of type and function that creates the instance of that type -NODE_TYPE_MAPPING = { - ("TreeNode", PROTOCOL): TreeNode, -} if SKLEARN_SGD_LOSS: - NODE_TYPE_MAPPING.update({ + NODE_TYPE_MAPPING = { + ("TreeNode", PROTOCOL): TreeNode, ("SGDNode", PROTOCOL): SGDNode, - }) - + } +else: + NODE_TYPE_MAPPING = { + ("TreeNode", PROTOCOL): TreeNode, + } # TODO: remove once support for sklearn<1.2 is dropped. # Starting from sklearn 1.2, _DictWithDeprecatedKeys is removed as it's no From d9a163b833a8dd06c5f927b8431fe9498deea66f Mon Sep 17 00:00:00 2001 From: Tamara Date: Mon, 11 Nov 2024 13:47:38 +0100 Subject: [PATCH 10/43] Add clearer message about warning supression --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 36e7c998..6e7a2ae5 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -25,7 +25,7 @@ filterwarnings = [ "ignore:DataFrameGroupBy.apply operated on the grouping columns.:DeprecationWarning", # Ignore Pandas 2.2 warning on PyArrow. It might be reverted in a later release. "ignore:\\s*Pyarrow will become a required dependency of pandas.*:DeprecationWarning", - # sklearn 1.6 deprecation warning, currently raised in the library itself + # LightGBM sklearn 1.6 deprecation warning, fixed in the next release "ignore:'force_all_finite' was renamed to 'ensure_all_finite' in 1.6 and will be removed in 1.8.:FutureWarning", ] markers = [ From cb0b2156b7487f1a7250da7081ea1d4711cf07ba Mon Sep 17 00:00:00 2001 From: Tamara Date: Tue, 12 Nov 2024 09:54:03 +0100 Subject: [PATCH 11/43] WIP --- skops/io/tests/_utils.py | 2 ++ skops/io/tests/test_persist.py | 5 ++++- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/skops/io/tests/_utils.py b/skops/io/tests/_utils.py index b1f41c25..ae3a7165 100644 --- a/skops/io/tests/_utils.py +++ b/skops/io/tests/_utils.py @@ -126,6 +126,7 @@ def _assert_vals_equal(val1, val2): elif isinstance(val1, np.ufunc): assert val1 == val2 elif val1.__class__.__module__ == "builtins": + print(val1, val2) assert val1 == val2 else: _assert_generic_objects_equal(val1, val2) @@ -145,6 +146,7 @@ def _clean_params(params): def assert_params_equal(params1, params2): + print(params1, params2) # helper function to compare estimator dictionaries of parameters if params1 is None and params2 is None: return diff --git a/skops/io/tests/test_persist.py b/skops/io/tests/test_persist.py index cfffc2f2..694b0518 100644 --- a/skops/io/tests/test_persist.py +++ b/skops/io/tests/test_persist.py @@ -41,10 +41,11 @@ PolynomialFeatures, StandardScaler, ) -from sklearn.utils import all_estimators, check_random_state +from sklearn.utils import check_random_state from sklearn.utils._tags import get_tags from sklearn.utils._test_common.instance_generator import _construct_instances from sklearn.utils._testing import SkipTest, set_random_state +from sklearn.utils.discovery import all_estimators from sklearn.utils.estimator_checks import ( _enforce_estimator_tags_y, _get_check_estimator_ids, @@ -373,8 +374,10 @@ def test_can_persist_fitted(estimator): with warnings.catch_warnings(): warnings.filterwarnings("ignore", module="sklearn") if y is not None: + print(estimator.__class__.__name__) estimator.fit(X, y) else: + print(estimator.__class__.__name__) estimator.fit(X) # test that we can get a list of untrusted types. This is a smoke test From c367109de46b1ab289f34f94f9c11d97dd9a16d2 Mon Sep 17 00:00:00 2001 From: Tamara Date: Thu, 14 Nov 2024 12:57:45 +0100 Subject: [PATCH 12/43] Add explicit typing --- skops/io/_sklearn.py | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/skops/io/_sklearn.py b/skops/io/_sklearn.py index 506861a1..33f07664 100644 --- a/skops/io/_sklearn.py +++ b/skops/io/_sklearn.py @@ -1,6 +1,6 @@ from __future__ import annotations -from typing import Any, Optional, Sequence, Type +from typing import Any, Dict, Optional, Sequence, Tuple, Type, Union from sklearn.cluster import Birch @@ -257,15 +257,15 @@ def _construct(self): GET_STATE_DISPATCH_FUNCTIONS.append((type_, unsupported_get_state)) # tuples of type and function that creates the instance of that type +NODE_TYPE_MAPPING: Dict[Tuple[str, int], Union[Type[TreeNode], Type[SGDNode]]] = { + ("TreeNode", PROTOCOL): TreeNode, +} if SKLEARN_SGD_LOSS: - NODE_TYPE_MAPPING = { - ("TreeNode", PROTOCOL): TreeNode, - ("SGDNode", PROTOCOL): SGDNode, - } -else: - NODE_TYPE_MAPPING = { - ("TreeNode", PROTOCOL): TreeNode, - } + NODE_TYPE_MAPPING.update( + { + ("SGDNode", PROTOCOL): SGDNode, + } + ) # TODO: remove once support for sklearn<1.2 is dropped. # Starting from sklearn 1.2, _DictWithDeprecatedKeys is removed as it's no From 051eead14487d46fd719449647e7f28dd5838f09 Mon Sep 17 00:00:00 2001 From: Tamara Date: Thu, 14 Nov 2024 13:10:35 +0100 Subject: [PATCH 13/43] Remove stray WIP with prints --- permutation-importances.png | Bin 0 -> 27767 bytes skops/io/tests/_utils.py | 2 -- skops/io/tests/test_persist.py | 2 -- 3 files changed, 4 deletions(-) create mode 100644 permutation-importances.png diff --git a/permutation-importances.png b/permutation-importances.png new file mode 100644 index 0000000000000000000000000000000000000000..aab4a8a825f7348c39ad7860686a27132b461db2 GIT binary patch literal 27767 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 Date: Thu, 14 Nov 2024 14:18:05 +0100 Subject: [PATCH 14/43] Fix tags issues --- permutation-importances.png | Bin 27767 -> 0 bytes scripts/check_file_size.py | 6 +++--- scripts/check_persistence_performance.py | 6 +++--- 3 files changed, 6 insertions(+), 6 deletions(-) delete mode 100644 permutation-importances.png diff --git a/permutation-importances.png b/permutation-importances.png deleted file mode 100644 index aab4a8a825f7348c39ad7860686a27132b461db2..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 27767 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 None: set_random_state(estimator, random_state=0) X, y = get_input(estimator) - tags = _safe_tags(estimator) - if tags.get("requires_fit", True): + tags = get_tags(estimator) + if tags.requires_fit: with warnings.catch_warnings(): warnings.filterwarnings("ignore", module="sklearn") if y is not None: diff --git a/scripts/check_persistence_performance.py b/scripts/check_persistence_performance.py index 2b1a6c2e..d47a3e2f 100644 --- a/scripts/check_persistence_performance.py +++ b/scripts/check_persistence_performance.py @@ -15,7 +15,7 @@ from typing import Any import pandas as pd -from sklearn.utils._tags import _safe_tags +from sklearn.utils._tags import get_tags from sklearn.utils._testing import set_random_state import skops.io as sio @@ -43,8 +43,8 @@ def check_persist_performance() -> None: set_random_state(estimator, random_state=0) X, y = get_input(estimator) - tags = _safe_tags(estimator) - if tags.get("requires_fit", True): + tags = get_tags(estimator) + if tags.requires_fit: with warnings.catch_warnings(): warnings.filterwarnings("ignore", module="sklearn") if y is not None: From e6b4df3c0cb1e8d1580d3e6e4ce1146f323f87d8 Mon Sep 17 00:00:00 2001 From: Tamara Date: Mon, 18 Nov 2024 11:43:15 +0100 Subject: [PATCH 15/43] Update skops/io/_sklearn.py Co-authored-by: Adrin Jalali --- skops/io/_sklearn.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/skops/io/_sklearn.py b/skops/io/_sklearn.py index 33f07664..4b3976f9 100644 --- a/skops/io/_sklearn.py +++ b/skops/io/_sklearn.py @@ -257,7 +257,7 @@ def _construct(self): GET_STATE_DISPATCH_FUNCTIONS.append((type_, unsupported_get_state)) # tuples of type and function that creates the instance of that type -NODE_TYPE_MAPPING: Dict[Tuple[str, int], Union[Type[TreeNode], Type[SGDNode]]] = { +NODE_TYPE_MAPPING: Dict[Tuple[str, int], Any] = { ("TreeNode", PROTOCOL): TreeNode, } if SKLEARN_SGD_LOSS: From ed77ceda7eb03d084f6e3a05bdb188b528e1b54a Mon Sep 17 00:00:00 2001 From: Tamara Date: Mon, 18 Nov 2024 13:05:02 +0100 Subject: [PATCH 16/43] Make the use of SGD models conditional on sklearn version --- skops/io/tests/test_persist.py | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/skops/io/tests/test_persist.py b/skops/io/tests/test_persist.py index 11cc7633..7f833fa0 100644 --- a/skops/io/tests/test_persist.py +++ b/skops/io/tests/test_persist.py @@ -13,7 +13,9 @@ import joblib import numpy as np import pytest +from packaging.version import Version from scipy import sparse, special +from sklearn import __version__ as sklearn_version from sklearn.base import BaseEstimator, is_regressor from sklearn.compose import ColumnTransformer from sklearn.datasets import load_sample_images, make_classification, make_regression @@ -131,6 +133,17 @@ def _tested_estimators(type_filter=None): for name, Estimator in all_estimators(type_filter=type_filter): if Estimator in UNSUPPORTED_TYPES: continue + + # Skip SGD-based estimators for sklearn > 1.5 + if Version(sklearn_version) > Version("1.5") and name in { + "SGDClassifier", + "SGDRegressor", + "SGDOneClassSVM", + "PassiveAggressiveClassifier", + "GraphicalLassoCV", + }: + continue + try: # suppress warnings here for skipped estimators. with warnings.catch_warnings(): From 0983b80749f7176ea562ddb14af7438bb2a1d330 Mon Sep 17 00:00:00 2001 From: Tamara Date: Mon, 18 Nov 2024 16:09:12 +0100 Subject: [PATCH 17/43] Add relative paths to fix import errors --- spaces/skops_model_card_creator/app.py | 9 +++++---- spaces/skops_model_card_creator/create.py | 3 ++- spaces/skops_model_card_creator/edit.py | 11 ++++++----- 3 files changed, 13 insertions(+), 10 deletions(-) diff --git a/spaces/skops_model_card_creator/app.py b/spaces/skops_model_card_creator/app.py index 446ddd62..dd6cac34 100644 --- a/spaces/skops_model_card_creator/app.py +++ b/spaces/skops_model_card_creator/app.py @@ -11,10 +11,11 @@ from typing import Literal import streamlit as st -from create import create_repo_input_form -from edit import edit_input_form -from gethelp import help_page -from start import start_input_form + +from .create import create_repo_input_form +from .edit import edit_input_form +from .help import help_page +from .start import start_input_form # Change cwd to a temporary path if "work_dir" not in st.session_state: diff --git a/spaces/skops_model_card_creator/create.py b/spaces/skops_model_card_creator/create.py index 7ac069c1..33022b94 100644 --- a/spaces/skops_model_card_creator/create.py +++ b/spaces/skops_model_card_creator/create.py @@ -2,10 +2,11 @@ from pathlib import Path import streamlit as st -from utils import get_rendered_model_card from skops import hub_utils +from .utils import get_rendered_model_card + def _add_back_button(): def fn(): diff --git a/spaces/skops_model_card_creator/edit.py b/spaces/skops_model_card_creator/edit.py index 6d0f5bb7..1d736c33 100644 --- a/spaces/skops_model_card_creator/edit.py +++ b/spaces/skops_model_card_creator/edit.py @@ -32,7 +32,11 @@ import streamlit as st from huggingface_hub import hf_hub_download -from tasks import ( + +from skops import card +from skops.card._model_card import PlotSection, split_subsection_names + +from .tasks import ( AddFigureTask, AddMetricsTask, AddSectionTask, @@ -42,15 +46,12 @@ UpdateFigureTitleTask, UpdateSectionTask, ) -from utils import ( +from .utils import ( get_rendered_model_card, iterate_key_section_content, process_card_for_rendering, ) -from skops import card -from skops.card._model_card import PlotSection, split_subsection_names - arepr = reprlib.Repr() arepr.maxstring = 24 tmp_path = Path(mkdtemp(prefix="skops-")) # temporary files From 0388d0b3ee4659532fbafd55a00cc681dedef828 Mon Sep 17 00:00:00 2001 From: Tamara Date: Thu, 21 Nov 2024 14:51:03 +0100 Subject: [PATCH 18/43] Add construct_instances for both versions --- skops/io/tests/test_persist.py | 11 ++++------- skops/utils/_fixes.py | 14 ++++++++++++++ 2 files changed, 18 insertions(+), 7 deletions(-) diff --git a/skops/io/tests/test_persist.py b/skops/io/tests/test_persist.py index 7f833fa0..216824bf 100644 --- a/skops/io/tests/test_persist.py +++ b/skops/io/tests/test_persist.py @@ -45,7 +45,6 @@ ) from sklearn.utils import check_random_state from sklearn.utils._tags import get_tags -from sklearn.utils._test_common.instance_generator import _construct_instances from sklearn.utils._testing import SkipTest, set_random_state from sklearn.utils.discovery import all_estimators from sklearn.utils.estimator_checks import ( @@ -69,6 +68,7 @@ from skops.io._utils import LoadContext, SaveContext, _get_state, get_state, gettype from skops.io.exceptions import UnsupportedTypeException, UntrustedTypesFoundException from skops.io.tests._utils import assert_method_outputs_equal, assert_params_equal +from skops.utils._fixes import construct_instances # Default settings for X N_SAMPLES = 50 @@ -143,7 +143,6 @@ def _tested_estimators(type_filter=None): "GraphicalLassoCV", }: continue - try: # suppress warnings here for skipped estimators. with warnings.catch_warnings(): @@ -159,11 +158,9 @@ def _tested_estimators(type_filter=None): # scikit-learn < 1.4.0) is not available in scipy >= 1.11.0. The # default solver will be "highs" from scikit-learn >= 1.4.0. # https://scikit-learn.org/stable/modules/generated/sklearn.linear_model.QuantileRegressor.html - estimator = next( - _construct_instances(partial(Estimator, solver="highs")) - ) + estimator = construct_instances(partial(Estimator, solver="highs")) else: - estimator = next(_construct_instances(Estimator)) + estimator = construct_instances(Estimator) # with the kind of data we pass, it needs to be 1 for the few # estimators which have this. @@ -285,7 +282,7 @@ def _unsupported_estimators(type_filter=None): message="Can't instantiate estimator", ) # Get the first instance directly from the generator - estimator = next(_construct_instances(Estimator)) + estimator = construct_instances(Estimator) # with the kind of data we pass, it needs to be 1 for the few # estimators which have this. if "n_components" in estimator.get_params(): diff --git a/skops/utils/_fixes.py b/skops/utils/_fixes.py index 212cb08f..8d558ad9 100644 --- a/skops/utils/_fixes.py +++ b/skops/utils/_fixes.py @@ -1,3 +1,9 @@ +try: + from sklearn.utils._test_common.instance_generator import _construct_instances +except ImportError: + from sklearn.utils.estimator_checks import _construct_instances + + def boxplot(ax, *, tick_labels, **kwargs): """A function to handle labels->tick_labels deprecation. labels is deprecated in 3.9 and removed in 3.11. @@ -6,3 +12,11 @@ def boxplot(ax, *, tick_labels, **kwargs): return ax.boxplot(tick_labels=tick_labels, **kwargs) except TypeError: return ax.boxplot(labels=tick_labels, **kwargs) + + +def construct_instances(estimator): + """Added for sklearn<1.6 support.""" + try: + return next(_construct_instances(estimator)) + except TypeError: + return _construct_instances(estimator) From 926f9721c3860cc728aed00cd08f371933d455b2 Mon Sep 17 00:00:00 2001 From: Tamara Date: Thu, 21 Nov 2024 15:31:02 +0100 Subject: [PATCH 19/43] Move imports for construct_instances --- skops/utils/_fixes.py | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/skops/utils/_fixes.py b/skops/utils/_fixes.py index 8d558ad9..28482f82 100644 --- a/skops/utils/_fixes.py +++ b/skops/utils/_fixes.py @@ -1,9 +1,3 @@ -try: - from sklearn.utils._test_common.instance_generator import _construct_instances -except ImportError: - from sklearn.utils.estimator_checks import _construct_instances - - def boxplot(ax, *, tick_labels, **kwargs): """A function to handle labels->tick_labels deprecation. labels is deprecated in 3.9 and removed in 3.11. @@ -17,6 +11,11 @@ def boxplot(ax, *, tick_labels, **kwargs): def construct_instances(estimator): """Added for sklearn<1.6 support.""" try: + from sklearn.utils._test_common.instance_generator import _construct_instances + return next(_construct_instances(estimator)) - except TypeError: + + except ImportError: + from sklearn.utils.estimator_checks import _construct_instances + return _construct_instances(estimator) From 1fd843250f528242c5094b2a0cb479c2bba8d6fa Mon Sep 17 00:00:00 2001 From: Tamara Date: Thu, 21 Nov 2024 16:52:29 +0100 Subject: [PATCH 20/43] Partially make tags work between the two versions --- scripts/check_file_size.py | 5 ++--- scripts/check_persistence_performance.py | 5 ++--- skops/io/tests/test_persist.py | 8 +++----- skops/utils/_fixes.py | 12 ++++++++++++ 4 files changed, 19 insertions(+), 11 deletions(-) diff --git a/scripts/check_file_size.py b/scripts/check_file_size.py index eb1d204f..cf394108 100644 --- a/scripts/check_file_size.py +++ b/scripts/check_file_size.py @@ -20,7 +20,6 @@ from zipfile import ZIP_DEFLATED, ZipFile import pandas as pd -from sklearn.utils._tags import get_tags from sklearn.utils._testing import set_random_state import skops.io as sio @@ -29,6 +28,7 @@ _tested_estimators, get_input, ) +from skops.utils._fixes import requires_fit TOPK = 10 # number of largest estimators reported MAX_ALLOWED_SIZE = 1024 # maximum allowed file size in kb @@ -46,8 +46,7 @@ def check_file_size() -> None: set_random_state(estimator, random_state=0) X, y = get_input(estimator) - tags = get_tags(estimator) - if tags.requires_fit: + if requires_fit(estimator): with warnings.catch_warnings(): warnings.filterwarnings("ignore", module="sklearn") if y is not None: diff --git a/scripts/check_persistence_performance.py b/scripts/check_persistence_performance.py index d47a3e2f..975305e7 100644 --- a/scripts/check_persistence_performance.py +++ b/scripts/check_persistence_performance.py @@ -15,7 +15,6 @@ from typing import Any import pandas as pd -from sklearn.utils._tags import get_tags from sklearn.utils._testing import set_random_state import skops.io as sio @@ -24,6 +23,7 @@ _tested_estimators, get_input, ) +from skops.utils._fixes import requires_fit ATOL = 1 # seconds absolute difference allowed at max NUM_REPS = 10 # number of times the check is repeated @@ -43,8 +43,7 @@ def check_persist_performance() -> None: set_random_state(estimator, random_state=0) X, y = get_input(estimator) - tags = get_tags(estimator) - if tags.requires_fit: + if requires_fit(estimator): with warnings.catch_warnings(): warnings.filterwarnings("ignore", module="sklearn") if y is not None: diff --git a/skops/io/tests/test_persist.py b/skops/io/tests/test_persist.py index 216824bf..9893d9cb 100644 --- a/skops/io/tests/test_persist.py +++ b/skops/io/tests/test_persist.py @@ -68,7 +68,7 @@ from skops.io._utils import LoadContext, SaveContext, _get_state, get_state, gettype from skops.io.exceptions import UnsupportedTypeException, UntrustedTypesFoundException from skops.io.tests._utils import assert_method_outputs_equal, assert_params_equal -from skops.utils._fixes import construct_instances +from skops.utils._fixes import construct_instances, requires_fit # Default settings for X N_SAMPLES = 50 @@ -379,8 +379,7 @@ def test_can_persist_fitted(estimator): set_random_state(estimator, random_state=0) X, y = get_input(estimator) - tags = get_tags(estimator) - if tags.requires_fit: + if requires_fit(estimator): with warnings.catch_warnings(): warnings.filterwarnings("ignore", module="sklearn") if y is not None: @@ -432,8 +431,7 @@ def test_unsupported_type_raises(estimator): set_random_state(estimator, random_state=0) X, y = get_input(estimator) - tags = get_tags(estimator) - if tags.requires_fit: + if requires_fit(estimator): with warnings.catch_warnings(): warnings.filterwarnings("ignore", module="sklearn") if y is not None: diff --git a/skops/utils/_fixes.py b/skops/utils/_fixes.py index 28482f82..59383c68 100644 --- a/skops/utils/_fixes.py +++ b/skops/utils/_fixes.py @@ -19,3 +19,15 @@ def construct_instances(estimator): from sklearn.utils.estimator_checks import _construct_instances return _construct_instances(estimator) + + +def requires_fit(estimator): + """Added for sklearn<1.6 support.""" + try: + from sklearn.utils._tags import get_tags + + return get_tags(estimator).requires_fit + except ImportError: + from sklearn.utils._tags import _safe_tags + + return _safe_tags(estimator).get("requires_fit", True) From de4774d8ed01f11a5d404f22dbda920f139f95bd Mon Sep 17 00:00:00 2001 From: Tamara Date: Thu, 21 Nov 2024 17:32:06 +0100 Subject: [PATCH 21/43] Tags working with both versions --- scripts/check_file_size.py | 4 +- scripts/check_persistence_performance.py | 4 +- skops/io/tests/test_persist.py | 29 ++++++----- skops/utils/_fixes.py | 62 +++++++++++++++++++++--- 4 files changed, 73 insertions(+), 26 deletions(-) diff --git a/scripts/check_file_size.py b/scripts/check_file_size.py index cf394108..e9aca319 100644 --- a/scripts/check_file_size.py +++ b/scripts/check_file_size.py @@ -28,7 +28,7 @@ _tested_estimators, get_input, ) -from skops.utils._fixes import requires_fit +from skops.utils._fixes import get_tags TOPK = 10 # number of largest estimators reported MAX_ALLOWED_SIZE = 1024 # maximum allowed file size in kb @@ -46,7 +46,7 @@ def check_file_size() -> None: set_random_state(estimator, random_state=0) X, y = get_input(estimator) - if requires_fit(estimator): + if get_tags(estimator).requires_fit: with warnings.catch_warnings(): warnings.filterwarnings("ignore", module="sklearn") if y is not None: diff --git a/scripts/check_persistence_performance.py b/scripts/check_persistence_performance.py index 975305e7..a8b04279 100644 --- a/scripts/check_persistence_performance.py +++ b/scripts/check_persistence_performance.py @@ -23,7 +23,7 @@ _tested_estimators, get_input, ) -from skops.utils._fixes import requires_fit +from skops.utils._fixes import get_tags ATOL = 1 # seconds absolute difference allowed at max NUM_REPS = 10 # number of times the check is repeated @@ -43,7 +43,7 @@ def check_persist_performance() -> None: set_random_state(estimator, random_state=0) X, y = get_input(estimator) - if requires_fit(estimator): + if get_tags(estimator).requires_fit: with warnings.catch_warnings(): warnings.filterwarnings("ignore", module="sklearn") if y is not None: diff --git a/skops/io/tests/test_persist.py b/skops/io/tests/test_persist.py index 9893d9cb..694fafc2 100644 --- a/skops/io/tests/test_persist.py +++ b/skops/io/tests/test_persist.py @@ -44,7 +44,6 @@ StandardScaler, ) from sklearn.utils import check_random_state -from sklearn.utils._tags import get_tags from sklearn.utils._testing import SkipTest, set_random_state from sklearn.utils.discovery import all_estimators from sklearn.utils.estimator_checks import ( @@ -68,7 +67,7 @@ from skops.io._utils import LoadContext, SaveContext, _get_state, get_state, gettype from skops.io.exceptions import UnsupportedTypeException, UntrustedTypesFoundException from skops.io.tests._utils import assert_method_outputs_equal, assert_params_equal -from skops.utils._fixes import construct_instances, requires_fit +from skops.utils._fixes import construct_instances, get_tags # Default settings for X N_SAMPLES = 50 @@ -328,35 +327,35 @@ def get_input(estimator): y = _enforce_estimator_tags_y(estimator, y) tags = get_tags(estimator) - if tags.input_tags.pairwise: + if tags.pairwise: return np.random.rand(N_FEATURES, N_FEATURES), None - if tags.input_tags.two_d_array: + if tags.two_d_array: # Some models require positive X return np.abs(X), y - if tags.input_tags.one_d_array: + if tags.one_d_array: return X[:, 0], y - if tags.input_tags.three_d_array: + if tags.three_d_array: return load_sample_images().images[1], None - if tags.target_tags.one_d_labels: + if tags.one_d_labels: # model only expects y return y, None - if tags.target_tags.two_d_labels: + if tags.two_d_labels: return [(1, 2), (3,)], None - if tags.input_tags.categorical: + if tags.categorical: X = [["Male", 1], ["Female", 3], ["Female", 2]] - y = y[: len(X)] if tags.target_tags.required else None + y = y[: len(X)] if tags.y_required else None return X, y - if tags.input_tags.dict: + if tags.dict: return [{"foo": 1, "bar": 2}, {"foo": 3, "baz": 1}], None - if tags.input_tags.string: + if tags.string: return [ "This is the first document.", "This document is the second document.", @@ -364,7 +363,7 @@ def get_input(estimator): "Is this the first document?", ], None - if tags.input_tags.sparse: + if tags.sparse: # TfidfTransformer in sklearn 0.24 needs this return sparse.csr_matrix(X), y @@ -379,7 +378,7 @@ def test_can_persist_fitted(estimator): set_random_state(estimator, random_state=0) X, y = get_input(estimator) - if requires_fit(estimator): + if get_tags(estimator).requires_fit: with warnings.catch_warnings(): warnings.filterwarnings("ignore", module="sklearn") if y is not None: @@ -431,7 +430,7 @@ def test_unsupported_type_raises(estimator): set_random_state(estimator, random_state=0) X, y = get_input(estimator) - if requires_fit(estimator): + if get_tags(estimator).requires_fit: with warnings.catch_warnings(): warnings.filterwarnings("ignore", module="sklearn") if y is not None: diff --git a/skops/utils/_fixes.py b/skops/utils/_fixes.py index 59383c68..3f9604d5 100644 --- a/skops/utils/_fixes.py +++ b/skops/utils/_fixes.py @@ -9,25 +9,73 @@ def boxplot(ax, *, tick_labels, **kwargs): def construct_instances(estimator): - """Added for sklearn<1.6 support.""" + """Create a test instance of an estimator for compatibility testing. + + This function provides compatibility between different scikit-learn versions + (before and after 1.6) for creating test instances of estimators. It handles + the API change where _construct_instances was moved from estimator_checks to + instance_generator. + """ try: from sklearn.utils._test_common.instance_generator import _construct_instances return next(_construct_instances(estimator)) except ImportError: - from sklearn.utils.estimator_checks import _construct_instances + from sklearn.utils.estimator_checks import _construct_instance + + return _construct_instance(estimator) - return _construct_instances(estimator) +def get_tags(estimator): + """Get estimator tags in a consistent format across different sklearn versions. + + This function provides compatibility between sklearn versions before and after 1.6. + It returns a SimpleNamespace object containing metadata about the estimator's + requirements and capabilities (e.g., input types, fitting requirements). + + Parameters + ---------- + estimator : estimator object + A scikit-learn estimator instance. + """ + from types import SimpleNamespace -def requires_fit(estimator): - """Added for sklearn<1.6 support.""" try: from sklearn.utils._tags import get_tags - return get_tags(estimator).requires_fit + tags = get_tags(estimator) + return SimpleNamespace( + input_tags=tags.input_tags, + requires_fit=tags.requires_fit, + pairwise=tags.input_tags.pairwise, + two_d_array=tags.input_tags.two_d_array, + one_d_array=tags.input_tags.one_d_array, + three_d_array=tags.input_tags.three_d_array, + one_d_labels=tags.target_tags.one_d_labels, + two_d_labels=tags.target_tags.two_d_labels, + y_required=tags.target_tags.required, + categorical=tags.input_tags.categorical, + dict=tags.input_tags.dict, + string=tags.input_tags.string, + sparse=tags.input_tags.sparse, + ) except ImportError: from sklearn.utils._tags import _safe_tags - return _safe_tags(estimator).get("requires_fit", True) + tags = _safe_tags(estimator) + return SimpleNamespace( + input_tags=tags["X_types"], + requires_fit=tags.get("requires_fit", True), + pairwise=tags["pairwise"], + two_d_array="2darray" in tags["X_types"], + one_d_array="1darray" in tags["X_types"], + three_d_array="3darray" in tags["X_types"], + one_d_labels="1dlabels" in tags["X_types"], + two_d_labels="2dlabels" in tags["X_types"], + y_required=tags["requires_y"], + categorical="categorical" in tags["X_types"], + dict="dict" in tags["X_types"], + string="string" in tags["X_types"], + sparse="sparse" in tags["X_types"], + ) From c043a829fe6fc971a02eded182158973726143b8 Mon Sep 17 00:00:00 2001 From: Tamara Date: Fri, 22 Nov 2024 16:54:35 +0100 Subject: [PATCH 22/43] Remove typing import --- skops/io/_sklearn.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/skops/io/_sklearn.py b/skops/io/_sklearn.py index 4b3976f9..c0916e5d 100644 --- a/skops/io/_sklearn.py +++ b/skops/io/_sklearn.py @@ -1,6 +1,6 @@ from __future__ import annotations -from typing import Any, Dict, Optional, Sequence, Tuple, Type, Union +from typing import Any, Dict, Optional, Sequence, Tuple, Type from sklearn.cluster import Birch From 81950ff1cf61c20173f6653679119b20bf7e1a03 Mon Sep 17 00:00:00 2001 From: Tamara Date: Mon, 25 Nov 2024 12:50:13 +0100 Subject: [PATCH 23/43] Attepmt to fix catboost issues --- skops/_min_dependencies.py | 2 +- skops/io/tests/test_external.py | 18 +++++++++--------- skops/utils/_fixes.py | 5 +++-- .../skops_model_card_creator/requirements.txt | 2 +- 4 files changed, 14 insertions(+), 13 deletions(-) diff --git a/skops/_min_dependencies.py b/skops/_min_dependencies.py index 58f12c4c..d1f807f2 100644 --- a/skops/_min_dependencies.py +++ b/skops/_min_dependencies.py @@ -33,7 +33,7 @@ # required for persistence tests of external libraries "lightgbm": ("3", "tests", None), "xgboost": ("1.6", "tests", None), - "catboost": ("1.0", "tests", None), + "catboost": ("1.0", "tests", 'python_version < "3.13"'), "fairlearn": ("0.7.0", "docs, tests", None), "rich": ("12", "tests, rich", None), } diff --git a/skops/io/tests/test_external.py b/skops/io/tests/test_external.py index d9fa7916..63ee0a78 100644 --- a/skops/io/tests/test_external.py +++ b/skops/io/tests/test_external.py @@ -289,6 +289,15 @@ def test_ranker(self, xgboost, rank_data, trusted, booster, tree_method): class TestCatboost: """Tests for CatBoostClassifier, CatBoostRegressor, and CatBoostRanker""" + @pytest.fixture(autouse=True) + def catboost(self): + """Skip all tests in this class if catboost is not available.""" + try: + catboost = pytest.importorskip("catboost") + except (ImportError, ValueError): # ValueError for numpy2 incompatibility + pytest.skip("Catboost not available or incompatible") + return catboost + @pytest.fixture(autouse=True) def capture_stdout(self): # Mock print and rich.print so that running these tests with pytest -s @@ -317,15 +326,6 @@ def cb_rank_data(self, rank_data): group_id = sum([[i] * n for i, n in enumerate(group)], []) return X, y, group_id - @pytest.fixture(autouse=True) - def catboost(self): - try: - catboost = pytest.importorskip("catboost") - except ValueError: # TODO(numpy2) remove when catboost supports numpy2 - pytest.skip("Catboost not supporting numpy2 yet") - - return catboost - @pytest.fixture def trusted(self): # TODO: adjust once more types are trusted by default diff --git a/skops/utils/_fixes.py b/skops/utils/_fixes.py index 3f9604d5..abf78bfc 100644 --- a/skops/utils/_fixes.py +++ b/skops/utils/_fixes.py @@ -1,3 +1,6 @@ +from types import SimpleNamespace + + def boxplot(ax, *, tick_labels, **kwargs): """A function to handle labels->tick_labels deprecation. labels is deprecated in 3.9 and removed in 3.11. @@ -39,8 +42,6 @@ def get_tags(estimator): estimator : estimator object A scikit-learn estimator instance. """ - from types import SimpleNamespace - try: from sklearn.utils._tags import get_tags diff --git a/spaces/skops_model_card_creator/requirements.txt b/spaces/skops_model_card_creator/requirements.txt index e1747269..3518cad8 100644 --- a/spaces/skops_model_card_creator/requirements.txt +++ b/spaces/skops_model_card_creator/requirements.txt @@ -1,4 +1,4 @@ -catboost +catboost; python_version < "3.13" huggingface_hub lightgbm pandas From bb66ac7b82c89dbc328e63a77b0f253094116f9f Mon Sep 17 00:00:00 2001 From: Tamara Date: Mon, 25 Nov 2024 13:21:49 +0100 Subject: [PATCH 24/43] Skip quantile-forest futurewarning sklearn 1.7 --- pyproject.toml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/pyproject.toml b/pyproject.toml index 6e7a2ae5..64a6ebf0 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -27,6 +27,10 @@ filterwarnings = [ "ignore:\\s*Pyarrow will become a required dependency of pandas.*:DeprecationWarning", # LightGBM sklearn 1.6 deprecation warning, fixed in the next release "ignore:'force_all_finite' was renamed to 'ensure_all_finite' in 1.6 and will be removed in 1.8.:FutureWarning", + # RandomForestQuantileRegressor tags deprecation warning in sklearn 1.7 + "ignore:The RandomForestQuantileRegressor or classes from which it inherits use `_get_tags` and `_more_tags`:FutureWarning", + # ExtraTreesQuantileRegressor tags deprecation warning in sklearn 1.7 + "ignore:The ExtraTreesQuantileRegressor or classes from which it inherits use `_get_tags` and `_more_tags`:FutureWarning", ] markers = [ "network: marks tests as requiring internet (deselect with '-m \"not network\"')", From aeb6bafa22a8c6e51d6325d47f54a90850dd0006 Mon Sep 17 00:00:00 2001 From: Tamara Date: Mon, 25 Nov 2024 14:00:39 +0100 Subject: [PATCH 25/43] Supress quantile-foreset warning --- pyproject.toml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/pyproject.toml b/pyproject.toml index 64a6ebf0..b5e47d5b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -31,6 +31,8 @@ filterwarnings = [ "ignore:The RandomForestQuantileRegressor or classes from which it inherits use `_get_tags` and `_more_tags`:FutureWarning", # ExtraTreesQuantileRegressor tags deprecation warning in sklearn 1.7 "ignore:The ExtraTreesQuantileRegressor or classes from which it inherits use `_get_tags` and `_more_tags`:FutureWarning", + # BaseEstimator._validate_data deprecation warning in sklearn 1.6 #TODO can be removed when a new release of quantile-forest is out + "ignore:`BaseEstimator._validate_data` is deprecated in 1.6 and will be removed in 1.7:FutureWarning", ] markers = [ "network: marks tests as requiring internet (deselect with '-m \"not network\"')", From bdfc37da5573cbd554d9d5cc6d488d876eb1ac24 Mon Sep 17 00:00:00 2001 From: Tamara Date: Mon, 25 Nov 2024 14:19:49 +0100 Subject: [PATCH 26/43] Update spaces/skops_model_card_creator/requirements.txt Co-authored-by: Adrin Jalali --- spaces/skops_model_card_creator/requirements.txt | 2 ++ 1 file changed, 2 insertions(+) diff --git a/spaces/skops_model_card_creator/requirements.txt b/spaces/skops_model_card_creator/requirements.txt index 3518cad8..bbbe7a10 100644 --- a/spaces/skops_model_card_creator/requirements.txt +++ b/spaces/skops_model_card_creator/requirements.txt @@ -1,3 +1,5 @@ +# remove python constraint when catboost supports 3.13 +# https://github.com/catboost/catboost/issues/2748 catboost; python_version < "3.13" huggingface_hub lightgbm From 1cf9c87592e6d7a39c6572f322db373dfcf59e7b Mon Sep 17 00:00:00 2001 From: Tamara Date: Mon, 25 Nov 2024 14:19:56 +0100 Subject: [PATCH 27/43] Update skops/_min_dependencies.py Co-authored-by: Adrin Jalali --- skops/_min_dependencies.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/skops/_min_dependencies.py b/skops/_min_dependencies.py index d1f807f2..c90b3716 100644 --- a/skops/_min_dependencies.py +++ b/skops/_min_dependencies.py @@ -33,6 +33,8 @@ # required for persistence tests of external libraries "lightgbm": ("3", "tests", None), "xgboost": ("1.6", "tests", None), + # remove python constraint when catboost supports 3.13 + # https://github.com/catboost/catboost/issues/2748 "catboost": ("1.0", "tests", 'python_version < "3.13"'), "fairlearn": ("0.7.0", "docs, tests", None), "rich": ("12", "tests, rich", None), From d5696ff18c26dd3e13849a4008f7d4f7f02b8afe Mon Sep 17 00:00:00 2001 From: Tamara Date: Mon, 25 Nov 2024 14:49:43 +0100 Subject: [PATCH 28/43] Add error for SGD class and incompatible sklearn version --- skops/io/_sklearn.py | 40 ++++++++++++++++++++++------------------ 1 file changed, 22 insertions(+), 18 deletions(-) diff --git a/skops/io/_sklearn.py b/skops/io/_sklearn.py index c0916e5d..22e40b56 100644 --- a/skops/io/_sklearn.py +++ b/skops/io/_sklearn.py @@ -175,25 +175,29 @@ def sgd_loss_get_state(obj: Any, save_context: SaveContext) -> dict[str, Any]: return state -if SKLEARN_SGD_LOSS: - - class SGDNode(ReduceNode): - def __init__( - self, - state: dict[str, Any], - load_context: LoadContext, - trusted: Optional[Sequence[str]] = None, - ) -> None: - # TODO: make sure trusted here makes sense and used. - self.trusted = self._get_trusted( - trusted, [get_module(x) + "." + x.__name__ for x in ALLOWED_SGD_LOSSES] - ) - super().__init__( - state, - load_context, - constructor=gettype(state["__module__"], state["__class__"]), - trusted=self.trusted, +class SGDNode(ReduceNode): + def __init__( + self, + state: dict[str, Any], + load_context: LoadContext, + trusted: Optional[Sequence[str]] = None, + ) -> None: + if not SKLEARN_SGD_LOSS: + raise ImportError( + "Cannot load SGD loss functions. This might happen if you're trying to " + "load a model that was saved with an older version of scikit-learn. " + "Please make sure you're using a compatible scikit-learn version." ) + # TODO: make sure trusted here makes sense and used. + self.trusted = self._get_trusted( + trusted, [get_module(x) + "." + x.__name__ for x in ALLOWED_SGD_LOSSES] + ) + super().__init__( + state, + load_context, + constructor=gettype(state["__module__"], state["__class__"]), + trusted=self.trusted, + ) # TODO: remove once support for sklearn<1.2 is dropped. From cfeef0a7d3930118916d554ee459c126114f91bc Mon Sep 17 00:00:00 2001 From: Tamara Date: Tue, 26 Nov 2024 14:02:45 +0100 Subject: [PATCH 29/43] Copy code for scikit-learn for est tags --- skops/io/tests/test_persist.py | 20 +- skops/utils/_fixes.py | 363 ++++++++++++++++++++++++++++++--- 2 files changed, 339 insertions(+), 44 deletions(-) diff --git a/skops/io/tests/test_persist.py b/skops/io/tests/test_persist.py index 694fafc2..eff72d99 100644 --- a/skops/io/tests/test_persist.py +++ b/skops/io/tests/test_persist.py @@ -327,35 +327,35 @@ def get_input(estimator): y = _enforce_estimator_tags_y(estimator, y) tags = get_tags(estimator) - if tags.pairwise: + if tags.input_tags.pairwise: return np.random.rand(N_FEATURES, N_FEATURES), None - if tags.two_d_array: + if tags.input_tags.two_d_array: # Some models require positive X return np.abs(X), y - if tags.one_d_array: + if tags.input_tags.one_d_array: return X[:, 0], y - if tags.three_d_array: + if tags.input_tags.three_d_array: return load_sample_images().images[1], None - if tags.one_d_labels: + if tags.target_tags.one_d_labels: # model only expects y return y, None - if tags.two_d_labels: + if tags.target_tags.two_d_labels: return [(1, 2), (3,)], None - if tags.categorical: + if tags.input_tags.categorical: X = [["Male", 1], ["Female", 3], ["Female", 2]] y = y[: len(X)] if tags.y_required else None return X, y - if tags.dict: + if tags.input_tags.dict: return [{"foo": 1, "bar": 2}, {"foo": 3, "baz": 1}], None - if tags.string: + if tags.input_tags.string: return [ "This is the first document.", "This document is the second document.", @@ -363,7 +363,7 @@ def get_input(estimator): "Is this the first document?", ], None - if tags.sparse: + if tags.input_tags.sparse: # TfidfTransformer in sklearn 0.24 needs this return sparse.csr_matrix(X), y diff --git a/skops/utils/_fixes.py b/skops/utils/_fixes.py index abf78bfc..8fc27d1e 100644 --- a/skops/utils/_fixes.py +++ b/skops/utils/_fixes.py @@ -1,4 +1,5 @@ -from types import SimpleNamespace +import sys +from dataclasses import dataclass, field def boxplot(ax, *, tick_labels, **kwargs): @@ -30,53 +31,347 @@ def construct_instances(estimator): return _construct_instance(estimator) +""" +Estimator Tags +-------------- +The following code implements a tag system for scikit-learn estimators that provides +metadata about their capabilities and requirements. This includes support for both +the new Tags dataclass format (sklearn >= 1.6) and backwards compatibility with +the dictionary format (sklearn < 1.6). + +Most of the code below is copied from scikit-learn: + `link to commit `_ + +This code can be removed when support for scikit-learn < 1.6 is dropped. +""" + + def get_tags(estimator): """Get estimator tags in a consistent format across different sklearn versions. This function provides compatibility between sklearn versions before and after 1.6. - It returns a SimpleNamespace object containing metadata about the estimator's - requirements and capabilities (e.g., input types, fitting requirements). + It returns either a Tags object (sklearn >= 1.6) or a converted Tags object from + the dictionary format (sklearn < 1.6) containing metadata about the estimator's + requirements and capabilities. Parameters ---------- estimator : estimator object A scikit-learn estimator instance. + + Returns + ------- + tags : Tags + An object containing metadata about the estimator's requirements and + capabilities (e.g., input types, fitting requirements, classifier/regressor + specific tags). """ try: from sklearn.utils._tags import get_tags - tags = get_tags(estimator) - return SimpleNamespace( - input_tags=tags.input_tags, - requires_fit=tags.requires_fit, - pairwise=tags.input_tags.pairwise, - two_d_array=tags.input_tags.two_d_array, - one_d_array=tags.input_tags.one_d_array, - three_d_array=tags.input_tags.three_d_array, - one_d_labels=tags.target_tags.one_d_labels, - two_d_labels=tags.target_tags.two_d_labels, - y_required=tags.target_tags.required, - categorical=tags.input_tags.categorical, - dict=tags.input_tags.dict, - string=tags.input_tags.string, - sparse=tags.input_tags.sparse, - ) + return get_tags(estimator) except ImportError: from sklearn.utils._tags import _safe_tags - tags = _safe_tags(estimator) - return SimpleNamespace( - input_tags=tags["X_types"], - requires_fit=tags.get("requires_fit", True), - pairwise=tags["pairwise"], - two_d_array="2darray" in tags["X_types"], - one_d_array="1darray" in tags["X_types"], - three_d_array="3darray" in tags["X_types"], - one_d_labels="1dlabels" in tags["X_types"], - two_d_labels="2dlabels" in tags["X_types"], - y_required=tags["requires_y"], - categorical="categorical" in tags["X_types"], - dict="dict" in tags["X_types"], - string="string" in tags["X_types"], - sparse="sparse" in tags["X_types"], + return _to_new_tags(_safe_tags(estimator), estimator) + + +def _dataclass_args(): + if sys.version_info < (3, 10): + return {} + return {"slots": True} + + +@dataclass(**_dataclass_args()) +class InputTags: + """Tags for the input data. + + Parameters + ---------- + one_d_array : bool, default=False + Whether the input can be a 1D array. + + two_d_array : bool, default=True + Whether the input can be a 2D array. Note that most common + tests currently run only if this flag is set to ``True``. + + three_d_array : bool, default=False + Whether the input can be a 3D array. + + sparse : bool, default=False + Whether the input can be a sparse matrix. + + categorical : bool, default=False + Whether the input can be categorical. + + string : bool, default=False + Whether the input can be an array-like of strings. + + dict : bool, default=False + Whether the input can be a dictionary. + + positive_only : bool, default=False + Whether the estimator requires positive X. + + allow_nan : bool, default=False + Whether the estimator supports data with missing values encoded as `np.nan`. + + pairwise : bool, default=False + This boolean attribute indicates whether the data (`X`), + :term:`fit` and similar methods consists of pairwise measures + over samples rather than a feature representation for each + sample. It is usually `True` where an estimator has a + `metric` or `affinity` or `kernel` parameter with value + 'precomputed'. Its primary purpose is to support a + :term:`meta-estimator` or a cross validation procedure that + extracts a sub-sample of data intended for a pairwise + estimator, where the data needs to be indexed on both axes. + Specifically, this tag is used by + `sklearn.utils.metaestimators._safe_split` to slice rows and + columns. + """ + + one_d_array: bool = False + two_d_array: bool = True + three_d_array: bool = False + sparse: bool = False + categorical: bool = False + string: bool = False + dict: bool = False + positive_only: bool = False + allow_nan: bool = False + pairwise: bool = False + + +@dataclass(**_dataclass_args()) +class TargetTags: + """Tags for the target data. + + Parameters + ---------- + required : bool + Whether the estimator requires y to be passed to `fit`, + `fit_predict` or `fit_transform` methods. The tag is ``True`` + for estimators inheriting from `~sklearn.base.RegressorMixin` + and `~sklearn.base.ClassifierMixin`. + + one_d_labels : bool, default=False + Whether the input is a 1D labels (y). + + two_d_labels : bool, default=False + Whether the input is a 2D labels (y). + + positive_only : bool, default=False + Whether the estimator requires a positive y (only applicable + for regression). + + multi_output : bool, default=False + Whether a regressor supports multi-target outputs or a classifier supports + multi-class multi-output. + + single_output : bool, default=True + Whether the target can be single-output. This can be ``False`` if the + estimator supports only multi-output cases. + """ + + required: bool + one_d_labels: bool = False + two_d_labels: bool = False + positive_only: bool = False + multi_output: bool = False + single_output: bool = True + + +@dataclass(**_dataclass_args()) +class TransformerTags: + """Tags for the transformer. + + Parameters + ---------- + preserves_dtype : list[str], default=["float64"] + Applies only on transformers. It corresponds to the data types + which will be preserved such that `X_trans.dtype` is the same + as `X.dtype` after calling `transformer.transform(X)`. If this + list is empty, then the transformer is not expected to + preserve the data type. The first value in the list is + considered as the default data type, corresponding to the data + type of the output when the input data type is not going to be + preserved. + """ + + preserves_dtype: list[str] = field(default_factory=lambda: ["float64"]) + + +@dataclass(**_dataclass_args()) +class ClassifierTags: + """Tags for the classifier. + + Parameters + ---------- + poor_score : bool, default=False + Whether the estimator fails to provide a "reasonable" test-set + score, which currently for classification is an accuracy of + 0.83 on ``make_blobs(n_samples=300, random_state=0)``. The + datasets and values are based on current estimators in scikit-learn + and might be replaced by something more systematic. + + multi_class : bool, default=True + Whether the classifier can handle multi-class + classification. Note that all classifiers support binary + classification. Therefore this flag indicates whether the + classifier is a binary-classifier-only or not. + + multi_label : bool, default=False + Whether the classifier supports multi-label output. + """ + + poor_score: bool = False + multi_class: bool = True + multi_label: bool = False + + +@dataclass(**_dataclass_args()) +class RegressorTags: + """Tags for the regressor. + + Parameters + ---------- + poor_score : bool, default=False + Whether the estimator fails to provide a "reasonable" test-set + score, which currently for regression is an R2 of 0.5 on + ``make_regression(n_samples=200, n_features=10, + n_informative=1, bias=5.0, noise=20, random_state=42)``. The + dataset and values are based on current estimators in scikit-learn + and might be replaced by something more systematic. + + multi_label : bool, default=False + Whether the regressor supports multilabel output. + """ + + poor_score: bool = False + multi_label: bool = False + + +@dataclass(**_dataclass_args()) +class Tags: + """Tags for the estimator. + + See :ref:`estimator_tags` for more information. + + Parameters + ---------- + estimator_type : str or None + The type of the estimator. Can be one of: + - "classifier" + - "regressor" + - "transformer" + - "clusterer" + - "outlier_detector" + - "density_estimator" + + target_tags : :class:`TargetTags` + The target(y) tags. + + transformer_tags : :class:`TransformerTags` or None + The transformer tags. + + classifier_tags : :class:`ClassifierTags` or None + The classifier tags. + + regressor_tags : :class:`RegressorTags` or None + The regressor tags. + + array_api_support : bool, default=False + Whether the estimator supports Array API compatible inputs. + + no_validation : bool, default=False + Whether the estimator skips input-validation. This is only meant for + stateless and dummy transformers! + + non_deterministic : bool, default=False + Whether the estimator is not deterministic given a fixed ``random_state``. + + requires_fit : bool, default=True + Whether the estimator requires to be fitted before calling one of + `transform`, `predict`, `predict_proba`, or `decision_function`. + + _skip_test : bool, default=False + Whether to skip common tests entirely. Don't use this unless + you have a *very good* reason. + + input_tags : :class:`InputTags` + The input data(X) tags. + """ + + estimator_type: str | None + target_tags: TargetTags + transformer_tags: TransformerTags | None = None + classifier_tags: ClassifierTags | None = None + regressor_tags: RegressorTags | None = None + array_api_support: bool = False + no_validation: bool = False + non_deterministic: bool = False + requires_fit: bool = True + _skip_test: bool = False + input_tags: InputTags = field(default_factory=InputTags) + + +def _to_new_tags(old_tags, estimator=None): + """Utility function convert old tags (dictionary) to new tags (dataclass).""" + input_tags = InputTags( + one_d_array="1darray" in old_tags["X_types"], + two_d_array="2darray" in old_tags["X_types"], + three_d_array="3darray" in old_tags["X_types"], + sparse="sparse" in old_tags["X_types"], + categorical="categorical" in old_tags["X_types"], + string="string" in old_tags["X_types"], + dict="dict" in old_tags["X_types"], + positive_only=old_tags["requires_positive_X"], + allow_nan=old_tags["allow_nan"], + pairwise=old_tags["pairwise"], + ) + target_tags = TargetTags( + required=old_tags["requires_y"], + one_d_labels="1dlabels" in old_tags["X_types"], + two_d_labels="2dlabels" in old_tags["X_types"], + positive_only=old_tags["requires_positive_y"], + multi_output=old_tags["multioutput"] or old_tags["multioutput_only"], + single_output=not old_tags["multioutput_only"], + ) + if estimator is not None and ( + hasattr(estimator, "transform") or hasattr(estimator, "fit_transform") + ): + transformer_tags = TransformerTags( + preserves_dtype=old_tags["preserves_dtype"], + ) + else: + transformer_tags = None + estimator_type = getattr(estimator, "_estimator_type", None) + if estimator_type == "classifier": + classifier_tags = ClassifierTags( + poor_score=old_tags["poor_score"], + multi_class=not old_tags["binary_only"], + multi_label=old_tags["multilabel"], + ) + else: + classifier_tags = None + if estimator_type == "regressor": + regressor_tags = RegressorTags( + poor_score=old_tags["poor_score"], + multi_label=old_tags["multilabel"], ) + else: + regressor_tags = None + return Tags( + estimator_type=estimator_type, + target_tags=target_tags, + transformer_tags=transformer_tags, + classifier_tags=classifier_tags, + regressor_tags=regressor_tags, + input_tags=input_tags, + array_api_support=old_tags["array_api_support"], + no_validation=old_tags["no_validation"], + non_deterministic=old_tags["non_deterministic"], + requires_fit=old_tags["requires_fit"], + _skip_test=old_tags["_skip_test"], + ) From e1751fc8c833ddb08761e17e2eea8779548dd20d Mon Sep 17 00:00:00 2001 From: adrinjalali Date: Wed, 27 Nov 2024 14:53:34 +0100 Subject: [PATCH 30/43] Fix loss issues --- skops/io/_sklearn.py | 108 +++++++++++++++++++++------------ skops/io/tests/_utils.py | 89 +++++++++++++++------------ skops/io/tests/test_persist.py | 11 ---- 3 files changed, 118 insertions(+), 90 deletions(-) diff --git a/skops/io/_sklearn.py b/skops/io/_sklearn.py index 22e40b56..801ee6fe 100644 --- a/skops/io/_sklearn.py +++ b/skops/io/_sklearn.py @@ -3,49 +3,91 @@ from typing import Any, Dict, Optional, Sequence, Tuple, Type from sklearn.cluster import Birch +from sklearn.tree._tree import Tree -from ._general import TypeNode +from ._audit import Node, get_tree +from ._general import TypeNode, unsupported_get_state from ._protocol import PROTOCOL +from ._utils import LoadContext, SaveContext, get_module, get_state, gettype +from .exceptions import UnsupportedTypeException try: # TODO: remove once support for sklearn<1.2 is dropped. See #187 from sklearn.covariance._graph_lasso import _DictWithDeprecatedKeys except ImportError: _DictWithDeprecatedKeys = None + +from sklearn.linear_model._sgd_fast import ( + EpsilonInsensitive, + Hinge, + ModifiedHuber, + SquaredEpsilonInsensitive, + SquaredHinge, +) + +ALLOWED_LOSSES = { + EpsilonInsensitive, + Hinge, + ModifiedHuber, + SquaredEpsilonInsensitive, + SquaredHinge, +} + try: # TODO: remove once support for sklearn<1.6 is dropped. from sklearn.linear_model._sgd_fast import ( - EpsilonInsensitive, - Hinge, Huber, Log, - LossFunction, - ModifiedHuber, - SquaredEpsilonInsensitive, - SquaredHinge, SquaredLoss, ) - ALLOWED_SGD_LOSSES = { - ModifiedHuber, - Hinge, - SquaredHinge, + ALLOWED_LOSSES |= { + Huber, Log, SquaredLoss, - Huber, - EpsilonInsensitive, - SquaredEpsilonInsensitive, } - SKLEARN_SGD_LOSS = True except ImportError: - SKLEARN_SGD_LOSS = False + pass -from sklearn.tree._tree import Tree +try: + # sklearn>=1.6 + from sklearn._loss._loss import ( + CyAbsoluteError, + CyExponentialLoss, + CyHalfBinomialLoss, + CyHalfGammaLoss, + CyHalfMultinomialLoss, + CyHalfPoissonLoss, + CyHalfSquaredError, + CyHalfTweedieLoss, + CyHalfTweedieLossIdentity, + CyHuberLoss, + CyPinballLoss, + ) + + ALLOWED_LOSSES |= { + CyAbsoluteError, + CyExponentialLoss, + CyHalfBinomialLoss, + CyHalfGammaLoss, + CyHalfMultinomialLoss, + CyHalfPoissonLoss, + CyHalfSquaredError, + CyHalfTweedieLoss, + CyHalfTweedieLossIdentity, + CyHuberLoss, + CyPinballLoss, + } +except ImportError: + pass + +try: + # From sklearn>=1.6 + from sklearn._loss._loss import CyLossFunction as ParentLossClass +except ImportError: + # sklearn<1.6 + from sklearn.linear_model._sgd_fast import LossFunction as ParentLossClass -from ._audit import Node, get_tree -from ._general import unsupported_get_state -from ._utils import LoadContext, SaveContext, get_module, get_state, gettype -from .exceptions import UnsupportedTypeException UNSUPPORTED_TYPES = {Birch} @@ -169,28 +211,22 @@ def __init__( super().__init__(state, load_context, constructor=Tree, trusted=self.trusted) -def sgd_loss_get_state(obj: Any, save_context: SaveContext) -> dict[str, Any]: +def loss_get_state(obj: Any, save_context: SaveContext) -> dict[str, Any]: state = reduce_get_state(obj, save_context) - state["__loader__"] = "SGDNode" + state["__loader__"] = "LossNode" return state -class SGDNode(ReduceNode): +class LossNode(ReduceNode): def __init__( self, state: dict[str, Any], load_context: LoadContext, trusted: Optional[Sequence[str]] = None, ) -> None: - if not SKLEARN_SGD_LOSS: - raise ImportError( - "Cannot load SGD loss functions. This might happen if you're trying to " - "load a model that was saved with an older version of scikit-learn. " - "Please make sure you're using a compatible scikit-learn version." - ) # TODO: make sure trusted here makes sense and used. self.trusted = self._get_trusted( - trusted, [get_module(x) + "." + x.__name__ for x in ALLOWED_SGD_LOSSES] + trusted, [get_module(x) + "." + x.__name__ for x in ALLOWED_LOSSES] ) super().__init__( state, @@ -253,9 +289,8 @@ def _construct(self): # tuples of type and function that gets the state of that type GET_STATE_DISPATCH_FUNCTIONS = [ (Tree, tree_get_state), + (ParentLossClass, loss_get_state), ] -if SKLEARN_SGD_LOSS: - GET_STATE_DISPATCH_FUNCTIONS.append((LossFunction, sgd_loss_get_state)) for type_ in UNSUPPORTED_TYPES: GET_STATE_DISPATCH_FUNCTIONS.append((type_, unsupported_get_state)) @@ -263,13 +298,8 @@ def _construct(self): # tuples of type and function that creates the instance of that type NODE_TYPE_MAPPING: Dict[Tuple[str, int], Any] = { ("TreeNode", PROTOCOL): TreeNode, + ("LossNode", PROTOCOL): LossNode, } -if SKLEARN_SGD_LOSS: - NODE_TYPE_MAPPING.update( - { - ("SGDNode", PROTOCOL): SGDNode, - } - ) # TODO: remove once support for sklearn<1.2 is dropped. # Starting from sklearn 1.2, _DictWithDeprecatedKeys is removed as it's no diff --git a/skops/io/tests/_utils.py b/skops/io/tests/_utils.py index b1f41c25..66dbf47f 100644 --- a/skops/io/tests/_utils.py +++ b/skops/io/tests/_utils.py @@ -44,36 +44,38 @@ def _is_steps_like(obj): return True -def _assert_generic_objects_equal(val1, val2): +def _assert_generic_objects_equal(val1, val2, path=""): def _is_builtin(val): # Check if value is a builtin type return getattr(getattr(val, "__class__", {}), "__module__", None) == "builtins" if isinstance(val1, (list, tuple, np.ndarray)): - assert len(val1) == len(val2) + assert len(val1) == len(val2), f"Path: len({path})" for subval1, subval2 in zip(val1, val2): - _assert_generic_objects_equal(subval1, subval2) + _assert_generic_objects_equal(subval1, subval2, path=f"{path}[]") return - assert type(val1) == type(val2) + assert type(val1) == type(val2), f"Path: type({path})" if hasattr(val1, "__dict__"): - assert_params_equal(val1.__dict__, val2.__dict__) + assert_params_equal(val1.__dict__, val2.__dict__, path=f"{path}.__dict__") elif _is_builtin(val1): - assert val1 == val2 + assert val1 == val2, f"Path: {path}" else: # not a normal Python class, could be e.g. a Cython class - _assert_tuples_equal(val1.__reduce__(), val2.__reduce__()) + _assert_tuples_equal( + val1.__reduce__(), val2.__reduce__(), path=f"{path}.__reduce__" + ) -def _assert_tuples_equal(val1, val2): - assert len(val1) == len(val2) +def _assert_tuples_equal(val1, val2, path=""): + assert len(val1) == len(val2), f"Path: len({path})" for subval1, subval2 in zip(val1, val2): - _assert_vals_equal(subval1, subval2) + _assert_vals_equal(subval1, subval2, path=f"{path}[]") -def _assert_vals_equal(val1, val2): +def _assert_vals_equal(val1, val2, path=""): if isinstance(val1, type): # e.g. could be np.int64 - assert val1 is val2 + assert val1 is val2, f"Path: {path}" elif hasattr(val1, "__getstate__") and (val1.__getstate__() is not None): # This includes BaseEstimator since they implement __getstate__ and # that returns the parameters as well. @@ -82,53 +84,59 @@ def _assert_vals_equal(val1, val2): # Some objects return a tuple of parameters, others a dict. state1 = val1.__getstate__() state2 = val2.__getstate__() - assert type(state1) == type(state2) + assert type(state1) == type(state2), f"Path: {path}" if isinstance(state1, tuple): - _assert_tuples_equal(state1, state2) + _assert_tuples_equal(state1, state2, path=path) else: - assert_params_equal(val1.__getstate__(), val2.__getstate__()) + assert_params_equal( + val1.__getstate__(), val2.__getstate__(), path=f"{path}.__getstate__()" + ) elif sparse.issparse(val1): - assert sparse.issparse(val2) and ((val1 - val2).nnz == 0) + assert sparse.issparse(val2) and ((val1 - val2).nnz == 0), f"Path: {path}" elif isinstance(val1, (np.ndarray, np.generic)): if len(val1.dtype) == 0: # for arrays with at least 2 dimensions, check that contiguity is # preserved if val1.squeeze().ndim > 1: - assert val1.flags["C_CONTIGUOUS"] is val2.flags["C_CONTIGUOUS"] - assert val1.flags["F_CONTIGUOUS"] is val2.flags["F_CONTIGUOUS"] + assert ( + val1.flags["C_CONTIGUOUS"] is val2.flags["C_CONTIGUOUS"] + ), f"Path: {path}.flags" + assert ( + val1.flags["F_CONTIGUOUS"] is val2.flags["F_CONTIGUOUS"] + ), f"Path: {path}.flags" if val1.dtype == object: - assert val2.dtype == object - assert val1.shape == val2.shape + assert val2.dtype == object, f"Path: {path}.dtype" + assert val1.shape == val2.shape, f"Path: {path}.shape" for subval1, subval2 in zip(val1, val2): - _assert_generic_objects_equal(subval1, subval2) + _assert_generic_objects_equal(subval1, subval2, path=f"{path}[]") else: # simple comparison of arrays with simple dtypes, almost all # arrays are of this sort. - np.testing.assert_array_equal(val1, val2) + np.testing.assert_array_equal(val1, val2, err_msg=f"Path: {path}") elif len(val1.shape) == 1: # comparing arrays with structured dtypes, but they have to be 1D # arrays. This is what we get from the Tree's state. - assert np.all([x == y for x, y in zip(val1, val2)]) + assert np.all([x == y for x, y in zip(val1, val2)]), f"Path: {path}" else: # we don't know what to do with these values, for now. - assert False + assert False, f"Path: {path}" elif isinstance(val1, (tuple, list)): - _assert_tuples_equal(val1, val2) + _assert_tuples_equal(val1, val2, path=path) elif isinstance(val1, float) and np.isnan(val1): - assert np.isnan(val2) + assert np.isnan(val2), f"Path: {path}" elif isinstance(val1, dict): # dictionaries are compared by comparing their values recursively. - assert set(val1.keys()) == set(val2.keys()) + assert set(val1.keys()) == set(val2.keys()), f"Path: {path}.keys()" for key in val1: - _assert_vals_equal(val1[key], val2[key]) + _assert_vals_equal(val1[key], val2[key], path=f"{path}[{key}]") elif hasattr(val1, "__dict__") and hasattr(val2, "__dict__"): - _assert_vals_equal(val1.__dict__, val2.__dict__) + _assert_vals_equal(val1.__dict__, val2.__dict__, path=f"{path}.__dict__") elif isinstance(val1, np.ufunc): - assert val1 == val2 + assert val1 == val2, f"Path: {path}" elif val1.__class__.__module__ == "builtins": - assert val1 == val2 + assert val1 == val2, f"Path: {path}" else: - _assert_generic_objects_equal(val1, val2) + _assert_generic_objects_equal(val1, val2, path=path) def _clean_params(params): @@ -144,34 +152,35 @@ def _clean_params(params): return params -def assert_params_equal(params1, params2): +def assert_params_equal(params1, params2, path=""): # helper function to compare estimator dictionaries of parameters if params1 is None and params2 is None: return params1, params2 = _clean_params(params1), _clean_params(params2) - assert len(params1) == len(params2) - assert set(params1.keys()) == set(params2.keys()) + assert len(params1) == len(params2), f"Path: len({path})" + assert set(params1.keys()) == set(params2.keys()), f"Path: {path}.keys()" for key in params1: with warnings.catch_warnings(): # this is to silence the deprecation warning from _DictWithDeprecatedKeys warnings.filterwarnings("ignore", category=FutureWarning, module="sklearn") val1, val2 = params1[key], params2[key] - assert type(val1) == type(val2) + subpath = f"{path}[{key}]" + assert type(val1) == type(val2), f"Path: type({subpath})" if _is_steps_like(val1): # Deal with Pipeline.steps, FeatureUnion.transformer_list, etc. - assert _is_steps_like(val2) + assert _is_steps_like(val2), f"Path: {subpath}" val1, val2 = dict(val1), dict(val2) if isinstance(val1, (tuple, list)): assert len(val1) == len(val2) for subval1, subval2 in zip(val1, val2): - _assert_vals_equal(subval1, subval2) + _assert_vals_equal(subval1, subval2, path=f"{subpath}[]") elif isinstance(val1, dict): - assert_params_equal(val1, val2) + assert_params_equal(val1, val2, path=subpath) else: - _assert_vals_equal(val1, val2) + _assert_vals_equal(val1, val2, path=subpath) def assert_method_outputs_equal(estimator, loaded, X): diff --git a/skops/io/tests/test_persist.py b/skops/io/tests/test_persist.py index eff72d99..d6785050 100644 --- a/skops/io/tests/test_persist.py +++ b/skops/io/tests/test_persist.py @@ -13,9 +13,7 @@ import joblib import numpy as np import pytest -from packaging.version import Version from scipy import sparse, special -from sklearn import __version__ as sklearn_version from sklearn.base import BaseEstimator, is_regressor from sklearn.compose import ColumnTransformer from sklearn.datasets import load_sample_images, make_classification, make_regression @@ -133,15 +131,6 @@ def _tested_estimators(type_filter=None): if Estimator in UNSUPPORTED_TYPES: continue - # Skip SGD-based estimators for sklearn > 1.5 - if Version(sklearn_version) > Version("1.5") and name in { - "SGDClassifier", - "SGDRegressor", - "SGDOneClassSVM", - "PassiveAggressiveClassifier", - "GraphicalLassoCV", - }: - continue try: # suppress warnings here for skipped estimators. with warnings.catch_warnings(): From b9163467d9729bdea17d59e4ca2893661832618d Mon Sep 17 00:00:00 2001 From: adrinjalali Date: Wed, 27 Nov 2024 17:37:18 +0100 Subject: [PATCH 31/43] minor fix --- skops/io/_sklearn.py | 2 ++ skops/io/tests/test_persist.py | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/skops/io/_sklearn.py b/skops/io/_sklearn.py index 801ee6fe..0e34b2af 100644 --- a/skops/io/_sklearn.py +++ b/skops/io/_sklearn.py @@ -81,6 +81,8 @@ except ImportError: pass +# This import is for the parent class of all loss functions, which is used to +# set the dispatch function for all loss functions. try: # From sklearn>=1.6 from sklearn._loss._loss import CyLossFunction as ParentLossClass diff --git a/skops/io/tests/test_persist.py b/skops/io/tests/test_persist.py index d6785050..4d2a3074 100644 --- a/skops/io/tests/test_persist.py +++ b/skops/io/tests/test_persist.py @@ -68,7 +68,7 @@ from skops.utils._fixes import construct_instances, get_tags # Default settings for X -N_SAMPLES = 50 +N_SAMPLES = 100 N_FEATURES = 20 From e1d01327d22cefec4c09e14ef066c8c5816f4e83 Mon Sep 17 00:00:00 2001 From: adrinjalali Date: Wed, 27 Nov 2024 18:47:14 +0100 Subject: [PATCH 32/43] reduce diff --- skops/io/_sklearn.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/skops/io/_sklearn.py b/skops/io/_sklearn.py index 0e34b2af..10300757 100644 --- a/skops/io/_sklearn.py +++ b/skops/io/_sklearn.py @@ -1,6 +1,6 @@ from __future__ import annotations -from typing import Any, Dict, Optional, Sequence, Tuple, Type +from typing import Any, Optional, Sequence, Type from sklearn.cluster import Birch from sklearn.tree._tree import Tree @@ -290,17 +290,17 @@ def _construct(self): # tuples of type and function that gets the state of that type GET_STATE_DISPATCH_FUNCTIONS = [ - (Tree, tree_get_state), (ParentLossClass, loss_get_state), + (Tree, tree_get_state), ] for type_ in UNSUPPORTED_TYPES: GET_STATE_DISPATCH_FUNCTIONS.append((type_, unsupported_get_state)) # tuples of type and function that creates the instance of that type -NODE_TYPE_MAPPING: Dict[Tuple[str, int], Any] = { - ("TreeNode", PROTOCOL): TreeNode, +NODE_TYPE_MAPPING: dict[tuple[str, int], Any] = { ("LossNode", PROTOCOL): LossNode, + ("TreeNode", PROTOCOL): TreeNode, } # TODO: remove once support for sklearn<1.2 is dropped. From 960dff9a5c88daee89e5d5eb2576990f6869259f Mon Sep 17 00:00:00 2001 From: adrinjalali Date: Thu, 28 Nov 2024 10:13:55 +0100 Subject: [PATCH 33/43] annotations import --- skops/utils/_fixes.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/skops/utils/_fixes.py b/skops/utils/_fixes.py index 8fc27d1e..0876cd62 100644 --- a/skops/utils/_fixes.py +++ b/skops/utils/_fixes.py @@ -1,3 +1,5 @@ +from __future__ import annotations + import sys from dataclasses import dataclass, field From 712cb13c59287e476cb49a03e076af7cea3274cd Mon Sep 17 00:00:00 2001 From: Tamara Date: Thu, 28 Nov 2024 16:55:25 +0100 Subject: [PATCH 34/43] work with all instances from _construct_instances --- skops/io/tests/test_persist.py | 56 ++++++++++++++++++---------------- skops/utils/_fixes.py | 4 +-- 2 files changed, 32 insertions(+), 28 deletions(-) diff --git a/skops/io/tests/test_persist.py b/skops/io/tests/test_persist.py index 4d2a3074..ecf8c56f 100644 --- a/skops/io/tests/test_persist.py +++ b/skops/io/tests/test_persist.py @@ -68,7 +68,7 @@ from skops.utils._fixes import construct_instances, get_tags # Default settings for X -N_SAMPLES = 100 +N_SAMPLES = 120 N_FEATURES = 20 @@ -146,20 +146,21 @@ def _tested_estimators(type_filter=None): # scikit-learn < 1.4.0) is not available in scipy >= 1.11.0. The # default solver will be "highs" from scikit-learn >= 1.4.0. # https://scikit-learn.org/stable/modules/generated/sklearn.linear_model.QuantileRegressor.html - estimator = construct_instances(partial(Estimator, solver="highs")) + estimators = construct_instances(partial(Estimator, solver="highs")) else: - estimator = construct_instances(Estimator) - - # with the kind of data we pass, it needs to be 1 for the few - # estimators which have this. - if "n_components" in estimator.get_params(): - estimator.set_params(n_components=1) - # Then n_best needs to be <= n_components - if "n_best" in estimator.get_params(): - estimator.set_params(n_best=1) - if "patch_size" in estimator.get_params(): - # set patch size to fix PatchExtractor test. - estimator.set_params(patch_size=(3, 3)) + estimators = construct_instances(Estimator) + + for estimator in estimators: + # with the kind of data we pass, it needs to be 1 for the few + # estimators which have this. + if "n_components" in estimator.get_params(): + estimator.set_params(n_components=1) + # Then n_best needs to be <= n_components + if "n_best" in estimator.get_params(): + estimator.set_params(n_best=1) + if "patch_size" in estimator.get_params(): + # set patch size to fix PatchExtractor test. + estimator.set_params(patch_size=(3, 3)) except SkipTest: continue @@ -270,17 +271,18 @@ def _unsupported_estimators(type_filter=None): message="Can't instantiate estimator", ) # Get the first instance directly from the generator - estimator = construct_instances(Estimator) + estimators = construct_instances(Estimator) # with the kind of data we pass, it needs to be 1 for the few # estimators which have this. - if "n_components" in estimator.get_params(): - estimator.set_params(n_components=1) - # Then n_best needs to be <= n_components - if "n_best" in estimator.get_params(): - estimator.set_params(n_best=1) - if "patch_size" in estimator.get_params(): - # set patch size to fix PatchExtractor test. - estimator.set_params(patch_size=(3, 3)) + for estimator in estimators: + if "n_components" in estimator.get_params(): + estimator.set_params(n_components=1) + # Then n_best needs to be <= n_components + if "n_best" in estimator.get_params(): + estimator.set_params(n_best=1) + if "patch_size" in estimator.get_params(): + # set patch size to fix PatchExtractor test. + estimator.set_params(patch_size=(3, 3)) except SkipTest: continue @@ -317,7 +319,10 @@ def get_input(estimator): tags = get_tags(estimator) if tags.input_tags.pairwise: - return np.random.rand(N_FEATURES, N_FEATURES), None + if not tags.target_tags.required: + return np.random.rand(N_FEATURES, N_FEATURES), None + else: + return np.random.rand(N_FEATURES, N_FEATURES), y[:N_FEATURES] if tags.input_tags.two_d_array: # Some models require positive X @@ -338,7 +343,7 @@ def get_input(estimator): if tags.input_tags.categorical: X = [["Male", 1], ["Female", 3], ["Female", 2]] - y = y[: len(X)] if tags.y_required else None + y = y[: len(X)] if tags.target_tags.required else None return X, y if tags.input_tags.dict: @@ -417,7 +422,6 @@ def test_can_trust_types(type_): def test_unsupported_type_raises(estimator): """Estimators that are known to fail should raise an error""" set_random_state(estimator, random_state=0) - X, y = get_input(estimator) if get_tags(estimator).requires_fit: with warnings.catch_warnings(): diff --git a/skops/utils/_fixes.py b/skops/utils/_fixes.py index 0876cd62..eaa1f02e 100644 --- a/skops/utils/_fixes.py +++ b/skops/utils/_fixes.py @@ -25,12 +25,12 @@ def construct_instances(estimator): try: from sklearn.utils._test_common.instance_generator import _construct_instances - return next(_construct_instances(estimator)) + return list(_construct_instances(estimator)) except ImportError: from sklearn.utils.estimator_checks import _construct_instance - return _construct_instance(estimator) + return [_construct_instance(estimator)] """ From c3da1b9a13dd63c7349b5c3b27eae7d1bcf62b6f Mon Sep 17 00:00:00 2001 From: Tamara Date: Fri, 29 Nov 2024 14:47:16 +0100 Subject: [PATCH 35/43] Refactor get_input() --- skops/io/tests/test_persist.py | 25 ++++++++++++++++++++----- 1 file changed, 20 insertions(+), 5 deletions(-) diff --git a/skops/io/tests/test_persist.py b/skops/io/tests/test_persist.py index ecf8c56f..cd473d63 100644 --- a/skops/io/tests/test_persist.py +++ b/skops/io/tests/test_persist.py @@ -45,6 +45,7 @@ from sklearn.utils._testing import SkipTest, set_random_state from sklearn.utils.discovery import all_estimators from sklearn.utils.estimator_checks import ( + _enforce_estimator_tags_X, _enforce_estimator_tags_y, _get_check_estimator_ids, ) @@ -161,6 +162,15 @@ def _tested_estimators(type_filter=None): if "patch_size" in estimator.get_params(): # set patch size to fix PatchExtractor test. estimator.set_params(patch_size=(3, 3)) + if "skewedness" in estimator.get_params(): + # prevent data generation errors for SkewedChi2Sampler + estimator.set_params(skewedness=20) + if estimator.__class__.__name__ == "GraphicalLasso": + # prevent data generation errors + estimator.set_params(alpha=1) + if estimator.__class__.__name__ == "GraphicalLassoCV": + # prevent data generation errors + estimator.set_params(alphas=[1, 2]) except SkipTest: continue @@ -316,19 +326,24 @@ def get_input(estimator): n_samples=N_SAMPLES, n_features=N_FEATURES, random_state=0 ) y = _enforce_estimator_tags_y(estimator, y) + X = _enforce_estimator_tags_X(estimator, X) + tags = get_tags(estimator) if tags.input_tags.pairwise: - if not tags.target_tags.required: - return np.random.rand(N_FEATURES, N_FEATURES), None - else: - return np.random.rand(N_FEATURES, N_FEATURES), y[:N_FEATURES] + # return a square matrix of size N_FEATURES x N_FEATURES and positive values + return np.abs(X[:N_FEATURES, :N_FEATURES]), y[:N_FEATURES] - if tags.input_tags.two_d_array: + if tags.input_tags.positive_only: # Some models require positive X return np.abs(X), y + if tags.input_tags.two_d_array: + return X, y + if tags.input_tags.one_d_array: + if X.ndim == 1: + return X, y return X[:, 0], y if tags.input_tags.three_d_array: From a8dad871c2ece226c3397b140ad5ddc343dc5915 Mon Sep 17 00:00:00 2001 From: adrinjalali Date: Mon, 2 Dec 2024 11:06:23 +0100 Subject: [PATCH 36/43] trigger CI From 69325c0653f501197842efd6161a16dc7153a972 Mon Sep 17 00:00:00 2001 From: adrinjalali Date: Mon, 2 Dec 2024 12:15:51 +0100 Subject: [PATCH 37/43] debug CI --- .github/workflows/build-test.yml | 3 ++- skops/io/tests/test_persist.py | 3 +++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/.github/workflows/build-test.yml b/.github/workflows/build-test.yml index d4e9439a..b49715ed 100644 --- a/.github/workflows/build-test.yml +++ b/.github/workflows/build-test.yml @@ -59,6 +59,7 @@ jobs: - name: Install dependencies run: | + set -x python -m pip install -U pip if [ "${{ matrix.os }}" == "macos-latest" ]; then brew install libomp @@ -98,7 +99,7 @@ jobs: - name: Inference tests (conditional) if: contains(env.PR_COMMIT_MESSAGE, '[CI inference]') run: | - python -m pytest -s -v -m "inference" skops/ + python -l -m pytest -s -v -m "inference" skops/ - name: Upload coverage to Codecov uses: codecov/codecov-action@v5 diff --git a/skops/io/tests/test_persist.py b/skops/io/tests/test_persist.py index cd473d63..b0cb2a53 100644 --- a/skops/io/tests/test_persist.py +++ b/skops/io/tests/test_persist.py @@ -386,6 +386,9 @@ def test_can_persist_fitted(estimator): """Check that fitted estimators can be persisted and return the right results.""" set_random_state(estimator, random_state=0) + # A list of estimators which fail on sklearn versions bellow what's indicated + # in the tuple. + X, y = get_input(estimator) if get_tags(estimator).requires_fit: with warnings.catch_warnings(): From d2ecc456722194d238f76b3c1e72cfa2dd1a269b Mon Sep 17 00:00:00 2001 From: adrinjalali Date: Mon, 2 Dec 2024 12:22:00 +0100 Subject: [PATCH 38/43] ... --- .github/workflows/build-test.yml | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/.github/workflows/build-test.yml b/.github/workflows/build-test.yml index b49715ed..3c0ce0b6 100644 --- a/.github/workflows/build-test.yml +++ b/.github/workflows/build-test.yml @@ -20,16 +20,16 @@ jobs: # this is to make the CI run on different sklearn versions include: - python: "3.9" - sklearn_version: "1.1" + sklearn_version: ">=1.1,<1.2" numpy_version: "numpy<2" - python: "3.10" - sklearn_version: "1.2" + sklearn_version: ">=1.2,<1.3" numpy_version: "numpy" - python: "3.11" - sklearn_version: "1.4" + sklearn_version: ">=1.4,<1.5" numpy_version: "numpy" - python: "3.12" - sklearn_version: "1.5" + sklearn_version: ">=1.5,<1.6" numpy_version: "numpy" - python: "3.13" sklearn_version: "nightly" @@ -68,7 +68,7 @@ jobs: pip install "${{ matrix.numpy_version }}" if [ ${{ matrix.sklearn_version }} == "nightly" ]; then pip install --pre --extra-index https://pypi.anaconda.org/scientific-python-nightly-wheels/simple scikit-learn; - else pip install "scikit-learn~=${{ matrix.sklearn_version }}"; + else pip install "scikit-learn${{ matrix.sklearn_version }}"; fi pip install .[docs,tests] pip install black=="23.9.1" ruff=="0.0.292" mypy=="1.6.0" From 87065a33fc802e9b9813525716cc29a47d1deb07 Mon Sep 17 00:00:00 2001 From: adrinjalali Date: Mon, 2 Dec 2024 12:27:42 +0100 Subject: [PATCH 39/43] ... --- .github/workflows/build-test.yml | 13 +++++++------ skops/io/tests/test_persist.py | 10 ++++++++++ 2 files changed, 17 insertions(+), 6 deletions(-) diff --git a/.github/workflows/build-test.yml b/.github/workflows/build-test.yml index 3c0ce0b6..c079ccc7 100644 --- a/.github/workflows/build-test.yml +++ b/.github/workflows/build-test.yml @@ -66,14 +66,15 @@ jobs: fi pip install "pytest<8" pip install "${{ matrix.numpy_version }}" - if [ ${{ matrix.sklearn_version }} == "nightly" ]; - then pip install --pre --extra-index https://pypi.anaconda.org/scientific-python-nightly-wheels/simple scikit-learn; - else pip install "scikit-learn${{ matrix.sklearn_version }}"; + if [ ${{ matrix.sklearn_version }} == "nightly" ]; then + pip install --pre --extra-index https://pypi.anaconda.org/scientific-python-nightly-wheels/simple scikit-learn + pip install .[docs,tests] + else + pip install .[docs,tests] "scikit-learn${{ matrix.sklearn_version }}" fi - pip install .[docs,tests] pip install black=="23.9.1" ruff=="0.0.292" mypy=="1.6.0" - if [ ${{ matrix.os }} == "ubuntu-latest" ]; - then sudo apt install pandoc && pandoc --version; + if [ ${{ matrix.os }} == "ubuntu-latest" ]; then + sudo apt install pandoc && pandoc --version; fi python --version pip --version diff --git a/skops/io/tests/test_persist.py b/skops/io/tests/test_persist.py index b0cb2a53..9c034abf 100644 --- a/skops/io/tests/test_persist.py +++ b/skops/io/tests/test_persist.py @@ -388,6 +388,16 @@ def test_can_persist_fitted(estimator): # A list of estimators which fail on sklearn versions bellow what's indicated # in the tuple. + xfail = [ + # These are related to loss classes not having the right __reduce__ method. + ("PassiveAggressiveClassifier", "1.6"), + ("SGDClassifier", "1.6"), + ("SGDOneClassSVM", "1.6"), + ("TweedieRegressor", "1.6"), + ] + + if xfail: + pass X, y = get_input(estimator) if get_tags(estimator).requires_fit: From d9eaaff5461ed9a89410badfc47450d08293c387 Mon Sep 17 00:00:00 2001 From: adrinjalali Date: Mon, 2 Dec 2024 12:55:54 +0100 Subject: [PATCH 40/43] ... --- skops/io/tests/test_persist.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/skops/io/tests/test_persist.py b/skops/io/tests/test_persist.py index 9c034abf..a0ca3269 100644 --- a/skops/io/tests/test_persist.py +++ b/skops/io/tests/test_persist.py @@ -41,9 +41,8 @@ PolynomialFeatures, StandardScaler, ) -from sklearn.utils import check_random_state +from sklearn.utils import all_estimators, check_random_state from sklearn.utils._testing import SkipTest, set_random_state -from sklearn.utils.discovery import all_estimators from sklearn.utils.estimator_checks import ( _enforce_estimator_tags_X, _enforce_estimator_tags_y, From bcc78d4669ea69459377c21a8a1e8b8c7e79bcbd Mon Sep 17 00:00:00 2001 From: adrinjalali Date: Mon, 2 Dec 2024 13:03:10 +0100 Subject: [PATCH 41/43] ... --- skops/io/tests/test_persist.py | 13 ++--- skops/utils/_fixes.py | 91 ++++++++++++++++++++++++++++++++++ 2 files changed, 98 insertions(+), 6 deletions(-) diff --git a/skops/io/tests/test_persist.py b/skops/io/tests/test_persist.py index a0ca3269..1c6b92bd 100644 --- a/skops/io/tests/test_persist.py +++ b/skops/io/tests/test_persist.py @@ -43,11 +43,7 @@ ) from sklearn.utils import all_estimators, check_random_state from sklearn.utils._testing import SkipTest, set_random_state -from sklearn.utils.estimator_checks import ( - _enforce_estimator_tags_X, - _enforce_estimator_tags_y, - _get_check_estimator_ids, -) +from sklearn.utils.estimator_checks import _get_check_estimator_ids from sklearn.utils.fixes import parse_version, sp_version import skops @@ -65,7 +61,12 @@ from skops.io._utils import LoadContext, SaveContext, _get_state, get_state, gettype from skops.io.exceptions import UnsupportedTypeException, UntrustedTypesFoundException from skops.io.tests._utils import assert_method_outputs_equal, assert_params_equal -from skops.utils._fixes import construct_instances, get_tags +from skops.utils._fixes import ( + _enforce_estimator_tags_X, + _enforce_estimator_tags_y, + construct_instances, + get_tags, +) # Default settings for X N_SAMPLES = 120 diff --git a/skops/utils/_fixes.py b/skops/utils/_fixes.py index eaa1f02e..f615e3a4 100644 --- a/skops/utils/_fixes.py +++ b/skops/utils/_fixes.py @@ -3,6 +3,97 @@ import sys from dataclasses import dataclass, field +try: + from sklearn.utils.estimator_checks import ( + _enforce_estimator_tags_X, + _enforce_estimator_tags_y, + ) +except ImportError: + import numpy as np + from sklearn.metrics.pairwise import linear_kernel, pairwise_distances + + def _enforce_estimator_tags_y(estimator, y): + # Estimators with a `requires_positive_y` tag only accept strictly positive + # data + tags = get_tags(estimator) + if tags.target_tags.positive_only: + # Create strictly positive y. The minimal increment above 0 is 1, as + # y could be of integer dtype. + y += 1 + abs(y.min()) + if ( + tags.classifier_tags is not None + and not tags.classifier_tags.multi_class + and y.size > 0 + ): + y = np.where(y == y.flat[0], y, y.flat[0] + 1) + # Estimators in mono_output_task_error raise ValueError if y is of 1-D + # Convert into a 2-D y for those estimators. + if tags.target_tags.multi_output and not tags.target_tags.single_output: + return np.reshape(y, (-1, 1)) + return y + + def _enforce_estimator_tags_X(estimator, X, X_test=None, kernel=linear_kernel): + def _is_pairwise_metric(estimator): + """Returns True if estimator accepts pairwise metric. + + Parameters + ---------- + estimator : object + Estimator object to test. + + Returns + ------- + out : bool + True if _pairwise is set to True and False otherwise. + """ + metric = getattr(estimator, "metric", None) + + return bool(metric == "precomputed") + + # Estimators with `1darray` in `X_types` tag only accept + # X of shape (`n_samples`,) + if get_tags(estimator).input_tags.one_d_array: + X = X[:, 0] + if X_test is not None: + X_test = X_test[:, 0] # pragma: no cover + # Estimators with a `requires_positive_X` tag only accept + # strictly positive data + if get_tags(estimator).input_tags.positive_only: + X = X - X.min() + if X_test is not None: + X_test = X_test - X_test.min() # pragma: no cover + if get_tags(estimator).input_tags.categorical: + dtype = np.float64 if get_tags(estimator).input_tags.allow_nan else np.int32 + X = np.round((X - X.min())).astype(dtype) + if X_test is not None: + X_test = np.round((X_test - X_test.min())).astype( + dtype + ) # pragma: no cover + + if estimator.__class__.__name__ == "SkewedChi2Sampler": + # SkewedChi2Sampler requires X > -skewdness in transform + X = X - X.min() + if X_test is not None: + X_test = X_test - X_test.min() # pragma: no cover + + X_res = X + + # Pairwise estimators only accept + # X of shape (`n_samples`, `n_samples`) + if _is_pairwise_metric(estimator): + X_res = pairwise_distances(X, metric="euclidean") + if X_test is not None: + X_test = pairwise_distances( + X_test, X, metric="euclidean" + ) # pragma: no cover + elif get_tags(estimator).input_tags.pairwise: + X_res = kernel(X, X) + if X_test is not None: + X_test = kernel(X_test, X) # pragma: no cover + if X_test is not None: + return X_res, X_test + return X_res + def boxplot(ax, *, tick_labels, **kwargs): """A function to handle labels->tick_labels deprecation. From 24783a4b1a68b6433d9db9a851ae233efc571f20 Mon Sep 17 00:00:00 2001 From: adrinjalali Date: Mon, 2 Dec 2024 13:10:44 +0100 Subject: [PATCH 42/43] ... --- skops/io/tests/test_persist.py | 23 +++++++++++++++-------- skops/utils/_fixes.py | 1 + 2 files changed, 16 insertions(+), 8 deletions(-) diff --git a/skops/io/tests/test_persist.py b/skops/io/tests/test_persist.py index 1c6b92bd..5c9699bc 100644 --- a/skops/io/tests/test_persist.py +++ b/skops/io/tests/test_persist.py @@ -13,6 +13,7 @@ import joblib import numpy as np import pytest +import sklearn from scipy import sparse, special from sklearn.base import BaseEstimator, is_regressor from sklearn.compose import ColumnTransformer @@ -386,18 +387,24 @@ def test_can_persist_fitted(estimator): """Check that fitted estimators can be persisted and return the right results.""" set_random_state(estimator, random_state=0) - # A list of estimators which fail on sklearn versions bellow what's indicated - # in the tuple. + # A list of estimators which fail on sklearn versions indicated in the list. xfail = [ # These are related to loss classes not having the right __reduce__ method. - ("PassiveAggressiveClassifier", "1.6"), - ("SGDClassifier", "1.6"), - ("SGDOneClassSVM", "1.6"), - ("TweedieRegressor", "1.6"), + ("PassiveAggressiveClassifier", ["1.4"]), + ("SGDClassifier", ["1.4"]), + ("SGDOneClassSVM", ["1.4"]), + ("TweedieRegressor", ["1.4"]), ] - if xfail: - pass + if any( + estimator.__class__.__name__ == name and sklearn.__version__.startswith(version) + for name, versions in xfail + for version in versions + ): + pytest.xfail( + f"Known issue with {estimator.__class__.__name__} on sklearn version" + f" {sklearn.__version__}" + ) X, y = get_input(estimator) if get_tags(estimator).requires_fit: diff --git a/skops/utils/_fixes.py b/skops/utils/_fixes.py index f615e3a4..2c16f7ee 100644 --- a/skops/utils/_fixes.py +++ b/skops/utils/_fixes.py @@ -4,6 +4,7 @@ from dataclasses import dataclass, field try: + # new in sklearn 1.1 from sklearn.utils.estimator_checks import ( _enforce_estimator_tags_X, _enforce_estimator_tags_y, From e2f0c8202fe5d9b662e8eacdd70c3c9020a4a712 Mon Sep 17 00:00:00 2001 From: adrinjalali Date: Mon, 2 Dec 2024 13:16:19 +0100 Subject: [PATCH 43/43] ... --- skops/io/tests/test_persist.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/skops/io/tests/test_persist.py b/skops/io/tests/test_persist.py index 5c9699bc..649577e9 100644 --- a/skops/io/tests/test_persist.py +++ b/skops/io/tests/test_persist.py @@ -390,10 +390,10 @@ def test_can_persist_fitted(estimator): # A list of estimators which fail on sklearn versions indicated in the list. xfail = [ # These are related to loss classes not having the right __reduce__ method. - ("PassiveAggressiveClassifier", ["1.4"]), - ("SGDClassifier", ["1.4"]), - ("SGDOneClassSVM", ["1.4"]), - ("TweedieRegressor", ["1.4"]), + ("PassiveAggressiveClassifier", ["1.4", "1.5"]), + ("SGDClassifier", ["1.4", "1.5"]), + ("SGDOneClassSVM", ["1.4", "1.5"]), + ("TweedieRegressor", ["1.4", "1.5"]), ] if any(