Repository: sktime/sktime
Pull request #7446 (2024-11-25 17:08:50)

[ENH] conditional testing and imports for estimator specific tests in `detection` module
This PR cleans up the tests in the `detection` module:

* conditional testing via `run_test_for_class`
* imports are changed to `detection` module consistently


Patch:
diff --git a/sktime/detection/base/_base.py b/sktime/detection/base/_base.py
index f42f15047..f98b7d579 100644
--- a/sktime/detection/base/_base.py
+++ b/sktime/detection/base/_base.py
@@ -237,7 +237,7 @@ class BaseDetector(BaseEstimator):
             Labels for sequence ``X``.
 
             * If ``task`` is ``"anomaly_detection"``, the values are integer labels.
-              A value of 0 indicatesthat ``X``, at the same time index, has no anomaly.
+              A value of 0 indicates that ``X``, at the same time index, has no anomaly.
               Other values indicate an anomaly.
               Most detectors will return 0 or 1, but some may return more values,
               if they can detect different types of anomalies.
@@ -587,6 +587,7 @@ class BaseDetector(BaseEstimator):
         y : pd.Series with IntervalIndex
             A series with an index of intervals. Each interval is the range of a
             segment and the corresponding value is the label of the segment.
+            Values are ``iloc`` references to indices of ``X``.
 
             * If ``task`` is ``"anomaly_detection"`` or ``"change_point_detection"``,
               the intervals are intervals between changepoints/anomalies, and
@@ -599,7 +600,7 @@ class BaseDetector(BaseEstimator):
         task = self.get_tag("task")
         if task in ["anomaly_detection", "change_point_detection"]:
             return self.change_points_to_segments(
-                self.predict_points(X), start=X.index.min(), end=X.index.max()
+                self.predict_points(X), start=0, end=len(X)
             )
         elif task == "segmentation":
             return self._predict_segments(X)
@@ -865,12 +866,14 @@ class BaseDetector(BaseEstimator):
 
         Parameters
         ----------
-        y_sparse : pd.Series
-            A series containing the indexes of change points.
-        start : optional
+        y_sparse : pd.Series of int, sorted ascendingly
+            A series containing the iloc indexes of change points.
+        start : optional, default=0
             Starting point of the first segment.
-        end : optional
-            Ending point of the last segment
+            Must be before the first change point, i.e., < y_sparse[0].
+        end : optional, default=y_sparse[-1] + 1
+            End point of the last segment.
+            Must be after the last change point, i.e., > y_sparse[-1].
 
         Returns
         -------
@@ -884,7 +887,7 @@ class BaseDetector(BaseEstimator):
         >>> from sktime.detection.base import BaseDetector
         >>> change_points = pd.Series([1, 2, 5])
         >>> BaseDetector.change_points_to_segments(change_points, 0, 7)
-        [0, 1)   -1
+        [0, 1)    0
         [1, 2)    1
         [2, 5)    2
         [5, 7)    3
@@ -895,25 +898,26 @@ class BaseDetector(BaseEstimator):
 
         breaks = y_sparse.values
 
-        if start > breaks.min():
-            raise ValueError(
-                "The starting index must be before the first change point."
-            )
-        first_change_point = breaks.min()
+        if start is not None and start > breaks.min():
+            raise ValueError("The start index must be before the first change point.")
+        if end is not None and end < breaks.max():
+            raise ValueError("The end index must be after the last change point.")
+
+        if start is None:
+            start = 0
+        if end is None:
+            end = breaks[-1] + 1
 
-        if start is not None:
-            breaks = np.insert(breaks, 0, start)
-        if end is not None:
-            breaks = np.append(breaks, end)
+        breaks = np.insert(breaks, 0, start)
+        breaks = np.append(breaks, end)
 
         index = pd.IntervalIndex.from_breaks(breaks, copy=True, closed="left")
         segments = pd.Series(0, index=index)
 
-        in_range = index.left >= first_change_point
+        in_range = index.left >= start
 
         number_of_segments = in_range.sum()
-        segments.loc[in_range] = range(1, number_of_segments + 1)
-        segments.loc[~in_range] = -1
+        segments.loc[in_range] = range(0, number_of_segments)
 
         return segments
 
