CoolFace
Apppublic

Aluode/PerceptionLabPortable

sourceHugging Faceupdated 9mo agoView on Hugging Face
0likes
test_metaestimators_metadata_routing.py928 linesDownload Raw Back to tests
1import copy
2import re
3
4import numpy as np
5import pytest
6
7from sklearn import config_context
8from sklearn.base import BaseEstimator, is_classifier
9from sklearn.calibration import CalibratedClassifierCV
10from sklearn.compose import TransformedTargetRegressor
11from sklearn.covariance import GraphicalLassoCV
12from sklearn.ensemble import (
13    AdaBoostClassifier,
14    AdaBoostRegressor,
15    BaggingClassifier,
16    BaggingRegressor,
17)
18from sklearn.exceptions import UnsetMetadataPassedError
19from sklearn.experimental import (
20    enable_halving_search_cv,  # noqa: F401
21    enable_iterative_imputer,  # noqa: F401
22)
23from sklearn.feature_selection import (
24    RFE,
25    RFECV,
26    SelectFromModel,
27    SequentialFeatureSelector,
28)
29from sklearn.impute import IterativeImputer
30from sklearn.linear_model import (
31    ElasticNetCV,
32    LarsCV,
33    LassoCV,
34    LassoLarsCV,
35    LogisticRegressionCV,
36    MultiTaskElasticNetCV,
37    MultiTaskLassoCV,
38    OrthogonalMatchingPursuitCV,
39    RANSACRegressor,
40    RidgeClassifierCV,
41    RidgeCV,
42)
43from sklearn.metrics._regression import mean_squared_error
44from sklearn.metrics._scorer import make_scorer
45from sklearn.model_selection import (
46    FixedThresholdClassifier,
47    GridSearchCV,
48    GroupKFold,
49    HalvingGridSearchCV,
50    HalvingRandomSearchCV,
51    RandomizedSearchCV,
52    TunedThresholdClassifierCV,
53    cross_validate,
54)
55from sklearn.multiclass import (
56    OneVsOneClassifier,
57    OneVsRestClassifier,
58    OutputCodeClassifier,
59)
60from sklearn.multioutput import (
61    ClassifierChain,
62    MultiOutputClassifier,
63    MultiOutputRegressor,
64    RegressorChain,
65)
66from sklearn.semi_supervised import SelfTrainingClassifier
67from sklearn.tests.metadata_routing_common import (
68    ConsumingClassifier,
69    ConsumingRegressor,
70    ConsumingScorer,
71    ConsumingSplitter,
72    NonConsumingClassifier,
73    NonConsumingRegressor,
74    _Registry,
75    assert_request_is_empty,
76    check_recorded_metadata,
77)
78from sklearn.utils.metadata_routing import MetadataRouter
79
80rng = np.random.RandomState(42)
81N, M = 100, 4
82X = rng.rand(N, M)
83y = rng.randint(0, 3, size=N)
84y_binary = (y >= 1).astype(int)
85classes = np.unique(y)
86y_multi = rng.randint(0, 3, size=(N, 3))
87classes_multi = [np.unique(y_multi[:, i]) for i in range(y_multi.shape[1])]
88metadata = rng.randint(0, 10, size=N)
89sample_weight = rng.rand(N)
90groups = rng.randint(0, 10, size=len(y))
91
92
93METAESTIMATORS: list = [
94    {
95        "metaestimator": MultiOutputRegressor,
96        "estimator_name": "estimator",
97        "estimator": "regressor",
98        "X": X,
99        "y": y_multi,
100        "estimator_routing_methods": ["fit", "partial_fit"],
101    },
102    {
103        "metaestimator": MultiOutputClassifier,
104        "estimator_name": "estimator",
105        "estimator": "classifier",
106        "X": X,
107        "y": y_multi,
108        "estimator_routing_methods": ["fit", "partial_fit"],
109        "method_args": {"partial_fit": {"classes": classes_multi}},
110    },
111    {
112        "metaestimator": CalibratedClassifierCV,
113        "estimator_name": "estimator",
114        "estimator": "classifier",
115        "X": X,
116        "y": y,
117        "estimator_routing_methods": ["fit"],
118        "preserves_metadata": "subset",
119    },
120    {
121        "metaestimator": ClassifierChain,
122        "estimator_name": "estimator",
123        "estimator": "classifier",
124        "X": X,
125        "y": y_multi,
126        "estimator_routing_methods": ["fit"],
127    },
128    {
129        "metaestimator": RegressorChain,
130        "estimator_name": "estimator",
131        "estimator": "regressor",
132        "X": X,
133        "y": y_multi,
134        "estimator_routing_methods": ["fit"],
135    },
136    {
137        "metaestimator": LogisticRegressionCV,
138        "X": X,
139        "y": y,
140        "scorer_name": "scoring",
141        "scorer_routing_methods": ["fit", "score"],
142        "cv_name": "cv",
143        "cv_routing_methods": ["fit"],
144    },
145    {
146        "metaestimator": GridSearchCV,
147        "estimator_name": "estimator",
148        "estimator": "classifier",
149        "init_args": {"param_grid": {"alpha": [0.1, 0.2]}},
150        "X": X,
151        "y": y,
152        "estimator_routing_methods": ["fit"],
153        "preserves_metadata": "subset",
154        "scorer_name": "scoring",
155        "scorer_routing_methods": ["fit", "score"],
156        "cv_name": "cv",
157        "cv_routing_methods": ["fit"],
158    },
159    {
160        "metaestimator": RandomizedSearchCV,
161        "estimator_name": "estimator",
162        "estimator": "classifier",
163        "init_args": {"param_distributions": {"alpha": [0.1, 0.2]}},
164        "X": X,
165        "y": y,
166        "estimator_routing_methods": ["fit"],
167        "preserves_metadata": "subset",
168        "scorer_name": "scoring",
169        "scorer_routing_methods": ["fit", "score"],
170        "cv_name": "cv",
171        "cv_routing_methods": ["fit"],
172    },
173    {
174        "metaestimator": HalvingGridSearchCV,
175        "estimator_name": "estimator",
176        "estimator": "classifier",
177        "init_args": {"param_grid": {"alpha": [0.1, 0.2]}},
178        "X": X,
179        "y": y,
180        "estimator_routing_methods": ["fit"],
181        "preserves_metadata": "subset",
182        "scorer_name": "scoring",
183        "scorer_routing_methods": ["fit", "score"],
184        "cv_name": "cv",
185        "cv_routing_methods": ["fit"],
186    },
187    {
188        "metaestimator": HalvingRandomSearchCV,
189        "estimator_name": "estimator",
190        "estimator": "classifier",
191        "init_args": {"param_distributions": {"alpha": [0.1, 0.2]}},
192        "X": X,
193        "y": y,
194        "estimator_routing_methods": ["fit"],
195        "preserves_metadata": "subset",
196        "scorer_name": "scoring",
197        "scorer_routing_methods": ["fit", "score"],
198        "cv_name": "cv",
199        "cv_routing_methods": ["fit"],
200    },
201    {
202        "metaestimator": FixedThresholdClassifier,
203        "estimator_name": "estimator",
204        "estimator": "classifier",
205        "X": X,
206        "y": y_binary,
207        "estimator_routing_methods": ["fit"],
208        "preserves_metadata": "subset",
209    },
210    {
211        "metaestimator": TunedThresholdClassifierCV,
212        "estimator_name": "estimator",
213        "estimator": "classifier",
214        "X": X,
215        "y": y_binary,
216        "estimator_routing_methods": ["fit"],
217        "preserves_metadata": "subset",
218    },
219    {
220        "metaestimator": OneVsRestClassifier,
221        "estimator_name": "estimator",
222        "estimator": "classifier",
223        "X": X,
224        "y": y,
225        "estimator_routing_methods": ["fit", "partial_fit"],
226        "method_args": {"partial_fit": {"classes": classes}},
227    },
228    {
229        "metaestimator": OneVsOneClassifier,
230        "estimator_name": "estimator",
231        "estimator": "classifier",
232        "X": X,
233        "y": y,
234        "estimator_routing_methods": ["fit", "partial_fit"],
235        "preserves_metadata": "subset",
236        "method_args": {"partial_fit": {"classes": classes}},
237    },
238    {
239        "metaestimator": OutputCodeClassifier,
240        "estimator_name": "estimator",
241        "estimator": "classifier",
242        "init_args": {"random_state": 42},
243        "X": X,
244        "y": y,
245        "estimator_routing_methods": ["fit"],
246    },
247    {
248        "metaestimator": SelectFromModel,
249        "estimator_name": "estimator",
250        "estimator": "classifier",
251        "X": X,
252        "y": y,
253        "estimator_routing_methods": ["fit", "partial_fit"],
254        "method_args": {"partial_fit": {"classes": classes}},
255    },
256    {
257        "metaestimator": OrthogonalMatchingPursuitCV,
258        "X": X,
259        "y": y,
260        "cv_name": "cv",
261        "cv_routing_methods": ["fit"],
262    },
263    {
264        "metaestimator": ElasticNetCV,
265        "X": X,
266        "y": y,
267        "cv_name": "cv",
268        "cv_routing_methods": ["fit"],
269    },
270    {
271        "metaestimator": LassoCV,
272        "X": X,
273        "y": y,
274        "cv_name": "cv",
275        "cv_routing_methods": ["fit"],
276    },
277    {
278        "metaestimator": MultiTaskElasticNetCV,
279        "X": X,
280        "y": y_multi,
281        "cv_name": "cv",
282        "cv_routing_methods": ["fit"],
283    },
284    {
285        "metaestimator": MultiTaskLassoCV,
286        "X": X,
287        "y": y_multi,
288        "cv_name": "cv",
289        "cv_routing_methods": ["fit"],
290    },
291    {
292        "metaestimator": LarsCV,
293        "X": X,
294        "y": y,
295        "cv_name": "cv",
296        "cv_routing_methods": ["fit"],
297    },
298    {
299        "metaestimator": LassoLarsCV,
300        "X": X,
301        "y": y,
302        "cv_name": "cv",
303        "cv_routing_methods": ["fit"],
304    },
305    {
306        "metaestimator": RANSACRegressor,
307        "estimator_name": "estimator",
308        "estimator": "regressor",
309        "init_args": {"min_samples": 0.5},
310        "X": X,
311        "y": y,
312        "preserves_metadata": "subset",
313        "estimator_routing_methods": ["fit", "predict", "score"],
314        "method_mapping": {"fit": ["fit", "score"]},
315    },
316    {
317        "metaestimator": IterativeImputer,
318        "estimator_name": "estimator",
319        "estimator": "regressor",
320        "init_args": {"skip_complete": False},
321        "X": X,
322        "y": y,
323        "estimator_routing_methods": ["fit"],
324    },
325    {
326        "metaestimator": BaggingClassifier,
327        "estimator_name": "estimator",
328        "estimator": "classifier",
329        "X": X,
330        "y": y,
331        "preserves_metadata": False,
332        "estimator_routing_methods": [
333            "fit",
334            "predict",
335            "predict_proba",
336            "predict_log_proba",
337            "decision_function",
338        ],
339        "method_mapping": {
340            "predict": ["predict", "predict_proba"],
341            "predict_proba": ["predict", "predict_proba"],
342            "predict_log_proba": ["predict", "predict_proba", "predict_log_proba"],
343        },
344    },
345    {
346        "metaestimator": BaggingRegressor,
347        "estimator_name": "estimator",
348        "estimator": "regressor",
349        "X": X,
350        "y": y,
351        "preserves_metadata": False,
352        "estimator_routing_methods": ["fit", "predict"],
353    },
354    {
355        "metaestimator": RidgeCV,
356        "X": X,
357        "y": y,
358        "scorer_name": "scoring",
359        "scorer_routing_methods": ["fit"],
360    },
361    {
362        "metaestimator": RidgeClassifierCV,
363        "X": X,
364        "y": y,
365        "scorer_name": "scoring",
366        "scorer_routing_methods": ["fit"],
367    },
368    {
369        "metaestimator": RidgeCV,
370        "X": X,
371        "y": y,
372        "scorer_name": "scoring",
373        "scorer_routing_methods": ["fit"],
374        "cv_name": "cv",
375        "cv_routing_methods": ["fit"],
376    },
377    {
378        "metaestimator": RidgeClassifierCV,
379        "X": X,
380        "y": y,
381        "scorer_name": "scoring",
382        "scorer_routing_methods": ["fit"],
383        "cv_name": "cv",
384        "cv_routing_methods": ["fit"],
385    },
386    {
387        "metaestimator": GraphicalLassoCV,
388        "X": X,
389        "y": y,
390        "cv_name": "cv",
391        "cv_routing_methods": ["fit"],
392    },
393    {
394        "metaestimator": TransformedTargetRegressor,
395        "estimator": "regressor",
396        "estimator_name": "regressor",
397        "X": X,
398        "y": y,
399        "estimator_routing_methods": ["fit", "predict"],
400    },
401    {
402        "metaestimator": SelfTrainingClassifier,
403        "estimator_name": "estimator",
404        "estimator": "classifier",
405        "X": X,
406        "y": y,
407        "preserves_metadata": True,
408        "estimator_routing_methods": [
409            "fit",
410            "predict",
411            "predict_proba",
412            "predict_log_proba",
413            "decision_function",
414            "score",
415        ],
416        "method_mapping": {"fit": ["fit", "score"]},
417    },
418    {
419        "metaestimator": SequentialFeatureSelector,
420        "estimator_name": "estimator",
421        "estimator": "classifier",
422        "X": X,
423        "y": y,
424        "estimator_routing_methods": ["fit"],
425        "scorer_name": "scoring",
426        "scorer_routing_methods": ["fit"],
427        "cv_name": "cv",
428        "cv_routing_methods": ["fit"],
429    },
430    {
431        "metaestimator": RFE,
432        "estimator": "classifier",
433        "estimator_name": "estimator",
434        "X": X,
435        "y": y,
436        "estimator_routing_methods": ["fit", "predict", "score"],
437    },
438    {
439        "metaestimator": RFECV,
440        "estimator": "classifier",
441        "estimator_name": "estimator",
442        "estimator_routing_methods": ["fit"],
443        "cv_name": "cv",
444        "cv_routing_methods": ["fit"],
445        "scorer_name": "scoring",
446        "scorer_routing_methods": ["fit", "score"],
447        "X": X,
448        "y": y,
449    },
450]
451"""List containing all metaestimators to be tested and their settings
452
453The keys are as follows:
454
455- metaestimator: The metaestimator to be tested
456- estimator_name: The name of the argument for the sub-estimator
457- estimator: The sub-estimator type, either "regressor" or "classifier"
458- init_args: The arguments to be passed to the metaestimator's constructor
459- X: X-data to fit and predict
460- y: y-data to fit
461- estimator_routing_methods: list of all methods to check for routing metadata
462  to the sub-estimator
463- preserves_metadata:
464    - True (default): the metaestimator passes the metadata to the
465      sub-estimator without modification. We check that the values recorded by
466      the sub-estimator are identical to what we've passed to the
467      metaestimator.
468    - False: no check is performed regarding values, we only check that a
469      metadata with the expected names/keys are passed.
470    - "subset": we check that the recorded metadata by the sub-estimator is a
471      subset of what is passed to the metaestimator.
472- scorer_name: The name of the argument for the scorer
473- scorer_routing_methods: list of all methods to check for routing metadata
474  to the scorer
475- cv_name: The name of the argument for the CV splitter
476- cv_routing_methods: list of all methods to check for routing metadata
477  to the splitter
478- method_args: a dict of dicts, defining extra arguments needed to be passed to
479  methods, such as passing `classes` to `partial_fit`.
480- method_mapping: a dict of the form `{caller: [callee1, ...]}` which signals
481  which `.set_{method}_request` methods should be called to set request values.
482  If not present, a one-to-one mapping is assumed.
483"""
484
485# IDs used by pytest to get meaningful verbose messages when running the tests
486METAESTIMATOR_IDS = [str(row["metaestimator"].__name__) for row in METAESTIMATORS]
487
488UNSUPPORTED_ESTIMATORS = [
489    AdaBoostClassifier(),
490    AdaBoostRegressor(),
491]
492
493
494def get_init_args(metaestimator_info, sub_estimator_consumes):
495    """Get the init args for a metaestimator
496
497    This is a helper function to get the init args for a metaestimator from
498    the METAESTIMATORS list. It returns an empty dict if no init args are
499    required.
500
501    Parameters
502    ----------
503    metaestimator_info : dict
504        The metaestimator info from METAESTIMATORS
505
506    sub_estimator_consumes : bool
507        Whether the sub-estimator consumes metadata or not.
508
509    Returns
510    -------
511    kwargs : dict
512        The init args for the metaestimator.
513
514    (estimator, estimator_registry) : (estimator, registry)
515        The sub-estimator and the corresponding registry.
516
517    (scorer, scorer_registry) : (scorer, registry)
518        The scorer and the corresponding registry.
519
520    (cv, cv_registry) : (CV splitter, registry)
521        The CV splitter and the corresponding registry.
522    """
523    kwargs = metaestimator_info.get("init_args", {})
524    estimator, estimator_registry = None, None
525    scorer, scorer_registry = None, None
526    cv, cv_registry = None, None
527    if "estimator" in metaestimator_info:
528        estimator_name = metaestimator_info["estimator_name"]
529        estimator_registry = _Registry()
530        sub_estimator_type = metaestimator_info["estimator"]
531        if sub_estimator_consumes:
532            if sub_estimator_type == "regressor":
533                estimator = ConsumingRegressor(estimator_registry)
534            elif sub_estimator_type == "classifier":
535                estimator = ConsumingClassifier(estimator_registry)
536            else:
537                raise ValueError("Unpermitted `sub_estimator_type`.")  # pragma: nocover
538        else:
539            if sub_estimator_type == "regressor":
540                estimator = NonConsumingRegressor()
541            elif sub_estimator_type == "classifier":
542                estimator = NonConsumingClassifier()
543            else:
544                raise ValueError("Unpermitted `sub_estimator_type`.")  # pragma: nocover
545        kwargs[estimator_name] = estimator
546    if "scorer_name" in metaestimator_info:
547        scorer_name = metaestimator_info["scorer_name"]
548        scorer_registry = _Registry()
549        scorer = ConsumingScorer(registry=scorer_registry)
550        kwargs[scorer_name] = scorer
551    if "cv_name" in metaestimator_info:
552        cv_name = metaestimator_info["cv_name"]
553        cv_registry = _Registry()
554        cv = ConsumingSplitter(registry=cv_registry)
555        kwargs[cv_name] = cv
556
557    return (
558        kwargs,
559        (estimator, estimator_registry),
560        (scorer, scorer_registry),
561        (cv, cv_registry),
562    )
563
564
565def set_requests(obj, *, method_mapping, methods, metadata_name, value=True):
566    """Call `set_{method}_request` on a list of methods from the sub-estimator.
567
568    Parameters
569    ----------
570    obj : BaseEstimator
571        The object for which `set_{method}_request` methods are called.
572
573    method_mapping : dict
574        The method mapping in the form of `{caller: [callee, ...]}`.
575        If a "caller" is not present in the method mapping, a one-to-one mapping is
576        assumed.
577
578    methods : list of str
579        The list of methods as "caller"s for which the request for the child should
580        be set.
581
582    metadata_name : str
583        The name of the metadata to be routed, usually either `"metadata"` or
584        `"sample_weight"` in our tests.
585
586    value : None, bool, or str
587        The request value to be set, by default it's `True`
588    """
589    for caller in methods:
590        for callee in method_mapping.get(caller, [caller]):
591            set_request_for_method = getattr(obj, f"set_{callee}_request")
592            set_request_for_method(**{metadata_name: value})
593            if (
594                isinstance(obj, BaseEstimator)
595                and is_classifier(obj)
596                and callee == "partial_fit"
597            ):
598                set_request_for_method(classes=True)
599
600
601@pytest.mark.parametrize("estimator", UNSUPPORTED_ESTIMATORS)
602@config_context(enable_metadata_routing=True)
603def test_unsupported_estimators_get_metadata_routing(estimator):
604    """Test that get_metadata_routing is not implemented on meta-estimators for
605    which we haven't implemented routing yet."""
606    with pytest.raises(NotImplementedError):
607        estimator.get_metadata_routing()
608
609
610@pytest.mark.parametrize("estimator", UNSUPPORTED_ESTIMATORS)
611@config_context(enable_metadata_routing=True)
612def test_unsupported_estimators_fit_with_metadata(estimator):
613    """Test that fit raises NotImplementedError when metadata routing is
614    enabled and a metadata is passed on meta-estimators for which we haven't
615    implemented routing yet."""
616    with pytest.raises(NotImplementedError):
617        try:
618            estimator.fit([[1]], [1], sample_weight=[1])
619        except TypeError:
620            # not all meta-estimators in the list support sample_weight,
621            # and for those we skip this test.
622            raise NotImplementedError
623
624
625@config_context(enable_metadata_routing=True)
626def test_registry_copy():
627    # test that _Registry is not copied into a new instance.
628    a = _Registry()
629    b = _Registry()
630    assert a is not b
631    assert a is copy.copy(a)
632    assert a is copy.deepcopy(a)
633
634
635@pytest.mark.parametrize("metaestimator", METAESTIMATORS, ids=METAESTIMATOR_IDS)
636@config_context(enable_metadata_routing=True)
637def test_default_request(metaestimator):
638    # Check that by default request is empty and the right type
639    metaestimator_class = metaestimator["metaestimator"]
640    kwargs, *_ = get_init_args(metaestimator, sub_estimator_consumes=True)
641    instance = metaestimator_class(**kwargs)
642    if "cv_name" in metaestimator:
643        # Our GroupCV splitters request groups by default, which we should
644        # ignore in this test.
645        exclude = {"splitter": ["split"]}
646    else:
647        exclude = None
648    assert_request_is_empty(instance.get_metadata_routing(), exclude=exclude)
649    assert isinstance(instance.get_metadata_routing(), MetadataRouter)
650
651
652@pytest.mark.parametrize("metaestimator", METAESTIMATORS, ids=METAESTIMATOR_IDS)
653@config_context(enable_metadata_routing=True)
654def test_error_on_missing_requests_for_sub_estimator(metaestimator):
655    # Test that a UnsetMetadataPassedError is raised when the sub-estimator's
656    # requests are not set
657    if "estimator" not in metaestimator:
658        # This test only makes sense for metaestimators which have a
659        # sub-estimator, e.g. MyMetaEstimator(estimator=MySubEstimator())
660        return
661
662    metaestimator_class = metaestimator["metaestimator"]
663    X = metaestimator["X"]
664    y = metaestimator["y"]
665    routing_methods = metaestimator["estimator_routing_methods"]
666
667    for method_name in routing_methods:
668        for key in ["sample_weight", "metadata"]:
669            kwargs, (estimator, _), (scorer, _), *_ = get_init_args(
670                metaestimator, sub_estimator_consumes=True
671            )
672            if scorer:
673                scorer.set_score_request(**{key: True})
674            val = {"sample_weight": sample_weight, "metadata": metadata}[key]
675            method_kwargs = {key: val}
676            instance = metaestimator_class(**kwargs)
677            msg = (
678                f"[{key}] are passed but are not explicitly set as requested or not"
679                f" requested for {estimator.__class__.__name__}.{method_name}"
680            )
681            with pytest.raises(UnsetMetadataPassedError, match=re.escape(msg)):
682                method = getattr(instance, method_name)
683                if "fit" not in method_name:
684                    # set request on fit
685                    set_requests(
686                        estimator,
687                        method_mapping=metaestimator.get("method_mapping", {}),
688                        methods=["fit"],
689                        metadata_name=key,
690                    )
691                    instance.fit(X, y, **method_kwargs)
692                # making sure the requests are unset, in case they were set as a
693                # side effect of setting them for fit. For instance, if method
694                # mapping for fit is: `"fit": ["fit", "score"]`, that would mean
695                # calling `.score` here would not raise, because we have already
696                # set request value for child estimator's `score`.
697                set_requests(
698                    estimator,
699                    method_mapping=metaestimator.get("method_mapping", {}),
700                    methods=["fit"],
701                    metadata_name=key,
702                    value=None,
703                )
704                try:
705                    # `fit`, `partial_fit`, 'score' accept y, others don't.
706                    method(X, y, **method_kwargs)
707                except TypeError:
708                    method(X, **method_kwargs)
709
710
711@pytest.mark.parametrize("metaestimator", METAESTIMATORS, ids=METAESTIMATOR_IDS)
712@config_context(enable_metadata_routing=True)
713def test_setting_request_on_sub_estimator_removes_error(metaestimator):
714    # When the metadata is explicitly requested on the sub-estimator, there
715    # should be no errors.
716    if "estimator" not in metaestimator:
717        # This test only makes sense for metaestimators which have a
718        # sub-estimator, e.g. MyMetaEstimator(estimator=MySubEstimator())
719        return
720
721    metaestimator_class = metaestimator["metaestimator"]
722    X = metaestimator["X"]
723    y = metaestimator["y"]
724    routing_methods = metaestimator["estimator_routing_methods"]
725    method_mapping = metaestimator.get("method_mapping", {})
726    preserves_metadata = metaestimator.get("preserves_metadata", True)
727
728    for method_name in routing_methods:
729        for key in ["sample_weight", "metadata"]:
730            val = {"sample_weight": sample_weight, "metadata": metadata}[key]
731            method_kwargs = {key: val}
732
733            kwargs, (estimator, registry), (scorer, _), (cv, _) = get_init_args(
734                metaestimator, sub_estimator_consumes=True
735            )
736            if scorer:
737                set_requests(
738                    scorer, method_mapping={}, methods=["score"], metadata_name=key
739                )
740            if cv:
741                cv.set_split_request(groups=True, metadata=True)
742
743            # `set_{method}_request({metadata}==True)` on the underlying objects
744            set_requests(
745                estimator,
746                method_mapping=method_mapping,
747                methods=[method_name],
748                metadata_name=key,
749            )
750
751            instance = metaestimator_class(**kwargs)
752            method = getattr(instance, method_name)
753            extra_method_args = metaestimator.get("method_args", {}).get(
754                method_name, {}
755            )
756            if "fit" not in method_name:
757                # fit before calling method
758                instance.fit(X, y)
759            try:
760                # `fit` and `partial_fit` accept y, others don't.
761                method(X, y, **method_kwargs, **extra_method_args)
762            except TypeError:
763                method(X, **method_kwargs, **extra_method_args)
764
765            # sanity check that registry is not empty, or else the test passes
766            # trivially
767            assert registry
768            split_params = (
769                method_kwargs.keys() if preserves_metadata == "subset" else ()
770            )
771            for estimator in registry:
772                check_recorded_metadata(
773                    estimator,
774                    method=method_name,
775                    parent=method_name,
776                    split_params=split_params,
777                    **method_kwargs,
778                )
779
780
781@pytest.mark.parametrize("metaestimator", METAESTIMATORS, ids=METAESTIMATOR_IDS)
782@config_context(enable_metadata_routing=True)
783def test_non_consuming_estimator_works(metaestimator):
784    # Test that when a non-consuming estimator is given, the meta-estimator
785    # works w/o setting any requests.
786    # Regression test for https://github.com/scikit-learn/scikit-learn/issues/28239
787    if "estimator" not in metaestimator:
788        # This test only makes sense for metaestimators which have a
789        # sub-estimator, e.g. MyMetaEstimator(estimator=MySubEstimator())
790        return
791
792    def set_request(estimator, method_name):
793        # e.g. call set_fit_request on estimator
794        if is_classifier(estimator) and method_name == "partial_fit":
795            estimator.set_partial_fit_request(classes=True)
796
797    metaestimator_class = metaestimator["metaestimator"]
798    X = metaestimator["X"]
799    y = metaestimator["y"]
800    routing_methods = metaestimator["estimator_routing_methods"]
801
802    for method_name in routing_methods:
803        kwargs, (estimator, _), (_, _), (_, _) = get_init_args(
804            metaestimator, sub_estimator_consumes=False
805        )
806        instance = metaestimator_class(**kwargs)
807        set_request(estimator, method_name)
808        method = getattr(instance, method_name)
809        extra_method_args = metaestimator.get("method_args", {}).get(method_name, {})
810        if "fit" not in method_name:
811            instance.fit(X, y, **extra_method_args)
812        # The following should pass w/o raising a routing error.
813        try:
814            # `fit` and `partial_fit` accept y, others don't.
815            method(X, y, **extra_method_args)
816        except TypeError:
817            method(X, **extra_method_args)
818
819
820@pytest.mark.parametrize("metaestimator", METAESTIMATORS, ids=METAESTIMATOR_IDS)
821@config_context(enable_metadata_routing=True)
822def test_metadata_is_routed_correctly_to_scorer(metaestimator):
823    """Test that any requested metadata is correctly routed to the underlying
824    scorers in CV estimators.
825    """
826    if "scorer_name" not in metaestimator:
827        # This test only makes sense for CV estimators
828        return
829
830    metaestimator_class = metaestimator["metaestimator"]
831    routing_methods = metaestimator["scorer_routing_methods"]
832    method_mapping = metaestimator.get("method_mapping", {})
833
834    for method_name in routing_methods:
835        kwargs, (estimator, _), (scorer, registry), (cv, _) = get_init_args(
836            metaestimator, sub_estimator_consumes=True
837        )
838        scorer.set_score_request(sample_weight=True)
839        if cv:
840            cv.set_split_request(groups=True, metadata=True)
841        if estimator is not None:
842            set_requests(
843                estimator,
844                method_mapping=method_mapping,
845                methods=[method_name],
846                metadata_name="sample_weight",
847            )
848        instance = metaestimator_class(**kwargs)
849        method = getattr(instance, method_name)
850        method_kwargs = {"sample_weight": sample_weight}
851        if "fit" not in method_name:
852            instance.fit(X, y)
853        method(X, y, **method_kwargs)
854
855        assert registry
856        for _scorer in registry:
857            check_recorded_metadata(
858                obj=_scorer,
859                method="score",
860                parent=method_name,
861                split_params=("sample_weight",),
862                **method_kwargs,
863            )
864
865
866@pytest.mark.parametrize("metaestimator", METAESTIMATORS, ids=METAESTIMATOR_IDS)
867@config_context(enable_metadata_routing=True)
868def test_metadata_is_routed_correctly_to_splitter(metaestimator):
869    """Test that any requested metadata is correctly routed to the underlying
870    splitters in CV estimators.
871    """
872    if "cv_routing_methods" not in metaestimator:
873        # This test is only for metaestimators accepting a CV splitter
874        return
875
876    metaestimator_class = metaestimator["metaestimator"]
877    routing_methods = metaestimator["cv_routing_methods"]
878    X_ = metaestimator["X"]
879    y_ = metaestimator["y"]
880
881    for method_name in routing_methods:
882        kwargs, (estimator, _), (scorer, _), (cv, registry) = get_init_args(
883            metaestimator, sub_estimator_consumes=True
884        )
885        if estimator:
886            estimator.set_fit_request(sample_weight=False, metadata=False)
887        if scorer:
888            scorer.set_score_request(sample_weight=False, metadata=False)
889        cv.set_split_request(groups=True, metadata=True)
890        instance = metaestimator_class(**kwargs)
891        method_kwargs = {"groups": groups, "metadata": metadata}
892        method = getattr(instance, method_name)
893        method(X_, y_, **method_kwargs)
894        assert registry
895        for _splitter in registry:
896            check_recorded_metadata(
897                obj=_splitter, method="split", parent=method_name, **method_kwargs
898            )
899
900
901@pytest.mark.parametrize("metaestimator", METAESTIMATORS, ids=METAESTIMATOR_IDS)
902@config_context(enable_metadata_routing=True)
903def test_metadata_routed_to_group_splitter(metaestimator):
904    """Test that groups are routed correctly if group splitter of CV estimator is used
905    within cross_validate. Regression test for issue described in PR #29634 to test that
906    `ValueError: The 'groups' parameter should not be None.` is not raised."""
907
908    if "cv_routing_methods" not in metaestimator:
909        # This test is only for metaestimators accepting a CV splitter
910        return
911
912    metaestimator_class = metaestimator["metaestimator"]
913    X_ = metaestimator["X"]
914    y_ = metaestimator["y"]
915
916    kwargs, *_ = get_init_args(metaestimator, sub_estimator_consumes=True)
917    # remove `ConsumingSplitter` from kwargs, so 'cv' param isn't passed twice:
918    kwargs.pop("cv", None)
919    instance = metaestimator_class(cv=GroupKFold(n_splits=2), **kwargs)
920    cross_validate(
921        instance,
922        X_,
923        y_,
924        params={"groups": groups},
925        cv=GroupKFold(n_splits=2),
926        scoring=make_scorer(mean_squared_error, response_method="predict"),
927    )
928 
Aluode/PerceptionLabPortable · CoolFace