Repository: sktime/sktime
Pull request #7395 (2024-11-15 17:44:44)

[ENH] refactor `_check_estimator_types` to use record class interface
This refactors `_check_estimator_types` to use the new record class interface, removing the need to construct a full lookup of base classes, reducing coupling in the process.


Patch:
diff --git a/.all-contributorsrc b/.all-contributorsrc
index 153782caa..f48ba9a42 100644
--- a/.all-contributorsrc
+++ b/.all-contributorsrc
@@ -3174,6 +3174,15 @@
         "code"
       ]
     },
+    {
+      "login": "jgyfutub",
+      "name": "Vedant Pandey",
+      "avatar_url": "https://avatars.githubusercontent.com/u/97391064?s=400&v=4",
+      "profile": "https://github.com/jgyfutub",
+      "contributions": [
+        "code"
+      ]
+    },
     {
       "login": "manolotis",
       "name": "Manuel Muñoz Sánchez",
@@ -3182,7 +3191,6 @@
       "contributions": [
         "doc"
       ]
-     }
-
+    }
   ]
 }
diff --git a/sktime/detection/base/_base.py b/sktime/detection/base/_base.py
index a85d441c1..c18f1d3f2 100644
--- a/sktime/detection/base/_base.py
+++ b/sktime/detection/base/_base.py
@@ -65,7 +65,9 @@ class BaseDetector(BaseEstimator):
     """
 
     _tags = {
-        "object_type": "series-annotator",  # type of object
+        # todo 0.37.0 switch order of series-annotator and detector
+        # todo 1.0.0 - remove series-annotator
+        "object_type": ["series-annotator", "detector"],  # type of object
         "learning_type": "None",  # Tag to determine test in test_all_annotators
         "task": "None",  # Tag to determine test in test_all_annotators
         #
diff --git a/sktime/registry/_base_classes.py b/sktime/registry/_base_classes.py
index 4ac44c56e..c7819d8bc 100644
--- a/sktime/registry/_base_classes.py
+++ b/sktime/registry/_base_classes.py
@@ -261,12 +261,12 @@ class regressor(_BaseScitypeOfObject):
         return BaseRegressor
 
 
-class series_annotator(_BaseScitypeOfObject):
-    """Time series annotator."""
+class detector(_BaseScitypeOfObject):
+    """Detector of anomalies, outliers, or change points."""
 
     _tags = {
-        "scitype_name": "series-annotator",
-        "short_descr": "time series annotator",
+        "scitype_name": "detector",
+        "short_descr": "detector - anomalies, outliers, change points",
         "parent_scitype": "estimator",
     }
 
@@ -399,6 +399,30 @@ def _construct_base_class_register(mixin=False):
     return register
 
 
+def get_base_class_for_str(scitype_str):
+    """Return base class for a given scitype string.
+
+    Parameters
+    ----------
+    scitype_str : str, or list of str
+        scitype shorthand, as in scitype_name field of scitype classes
+
+    Returns
+    -------
+    base_cls : class or list of class
+        base class corresponding to the scitype string,
+        or list of base classes if input was a list
+    """
+    if isinstance(scitype_str, list):
+        return [get_base_class_for_str(s) for s in scitype_str]
+
+    base_classes = _get_base_classes()
+    base_classes += _get_base_classes(mixin=True)
+    base_class_lookup = {cl.get_class_tags()["scitype_name"]: cl for cl in base_classes}
+    base_cls = base_class_lookup[scitype_str].get_base_class()
+    return base_cls
+
+
 def get_base_class_register(mixin=False, include_baseobjs=True):
     """Return register of object scitypes and base classes in sktime.
 
@@ -519,6 +543,26 @@ def get_base_class_lookup(mixin=False, include_baseobjs=True):
     return base_class_lookup
 
 
+# LEGACY types - remove in 1.0.0
+# ------------------------------
+
+
+class series_annotator(_BaseScitypeOfObject):
+    """Time series annotator."""
+
+    _tags = {
+        "scitype_name": "series-annotator",
+        "short_descr": "detector - anomalies, outliers, change points",
+        "parent_scitype": "estimator",
+    }
+
+    @classmethod
+    def get_base_class(cls):
+        from sktime.detection.base import BaseDetector
+
+        return BaseDetector
+
+
 class transformer_series_to_primitives(_BaseScitypeOfObject):
     """LEGACY - time series to primitives transformer."""
 
diff --git a/sktime/registry/_lookup.py b/sktime/registry/_lookup.py
index 93894f7e3..b72b7b1c1 100644
--- a/sktime/registry/_lookup.py
+++ b/sktime/registry/_lookup.py
@@ -21,7 +21,10 @@ from pathlib import Path
 import pandas as pd
 from skbase.lookup import all_objects
 
-from sktime.registry._base_classes import get_base_class_lookup, get_obj_scitype_list
+from sktime.registry._base_classes import (
+    get_base_class_for_str,
+    get_obj_scitype_list,
+)
 from sktime.registry._tags import ESTIMATOR_TAG_REGISTER
 
 