diff --git a/sktime/regression/compose/_ensemble.py b/sktime/regression/compose/_ensemble.py
index 63caf349e..dd270b84d 100644
--- a/sktime/regression/compose/_ensemble.py
+++ b/sktime/regression/compose/_ensemble.py
@@ -412,4 +412,16 @@ class ComposableTimeSeriesForestRegressor(BaseTimeSeriesForest, BaseRegressor):
             instance.
             ``create_test_instance`` uses the first (or only) dictionary in ``params``
         """
-        return {"n_estimators": 3}
+        param1 = {"n_estimators": 3}
+        param2 = {
+            "n_estimators": 5,
+            "max_depth": 5,
+            "min_samples_split": 4,
+            "random_state": 42,
+        }
+        param3 = {
+            "n_estimators": 10,
+            "max_depth": 7,
+            "min_samples_split": 0.2,
+        }
+        return [param1, param2, param3]


Test patch:
diff --git a/sktime/detection/tests/test_base.py b/sktime/detection/tests/test_base.py
index 06d79e80f..4b9a8b129 100644
--- a/sktime/detection/tests/test_base.py
+++ b/sktime/detection/tests/test_base.py
@@ -8,8 +8,13 @@ import pytest
 from pandas import testing
 
 from sktime.detection.base import BaseDetector
+from sktime.tests.test_switch import run_test_for_class
 
 
+@pytest.mark.skipif(
+    not run_test_for_class(BaseDetector),
+    reason="run test only if softdeps are present and incrementally (if requested)",
+)
 @pytest.mark.parametrize(
     "y_sparse, y_dense_expected, index",
     [
@@ -47,6 +52,10 @@ def test_sparse_to_dense(y_sparse, y_dense_expected, index):
     testing.assert_series_equal(y_dense_actual, y_dense_expected)
 
 
+@pytest.mark.skipif(
+    not run_test_for_class(BaseDetector),
+    reason="run test only if softdeps are present and incrementally (if requested)",
+)
 @pytest.mark.parametrize(
     "y_dense, y_sparse_expected",
     [
@@ -67,18 +76,40 @@ def test_dense_to_sparse(y_dense, y_sparse_expected):
     testing.assert_series_equal(y_sparse_actual, y_sparse_expected)
 
 
+@pytest.mark.skipif(
+    not run_test_for_class(BaseDetector),
+    reason="run test only if softdeps are present and incrementally (if requested)",
+)
 @pytest.mark.parametrize(
     "change_points, expected_segments, start, end",
     [
         (
             pd.Series([1, 2, 5]),
             pd.Series(
-                [-1, 1, 2, 3],
+                [0, 1, 2, 3],
+                index=pd.IntervalIndex.from_breaks([-42, 1, 2, 5, 7], closed="left"),
+            ),
+            -42,
+            7,
+        ),
+        (
+            pd.Series([1, 2, 5]),
+            pd.Series(
+                [0, 1, 2, 3],
                 index=pd.IntervalIndex.from_breaks([0, 1, 2, 5, 7], closed="left"),
             ),
-            0,
+            None,
             7,
-        )
+        ),
+        (
+            pd.Series([1, 2, 5]),
+            pd.Series(
+                [0, 1, 2, 3],
+                index=pd.IntervalIndex.from_breaks([0, 1, 2, 5, 6], closed="left"),
+            ),
+            None,
+            None,
+        ),
     ],
 )
 def test_change_points_to_segments(change_points, expected_segments, start, end):
@@ -87,6 +118,10 @@ def test_change_points_to_segments(change_points, expected_segments, start, end)
     testing.assert_series_equal(actual_segments, expected_segments)
 
 
+@pytest.mark.skipif(
+    not run_test_for_class(BaseDetector),
+    reason="run test only if softdeps are present and incrementally (if requested)",
+)
 @pytest.mark.parametrize(
     "segments, expected_change_points",
     [
@@ -107,6 +142,10 @@ def test_segments_to_change_points(segments, expected_change_points):
     )
 
 
+@pytest.mark.skipif(
+    not run_test_for_class(BaseDetector),
+    reason="run test only if softdeps are present and incrementally (if requested)",
+)
 @pytest.mark.parametrize(
     "y_sparse, index, y_dense_expected",
     [
@@ -125,6 +164,10 @@ def test_sparse_segments_to_dense(y_sparse, index, y_dense_expected):
     testing.assert_series_equal(y_dense_expected, y_dense_actual)
 
 
+@pytest.mark.skipif(
+    not run_test_for_class(BaseDetector),
+    reason="run test only if softdeps are present and incrementally (if requested)",
+)
 @pytest.mark.parametrize(
     "y_sparse, index, y_dense_expected",
     [(pd.Series([2, 4]), [0, 1, 2, 3, 4, 5, 6], pd.Series([0, 0, 1, 0, 1, 0, 0]))],
diff --git a/sktime/detection/tests/test_bs.py b/sktime/detection/tests/test_bs.py
index 58078a706..88c8559f8 100644
--- a/sktime/detection/tests/test_bs.py
+++ b/sktime/detection/tests/test_bs.py
@@ -1,11 +1,16 @@
 import pandas as pd
 import pytest
 
-from sktime.annotation.bs import BinarySegmentation
+from sktime.detection.bs import BinarySegmentation
+from sktime.tests.test_switch import run_test_for_class
 
 __author__ = ["Alex-JG3"]
 
 
+@pytest.mark.skipif(
+    not run_test_for_class(BinarySegmentation),
+    reason="run test only if softdeps are present and incrementally (if requested)",
+)
 @pytest.mark.parametrize(
     "X,expected_change_points",
     [
@@ -21,6 +26,10 @@ def test_find_change_points(X, expected_change_points):
     assert change_points == expected_change_points
 
 
+@pytest.mark.skipif(
+    not run_test_for_class(BinarySegmentation),
+    reason="run test only if softdeps are present and incrementally (if requested)",
+)
 @pytest.mark.parametrize(
     "X,expected_change_points",
     [
@@ -35,6 +44,10 @@ def test_fit_predict(X, expected_change_points):
     assert change_points.values.tolist() == expected_change_points
 
 
+@pytest.mark.skipif(
+    not run_test_for_class(BinarySegmentation),
+    reason="run test only if softdeps are present and incrementally (if requested)",
+)
 @pytest.mark.parametrize(
     "X,expected_change_points,min_cp_distance",
     [
diff --git a/sktime/detection/tests/test_clasp.py b/sktime/detection/tests/test_clasp.py
index fc87a14f9..3565d4670 100644
--- a/sktime/detection/tests/test_clasp.py
+++ b/sktime/detection/tests/test_clasp.py
@@ -6,8 +6,8 @@ __all__ = []
 import pytest
 from pandas.api.types import is_interval_dtype
 
-from sktime.annotation.clasp import ClaSPSegmentation
 from sktime.datasets import load_gun_point_segmentation
+from sktime.detection.clasp import ClaSPSegmentation
 from sktime.tests.test_switch import run_test_for_class
 
 
diff --git a/sktime/detection/tests/test_datagen.py b/sktime/detection/tests/test_datagen.py
index 539321b25..1ff7c88f2 100644
--- a/sktime/detection/tests/test_datagen.py
+++ b/sktime/detection/tests/test_datagen.py
@@ -5,9 +5,14 @@ __author__ = ["klam-data", "mgorlin", "pyyim"]
 import pytest
 from numpy import array_equal
 
-from sktime.annotation.datagen import piecewise_poisson
+from sktime.detection.datagen import piecewise_poisson
+from sktime.tests.test_switch import run_test_for_class
 
 
+@pytest.mark.skipif(
+    not run_test_for_class(piecewise_poisson),
+    reason="run test only if softdeps are present and incrementally (if requested)",
+)
 @pytest.mark.parametrize(
     "lambdas, lengths, random_state, output",
     [
diff --git a/sktime/detection/tests/test_eagglo.py b/sktime/detection/tests/test_eagglo.py
index 9b6aef8eb..8d2a8d425 100644
--- a/sktime/detection/tests/test_eagglo.py
+++ b/sktime/detection/tests/test_eagglo.py
@@ -4,10 +4,16 @@ __author__ = ["KatieBuc"]
 
 import numpy as np
 import pandas as pd
+import pytest
 
-from sktime.annotation.eagglo import EAgglo
+from sktime.detection.eagglo import EAgglo
+from sktime.tests.test_switch import run_test_for_class
 
 
+@pytest.mark.skipif(
+    not run_test_for_class(EAgglo),
+    reason="run test only if softdeps are present and incrementally (if requested)",
+)
 def test_fit_default_params_univariate():
     """Test univariate data and default parameters.
 
