Repository: sktime/sktime
Pull request #8130 (2025-04-04 11:32:33)

[DOC] more docstring example fixes
This PR contains further fixes and skips of failing docstring examples.


Patch:
diff --git a/docs/source/api_reference/performance_metrics.rst b/docs/source/api_reference/performance_metrics.rst
index 8bf761258..2e45fd899 100644
--- a/docs/source/api_reference/performance_metrics.rst
+++ b/docs/source/api_reference/performance_metrics.rst
@@ -144,6 +144,7 @@ Event detection - anomalies, outliers
     DirectedHausdorff
     DetectionCount
     WindowedF1Score
+    TimeSeriesAUPRC
 
 Segment detection
 -----------------
diff --git a/sktime/alignment/dtw_python.py b/sktime/alignment/dtw_python.py
index 2c7e0ad89..068424267 100644
--- a/sktime/alignment/dtw_python.py
+++ b/sktime/alignment/dtw_python.py
@@ -253,7 +253,7 @@ class AlignerDTWfromDist(BaseAligner):
     Basic usage example:
     >>> import numpy as np
     >>> import pandas as pd
-    >>> from sktime.alignment.dtw import AlignerDTWfromDist
+    >>> from sktime.alignment.dtw_python import AlignerDTWfromDist
     >>> from sktime.dists_kernels import ScipyDist
     >>> X = [
     ...     pd.DataFrame({'col1': np.random.randn(100)}),
@@ -264,19 +264,6 @@ class AlignerDTWfromDist(BaseAligner):
     >>> aligner.fit(X)
     AlignerDTWfromDist(...)
     >>> alignment_df = aligner.get_alignment()
-
-    Advanced usage example with custom distance transformation:
-    >>> dist_trafo_custom = ScipyDist('cityblock')
-    >>> aligner_custom = AlignerDTWfromDist(
-    ...     dist_trafo=dist_trafo_custom,
-    ...     window_type='sakoechiba',
-    ... )
-    >>> X_custom = [
-    ...     pd.DataFrame({'col1': np.random.randn(200)}),
-    ...     pd.DataFrame({'col1': np.random.randn(200)})
-    ... ]
-    >>> aligner_custom.fit(X_custom)
-    >>> alignment_df_custom = aligner_custom.get_alignment()
     """
 
     _tags = {
diff --git a/sktime/base/_base.py b/sktime/base/_base.py
index ff6c131f1..60dce2411 100644
--- a/sktime/base/_base.py
+++ b/sktime/base/_base.py
@@ -529,6 +529,19 @@ def _clone_estimator(base_estimator, random_state=None):
     return estimator
 
 
+def _safe_clone(object):
+    """Clone an object.
+
+    If the object has a clone method, use that.
+
+    Otherwise delegates to sklearn's clone function.
+    """
+    if hasattr(object, "clone"):
+        return object.clone()
+    else:
+        return clone(object)
+
+
 def deepcopy_func(f, name=None):
     """Deepcopy of a function."""
     import types
diff --git a/sktime/base/_meta.py b/sktime/base/_meta.py
index 9f7ca96df..f2a58c2ea 100644
--- a/sktime/base/_meta.py
+++ b/sktime/base/_meta.py
@@ -11,6 +11,7 @@ import numpy as np
 import pandas as pd
 
 from sktime.base import BaseEstimator
+from sktime.base._base import _safe_clone
 from sktime.utils._estimator_html_repr import _VisualBlock
 
 
@@ -350,7 +351,7 @@ class _HeterogenousMetaEstimator:
             name = type(obj).__name__
 
         if clone_est:
-            return (name, est.clone())
+            return (name, _safe_clone(est))
         else:
             return (name, est)
 
@@ -405,7 +406,7 @@ class _HeterogenousMetaEstimator:
         """
         ests = self._get_estimator_list(estimators)
         if clone_ests:
-            ests = [e.clone() for e in ests]
+            ests = [_safe_clone(e) for e in ests]
         unique_names = self._get_estimator_names(estimators, make_unique=True)
         est_tuples = list(zip(unique_names, ests))
         return est_tuples
@@ -951,7 +952,7 @@ class _ColumnEstimator:
         if isinstance(estimators, cls):
             ycols = [str(col) for col in X.columns]
             colrange = range(len(ycols))
-            est_list = [estimators.clone() for _ in colrange]
+            est_list = [_safe_clone(estimators) for _ in colrange]
             return list(zip(ycols, est_list, colrange))
 
         if (
diff --git a/sktime/detection/clust.py b/sktime/detection/clust.py
index 76b05fbf8..5519c6118 100644
--- a/sktime/detection/clust.py
+++ b/sktime/detection/clust.py
@@ -25,17 +25,6 @@ class ClusterSegmenter(BaseDetector):
     ----------
     clusterer : sklearn.cluster
         The instance of clustering algorithm used for segmentation.
-
-    Examples
-    --------
-    >>> from sklearn.cluster import KMeans
-    >>> from sktime.detection.clust import ClusterSegmenter
-    >>> from sktime.datasets import load_gunpoint
-    >>> X, y = load_gunpoint()
-    >>> clusterer = KMeans()
-    >>> segmenter = ClusterSegmenter(clusterer)
-    >>> segmenter.fit(X)
-    >>> segment_labels = segmenter.predict(X)
     """
 
     _tags = {
diff --git a/sktime/detection/lof.py b/sktime/detection/lof.py
index bd8b02b23..0078929bd 100644
--- a/sktime/detection/lof.py
+++ b/sktime/detection/lof.py
@@ -122,7 +122,6 @@ class SubLOF(BaseDetector):
     12       0
     13       1
     14       0
-    dtype: int64
     """
 
     _tags = {
diff --git a/sktime/forecasting/enbpi.py b/sktime/forecasting/enbpi.py
index 10be5a13b..0c99cd3f5 100644
--- a/sktime/forecasting/enbpi.py
+++ b/sktime/forecasting/enbpi.py
@@ -61,6 +61,7 @@ class EnbPIForecaster(BaseForecaster):
 
     Examples
     --------
+    >>> import numpy as np
     >>> from tsbootstrap import MovingBlockBootstrap
     >>> from sktime.forecasting.enbpi import EnbPIForecaster
     >>> from sktime.forecasting.naive import NaiveForecaster
@@ -77,7 +78,6 @@ class EnbPIForecaster(BaseForecaster):
     >>> res = forecaster.predict()
     >>> res_int = forecaster.predict_interval(coverage=[0.5])
 
-
     References
     ----------
     .. [1] Chen Xu & Yao Xie (2021). Conformal Prediction Interval for Dynamic
diff --git a/sktime/forecasting/model_selection/_tune.py b/sktime/forecasting/model_selection/_tune.py
index deba31cf6..ff1815bd4 100644
--- a/sktime/forecasting/model_selection/_tune.py
+++ b/sktime/forecasting/model_selection/_tune.py
@@ -1711,7 +1711,7 @@ class ForecastingOptunaSearchCV(BaseGridSearch):
     ...             (True, False)
     ...         ),
     ...     "forecaster": CategoricalDistribution(
-    ...             (STLForecaster(), ThetaForecaster())
+    ...             (NaiveForecaster(), ThetaForecaster())
     ...         ),
     ...     }
     >>> gscv = ForecastingOptunaSearchCV(
diff --git a/sktime/performance_metrics/detection/__init__.py b/sktime/performance_metrics/detection/__init__.py
index 667282998..bf71f715f 100644
--- a/sktime/performance_metrics/detection/__init__.py
+++ b/sktime/performance_metrics/detection/__init__.py
@@ -5,6 +5,7 @@ from sktime.performance_metrics.detection._count import DetectionCount
 from sktime.performance_metrics.detection._f1score import WindowedF1Score
 from sktime.performance_metrics.detection._hausdorff import DirectedHausdorff
 from sktime.performance_metrics.detection._randindex import RandIndex
+from sktime.performance_metrics.detection._ts_auprc import TimeSeriesAUPRC
 
 __all__ = [
     "DirectedChamfer",
@@ -12,4 +13,5 @@ __all__ = [
     "DetectionCount",
     "WindowedF1Score",
     "RandIndex",
+    "TimeSeriesAUPRC",
 ]
diff --git a/sktime/performance_metrics/detection/_ts_auprc.py b/sktime/performance_metrics/detection/_ts_auprc.py
new file mode 100644
index 000000000..0b7633162
--- /dev/null
+++ b/sktime/performance_metrics/detection/_ts_auprc.py
@@ -0,0 +1,200 @@
+"""Metric for computing area and precision recall curve for timeseries."""
+
+import numpy as np
+from sklearn.metrics import auc
+
+from sktime.performance_metrics.detection._base import BaseDetectionMetric
+from sktime.performance_metrics.detection.utils import (
+    _compute_window_indices,
+    _improved_cardinality_fn,
+    _ts_precision_and_recall,
+)
+
+__author__ = ["ssarfraz"]
+__all__ = ["TimeSeriesAUPRC"]
+
+
+def _ts_auprc(y_true, y_pred, integration="trapezoid", weighted_precision=True):
+    thresholds = np.unique(y_pred)
+    precision = np.empty(len(thresholds) + 1)
+    recall = np.empty(len(thresholds) + 1)
+    predictions = np.empty_like(y_pred, dtype=int)
+
+    precision[-1] = 1
+    recall[-1] = 0
+    label_ranges = _compute_window_indices(y_true)
+    for i, t in enumerate(thresholds):
+        predictions = y_pred >= t
+        prec, rec = _ts_precision_and_recall(
+            y_true,
+            predictions,
+            alpha=0,
+            recall_cardinality_fn=_improved_cardinality_fn,
+            anomaly_ranges=label_ranges,
+            weighted_precision=weighted_precision,
+        )
+        precision[i] = prec
+        recall[i] = rec
+    if integration == "riemann":
+        area = -np.sum(np.diff(recall) * precision[:-1])
+    else:
+        area = auc(recall, precision)
+
+    return area
+
+
+class TimeSeriesAUPRC(BaseDetectionMetric):
+    r"""TimeSeriesAUPRC: TimeSeries area under precision recall curve.
+
+    This metric is used to evaluate the performan of anomaly detection models
+    calculating the precision and recall across different threshold of predicted
+    anomaly scores using window based method,and then subsequently calculating
+    the Area under precision recall curve. Based on the work in paper _[1] and _[2]
+
+    .. math::
+
+        Trapezoidal Rule (Default):
+
+        \text{AUPRC} \approx \sum_{i=1}^{N}
+        \frac{(R_i - R_{i-1}) \cdot (P_i + P_{i-1})}{2}
+
+
+        Left Riemann Sum (Alternative):
+
+        \text{AUPRC} \approx \sum_{i=1}^{N} (R_i - R_{i-1}) \cdot P_{i-1}
+
+
+        TimeSeAD: Benchmarking Deep Multivariate Time-Series Anomaly Detection,
+        TMLR, 2023 :
+
+        \left(\frac{\text{gt\_length}-1}{\text{gt\_length}}\right)^{\text{cardinality}-1}
+
+
+    Parameters
+    ----------
+    integration : str, optional (default=trapezoid)
+                This parameter specifies the method used to compute
+                the Area Under the Precision-Recall Curve (AUPRC).
+    weighted_precision: bool, optional (default=True)
+                parameter determines whether the precision should be
+                computed in a weighted fashion.
+    with_scores : bool, optional (default= False)
+                This parameter determines whether the input is in
+                label-score format. If False, then assumes input format
+                to be Predicted and Actual Events.
+
+    Returns
+    -------
+    area: float
+        calculated metric
+
+    References
+    ----------
+    .. [1] N. Tatbul,T.J. Lee,S. Zdonik,M. Alam,J. Gottschlich.
+    Precision and recall for time series.
+    Advances in neural information processing systems.
+    .. [2] D. Wagner,T. Michels,F.C.F. Schulz,A. Nair,M. Rudolph and M. Kloft.
+    TimeSeAD: Benchmarking Deep Multivariate Time-Series Anomaly Detection.
+    Transactions on Machine Learning Research (TMLR), (to appear) 2023.
+
+    Examples
+    --------
+    >>> import numpy as np
+    >>> from sktime.performance_metrics.detection import TimeSeriesAUPRC
+    >>> ts_auprc=TimeSeriesAUPRC(with_scores=True)
+    >>> y_true = np.array([0, 0, 1, 1, 0, 0, 1])
+    >>> y_pred = np.array([0.1, 0.3, 0.7, 0.8, 0.2, 0.0, 0.9])
+    >>> area=ts_auprc.evaluate(y_true, y_pred)
+    """
+
+    _tags = {
+        "object_type": ["metric_detection", "metric"],
+        "requires_X": False,  # not using X by default
+        "requires_y_true": True,  # supervised metric
+    }
+
+    def __init__(
+        self, integration="trapezoid", weighted_precision=True, with_scores=False
+    ):
+        self.integration = integration
+        self.weighted_precision = weighted_precision
+        self.with_scores = with_scores
+        super().__init__()
+
+    def _evaluate(self, y_true, y_pred, X=None):
+        """Evaluate the desired metric on given inputs.
+
+        private _evaluate containing core logic, called from evaluate.
+
+        Parameters
+        ----------
+        y_true :pd.DataFrame
+                time series in ``sktime`` compatible data container format.
+                Ground truth (correct) event locations, in ``X``\
+                Should only be ``pd.DataFrame``.
+                Expected format:
+                    Index: time indices or event identifiers
+                    Columns: depending on scitype (`points` or `segments`).
+                    `points` assumes single column, `segments` require ["start","end"].
+            For further details on data format, see glossary on :term:`mtype`.
+
+        y_pred :pd.DataFrame
+                time series in ``sktime`` compatible data container format \
+                Detected events to evaluate against ground truth. \
+                Must be same format as ``y_true``, same indices and columns if indexed.
+
+        X : optional, pd.DataFrame
+            Time series that is being labelled.
+            If not provided, assumes ``RangeIndex`` for ``X``, and that \
+            values in ``X`` do not matter.
+
+        Returns
+        -------
+        loss : float
+            Calculated metric.
+        """
+        self._integration = self.integration
+        self._weighted_precision = self.weighted_precision
+        self._with_scores = self.with_scores
+        if not self._with_scores:
+            n_timepoints = max(max(y_pred["ilocs"]), max(y_true["ilocs"])) + 1
+            scores = np.zeros(n_timepoints)
+            for pred_idx in y_pred["ilocs"]:
+                distances = np.abs(pred_idx - y_true["ilocs"])
+                scores[pred_idx] = 1 / (1 + np.min(distances))
+            true_events = np.zeros(n_timepoints, dtype=bool)
+            true_events[y_true["ilocs"]] = True
+        else:
+            true_events = y_true
+            scores = y_pred
+        return _ts_auprc(
+            true_events,
+            scores,
+            integration=self._integration,
+            weighted_precision=self._weighted_precision,
+        )
+
+    @classmethod
+    def get_test_params(cls, parameter_set="default"):
+        """Return testing parameter settings for the estimator.
+
+        Parameters
+        ----------
+        parameter_set : str, default="default"
+            Name of the set of test parameters to return, for use in tests. If no
+            special parameters are defined for a value, will return ``"default"`` set.
+
+        Returns
+        -------
+        params : dict or list of dict, default={}
+            Parameters to create testing instances of the class.
+            Each dict are parameters to construct an "interesting" test instance, i.e.,
+            ``MyClass(**params)`` or ``MyClass(**params[i])`` creates a valid test
+            instance.
+            ``create_test_instance`` uses the first (or only) dictionary in ``params``.
+        """
+        param1 = {}
+        param2 = {"integration": "trapezoid", "weighted_precision": True}
+        param3 = {"integration": "riemann", "weighted_precision": True}
+
+        return [param1, param2, param3]
diff --git a/sktime/performance_metrics/detection/utils/__init__.py b/sktime/performance_metrics/detection/utils/__init__.py
index a588073fb..ca2144ee0 100644
--- a/sktime/performance_metrics/detection/utils/__init__.py
+++ b/sktime/performance_metrics/detection/utils/__init__.py
@@ -1,5 +1,19 @@
 """Utilities for detection metrics."""
 
+from sktime.performance_metrics.detection.utils._bias_cardinality import (
+    _improved_cardinality_fn,
+    _ts_precision_and_recall,
+)
 from sktime.performance_metrics.detection.utils._closest import _find_closest_elements
+from sktime.performance_metrics.detection.utils._window import (
+    _compute_overlap,
+    _compute_window_indices,
+)
 
-__all__ = ["_find_closest_elements"]
+__all__ = [
+    "_find_closest_elements",
+    "_compute_overlap",
+    "_compute_window_indices",
+    "_ts_precision_and_recall",
+    "_improved_cardinality_fn",
+]
diff --git a/sktime/performance_metrics/detection/utils/_bias_cardinality.py b/sktime/performance_metrics/detection/utils/_bias_cardinality.py
new file mode 100644
index 000000000..6b92b588e
--- /dev/null
+++ b/sktime/performance_metrics/detection/utils/_bias_cardinality.py
@@ -0,0 +1,122 @@
+import numpy as np
+
+from sktime.performance_metrics.detection.utils._window import (
+    _compute_overlap,
+    _compute_window_indices,
+)
+
+
+def _constant_bias_fn(inputs: np.ndarray):
+    """Compute the overlap size for a constant bias function that assigns the.
+
+    same weight to all positions.
+
+    This function computes the average of the input values.
+    :inputs: A 1-D numpy array containing the predictions inside a
+                  ground-truth window.
+    :return: The overlap, which is the average of the input values.
+    """
+    return np.sum(inputs) / inputs.shape[0]
+
+
+def _inverse_proportional_cardinality_fn(cardinality: int, gt_length: int):
+    """Cardinality that assigns an inversely proportional weight to predictions.
+
+    within a single ground-truth window.
+
+    This is the default cardinality function recommended in [Tatbul2018]
+    [Tatbul2018] N. Tatbul, T.J. Lee, S. Zdonik, M. Alam, J. Gottschlich.
+    Precision and recall for time series.
+    Advances in neural information processing systems.
+
+    [Wagner2023]D. Wagner, T. Michels, F.C.F. Schulz, A. Nair, M. Rudolph, and M. Kloft.
+    TimeSeAD: Benchmarking Deep Multivariate Time-Series Anomaly Detection.
+    Transactions on Machine Learning Research (TMLR), (to appear) 2023.
+
+    :cardinality: No. of predicted windows that overlap ground-truth window in question.
+    :gt_length: Length of the ground-truth window (unused).
+    :return: The cardinality factor 1/cardinality, with a minimum value of 1.
+    """
+    return 1 / max(1, cardinality)
+
+
+def _improved_cardinality_fn(cardinality: int, gt_length: int):
+    """Compute recall-consistent cardinality factor from [Wagner2023]_.
+
+    ((gt_length - 1) / gt_length) ** (cardinality - 1)
+    TimeSeAD: Benchmarking Deep Multivariate Time-Series Anomaly Detection, TMLR, 2023.
+    :cardinality: Count of predicted windows overlapping the ground-truth window.
+    :gt_length: Length of the ground-truth window.
+    :return: Cardinality factor.
+    """
+    return ((gt_length - 1) / gt_length) ** (cardinality - 1)
+
+
+def _ts_precision_and_recall(
+    anomalies: np.ndarray,
+    predictions: np.ndarray,
+    alpha: float = 0,
+    recall_bias_fn=_constant_bias_fn,
+    recall_cardinality_fn=_inverse_proportional_cardinality_fn,
+    precision_bias_fn=None,
+    precision_cardinality_fn=None,
+    anomaly_ranges=None,
+    prediction_ranges=None,
+    weighted_precision=False,
+):
+    """Compute time-series precision and recall as defined in [Tatbul2018]_.
+
+    :anomalies: Binary 1-D numpy array of true labels.
+    :predictions: Binary 1-D numpy array of predicted labels.
+    :alpha: Weight for recall's existence term.
+    :recall_bias_fn: Function for recall bias term per ground-truth window.
+    :recall_cardinality_fn: Function for recall cardinality factor.
+    :precision_bias_fn: Function for precision bias term;
+    :precision_cardinality_fn: Function for precision cardinality factor;
+                                     defaults to recall_cardinality_fn.
+    :weighted_precision: Weight precision by window length if True.
+    :anomaly_ranges: List of (start, end) tuples for true anomaly windows;
+    :prediction_ranges: List of (start, end) tuples for predicted windows;
+    :return: Tuple of time-series precision and recall.
+    """
+    has_anomalies = np.any(anomalies > 0)
+    has_predictions = np.any(predictions > 0)
+
+    # Catch special cases which would cause a division by zero
+    if not has_predictions and not has_anomalies:
+        # classifier is perfect, so it makes sense to set precision and recall to 1
+        return 1, 1
+    elif not has_predictions or not has_anomalies:
+        return 0, 0
+
+    # Set precision functions to the same as recall functions if they are not given
+    if precision_bias_fn is None:
+        precision_bias_fn = recall_bias_fn
+    if precision_cardinality_fn is None:
+        precision_cardinality_fn = recall_cardinality_fn
+
+    if anomaly_ranges is None:
+        anomaly_ranges = _compute_window_indices(anomalies)
+    if prediction_ranges is None:
+        prediction_ranges = _compute_window_indices(predictions)
+
+    recall = _compute_overlap(
+        predictions,
+        prediction_ranges,
+        anomaly_ranges,
+        alpha,
+        recall_bias_fn,
+        recall_cardinality_fn,
+        use_window_weight=weighted_precision,
+    )
+    precision = _compute_overlap(
+        anomalies,
+        anomaly_ranges,
+        prediction_ranges,
+        0,
+        precision_bias_fn,
+        precision_cardinality_fn,
+        use_window_weight=weighted_precision,
+    )
+
+    return precision, recall
diff --git a/sktime/performance_metrics/detection/utils/_window.py b/sktime/performance_metrics/detection/utils/_window.py
new file mode 100644
index 000000000..943d5cff2
--- /dev/null
+++ b/sktime/performance_metrics/detection/utils/_window.py
@@ -0,0 +1,80 @@
+import numpy as np
+
+
+def _compute_window_indices(binary_labels: np.ndarray):
+    """Get anomaly window indices from binary labels.
+
+    :binary_labels: 1-D numpy array with 1 for anomalies, 0 otherwise.
+    :return: List of (start, end) tuples, where start is the window's start index and
+            end is the first index after its end.
+    """
+    # Compute the differences between consecutive elements
+    differences = np.diff(binary_labels, prepend=0)
+    # Find the indices where the differences are non-zero (start and end of windows)
+    indices = np.nonzero(differences)[0]
+    # If the number of indices is odd, append the last index
+    if len(indices) % 2 != 0:
+        indices = np.append(indices, binary_labels.size)
+    # Pair the start and end indices
+    window_indices = [(indices[i], indices[i + 1]) for i in range(0, len(indices), 2)]
+
+    return window_indices
+
+
+def _compute_overlap(
+    preds: np.ndarray,
+    pred_indices,
+    gt_indices,
+    alpha: float,
+    bias_fn,
+    cardinality_fn,
+    use_window_weight,
+) -> float:
+    n_gt_windows = len(gt_indices)
+    n_pred_windows = len(pred_indices)
+    total_score = 0.0
+    total_gt_points = 0
+
+    i = j = 0
+    while i < n_gt_windows and j < n_pred_windows:
+        gt_start, gt_end = gt_indices[i]
+        window_length = gt_end - gt_start
+        total_gt_points += window_length
+        i += 1
+
+        cardinality = 0
+        while j < n_pred_windows and pred_indices[j][1] <= gt_start:
+            j += 1
+        while j < n_pred_windows and pred_indices[j][0] < gt_end:
+            j += 1
+            cardinality += 1
+
+        if cardinality == 0:
+            # cardinality == 0 means no overlap at all, hence no contribution
+            continue
+
+        # The last predicted window that overlaps our current window could
+        # also overlap the next window.
+        # Therefore, we must consider it again in the next loop iteration.
+        j -= 1
+
+        cardinality_multiplier = cardinality_fn(cardinality, window_length)
+
+        prediction_inside_ground_truth = preds[gt_start:gt_end]
+        # We calculate omega directly in the bias function,
+        # because this can greatly improve running time
+
+        # for the constant bias, for example.
+        omega = bias_fn(prediction_inside_ground_truth)
+
+        # Either weight evenly across all windows or based on window length
+        weight = window_length if use_window_weight else 1
+
+        # Existence reward (if cardinality > 0 then this is certainly 1)
+        total_score += alpha * weight
+        # Overlap reward
+        total_score += (1 - alpha) * cardinality_multiplier * omega * weight
+
+    denom = total_gt_points if use_window_weight else n_gt_windows
+
+    return total_score / denom
diff --git a/sktime/registry/_scitype.py b/sktime/registry/_scitype.py
index 7ed5028e9..41837d253 100644
--- a/sktime/registry/_scitype.py
+++ b/sktime/registry/_scitype.py
@@ -36,52 +36,121 @@ def scitype(
 ):
     """Determine scitype string of obj.
 
+    This function returns the ``sktime`` internal type for the object ``obj``,
+    the so-called :term:`scitype`, e.g., ``"forecaster"``, ``"transformer"``.
+
+    A scitype defines a unified interface for a family of objects, e.g.,
+    all forecasters strictly adhere to the ``BaseForecaster`` API.
+
+    A list of all ``sktime`` scitypes can be found in the
+    ``BASE_CLASS_SCITYPE_LIST`` in the ``sktime.registry`` module,
+    a table with explanations in the ``BASE_CLASS_REGISTER`` in the same module.
+
+    For ``sktime`` objects, the scitype is determined by the ``object_type`` tag.
+
+    For ``sklearn`` objects, the scitype is determined by
+    inheritance from ``sklearn`` mixin classes.
+
     Parameters
     ----------
-    obj : class or object inheriting from sktime BaseObject
+    obj : class or object inheriting from sktime BaseObject or sklearn BaseEstimator
+
     force_single_scitype : bool, optional, default = True
-        whether only a single scitype is returned
-        if True, only the *first* scitype found will be returned
-        order is determined by the order in BASE_CLASS_REGISTER
+        whether only a single scitype is returned.
+
+        * if True, only the *first* scitype found will be returned
+          order is determined by the order in ``BASE_CLASS_REGISTER`` resp
+          ``BASE_CLASS_SCITYPE_LIST`` (both imply the same odrer)
+        * if False, a list of all scitypes is returned
+
     coerce_to_list : bool, optional, default = False
-        determines the return type: if True, returns a single str,
-        if False, returns a list of str
+        determines the return type:
+
+        * if True, always returns a list of str
+        * if False, returns a single str for a single scitype,
+          and a list of str for multiple scitypes
+
     raise_on_unknown : bool, optional, default = True
-        if True, raises an error if no scitype can be determined for obj
-        if False, returns "object" scitype
+
+        * if True, raises an error if no scitype can be determined for obj
+        * if False, returns "object" scitype
 
     Returns
     -------
-    scitype : str, or list of str of sktime scitype strings from BASE_CLASS_REGISTER
-        str, sktime scitype string, if exactly one scitype can be determined for obj
-        or force_single_scitype is True, and if coerce_to_list is False
-        list of str, of scitype strings, if more than one scitype are determined,
-        or if coerce_to_list is True
-        obj has scitype if it inherits from class in same row of BASE_CLASS_REGISTER
+    scitype : str, or list of str of sktime scitype strings
+
+        strings in the return object can be:
+
+        * an ``sktime`` scitype string, from ``BASE_CLASS_SCITYPE_LIST`` resp
+          ``BASE_CLASS_REGISTER`` (both contain the same strings).
+          These include (not an exhaustive list):
+
+            * ``"forecaster"`` - ``sktime`` forecaster
+            * ``"classifier"`` - ``sktime`` time series classifier
+            * ``"regressor"`` - ``sktime`` time series regressor
+            * ``"clusterer"`` - ``sktime`` time series clusterer
+            * ``"transformer"`` - ``sktime`` time series transformation
+
+        * an ``sklearn`` scitype string used in ``sktime``, this can be:
+
+            * ``"classifier_tabular"`` - ``sklearn`` classifier
+            * ``"regressor_tabular"`` - ``sklearn`` regressor
+            * ``"clusterer_tabular"`` - ``sklearn`` clusterer
+            * ``"transformer_tabular"`` - ``sklearn`` transformation
+
+        The return type is:
+
+        * str, sktime scitype string, if exactly one scitype can be determined for obj
+          or ``force_single_scitype`` is True, and if ``coerce_to_list`` is False
+        * list of str, of scitype strings, if more than one scitype are determined,
+          or if ``coerce_to_list`` is True
 
     Raises
     ------
     TypeError if no scitype can be determined for obj
     """
-    # if object has tag, return tag
+    # this function uses three passes to determine scitype:
+    #
+    # 1st pass: check if object has skbase tag manager, then use that
+    # 2nd pass: check if object is sklearn estimator, then use sklearn scitype
+    # 3rd pass: check if object is subclass of any of the base classes in
+    # the BASE_CLASS_REGISTER, then use that
+    # if neither: if no scitype can be determined, return "object" scitype
+
+    def handle_output_format(detected_scitype):
+        """Handle the return format of the detected scitype."""
+        if not isinstance(detected_scitype, list):
+            detected_scitype = [detected_scitype]
+        if force_single_scitype and len(detected_scitype) > 1:
+            detected_scitype = [detected_scitype[0]]
+        if not coerce_to_list:
+            detected_scitype = detected_scitype[0]
+        return detected_scitype
+
+    # 1st pass
+    # if object has scitype tag, return tag
     if hasattr(obj, "get_tag"):
         if isclass(obj):
-            tag_type = obj.get_class_tag("object_type", None)
+            detected_scitype = obj.get_class_tag("object_type", None)
         else:
-            tag_type = obj.get_tag("object_type", None, raise_error=False)
-        if tag_type is not None:
-            if not isinstance(tag_type, list):
-                tag_type = [tag_type]
-            if force_single_scitype and len(tag_type) > 1:
-                tag_type = [tag_type[0]]
-            if coerce_to_list:
-                return tag_type
-            else:
-                return tag_type[0]
+            detected_scitype = obj.get_tag("object_type", None, raise_error=False)
+        if detected_scitype is not None:
+            return handle_output_format(detected_scitype)
 
-    BASE_CLASS_REGISTER = get_base_class_register()
+    # 2nd pass
+    # check if object is sklearn estimator
+    # if it is, return sklearn scitype
+    from sktime.utils.sklearn import is_sklearn_estimator, sklearn_scitype
+
+    if is_sklearn_estimator(obj):
+        detected_scitype = f"{sklearn_scitype(obj)}_tabular"
+        return handle_output_format(detected_scitype)
 
+    # 3rd pass
+    # check in the base class register
     # if the tag is not present, determine scitype from legacy base class logic
+    BASE_CLASS_REGISTER = get_base_class_register()
+
     if isclass(obj):
         scitypes = [sci[0] for sci in BASE_CLASS_REGISTER if issubclass(obj, sci[1])]
     else:
diff --git a/sktime/registry/_scitype_coercion.py b/sktime/registry/_scitype_coercion.py
index c09d2fb4d..938f57133 100644
--- a/sktime/registry/_scitype_coercion.py
+++ b/sktime/registry/_scitype_coercion.py
@@ -103,6 +103,7 @@ def coerce_scitype(
     TypeError if ``raise_on_mismatch``, and ``from_scitype`` is not None, and
         the detected scitype does not match the expected scitype ``from_scitype``.
     """
+    from sktime.base._base import _safe_clone
     from sktime.registry._scitype import scitype
     from sktime.utils.sklearn import is_sklearn_estimator, sklearn_scitype
 
@@ -138,12 +139,7 @@ def coerce_scitype(
         from_scitype = detected_scitype
 
     if clone_obj:
-        if is_sklearn_estimator(obj) or not hasattr(obj, "clone"):
-            from sklearn.base import clone
-
-            obj = clone(obj)
-        else:
-            obj = obj.clone()
+        obj = _safe_clone(obj)
 
     if (from_scitype, to_scitype) not in _coerce_register:
         return obj
diff --git a/sktime/transformations/series/kalman_filter/_kalman_filter.py b/sktime/transformations/series/kalman_filter/_kalman_filter.py
index 8c7a0c34c..3f3b95f80 100644
--- a/sktime/transformations/series/kalman_filter/_kalman_filter.py
+++ b/sktime/transformations/series/kalman_filter/_kalman_filter.py
@@ -715,7 +715,7 @@ class KalmanFilterTransformerFP(BaseKalmanFilter, BaseTransformer):
     --------
         Basic example:
 
-    >>> import numpy as np  # doctest: +SKIP
+    >>> import numpy as np
     >>> import sktime.transformations.series.kalman_filter as kf
     >>> time_steps, state_dim, measurement_dim = 10, 2, 3
     >>>
@@ -724,7 +724,7 @@ class KalmanFilterTransformerFP(BaseKalmanFilter, BaseTransformer):
     >>> Xt = transformer.fit_transform(X=X)  # doctest: +SKIP
 
         Example of - denoising, matrix estimation, missing values and transform with y:
-    >>> import numpy as np  # doctest: +SKIP
+    >>> import numpy as np
     >>> import sktime.transformations.series.kalman_filter as kf
     >>> time_steps, state_dim, measurement_dim = 10, 3, 3
     >>> control_variable_dim = 2
@@ -748,7 +748,7 @@ class KalmanFilterTransformerFP(BaseKalmanFilter, BaseTransformer):
 
         Example of - dynamic inputs (matrix per time-step), missing values:
 
-    >>> import numpy as np  # doctest: +SKIP
+    >>> import numpy as np
     >>> import sktime.transformations.series.kalman_filter as kf
     >>> time_steps, state_dim, measurement_dim = 10, 4, 4
     >>> control_variable_dim = 4


Test patch:
diff --git a/sktime/registry/tests/test_scitype.py b/sktime/registry/tests/test_scitype.py
index 688801c2f..702f456d0 100644
--- a/sktime/registry/tests/test_scitype.py
+++ b/sktime/registry/tests/test_scitype.py
@@ -1,4 +1,4 @@
-"""Tests for scitype typipng function."""
+"""Tests for scitype typing function."""
 
 import pytest
 
@@ -104,3 +104,44 @@ def test_is_scitype():
     assert is_scitype(_DummyClass, "foo")
     assert is_scitype(_DummyClass, "bar")
     assert not is_scitype(_DummyClass, "baz")
+
+
+def test_sklearn_scitypes():
+    """Test that scitype correctly identifies sklearn scitypes."""
+    from sklearn.linear_model import LinearRegression
+    from sklearn.preprocessing import StandardScaler
+    from sklearn.svm import SVC
+
+    assert scitype(LinearRegression) == "regressor_tabular"
+    assert scitype(LinearRegression()) == "regressor_tabular"
+    assert scitype(StandardScaler) == "transformer_tabular"
+    assert scitype(StandardScaler()) == "transformer_tabular"
+    assert scitype(SVC) == "classifier_tabular"
+    assert scitype(SVC()) == "classifier_tabular"
+
+    assert is_scitype(LinearRegression, "regressor_tabular")
+    assert is_scitype(LinearRegression(), "regressor_tabular")
+    assert is_scitype(StandardScaler, "transformer_tabular")
+    assert is_scitype(StandardScaler(), "transformer_tabular")
+    assert is_scitype(SVC, "classifier_tabular")
+    assert is_scitype(SVC(), "classifier_tabular")
+
+    from sklearn.pipeline import Pipeline
+
+    class_pipe = Pipeline(
+        steps=[
+            ("scaler", StandardScaler()),
+            ("classifier", SVC()),
+        ]
+    )
+    assert scitype(class_pipe) == "classifier_tabular"
+    assert is_scitype(class_pipe, "classifier_tabular")
+
+    reg_pipe = Pipeline(
+        steps=[
+            ("scaler", StandardScaler()),
+            ("regressor", LinearRegression()),
+        ]
+    )
+    assert scitype(reg_pipe) == "regressor_tabular"
+    assert is_scitype(reg_pipe, "regressor_tabular")
diff --git a/sktime/tests/_config.py b/sktime/tests/_config.py
index 8fd99336b..962700956 100644
--- a/sktime/tests/_config.py
+++ b/sktime/tests/_config.py
@@ -202,6 +202,11 @@ EXCLUDED_TESTS = {
         "test__y_when_refitting",  # see 3176
         "test_update_predict_predicted_index",  # see 7985, timeout
         "test_hierarchical_with_exogeneous",  # see 7985, timeout
+        "test_persistence_via_pickle",  # more timeouts
+        "test_methods_have_no_side_effects",
+        "test_fit_idempotent",
+        "test_fit_does_not_overwrite_hyper_params",
+        "test_non_state_changing_method_contract",
     ],
     "InformationGainSegmentation": [
         "test_inheritance",
@@ -247,6 +252,7 @@ EXCLUDED_TESTS = {
         "test_save_estimators_to_file",
     ],
     "ClusterSegmenter": [
+        "test_doctest_examples",
         "test_predict_points",
         "test_predict_segments",
         "test_transform_output_type",
@@ -272,6 +278,9 @@ EXCLUDED_TESTS = {
         "test_classifier_on_unit_test_data",
         "test_fit_idempotent",  # not part of bug reports but due to randomness
     ],
+    "ARLagOrderSelector": [
+        "test_doctest_examples",  # doctest fails, see #8129
+    ],
 }
 
 # exclude tests but keyed by test name
@@ -397,7 +406,26 @@ EXCLUDED_TESTS_BY_TEST = {
         "TSBootstrapAdapter",
         "ThetaModularForecaster",
         "WeightedEnsembleClassifier",
-    ]
+    ],
+    "test_doctest_examples": [
+        # between-versions inconsistency how doctest handles np.float64.
+        # on lower version, prints 0.123456
+        # on higher version, prints np.float64(0.123456)
+        # therefore these doctests will fail either on lower or higher versions
+        "MedianSquaredScaledError",
+        "GeometricMeanAbsoluteError",
+        "MedianRelativeAbsoluteError",
+        "MeanSquaredScaledError",
+        "GeometricMeanRelativeAbsoluteError",
+        "GeometricMeanRelativeSquaredError",
+        "MedianSquaredPercentageError",
+        "MedianAbsoluteScaledError",
+        "MedianSquaredError",
+        "MeanAbsolutePercentageError",
+        "MeanAbsoluteScaledError",
+        "MeanAbsoluteError",
+        "MedianAbsoluteError",
+    ],
 }
 
 # estimators that have 2 test params only when their soft dependency is installed