@@ -362,7 +365,17 @@ def all_tags(
 
 
 def _check_estimator_types(estimator_types):
-    """Return list of classes corresponding to type strings."""
+    """Return list of classes corresponding to type strings.
+
+    Parameters
+    ----------
+    estimator_types: str, or list of str
+
+    Returns
+    -------
+    estimator_types: list of classes
+        base classes corresponding to scitype strings in estimator_types
+    """
     estimator_types = deepcopy(estimator_types)
 
     if not isinstance(estimator_types, list):
@@ -370,14 +383,12 @@ def _check_estimator_types(estimator_types):
 
     def _get_err_msg(estimator_type):
         return (
-            f"Parameter `estimator_type` must be None, a string or a list of "
+            f"Parameter `estimator_type` must be a string or a list of "
             f"strings. Valid string values are: "
             f"{get_obj_scitype_list()}, but found: "
             f"{repr(estimator_type)}"
         )
 
-    BASE_CLASS_LOOKUP = get_base_class_lookup()
-
     for i, estimator_type in enumerate(estimator_types):
         if not isinstance(estimator_type, (type, str)):
             raise ValueError(
@@ -386,8 +397,7 @@ def _check_estimator_types(estimator_types):
         if isinstance(estimator_type, str):
             if estimator_type not in get_obj_scitype_list():
                 raise ValueError(_get_err_msg(estimator_type))
-            estimator_type = BASE_CLASS_LOOKUP[estimator_type]
-            estimator_types[i] = estimator_type
+            estimator_types[i] = get_base_class_for_str(estimator_type)
         elif isinstance(estimator_type, type):
             pass
         else:
diff --git a/sktime/registry/_scitype_coercion.py b/sktime/registry/_scitype_coercion.py
index dfbf2fa71..2374661e3 100644
--- a/sktime/registry/_scitype_coercion.py
+++ b/sktime/registry/_scitype_coercion.py
@@ -22,11 +22,15 @@ def _coerce_series_annotator_to_transformer(obj):
     return AnnotatorAsTransformer(obj)
 
 
+# todo 1.0.0 - remove series-annotator
 _coerce_register[("series-annotator", "transformer")] = (
     _coerce_series_annotator_to_transformer
 )
 
 
+_coerce_register[("detector", "transformer")] = _coerce_series_annotator_to_transformer
+
+
 def _coerce_clusterer_to_transformer(obj):
     from sktime.clustering.compose import ClustererAsTransformer
 
diff --git a/sktime/registry/_tags.py b/sktime/registry/_tags.py
index 1eddff7b0..2ba57f918 100644
--- a/sktime/registry/_tags.py
+++ b/sktime/registry/_tags.py
@@ -1830,13 +1830,13 @@ ESTIMATOR_TAG_REGISTER = [
     ),
     (
         "task",
-        "series-annotator",
+        "detector",
         "str",
         "subtype of series annotator, e.g., 'anomaly_detection', 'segmentation'",
     ),
     (
         "learning_type",
-        "series-annotator",
+        "detector",
         "str",
         "type of learning, e.g., 'supervised', 'unsupervised'",
     ),


Test patch:
diff --git a/sktime/annotation/tests/test_all_annotators.py b/sktime/annotation/tests/test_all_annotators.py
index 9c5acc093..a4d9a212a 100644
--- a/sktime/annotation/tests/test_all_annotators.py
+++ b/sktime/annotation/tests/test_all_annotators.py
@@ -29,7 +29,7 @@ class AnnotatorsFixtureGenerator(BaseFixtureGenerator):
     #   additional fixtures, parameters, etc should be added here
     #   TestAllAnnotators should contain the tests only
 
-    estimator_type_filter = "series-annotator"
+    estimator_type_filter = "detector"
 
 
 class TestAllAnnotators(AnnotatorsFixtureGenerator, QuickTester):
diff --git a/sktime/registry/tests/test_lookup.py b/sktime/registry/tests/test_lookup.py
index e192d9311..96ebd776f 100644
--- a/sktime/registry/tests/test_lookup.py
+++ b/sktime/registry/tests/test_lookup.py
@@ -239,7 +239,15 @@ def test_all_estimators_tag_filter(pred_int):
 def test_scitype_inference(estimator_scitype):
     """Check that scitype inverts _check_estimator_types."""
     base_class = _check_estimator_types(estimator_scitype)[0]
-    inferred_scitype = scitype(base_class)
+    all_scitypes = scitype(base_class, force_single_scitype=False, coerce_to_list=True)
+    inferred_scitype = all_scitypes[0]
+
+    # stepout for detector due to rename in scitype
+    # todo 0.37.0 - replace "detector" with "series-annotator"
+    # todo 1.0.0 - remove this stepout entirely
+    if estimator_scitype == "detector":
+        assert "detector" in all_scitypes
+        return None
 
     assert (
         inferred_scitype == estimator_scitype
diff --git a/sktime/tests/test_class_register.py b/sktime/tests/test_class_register.py
index 461e55268..8f1f57a73 100644
--- a/sktime/tests/test_class_register.py
+++ b/sktime/tests/test_class_register.py
@@ -54,12 +54,14 @@ def get_test_class_registry():
     # so also imply estimator and object tests, or only object tests
     testclass_dict["aligner"] = TestAllAligners
     testclass_dict["classifier"] = TestAllClassifiers
+    testclass_dict["detector"] = TestAllAnnotators
     testclass_dict["distribution"] = TestAllDistributions
     testclass_dict["early_classifier"] = TestAllEarlyClassifiers
     testclass_dict["forecaster"] = TestAllForecasters
     testclass_dict["global_forecaster"] = TestAllGlobalForecasters
     testclass_dict["param_est"] = TestAllParamFitters
     testclass_dict["regressor"] = TestAllRegressors
+    # todo 1.0.0 - remove series-annotator
     testclass_dict["series-annotator"] = TestAllAnnotators
     testclass_dict["splitter"] = TestAllSplitters
     testclass_dict["transformer"] = TestAllTransformers