@@ -32,6 +38,10 @@ def test_fit_default_params_univariate():
     assert np.allclose(fit_actual, fit_expected)
 
 
+@pytest.mark.skipif(
+    not run_test_for_class(EAgglo),
+    reason="run test only if softdeps are present and incrementally (if requested)",
+)
 def test_fit_other_params_univariate():
     """Test univariate data with alternative starting clusters."""
     X = pd.DataFrame([-7.207066, -5.722571, 5.889715, 5.488990])
@@ -49,6 +59,10 @@ def test_fit_other_params_univariate():
     assert np.allclose(fit_actual, fit_expected)
 
 
+@pytest.mark.skipif(
+    not run_test_for_class(EAgglo),
+    reason="run test only if softdeps are present and incrementally (if requested)",
+)
 def test_fit_default_params_multivariate():
     """Test multivariate data with default parameters.
 
diff --git a/sktime/detection/tests/test_ggs.py b/sktime/detection/tests/test_ggs.py
index 6ed35cad0..1dcb33505 100644
--- a/sktime/detection/tests/test_ggs.py
+++ b/sktime/detection/tests/test_ggs.py
@@ -4,6 +4,7 @@ import numpy as np
 import pytest
 
 from sktime.detection.ggs import GGS, GreedyGaussianSegmentation
+from sktime.tests.test_switch import run_test_for_class
 
 
 @pytest.fixture
@@ -13,6 +14,10 @@ def univariate_mean_shift():
     return x[:, np.newaxis]
 
 
+@pytest.mark.skipif(
+    not run_test_for_class(GGS),
+    reason="run test only if softdeps are present and incrementally (if requested)",
+)
 def test_GGS_find_change_points(univariate_mean_shift):
     """Test the GGS core estimator."""
     ggs = GGS(k_max=10, lamb=1.0)
@@ -21,6 +26,10 @@ def test_GGS_find_change_points(univariate_mean_shift):
     assert len(pred) == 5
 
 
+@pytest.mark.skipif(
+    not run_test_for_class(GreedyGaussianSegmentation),
+    reason="run test only if softdeps are present and incrementally (if requested)",
+)
 def test_GreedyGaussianSegmentation(univariate_mean_shift):
     """Test the GreedyGaussianSegmentation."""
     ggs = GreedyGaussianSegmentation(k_max=5, lamb=0.5)
diff --git a/sktime/detection/tests/test_hmm.py b/sktime/detection/tests/test_hmm.py
index 1eb4e37ba..71f117161 100644
--- a/sktime/detection/tests/test_hmm.py
+++ b/sktime/detection/tests/test_hmm.py
@@ -7,9 +7,14 @@ import pytest
 from numpy import array_equal, asarray
 from scipy.stats import norm
 
-from sktime.annotation.hmm import HMM
+from sktime.detection.hmm import HMM
+from sktime.tests.test_switch import run_test_for_class
 
 
+@pytest.mark.skipif(
+    not run_test_for_class(HMM),
+    reason="skip test if required soft dependency for hmmlearn not available",
+)
 def test_hmm_basic_gauss():
     """Test HMM works for basic Gaussian synthetic data.
 
@@ -41,6 +46,10 @@ def test_hmm_basic_gauss():
     assert len(predicted_labels == labels) >= 0.95 * len(predicted_labels)
 
 
+@pytest.mark.skipif(
+    not run_test_for_class(HMM),
+    reason="skip test if required soft dependency for hmmlearn not available",
+)
 def test_reject_bad_inputs():
     """Demonstrate failute when initialized with bad inputs."""
     # test we get a ValueError if we don't use square trans_prob
@@ -68,6 +77,10 @@ def test_reject_bad_inputs():
         )
 
 
+@pytest.mark.skipif(
+    not run_test_for_class(HMM),
+    reason="skip test if required soft dependency for hmmlearn not available",
+)
 def test_hmm_behaves_as_expected_on_simple_input():
     """Test HMM is consistent with expected ground truth in simple ex."""
     # define the emission probs for our HMM model:
diff --git a/sktime/detection/tests/test_hmm_learn.py b/sktime/detection/tests/test_hmm_learn.py
index e05a2c12f..6a8460721 100644
--- a/sktime/detection/tests/test_hmm_learn.py
+++ b/sktime/detection/tests/test_hmm_learn.py
@@ -5,12 +5,13 @@ __author__ = ["miraep8", "klam-data", "pyyim", "mgorlin"]
 import pytest
 from numpy import array_equal
 
-from sktime.annotation.datagen import piecewise_normal, piecewise_poisson
-from sktime.utils.dependencies import _check_soft_dependencies
+from sktime.detection.datagen import piecewise_normal, piecewise_poisson
+from sktime.detection.hmm_learn import GMMHMM, GaussianHMM, PoissonHMM
+from sktime.tests.test_switch import run_test_for_class
 
 
 @pytest.mark.skipif(
-    not _check_soft_dependencies("hmmlearn", severity="none"),
+    not run_test_for_class(GaussianHMM),
     reason="skip test if required soft dependency for hmmlearn not available",
 )
 def test_GaussianHMM_wrapper():
@@ -18,8 +19,6 @@ def test_GaussianHMM_wrapper():
     # moved all potential soft dependency import inside the test:
     from hmmlearn.hmm import GaussianHMM as _GaussianHMM
 
-    from sktime.annotation.hmm_learn import GaussianHMM
-
     data = piecewise_normal(
         means=[2, 4, 1], lengths=[10, 35, 40], random_state=7
     ).reshape((-1, 1))
@@ -33,7 +32,7 @@ def test_GaussianHMM_wrapper():
 
 
 @pytest.mark.skipif(
-    not _check_soft_dependencies("hmmlearn", severity="none"),
+    not run_test_for_class(GMMHMM),
     reason="skip test if required soft dependency for hmmlearn not available",
 )
 def test_GMMHMM_wrapper():
@@ -41,8 +40,6 @@ def test_GMMHMM_wrapper():
     # moved all potential soft dependency import inside the test:
     from hmmlearn.hmm import GMMHMM as _GMMHMM
 
-    from sktime.annotation.hmm_learn import GMMHMM
-
     data = piecewise_normal(
         means=[2, 4, 1], lengths=[10, 35, 40], random_state=7
     ).reshape((-1, 1))
@@ -56,7 +53,7 @@ def test_GMMHMM_wrapper():
 
 
 @pytest.mark.skipif(
-    not _check_soft_dependencies("hmmlearn", severity="none"),
+    not run_test_for_class(PoissonHMM),
     reason="skip test if required soft dependency for hmmlearn not available",
 )
 def test_PoissonHMM_wrapper():
@@ -64,8 +61,6 @@ def test_PoissonHMM_wrapper():
     # moved all potential soft dependency import inside the test:
     from hmmlearn.hmm import PoissonHMM as _PoissonHMM
 
-    from sktime.annotation.hmm_learn import PoissonHMM
-
     data = piecewise_poisson(
         lambdas=[1, 2, 3], lengths=[2, 4, 8], random_state=42
     ).reshape((-1, 1))
diff --git a/sktime/detection/tests/test_igts.py b/sktime/detection/tests/test_igts.py
index 35694dd8e..0895bffd5 100644
--- a/sktime/detection/tests/test_igts.py
+++ b/sktime/detection/tests/test_igts.py
@@ -4,6 +4,7 @@ import numpy as np
 import pytest
 
 from sktime.detection.igts import IGTS, InformationGainSegmentation, entropy
+from sktime.tests.test_switch import run_test_for_class
 
 
 @pytest.fixture
@@ -13,11 +14,19 @@ def multivariate_mean_shift():
     return np.vstack((x, x.max() - x)).T
 
 
+@pytest.mark.skipif(
+    not run_test_for_class(entropy),
+    reason="skip test if required soft dependency for hmmlearn not available",
+)
 def test_entropy():
     """Test entropy function."""
     assert entropy(np.ones((10, 1))) == 0.0
 
 
+@pytest.mark.skipif(
+    not run_test_for_class(IGTS),
+    reason="skip test if required soft dependency for hmmlearn not available",
+)
 def test_igts_identity():
     """Test identity segmentation."""
     X = np.random.random(12).reshape(4, 3)
@@ -25,12 +34,20 @@ def test_igts_identity():
     assert id_change_points == [0, 4]
 
 
+@pytest.mark.skipif(
+    not run_test_for_class(IGTS),
+    reason="skip test if required soft dependency for hmmlearn not available",
+)
 def test_igts_get_candidates():
     """Test get_candidates function."""
     candidates = IGTS(step=2).get_candidates(n_samples=10, change_points=[0, 4, 6])
     assert candidates == [2, 8]
 
 
+@pytest.mark.skipif(
+    not run_test_for_class(IGTS),
+    reason="skip test if required soft dependency for hmmlearn not available",
+)
 def test_IGTS_find_change_points(multivariate_mean_shift):
     """Test the IGTS core estimator."""
     igts = IGTS(k_max=3, step=1)
@@ -39,6 +56,10 @@ def test_IGTS_find_change_points(multivariate_mean_shift):
     assert len(pred) == 5
 
 
+@pytest.mark.skipif(
+    not run_test_for_class(InformationGainSegmentation),
+    reason="skip test if required soft dependency for hmmlearn not available",
+)
 def test_InformationGainSegmentation(multivariate_mean_shift):
     """Test the InformationGainSegmentation."""
     igts = InformationGainSegmentation(k_max=3, step=1)
diff --git a/sktime/detection/tests/test_lof.py b/sktime/detection/tests/test_lof.py
index f9aa634e5..27eb42ddf 100644
--- a/sktime/detection/tests/test_lof.py
+++ b/sktime/detection/tests/test_lof.py
@@ -7,7 +7,7 @@ import datetime
 import pandas as pd
 import pytest
 
-from sktime.annotation.lof import SubLOF
+from sktime.detection.lof import SubLOF
 from sktime.tests.test_switch import run_test_for_class
 
 
diff --git a/sktime/detection/tests/test_stray.py b/sktime/detection/tests/test_stray.py
index 6388c1953..4a431482f 100644
--- a/sktime/detection/tests/test_stray.py
+++ b/sktime/detection/tests/test_stray.py
@@ -3,11 +3,17 @@
 __author__ = ["KatieBuc"]
 
 import numpy as np
+import pytest
 from sklearn.preprocessing import MinMaxScaler, RobustScaler
 
-from sktime.annotation.stray import STRAY
+from sktime.detection.stray import STRAY
+from sktime.tests.test_switch import run_test_for_class
 
 
+@pytest.mark.skipif(
+    not run_test_for_class(STRAY),
+    reason="run test only if softdeps are present and incrementally (if requested)",
+)
 def test_default_1D():
     """Test with default parameters and 1D input array."""
     X = np.array(
@@ -91,6 +97,10 @@ def test_default_1D():
     assert np.allclose(y_actual, y_expected)
 
 
+@pytest.mark.skipif(
+    not run_test_for_class(STRAY),
+    reason="run test only if softdeps are present and incrementally (if requested)",
+)
 def test_default_2D():
     """Test with default parameters and 2D input array."""
     X = np.array(
@@ -138,6 +148,10 @@ def test_default_2D():
     assert np.allclose(y_actual, y_expected)
 
 
+@pytest.mark.skipif(
+    not run_test_for_class(STRAY),
+    reason="run test only if softdeps are present and incrementally (if requested)",
+)
 def test_1D_score_with_na():
     """Test score with 1D input array with missing values."""
     X = np.array(
@@ -180,6 +194,10 @@ def test_1D_score_with_na():
     assert np.allclose(y_scores_actual, y_scores_expected, equal_nan=True)
 
 
+@pytest.mark.skipif(
+    not run_test_for_class(STRAY),
+    reason="run test only if softdeps are present and incrementally (if requested)",
+)
 def test_1D_bool_with_na():
     """Test anomaly detection with 1D input array with missing values."""
     X = np.array(
@@ -208,6 +226,10 @@ def test_1D_bool_with_na():
     assert np.allclose(y_actual, y_expected)
 
 
+@pytest.mark.skipif(
+    not run_test_for_class(STRAY),
+    reason="run test only if softdeps are present and incrementally (if requested)",
+)
 def test_2D_score_with_na():
     """Test score with 2D input array with missing values."""
     X = np.array(
@@ -233,6 +255,10 @@ def test_2D_score_with_na():
     assert np.allclose(y_scores_actual, y_scores_expected, equal_nan=True)
 
 
+@pytest.mark.skipif(
+    not run_test_for_class(STRAY),
+    reason="run test only if softdeps are present and incrementally (if requested)",
+)
 def test_2D_bool_with_na():
     """Test anomaly detection with 2D input array with missing values."""
     X = np.array(
@@ -256,6 +282,10 @@ def test_2D_bool_with_na():
     assert np.allclose(y_actual, y_expected)
 
 
+@pytest.mark.skipif(
+    not run_test_for_class(STRAY),
+    reason="run test only if softdeps are present and incrementally (if requested)",
+)
 def test_2D_score_with_standardize():
     """Test score with 2D input array and median/IQR normalization."""
     X = np.array(
diff --git a/sktime/tests/_config.py b/sktime/tests/_config.py
index f18226226..8c7c02355 100644
--- a/sktime/tests/_config.py
+++ b/sktime/tests/_config.py
@@ -324,7 +324,6 @@ EXCLUDED_TESTS_BY_TEST = {
         "ColumnEnsembleClassifier",
         "ColumnTransformer",
         "ColumnwiseTransformer",
-        "ComposableTimeSeriesForestRegressor",
         "ContractableBOSS",
         "DOBIN",
         "DWTTransformer",