Repository: sktime/sktime
Pull request #5667 (2023-12-26 10:18:39)

[MNT] restore old export location of `temporal_train_test_split`
This PR restores the old export location of `temporal_train_test_split`, in `forecasting.model_selection`.

This is following the rationale that it might be a popular import, therefore leaving the export as silent will prevent breakage in legacy code such as the blog post tested here: https://github.com/sktime/sktime/pull/5663

Also removes author credits from the respective `__init__` which are not accurate anymore since splitters moved to `split` (author credits moved there).

fkiraly: Hm, ok - I added a warning to encourage people to migrate.

Patch:
diff --git a/sktime/base/__init__.py b/sktime/base/__init__.py
index 4a30db2d0..6cf431c83 100644
--- a/sktime/base/__init__.py
+++ b/sktime/base/__init__.py
@@ -6,10 +6,12 @@ __author__ = ["mloning", "RNKuhns", "fkiraly"]
 __all__ = [
     "BaseObject",
     "BaseEstimator",
+    "BasePanelMixin",
     "_HeterogenousMetaEstimator",
     "load",
 ]
 
 from sktime.base._base import BaseEstimator, BaseObject
+from sktime.base._base_panel import BasePanelMixin
 from sktime.base._meta import _HeterogenousMetaEstimator
 from sktime.base._serialize import load
diff --git a/sktime/base/_base.py b/sktime/base/_base.py
index 6ff54472b..677b35257 100644
--- a/sktime/base/_base.py
+++ b/sktime/base/_base.py
@@ -78,7 +78,15 @@ class BaseObject(_BaseObject):
     Extends skbase BaseObject with additional features.
     """
 
-    _config = {"warnings": "on"}
+    _config = {
+        "warnings": "on",
+        "backend:parallel": None,  # parallelization backend for broadcasting
+        #  {None, "dask", "loky", "multiprocessing", "threading"}
+        #  None: no parallelization
+        #  "loky", "multiprocessing" and "threading": uses `joblib` Parallel loops
+        #  "dask": uses `dask`, requires `dask` package in environment
+        "backend:parallel:params": None,  # params for parallelization backend,
+    }
 
     _config_doc = {
         "display": """
@@ -101,6 +109,36 @@ class BaseObject(_BaseObject):
             * "on" = will raise warnings from sktime
             * "off" = will not raise warnings from sktime
         """,
+        "backend:parallel": """
+        backend:parallel : str, optional, default="None"
+            backend to use for parallelization when broadcasting/vectorizing, one of
+
+            - "None": executes loop sequentally, simple list comprehension
+            - "loky", "multiprocessing" and "threading": uses ``joblib.Parallel``
+            - "joblib": custom and 3rd party ``joblib`` backends, e.g., ``spark``
+            - "dask": uses ``dask``, requires ``dask`` package in environment
+        """,
+        "backend:parallel:params": """
+        backend:parallel:params : dict, optional, default={} (no parameters passed)
+            additional parameters passed to the parallelization backend as config.
+            Valid keys depend on the value of ``backend:parallel``:
+
+            - "None": no additional parameters, ``backend_params`` is ignored
+            - "loky", "multiprocessing" and "threading": default ``joblib`` backends
+              any valid keys for ``joblib.Parallel`` can be passed here, e.g.,
+              ``n_jobs``, with the exception of ``backend`` which is directly
+              controlled by ``backend``.
+              If ``n_jobs`` is not passed, it will default to ``-1``, other parameters
+              will default to ``joblib`` defaults.
+            - "joblib": custom and 3rd party ``joblib`` backends,
+              e.g., ``spark``. Any valid keys for ``joblib.Parallel``
+              can be passed here, e.g., ``n_jobs``,
+              ``backend`` must be passed as a key of ``backend_params`` in this case.
+              If ``n_jobs`` is not passed, it will default to ``-1``, other parameters
+              will default to ``joblib`` defaults.
+            - "dask": any valid keys for ``dask.compute`` can be passed,
+              e.g., ``scheduler``
+        """,
     }
 
     def __init__(self):
diff --git a/sktime/base/_base_panel.py b/sktime/base/_base_panel.py
new file mode 100644
index 000000000..9cffa2a41
--- /dev/null
+++ b/sktime/base/_base_panel.py
@@ -0,0 +1,461 @@
+# copyright: sktime developers, BSD-3-Clause License (see LICENSE file)
+"""Intermediate base class mixin with common functionality for panel tasks.
+
+Inherits from BaseEstimator, descendents are:
+
+BaseClassifier
+BaseRegressor
+"""
+
+__author__ = ["fkiraly"]
+__all__ = ["BasePanelMixin"]
+
+
+import numpy as np
+import pandas as pd
+
+from sktime.base import BaseEstimator
+from sktime.datatypes import (
+    MTYPE_LIST_PANEL,
+    MTYPE_LIST_TABLE,
+    VectorizedDF,
+    check_is_error_msg,
+    check_is_scitype,
+    convert,
+)
+from sktime.utils.warnings import warn
+
+
+class BasePanelMixin(BaseEstimator):
+    """Abstract base class for time series panel tasks, e.g., classifiers, regressors.
+
+    The class contains boilerplate for checks and data conversions,
+    which are common to multiple descendants, most importantly,
+    BaseClassifier and BaseRegressor.
+    """
+
+    # convenience constant to control which metadata of input data
+    # are regularly retrieved in input checks
+    METADATA_REQ_IN_CHECKS = [
+        "n_instances",
+        "has_nans",
+        "is_univariate",
+        "is_equal_length",
+    ]
+
+    # attribute name where vectorized estimators are stored
+    VECTORIZATION_ATTR = "estimators_"  # e.g., classifiers_, regressors_
+
+    # used in error messages
+    TASK = "panel data tasks"  # e.g., classification, regression
+    EST_TYPE = "estimator"  # e.g., classifier, regressor
+    EST_TYPE_PLURAL = "estimators"  # e.g., classifiers, regressors
+
+    def _vectorize(self, methodname, **kwargs):
+        """Vectorized/iterated loop over methods of BaseClassifier, BaseRegressor.
+
+        Stores one estimator per loop index.
+        """
+        # retrieve data arguments
+        X = kwargs.pop("X", None)
+        y = kwargs.pop("y", None)
+
+        # add some common arguments to kwargs
+        kwargs["rowname_default"] = self.EST_TYPE_PLURAL
+        kwargs["colname_default"] = self.EST_TYPE_PLURAL
+        kwargs["backend"] = self.get_config()["backend:parallel"]
+        kwargs["backend_params"] = self.get_config()["backend:parallel:params"]
+
+        if methodname == "fit":
+            self._yvec = y
+
+            ests_ = self._yvec.vectorize_est(self, method="clone", **kwargs)
+            ests_fit = self._yvec.vectorize_est(
+                ests_,
+                method=methodname,
+                args={"y": y},
+                X=X,
+                **kwargs,
+            )
+            setattr(self, self.VECTORIZATION_ATTR, ests_fit)
+            return self
+        else:  # methodname == "predict" or methodname == "predict_proba":
+            ests_ = getattr(self, self.VECTORIZATION_ATTR)
+            y_pred = self._yvec.vectorize_est(
+                ests_,
+                method=methodname,
+                X=X,
+                **kwargs,
+            )
+            y_pred = pd.DataFrame(
+                {str(i): y_pred[col].values[0] for i, col in enumerate(y_pred.columns)}
+            )
+            return y_pred
+
+    def _fit_predict_boilerplate(self, X, y, cv, change_state, method):
+        """Boilerplate logic for fit_predict and fit_predict_proba."""
+        from sklearn.model_selection import KFold
+
+        if isinstance(cv, int):
+            random_state = getattr(self, "random_state", None)
+            cv = KFold(cv, random_state=random_state, shuffle=True)
+
+        if change_state:
+            self.reset()
+            est = self
+        else:
+            est = self.clone()
+
+        if cv is None:
+            return getattr(est.fit(X, y), method)(X)
+        elif change_state:
+            self.fit(X, y)
+
+        # we now know that cv is an sklearn splitter
+        X, y = self._internal_convert(X, y)
+        X_metadata = self._check_input(
+            X, y, return_metadata=self.METADATA_REQ_IN_CHECKS
+        )
+        X_mtype = X_metadata["mtype"]
+        # Check this estimator can handle characteristics
+        self._check_capabilities(X_metadata)
+
+        # handle single class case
+        if len(self._class_dictionary) == 1:
+            return self._single_class_y_pred(X)
+
+        # Convert data to format easily usable for applying cv
+        if isinstance(X, np.ndarray):
+            X = convert(
+                X,
+                from_type=X_mtype,
+                to_type="numpy3D",
+                as_scitype="Panel",
+                store_behaviour="freeze",
+            )
+        else:
+            X = convert(
+                X,
+                from_type=X_mtype,
+                to_type="nested_univ",
+                as_scitype="Panel",
+                store_behaviour="freeze",
+            )
+
+        if method == "predict_proba":
+            y_pred = np.empty([len(y), len(np.unique(y))])
+        else:
+            y_pred = np.empty_like(y)
+        y_pred[:] = -1
+        if isinstance(X, np.ndarray):
+            for tr_idx, tt_idx in cv.split(X):
+                X_train = X[tr_idx]
+                X_test = X[tt_idx]
+                y_train = y[tr_idx]
+                fitted_est = self.clone().fit(X_train, y_train)
+                y_pred[tt_idx] = getattr(fitted_est, method)(X_test)
+        else:
+            for tr_idx, tt_idx in cv.split(X):
+                X_train = X.iloc[tr_idx]
+                X_test = X.iloc[tt_idx]
+                y_train = y[tr_idx]
+                fitted_est = self.clone().fit(X_train, y_train)
+                y_pred[tt_idx] = getattr(fitted_est, method)(X_test)
+
+        return y_pred
+
+    def _check_convert_X_for_predict(self, X):
+        """Input checks, capability checks, repeated in all predict/score methods.
+
+        Parameters
+        ----------
+        X : any object (to check/convert)
+            should be of a supported Panel mtype or 2D numpy.ndarray
+
+        Returns
+        -------
+        X: an object of a supported Panel mtype, numpy3D if X was a 2D numpy.ndarray
+
+        Raises
+        ------
+        ValueError if X is of invalid input data type, or there is not enough data
+        ValueError if the capabilities in self._tags do not handle the data.
+        """
+        X = self._internal_convert(X)
+        X_metadata = self._check_input(X, return_metadata=self.METADATA_REQ_IN_CHECKS)
+        X_mtype = X_metadata["mtype"]
+        # Check that estimator can handle characteristics
+        self._check_capabilities(X_metadata)
+        # Convert data as dictated by the estimator tags
+        X = self._convert_X(X, X_mtype=X_mtype)
+
+        return X
+
+    def _check_capabilities(self, X_metadata):
+        """Check whether this estimator can handle the data characteristics.
+
+        Parameters
+        ----------
+        missing : boolean, does the data passed to fit contain missing values?
+        multivariate : boolean, does the data passed to fit contain missing values?
+        unequal : boolea, do the time series passed to fit have variable lengths?
+
+        Raises
+        ------
+        ValueError if the capabilities in self._tags do not handle the data.
+        """
+        missing = X_metadata["has_nans"]
+        multivariate = not X_metadata["is_univariate"]
+        unequal = not X_metadata["is_equal_length"]
+
+        allow_multivariate = self.get_tag("capability:multivariate")
+        allow_missing = self.get_tag("capability:missing_values")
+        allow_unequal = self.get_tag("capability:unequal_length")
+
+        self_name = type(self).__name__
+
+        # identify problems, mismatch of capability and inputs
+        problems = []
+        if missing and not allow_missing:
+            problems += ["missing values"]
+        if multivariate and not allow_multivariate:
+            problems += ["multivariate series"]
+        if unequal and not allow_unequal:
+            problems += ["unequal length series"]
+
+        # construct error message
+        problems_and = " and ".join(problems)
+        problems_or = " or ".join(problems)
+        msg = (
+            f"Data seen by {self_name} instance has {problems_and}, "
+            f"but this {self_name} instance cannot handle {problems_or}. "
+            f"Calls with {problems_or} may result in error or unreliable results."
+        )
+
+        # raise exception or warning with message
+        # if self is composite, raise a warning, since passing could be fine
+        #   see discussion in PR 2366 why
+        if len(problems) > 0:
+            if self.is_composite():
+                warn(msg, obj=self)
+            else:
+                raise ValueError(msg)
+
+    def _convert_X(self, X, X_mtype):
+        """Convert equal length series from DataFrame to numpy array or vice versa.
+
+        Parameters
+        ----------
+        X : input data for the estimator, any Panel mtype
+        X_mtype : str, a Panel mtype string, e.g., "pd_multiindex", "numpy3D"
+
+        Returns
+        -------
+        X : input X converted to type in "X_inner_mtype" tag
+            usually a pd.DataFrame (nested) or 3D np.ndarray
+            Checked and possibly converted input data
+        """
+        inner_type = self.get_tag("X_inner_mtype")
+        # convert pd.DataFrame
+        X = convert(
+            X,
+            from_type=X_mtype,
+            to_type=inner_type,
+            as_scitype="Panel",
+        )
+        return X
+
+    def _check_y(self, y=None):
+        """Check and coerce X/y for fit/transform functions.
+
+        Parameters
+        ----------
+        y : pd.DataFrame, pd.Series or np.ndarray
+
+        Returns
+        -------
+        y : object of sktime compatible time series type
+            can be Series, Panel, Hierarchical
+        y_metadata : dict
+            metadata of y, retured by check_is_scitype
+        """
+        if y is None:
+            return None
+
+        capa_multioutput = self.get_tag("capability:multioutput")
+        y_inner_mtype = self.get_tag("y_inner_mtype")
+
+        y_valid, y_msg, y_metadata = check_is_scitype(
+            y, "Table", return_metadata=["is_univariate"]
+        )
+
+        if not y_valid:
+            allowed_msg = (
+                f"In {self.TASK}, y must be of a supported type, "
+                f"for instance 1D or 2D numpy arrays, pd.DataFrame, or pd.Series. "
+                f"Allowed compatible mtype format specifications are:"
+                f" {MTYPE_LIST_TABLE} ."
+            )
+            check_is_error_msg(
+                y_msg, var_name="y", allowed_msg=allowed_msg, raise_exception=True
+            )
+
+        y_uni = y_metadata["is_univariate"]
+        y_mtype = y_metadata["mtype"]
+
+        requires_vectorization = not capa_multioutput and not y_uni
+
+        if requires_vectorization:
+            y_df = convert(
+                y,
+                from_type=y_mtype,
+                to_type="pd_DataFrame_Table",
+                as_scitype="Table",
+                store=self._converter_store_y,
+            )
+            y_vec = VectorizedDF([y_df], iterate_cols=True)
+            return y_vec, y_metadata
+
+        y_inner = convert(
+            y,
+            from_type=y_mtype,
+            to_type=y_inner_mtype,
+            as_scitype="Table",
+            store=self._converter_store_y,
+        )
+
+        return y_inner, y_metadata
+
+    def _convert_output_y(self, y):
+        """Convert output y to original format.
+
+        Parameters
+        ----------
+        y : np.ndarray or pd.DataFrame
+
+        Returns
+        -------
+        y : np.ndarray or pd.DataFrame
+        """
+        # for consistency with legacy behaviour:
+        # output is coerced to numpy1D in case of univariate output
+        if not self._y_metadata["is_univariate"]:
+            output_mtype = self._y_metadata["mtype"]
+            converter_store = self._converter_store_y
+        else:
+            output_mtype = "numpy1D"
+            converter_store = None
+
+        y = convert(
+            y,
+            from_type=self.get_tag("y_inner_mtype"),
+            to_type=output_mtype,
+            as_scitype="Table",
+            store=converter_store,
+            store_behaviour="freeze",
+        )
+        return y
+
+    def _check_input(self, X, y=None, enforce_min_instances=1, return_metadata=True):
+        """Check whether input X and y are valid formats with minimum data.
+
+        Raises a ValueError if the input is not valid.
+
+        Parameters
+        ----------
+        X : check whether conformant with any sktime Panel mtype specification
+        y : check whether a pd.Series or np.array
+        enforce_min_instances : int, optional (default=1)
+            check there are a minimum number of instances.
+        return_metadata : bool, str, or list of str
+            metadata fields to return with X_metadata, input to check_is_scitype
+
+        Returns
+        -------
+        metadata : dict with metadata for X returned by datatypes.check_is_scitype
+
+        Raises
+        ------
+        ValueError
+            If y or X is invalid input data type, or there is not enough data
+        """
+        # Check X is valid input type and recover the data characteristics
+        X_valid, msg, X_metadata = check_is_scitype(
+            X, scitype="Panel", return_metadata=return_metadata
+        )
+        # raise informative error message if X is in wrong format
+        allowed_msg = (
+            f"Allowed scitypes for {self.EST_TYPE_PLURAL} are Panel mtypes, "
+            f"for instance a pandas.DataFrame with MultiIndex and last(-1) "
+            f"level an sktime compatible time index. "
+            f"Allowed compatible mtype format specifications are: {MTYPE_LIST_PANEL} ."
+        )
+        if not X_valid:
+            check_is_error_msg(
+                msg, var_name="X", allowed_msg=allowed_msg, raise_exception=True
+            )
+
+        n_cases = X_metadata["n_instances"]
+        if n_cases < enforce_min_instances:
+            raise ValueError(
+                f"Minimum number of cases required is {enforce_min_instances} but X "
+                f"has : {n_cases}"
+            )
+
+        # Check y if passed
+        if y is not None:
+            # Check y valid input
+            if not isinstance(y, (pd.Series, pd.DataFrame, np.ndarray)):
+                raise ValueError(
+                    "y must be a np.array or a pd.Series or pd.DataFrame, but found ",
+                    f"type: {type(y)}",
+                )
+            # Check matching number of labels
+            n_labels = y.shape[0]
+            if n_cases != n_labels:
+                raise ValueError(
+                    f"Mismatch in number of cases. Number in X = {n_cases} nos in y = "
+                    f"{n_labels}"
+                )
+            if isinstance(y, np.ndarray):
+                if y.ndim > 2:
+                    raise ValueError(
+                        f"np.ndarray y must be 1-dimensional or 2-dimensional, "
+                        f"but found {y.ndim} dimensions"
+                    )
+            # warn if only a single class label is seen
+            # this should not raise exception since this can occur by train subsampling
+            if len(np.unique(y)) == 1:
+                warn(
+                    "only single label seen in y passed to "
+                    f"fit of {self.EST_TYPE} {type(self).__name__}",
+                    obj=self,
+                )
+
+        return X_metadata
+
+    def _internal_convert(self, X, y=None):
+        """Convert X and y if necessary as a user convenience.
+
+        Convert X to a 3D numpy array if already a 2D and convert y into an 1D numpy
+        array if passed as a Series.
+
+        Parameters
+        ----------
+        X : an object of a supported Panel mtype, or 2D numpy.ndarray
+        y : np.ndarray or pd.Series
+
+        Returns
+        -------
+        X: an object of a supported Panel mtype, numpy3D if X was a 2D numpy.ndarray
+        y: np.ndarray
+        """
+        if isinstance(X, np.ndarray):
+            # Temporary fix to insist on 3D numpy. For univariate problems,
+            # most panel estimators simply convert back to 2D. This squeezing should be
+            # done here, but touches a lot of files, so will get this to work first.
+            if X.ndim == 2:
+                X = X.reshape(X.shape[0], 1, X.shape[1])
+        if y is None:
+            return X
+        return X, y
diff --git a/sktime/classification/base.py b/sktime/classification/base.py
index 74728f65d..8f218969c 100644
--- a/sktime/classification/base.py
+++ b/sktime/classification/base.py
@@ -23,27 +23,17 @@ __all__ = [
 __author__ = ["mloning", "fkiraly", "TonyBagnall", "MatthewMiddlehurst"]
 
 import time
-from abc import ABC, abstractmethod
 
 import numpy as np
-import pandas as pd
-
-from sktime.base import BaseEstimator
-from sktime.datatypes import (
-    MTYPE_LIST_PANEL,
-    MTYPE_LIST_TABLE,
-    VectorizedDF,
-    check_is_error_msg,
-    check_is_scitype,
-    convert,
-)
+
+from sktime.base import BasePanelMixin
+from sktime.datatypes import VectorizedDF, check_is_scitype, convert
 from sktime.utils.sklearn import is_sklearn_transformer
 from sktime.utils.validation import check_n_jobs
 from sktime.utils.validation._dependencies import _check_estimator_deps
-from sktime.utils.warnings import warn
 
 
-class BaseClassifier(BaseEstimator, ABC):
+class BaseClassifier(BasePanelMixin):
     """Abstract base class for time series classifiers.
 
     The base classifier specifies the methods and method signatures that all
@@ -85,6 +75,14 @@ class BaseClassifier(BaseEstimator, ABC):
         "is_equal_length",
     ]
 
+    # attribute name where vectorized estimators are stored
+    VECTORIZATION_ATTR = "classifiers_"  # e.g., classifiers_, regressors_
+
+    # used in error messages
+    TASK = "classification"  # e.g., classification, regression
+    EST_TYPE = "classifier"  # e.g., classifier, regressor
+    EST_TYPE_PLURAL = "classifiers"  # e.g., classifiers, regressors
+
     def __init__(self):
         # reserved attributes written to in fit
         self.classes_ = []  # classes seen in y, unique labels
@@ -199,17 +197,14 @@ class BaseClassifier(BaseEstimator, ABC):
         # convenience conversions to allow user flexibility:
         # if X is 2D array, convert to 3D, if y is Series, convert to numpy
         X, y = self._internal_convert(X, y)
-        X_metadata = self._check_classifier_input(
+        X_metadata = self._check_input(
             X, y, return_metadata=self.METADATA_REQ_IN_CHECKS
         )
-        missing = X_metadata["has_nans"]
-        multivariate = not X_metadata["is_univariate"]
-        unequal = not X_metadata["is_equal_length"]
         X_mtype = X_metadata["mtype"]
         self._X_metadata = X_metadata
 
         # Check this classifier can handle characteristics
-        self._check_capabilities(missing, multivariate, unequal)
+        self._check_capabilities(X_metadata)
 
         # remember class labels
         self.classes_ = np.unique(y)
@@ -389,46 +384,6 @@ class BaseClassifier(BaseEstimator, ABC):
             X=X, y=y, cv=cv, change_state=change_state, method="predict"
         )
 
-    def _vectorize(self, methodname, **kwargs):
-        """Vectorized/iterated loop over method of BaseClassifier.
-
-        Uses classifiers_ attribute to store one classifier per loop index.
-        """
-        y = kwargs.get("y")
-        X = kwargs.get("X")
-        if X is not None:
-            kwargs.pop("X")
-        if y is not None:
-            kwargs.pop("y")
-            self._y_vec = y
-        classifiers_ = self._y_vec.vectorize_est(
-            self,
-            method="clone",
-        )
-        if methodname == "fit":
-            self.classifiers_ = self._y_vec.vectorize_est(
-                classifiers_,
-                method=methodname,
-                args={"y": y},
-                X=X,
-            )
-            return self
-        else:
-            if self.classifiers_ is not None:
-                classifiers_ = self.classifiers_
-            y_pred = self._y_vec.vectorize_est(
-                classifiers_,
-                method=methodname,
-                # return_type="list",
-                X=X,
-                args={"y": y} if y is not None else {},
-                **kwargs,  # contains X inside
-            )
-            y_pred = pd.DataFrame(
-                {str(i): y_pred[col].values[0] for i, col in enumerate(y_pred.columns)}
-            )
-            return y_pred
-
     def _fit_predict_boilerplate(self, X, y, cv, change_state, method):
         """Boilerplate logic for fit_predict and fit_predict_proba."""
         from sklearn.model_selection import KFold
@@ -450,15 +405,12 @@ class BaseClassifier(BaseEstimator, ABC):
 
         # we now know that cv is an sklearn splitter
         X, y = self._internal_convert(X, y)
-        X_metadata = self._check_classifier_input(
+        X_metadata = self._check_input(
             X, y, return_metadata=self.METADATA_REQ_IN_CHECKS
         )
-        missing = X_metadata["has_nans"]
-        multivariate = not X_metadata["is_univariate"]
-        unequal = not X_metadata["is_equal_length"]
         X_mtype = X_metadata["mtype"]
         # Check this classifier can handle characteristics
-        self._check_capabilities(missing, multivariate, unequal)
+        self._check_capabilities(X_metadata)
 
         # handle single class case
         if len(self._class_dictionary) == 1:
@@ -627,7 +579,6 @@ class BaseClassifier(BaseEstimator, ABC):
         """
         return super().get_test_params(parameter_set=parameter_set)
 
-    @abstractmethod
     def _fit(self, X, y):
         """Fit time series classifier to training data.
 
@@ -658,7 +609,7 @@ class BaseClassifier(BaseEstimator, ABC):
         -------
         self : Reference to self.
         """
-        ...
+        raise NotImplementedError("abstract method")
 
     def _predict(self, X) -> np.ndarray:
         """Predicts labels for sequences in X.
@@ -725,302 +676,3 @@ class BaseClassifier(BaseEstimator, ABC):
             dists[i, self._class_dictionary[preds[i]]] = 1
 
         return dists
-
-    def _check_convert_X_for_predict(self, X):
-        """Input checks, capability checks, repeated in all predict/score methods.
-
-        Parameters
-        ----------
-        X : any object (to check/convert)
-            should be of a supported Panel mtype or 2D numpy.ndarray
-
-        Returns
-        -------
-        X: an object of a supported Panel mtype, numpy3D if X was a 2D numpy.ndarray
-
-        Raises
-        ------
-        ValueError if X is of invalid input data type, or there is not enough data
-        ValueError if the capabilities in self._tags do not handle the data.
-        """
-        X = self._internal_convert(X)
-        X_metadata = self._check_classifier_input(
-            X, return_metadata=self.METADATA_REQ_IN_CHECKS
-        )
-        missing = X_metadata["has_nans"]
-        multivariate = not X_metadata["is_univariate"]
-        unequal = not X_metadata["is_equal_length"]
-        X_mtype = X_metadata["mtype"]
-        # Check this classifier can handle characteristics
-        self._check_capabilities(missing, multivariate, unequal)
-        # Convert data as dictated by the classifier tags
-        X = self._convert_X(X, X_mtype=X_mtype)
-
-        return X
-
-    def _check_capabilities(self, missing, multivariate, unequal):
-        """Check whether this classifier can handle the data characteristics.
-
-        Parameters
-        ----------
-        missing : boolean, does the data passed to fit contain missing values?
-        multivariate : boolean, does the data passed to fit contain missing values?
-        unequal : boolea, do the time series passed to fit have variable lengths?
-
-        Raises
-        ------
-        ValueError if the capabilities in self._tags do not handle the data.
-        """
-        allow_multivariate = self.get_tag("capability:multivariate")
-        allow_missing = self.get_tag("capability:missing_values")
-        allow_unequal = self.get_tag("capability:unequal_length")
-
-        self_name = type(self).__name__
-
-        # identify problems, mismatch of capability and inputs
-        problems = []
-        if missing and not allow_missing:
-            problems += ["missing values"]
-        if multivariate and not allow_multivariate:
-            problems += ["multivariate series"]
-        if unequal and not allow_unequal:
-            problems += ["unequal length series"]
-
-        # construct error message
-        problems_and = " and ".join(problems)
-        problems_or = " or ".join(problems)
-        msg = (
-            f"Data seen by {self_name} instance has {problems_and}, "
-            f"but this {self_name} instance cannot handle {problems_or}. "
-            f"Calls with {problems_or} may result in error or unreliable results."
-        )
-
-        # raise exception or warning with message
-        # if self is composite, raise a warning, since passing could be fine
-        #   see discussion in PR 2366 why
-        if len(problems) > 0:
-            if self.is_composite():
-                warn(msg, obj=self)
-            else:
-                raise ValueError(msg)
-
-    def _convert_X(self, X, X_mtype):
-        """Convert equal length series from DataFrame to numpy array or vice versa.
-
-        Parameters
-        ----------
-        X : input data for the classifier
-        X_mtype : str, a Panel mtype string, e.g., "pd_multiindex", "numpy3D"
-
-        Returns
-        -------
-        X : input X converted to type in "X_inner_mtype" tag
-            usually a pd.DataFrame (nested) or 3D np.ndarray
-            Checked and possibly converted input data
-        """
-        inner_type = self.get_tag("X_inner_mtype")
-        # convert pd.DataFrame
-        X = convert(
-            X,
-            from_type=X_mtype,
-            to_type=inner_type,
-            as_scitype="Panel",
-        )
-        return X
-
-    def _check_y(self, y=None):
-        """Check and coerce X/y for fit/transform functions.
-
-        Parameters
-        ----------
-        y : pd.DataFrame, pd.Series or np.ndarray
-
-        Returns
-        -------
-        y : object of sktime compatible time series type
-            can be Series, Panel, Hierarchical
-        y_metadata : dict
-            metadata of y, retured by check_is_scitype
-        """
-        if y is None:
-            return None
-
-        capa_multioutput = self.get_tag("capability:multioutput")
-        y_inner_mtype = self.get_tag("y_inner_mtype")
-
-        y_valid, y_msg, y_metadata = check_is_scitype(
-            y, "Table", return_metadata=["is_univariate"]
-        )
-
-        if not y_valid:
-            allowed_msg = (
-                f"In classification, y must be of a supported type, "
-                f"for instance 1D or 2D numpy arrays, pd.DataFrame, or pd.Series. "
-                f"Allowed compatible mtype format specifications are:"
-                f" {MTYPE_LIST_TABLE} ."
-            )
-            check_is_error_msg(
-                y_msg, var_name="y", allowed_msg=allowed_msg, raise_exception=True
-            )
-
-        y_uni = y_metadata["is_univariate"]
-        y_mtype = y_metadata["mtype"]
-
-        requires_vectorization = not capa_multioutput and not y_uni
-
-        if requires_vectorization:
-            y_df = convert(
-                y,
-                from_type=y_mtype,
-                to_type="pd_DataFrame_Table",
-                as_scitype="Table",
-                store=self._converter_store_y,
-            )
-            y_vec = VectorizedDF([y_df], iterate_cols=True)
-            return y_vec, y_metadata
-
-        y_inner = convert(
-            y,
-            from_type=y_mtype,
-            to_type=y_inner_mtype,
-            as_scitype="Table",
-            store=self._converter_store_y,
-        )
-
-        return y_inner, y_metadata
-
-    def _convert_output_y(self, y):
-        """Convert output y to original format.
-
-        Parameters
-        ----------
-        y : np.ndarray or pd.DataFrame
-
-        Returns
-        -------
-        y : np.ndarray or pd.DataFrame
-        """
-        # for consistency with legacy behaviour:
-        # output is coerced to numpy1D in case of univariate output
-        if not self._y_metadata["is_univariate"]:
-            output_mtype = self._y_metadata["mtype"]
-            converter_store = self._converter_store_y
-        else:
-            output_mtype = "numpy1D"
-            converter_store = None
-
-        y = convert(
-            y,
-            from_type=self.get_tag("y_inner_mtype"),
-            to_type=output_mtype,
-            as_scitype="Table",
-            store=converter_store,
-            store_behaviour="freeze",
-        )
-        return y
-
-    def _check_classifier_input(
-        self, X, y=None, enforce_min_instances=1, return_metadata=True
-    ):
-        """Check whether input X and y are valid formats with minimum data.
-
-        Raises a ValueError if the input is not valid.
-
-        Parameters
-        ----------
-        X : check whether conformant with any sktime Panel mtype specification
-        y : check whether a pd.Series or np.array
-        enforce_min_instances : int, optional (default=1)
-            check there are a minimum number of instances.
-        return_metadata : bool, str, or list of str
-            metadata fields to return with X_metadata, input to check_is_scitype
-
-        Returns
-        -------
-        metadata : dict with metadata for X returned by datatypes.check_is_scitype
-
-        Raises
-        ------
-        ValueError
-            If y or X is invalid input data type, or there is not enough data
-        """
-        # Check X is valid input type and recover the data characteristics
-        X_valid, msg, X_metadata = check_is_scitype(
-            X, scitype="Panel", return_metadata=return_metadata
-        )
-        # raise informative error message if X is in wrong format
-        allowed_msg = (
-            f"Allowed scitypes for classifiers are Panel mtypes, "
-            f"for instance a pandas.DataFrame with MultiIndex and last(-1) "
-            f"level an sktime compatible time index. "
-            f"Allowed compatible mtype format specifications are: {MTYPE_LIST_PANEL} ."
-        )
-        if not X_valid:
-            check_is_error_msg(
-                msg, var_name="X", allowed_msg=allowed_msg, raise_exception=True
-            )
-
-        n_cases = X_metadata["n_instances"]
-        if n_cases < enforce_min_instances:
-            raise ValueError(
-                f"Minimum number of cases required is {enforce_min_instances} but X "
-                f"has : {n_cases}"
-            )
-
-        # Check y if passed
-        if y is not None:
-            # Check y valid input
-            if not isinstance(y, (pd.Series, pd.DataFrame, np.ndarray)):
-                raise ValueError(
-                    "y must be a np.array or a pd.Series or pd.DataFrame, but found ",
-                    f"type: {type(y)}",
-                )
-            # Check matching number of labels
-            n_labels = y.shape[0]
-            if n_cases != n_labels:
-                raise ValueError(
-                    f"Mismatch in number of cases. Number in X = {n_cases} nos in y = "
-                    f"{n_labels}"
-                )
-            if isinstance(y, np.ndarray):
-                if y.ndim > 2:
-                    raise ValueError(
-                        f"np.ndarray y must be 1-dimensional or 2-dimensional, "
-                        f"but found {y.ndim} dimensions"
-                    )
-            # warn if only a single class label is seen
-            # this should not raise exception since this can occur by train subsampling
-            if len(np.unique(y)) == 1:
-                warn(
-                    "only single class label seen in y passed to "
-                    f"fit of classifier {type(self).__name__}",
-                    obj=self,
-                )
-
-        return X_metadata
-
-    def _internal_convert(self, X, y=None):
-        """Convert X and y if necessary as a user convenience.
-
-        Convert X to a 3D numpy array if already a 2D and convert y into an 1D numpy
-        array if passed as a Series.
-
-        Parameters
-        ----------
-        X : an object of a supported Panel mtype, or 2D numpy.ndarray
-        y : np.ndarray or pd.Series
-
-        Returns
-        -------
-        X: an object of a supported Panel mtype, numpy3D if X was a 2D numpy.ndarray
-        y: np.ndarray
-        """
-        if isinstance(X, np.ndarray):
-            # Temporary fix to insist on 3D numpy. For univariate problems,
-            # most classifiers simply convert back to 2D. This squeezing should be
-            # done here, but touches a lot of files, so will get this to work first.
-            if X.ndim == 2:
-                X = X.reshape(X.shape[0], 1, X.shape[1])
-        if y is None:
-            return X
-        return X, y
diff --git a/sktime/classification/early_classification/base.py b/sktime/classification/early_classification/base.py
index 3e6642569..f0702c5af 100644
--- a/sktime/classification/early_classification/base.py
+++ b/sktime/classification/early_classification/base.py
@@ -73,6 +73,14 @@ class BaseEarlyClassifier(BaseEstimator, ABC):
         "is_equal_length",
     ]
 
+    # attribute name where vectorized estimators are stored
+    VECTORIZATION_ATTR = "classifiers_"  # e.g., classifiers_, regressors_
+
+    # used in error messages
+    TASK = "early classification"  # e.g., classification, regression
+    EST_TYPE = "early classifier"  # e.g., classifier, regressor
+    EST_TYPE_PLURAL = "early classifiers"  # e.g., classifiers, regressors
+
     def __init__(self):
         self.classes_ = []
         self.n_classes_ = 0
@@ -591,21 +599,19 @@ class BaseEarlyClassifier(BaseEstimator, ABC):
         _check_convert_X_for_predict = BaseClassifier._check_convert_X_for_predict
         return _check_convert_X_for_predict(self, X)
 
-    def _check_capabilities(self, missing, multivariate, unequal):
+    def _check_capabilities(self, X_metadata):
         """Check whether this classifier can handle the data characteristics.
 
         Parameters
         ----------
-        missing : boolean, does the data passed to fit contain missing values?
-        multivariate : boolean, does the data passed to fit contain missing values?
-        unequal : boolea, do the time series passed to fit have variable lengths?
+        X_metadata : dict with metadata for X returned by datatypes.check_is_scitype
 
         Raises
         ------
         ValueError if the capabilities in self._tags do not handle the data.
         """
         _check_capabilities = BaseClassifier._check_capabilities
-        return _check_capabilities(self, missing, multivariate, unequal)
+        return _check_capabilities(self, X_metadata)
 
     def _convert_X(self, X, X_mtype):
         """Convert equal length series from DataFrame to numpy array or vice versa.
@@ -639,9 +645,7 @@ class BaseEarlyClassifier(BaseEstimator, ABC):
         _check_y = BaseClassifier._check_y
         return _check_y(self, y)
 
-    def _check_classifier_input(
-        self, X, y=None, enforce_min_instances=1, return_metadata=True
-    ):
+    def _check_input(self, X, y=None, enforce_min_instances=1, return_metadata=True):
         """Check whether input X and y are valid formats with minimum data.
 
         Raises a ValueError if the input is not valid.
@@ -664,10 +668,8 @@ class BaseEarlyClassifier(BaseEstimator, ABC):
         ValueError
             If y or X is invalid input data type, or there is not enough data
         """
-        _check_classifier_input = BaseClassifier._check_classifier_input
-        return _check_classifier_input(
-            self, X, y, enforce_min_instances, return_metadata
-        )
+        _check_input = BaseClassifier._check_input
+        return _check_input(self, X, y, enforce_min_instances, return_metadata)
 
     def _internal_convert(self, X, y=None):
         """Convert X and y if necessary as a user convenience.
diff --git a/sktime/forecasting/base/_base.py b/sktime/forecasting/base/_base.py
index ed0d26ff9..f22c39e6b 100644
--- a/sktime/forecasting/base/_base.py
+++ b/sktime/forecasting/base/_base.py
@@ -100,48 +100,17 @@ class BaseForecaster(BaseEstimator):
     }
 
     # configs and default config values
+    # see set_config documentation for details
     _config = {
         "backend:parallel": None,  # parallelization backend for broadcasting
         #  {None, "dask", "loky", "multiprocessing", "threading"}
         #  None: no parallelization
         #  "loky", "multiprocessing" and "threading": uses `joblib` Parallel loops
+        #  "joblib": uses custom joblib backend, set via `joblib_backend` tag
         #  "dask": uses `dask`, requires `dask` package in environment
         "backend:parallel:params": None,  # params for parallelization backend
     }
 
-    _config_doc = {
-        "backend:parallel": """
-        backend:parallel : str, optional, default="None"
-            backend to use for parallelization when broadcasting/vectorizing, one of
-
-            - "None": executes loop sequentally, simple list comprehension
-            - "loky", "multiprocessing" and "threading": uses ``joblib.Parallel``
-            - "joblib": custom and 3rd party ``joblib`` backends, e.g., ``spark``
-            - "dask": uses ``dask``, requires ``dask`` package in environment
-        """,
-        "backend:parallel:params": """
-        backend:parallel:params : dict, optional, default={} (no parameters passed)
-            additional parameters passed to the parallelization backend as config.
-            Valid keys depend on the value of ``backend:parallel``:
-
-            - "None": no additional parameters, ``backend_params`` is ignored
-            - "loky", "multiprocessing" and "threading": default ``joblib`` backends
-              any valid keys for ``joblib.Parallel`` can be passed here, e.g.,
-              ``n_jobs``, with the exception of ``backend`` which is directly
-              controlled by ``backend``.
-              If ``n_jobs`` is not passed, it will default to ``-1``, other parameters
-              will default to ``joblib`` defaults.
-            - "joblib": custom and 3rd party ``joblib`` backends,
-              e.g., ``spark``. Any valid keys for ``joblib.Parallel``
-              can be passed here, e.g., ``n_jobs``,
-            ``backend`` must be passed as a key of ``backend_params`` in this case.
-              If ``n_jobs`` is not passed, it will default to ``-1``, other parameters
-              will default to ``joblib`` defaults.
-            - "dask": any valid keys for ``dask.compute`` can be passed,
-              e.g., ``scheduler``
-        """,
-    }
-
     def __init__(self):
         self._is_fitted = False
 
diff --git a/sktime/forecasting/model_selection/__init__.py b/sktime/forecasting/model_selection/__init__.py
index 311a2157e..03a390848 100644
--- a/sktime/forecasting/model_selection/__init__.py
+++ b/sktime/forecasting/model_selection/__init__.py
@@ -2,11 +2,11 @@
 # copyright: sktime developers, BSD-3-Clause License (see LICENSE file)
 """Implements functionality for selecting forecasting models."""
 
-__author__ = ["mloning", "kkoralturk"]
 __all__ = [
     "ForecastingGridSearchCV",
     "ForecastingRandomizedSearchCV",
     "ForecastingSkoptSearchCV",
+    "temporal_train_test_split",
 ]
 
 from sktime.forecasting.model_selection._tune import (
@@ -14,3 +14,26 @@ from sktime.forecasting.model_selection._tune import (
     ForecastingRandomizedSearchCV,
     ForecastingSkoptSearchCV,
 )
+
+
+# todo 0.26.0 - check whether we should remove, otherwise bump
+def temporal_train_test_split(
+    y, X=None, test_size=None, train_size=None, fh=None, anchor="start"
+):
+    """Split time series data into temporal train and test sets."""
+    from warnings import warn
+
+    from sktime.split import temporal_train_test_split as _tts
+
+    warn(
+        "WARNING - the old location of temporal_train_test_split in "
+        "sktime.forecasting.model_selection is deprecated and is scheduled for "
+        "imminent removal in a MINOR version. "
+        "Please update any import statements to "
+        "from sktime.split import temporal_train_test_split.",
+        DeprecationWarning,
+    )
+
+    return _tts(
+        y=y, X=X, test_size=test_size, train_size=train_size, fh=fh, anchor=anchor
+    )
diff --git a/sktime/regression/base.py b/sktime/regression/base.py
index 4c0351608..f89e1aa86 100644
--- a/sktime/regression/base.py
+++ b/sktime/regression/base.py
@@ -23,26 +23,16 @@ __all__ = [
 __author__ = ["mloning", "fkiraly"]
 
 import time
-from abc import ABC, abstractmethod
 
 import numpy as np
-import pandas as pd
-
-from sktime.base import BaseEstimator
-from sktime.datatypes import (
-    MTYPE_LIST_PANEL,
-    MTYPE_LIST_TABLE,
-    VectorizedDF,
-    check_is_error_msg,
-    check_is_scitype,
-    convert,
-)
+
+from sktime.base import BasePanelMixin
+from sktime.datatypes import VectorizedDF
 from sktime.utils.sklearn import is_sklearn_transformer
 from sktime.utils.validation import check_n_jobs
-from sktime.utils.warnings import warn
 
 
-class BaseRegressor(BaseEstimator, ABC):
+class BaseRegressor(BasePanelMixin):
     """Abstract base class for time series regressors.
 
     The base regressor specifies the methods and method signatures that all
@@ -79,6 +69,14 @@ class BaseRegressor(BaseEstimator, ABC):
         "is_equal_length",
     ]
 
+    # attribute name where vectorized estimators are stored
+    VECTORIZATION_ATTR = "regressors_"  # e.g., classifiers_, regressors_
+
+    # used in error messages
+    TASK = "regression"  # e.g., classification, regression
+    EST_TYPE = "regressor"  # e.g., classifier, regressor
+    EST_TYPE_PLURAL = "regressors"  # e.g., classifiers, regressors
+
     def __init__(self):
         self.fit_time_ = 0
         self._class_dictionary = {}
@@ -192,19 +190,21 @@ class BaseRegressor(BaseEstimator, ABC):
 
         # convenience conversions to allow user flexibility:
         # if X is 2D array, convert to 3D, if y is Series, convert to numpy
-        X, y = _internal_convert(X, y)
-        X_metadata = _check_regressor_input(
+        X, y = self._internal_convert(X, y)
+
+        # y float coercion
+        if y is not None and isinstance(y, np.ndarray):
+            y = y.astype("float")
+
+        # input checks
+        X_metadata = self._check_input(
             X, y, return_metadata=self.METADATA_REQ_IN_CHECKS
         )
         self._X_metadata = X_metadata
-        missing = X_metadata["has_nans"]
-        multivariate = not X_metadata["is_univariate"]
-        unequal = not X_metadata["is_equal_length"]
         X_mtype = X_metadata["mtype"]
-        self._X_metadata = X_metadata
 
         # Check this regressor can handle characteristics
-        self._check_capabilities(missing, multivariate, unequal)
+        self._check_capabilities(X_metadata)
 
         # Convert data as dictated by the regressor tags
         X = self._convert_X(X, X_mtype)
@@ -298,47 +298,6 @@ class BaseRegressor(BaseEstimator, ABC):
 
         return r2_score(y, self.predict(X), normalize=True, multioutput=multioutput)
 
-    def _vectorize(self, methodname, **kwargs):
-        """Vectorized/iterated loop over method of BaseClassifier.
-
-        Uses regressors_ attribute to store one regressor per loop index.
-        """
-        y = kwargs.get("y")
-        X = kwargs.get("X")
-        if X is not None:
-            kwargs.pop("X")
-        if y is not None:
-            kwargs.pop("y")
-            self._y_vec = y
-        regressors_ = self._y_vec.vectorize_est(
-            self,
-            method="clone",
-        )
-        if methodname == "fit":
-            self.regressors_ = self._y_vec.vectorize_est(
-                regressors_,
-                method=methodname,
-                args={"y": y},
-                X=X,
-            )
-            return self
-        else:
-            regressors_ = self.regressors_
-            y_pred = self._y_vec.vectorize_est(
-                regressors_,
-                method=methodname,
-                # return_type="list",
-                X=X,
-                args={"y": y} if y is not None else {},
-                **kwargs,  # contains X inside
-            )
-            y_pred = pd.DataFrame(
-                {str(i): y_pred[col].values[0] for i, col in enumerate(y_pred.columns)}
-            )
-            return y_pred
-        # add code for score
-
-    @abstractmethod
     def _fit(self, X, y):
         """Fit time series regressor to training data.
 
@@ -372,9 +331,8 @@ class BaseRegressor(BaseEstimator, ABC):
         -----
         Changes state by creating a fitted model that updates attributes ending in "_"
         """
-        ...
+        raise NotImplementedError("abstract method")
 
-    @abstractmethod
     def _predict(self, X) -> np.ndarray:
         """Predicts labels for sequences in X.
 
@@ -395,304 +353,4 @@ class BaseRegressor(BaseEstimator, ABC):
         y : 1D np.array of float, of shape [n_instances] - predicted regression labels
             indices correspond to instance indices in X
         """
-        ...
-
-    def _check_convert_X_for_predict(self, X):
-        """Input checks, capability checks, repeated in all predict/score methods.
-
-        Parameters
-        ----------
-        X : any object (to check/convert)
-            should be of a supported Panel mtype or 2D numpy.ndarray
-
-        Returns
-        -------
-        X: an object of a supported Panel mtype, numpy3D if X was a 2D numpy.ndarray
-
-        Raises
-        ------
-        ValueError if X is of invalid input data type, or there is not enough data
-        ValueError if the capabilities in self._tags do not handle the data.
-        """
-        X = _internal_convert(X)
-        X_metadata = _check_regressor_input(
-            X, return_metadata=self.METADATA_REQ_IN_CHECKS
-        )
-        missing = X_metadata["has_nans"]
-        multivariate = not X_metadata["is_univariate"]
-        unequal = not X_metadata["is_equal_length"]
-        X_mtype = X_metadata["mtype"]
-        # Check this classifier can handle characteristics
-        self._check_capabilities(missing, multivariate, unequal)
-        # Convert data as dictated by the classifier tags
-        X = self._convert_X(X, X_mtype=X_mtype)
-
-        return X
-
-    def _check_y(self, y=None):
-        """Check and coerce X/y for fit/transform functions.
-
-        Parameters
-        ----------
-        y : pd.DataFrame, pd.Series or np.ndarray
-
-        Returns
-        -------
-        y : object of sktime compatible time series type
-            can be Series, Panel, Hierarchical
-        y_metadata : dict
-            metadata of y, retured by check_is_scitype
-        """
-        if y is None:
-            return None
-
-        capa_multioutput = self.get_tag("capability:multioutput")
-        y_inner_mtype = self.get_tag("y_inner_mtype")
-
-        y_valid, y_msg, y_metadata = check_is_scitype(
-            y, "Table", return_metadata=["is_univariate"]
-        )
-
-        if not y_valid:
-            allowed_msg = (
-                f"In regression, y must be of a supported type, "
-                f"for instance 1D or 2D numpy arrays, pd.DataFrame, or pd.Series. "
-                f"Allowed compatible mtype format specifications are:"
-                f" {MTYPE_LIST_TABLE} ."
-            )
-            check_is_error_msg(
-                y_msg, var_name="y", allowed_msg=allowed_msg, raise_exception=True
-            )
-
-        y_uni = y_metadata["is_univariate"]
-        y_mtype = y_metadata["mtype"]
-
-        requires_vectorization = not capa_multioutput and not y_uni
-
-        if requires_vectorization:
-            y_df = convert(
-                y,
-                from_type=y_mtype,
-                to_type="pd_DataFrame_Table",
-                as_scitype="Table",
-                store=self._converter_store_y,
-            )
-            y_vec = VectorizedDF([y_df], iterate_cols=True)
-            return y_vec, y_metadata
-
-        y_inner = convert(
-            y,
-            from_type=y_mtype,
-            to_type=y_inner_mtype,
-            as_scitype="Table",
-            store=self._converter_store_y,
-        )
-
-        return y_inner, y_metadata
-
-    def _convert_output_y(self, y):
-        """Convert output y to original format.
-
-        Parameters
-        ----------
-        y : np.ndarray or pd.DataFrame
-
-        Returns
-        -------
-        y : np.ndarray or pd.DataFrame
-        """
-        # for consistency with legacy behaviour:
-        # output is coerced to numpy1D in case of univariate output
-        if not self._y_metadata["is_univariate"]:
-            output_mtype = self._y_metadata["mtype"]
-            converter_store = self._converter_store_y
-        else:
-            output_mtype = "numpy1D"
-            converter_store = None
-
-        y = convert(
-            y,
-            from_type=self.get_tag("y_inner_mtype"),
-            to_type=output_mtype,
-            as_scitype="Table",
-            store=converter_store,
-            store_behaviour="freeze",
-        )
-        return y
-
-    def _check_capabilities(self, missing, multivariate, unequal):
-        """Check whether this regressor can handle the data characteristics.
-
-        Parameters
-        ----------
-        missing : boolean, does the data passed to fit contain missing values?
-        multivariate : boolean, does the data passed to fit contain missing values?
-        unequal : boolea, do the time series passed to fit have variable lengths?
-
-        Raises
-        ------
-        ValueError if the capabilities in self._tags do not handle the data.
-        """
-        allow_multivariate = self.get_tag("capability:multivariate")
-        allow_missing = self.get_tag("capability:missing_values")
-        allow_unequal = self.get_tag("capability:unequal_length")
-
-        self_name = type(self).__name__
-
-        # identify problems, mismatch of capability and inputs
-        problems = []
-        if missing and not allow_missing:
-            problems += ["missing values"]
-        if multivariate and not allow_multivariate:
-            problems += ["multivariate series"]
-        if unequal and not allow_unequal:
-            problems += ["unequal length series"]
-
-        # construct error message
-        problems_and = " and ".join(problems)
-        problems_or = " or ".join(problems)
-        msg = (
-            f"Data seen by {self_name} instance has {problems_and}, "
-            f"but this {self_name} instance cannot handle {problems_or}. "
-            f"Calls with {problems_or} may result in error or unreliable results."
-        )
-
-        # raise exception or warning with message
-        # if self is composite, raise a warning, since passing could be fine
-        #   see discussion in PR 2366 why
-        if len(problems) > 0:
-            if self.is_composite():
-                warn(msg, obj=self)
-            else:
-                raise ValueError(msg)
-
-    def _convert_X(self, X, X_mtype):
-        """Convert equal length series from DataFrame to numpy array or vice versa.
-
-        Parameters
-        ----------
-        X : input data for the classifier
-        X_mtype : str, a Panel mtype string, e.g., "pd_multiindex", "numpy3D"
-
-        Returns
-        -------
-        X : input X converted to type in "X_inner_mtype" tag
-            usually a pd.DataFrame (nested) or 3D np.ndarray
-            Checked and possibly converted input data
-        """
-        inner_type = self.get_tag("X_inner_mtype")
-        # convert pd.DataFrame
-        X = convert(
-            X,
-            from_type=X_mtype,
-            to_type=inner_type,
-            as_scitype="Panel",
-        )
-        return X
-
-
-def _check_regressor_input(
-    X,
-    y=None,
-    enforce_min_instances=1,
-    return_metadata=True,
-):
-    """Check whether input X and y are valid formats with minimum data.
-
-    Raises a ValueError if the input is not valid.
-
-    Parameters
-    ----------
-    X : check whether conformant with any sktime Panel mtype specification
-    y : check whether a pd.Series or np.array
-    enforce_min_instances : int, optional (default=1)
-        check there are a minimum number of instances.
-    return_metadata : bool, str, or list of str
-        metadata fields to return with X_metadata, input to check_is_scitype
-
-    Returns
-    -------
-    metadata : dict with metadata for X returned by datatypes.check_is_scitype
-
-    Raises
-    ------
-    ValueError
-        If y or X is invalid input data type, or there is not enough data
-    """
-    # Check X is valid input type and recover the data characteristics
-    X_valid, msg, X_metadata = check_is_scitype(
-        X, scitype="Panel", return_metadata=return_metadata
-    )
-    # raise informative error message if X is in wrong format
-    allowed_msg = (
-        f"Allowed scitypes for regressors are Panel mtypes, "
-        f"for instance a pandas.DataFrame with MultiIndex and last(-1) "
-        f"level an sktime compatible time index. "
-        f"Allowed compatible mtype format specifications are: {MTYPE_LIST_PANEL} ."
-    )
-    if not X_valid:
-        check_is_error_msg(
-            msg, var_name="X", allowed_msg=allowed_msg, raise_exception=True
-        )
-
-    n_cases = X_metadata["n_instances"]
-    if n_cases < enforce_min_instances:
-        raise ValueError(
-            f"Minimum number of cases required is {enforce_min_instances} but X "
-            f"has : {n_cases}"
-        )
-
-    # Check y if passed
-    if y is not None:
-        # Check y valid input
-        if not isinstance(y, (pd.Series, pd.DataFrame, np.ndarray)):
-            raise ValueError(
-                "y must be a np.array or a pd.Series or pd.DataFrame, but found ",
-                f"type: {type(y)}",
-            )
-        # Check matching number of labels
-        n_labels = y.shape[0]
-        if n_cases != n_labels:
-            raise ValueError(
-                f"Mismatch in number of cases. Number in X = {n_cases} nos in y = "
-                f"{n_labels}"
-            )
-        if isinstance(y, np.ndarray):
-            if y.ndim > 2:
-                raise ValueError(
-                    f"np.ndarray y must be 1-dimensional or 2-dimensional, "
-                    f"but found {y.ndim} dimensions"
-                )
-    return X_metadata
-
-
-def _internal_convert(X, y=None):
-    """Convert X and y if necessary as a user convenience.
-
-    Convert X to a 3D numpy array if already a 2D and convert y into an 1D numpy
-    array if passed as a Series.
-
-    Parameters
-    ----------
-    X : an object of a supported Panel mtype, or 2D numpy.ndarray
-    y : np.ndarray or pd.Series
-
-    Returns
-    -------
-    X: an object of a supported Panel mtype, numpy3D if X was a 2D numpy.ndarray
-    y: np.ndarray
-    """
-    if isinstance(X, np.ndarray):
-        # Temporary fix to insist on 3D numpy. For univariate problems,
-        # most regressors simply convert back to 2D. This squeezing should be
-        # done here, but touches a lot of files, so will get this to work first.
-        if X.ndim == 2:
-            X = X.reshape(X.shape[0], 1, X.shape[1])
-    if y is not None and isinstance(y, pd.Series):
-        # y should be a numpy array, although we allow Series for user convenience
-        y = pd.Series.to_numpy(y)
-    if y is not None and isinstance(y, np.ndarray):
-        y = y.astype("float")
-    if y is None:
-        return X
-    return X, y
+        raise NotImplementedError("abstract method")
diff --git a/sktime/transformations/base.py b/sktime/transformations/base.py
index 860eb91a3..23005244c 100644
--- a/sktime/transformations/base.py
+++ b/sktime/transformations/base.py
@@ -138,6 +138,7 @@ class BaseTransformer(BaseEstimator):
     }
 
     # default config values
+    # see set_config documentation for details
     _config = {
         "input_conversion": "on",
         # controls input checks and conversions,
@@ -156,41 +157,12 @@ class BaseTransformer(BaseEstimator):
         #  {None, "dask", "loky", "multiprocessing", "threading"}
         #  None: no parallelization
         #  "loky", "multiprocessing" and "threading": uses `joblib` Parallel loops
+        #  "joblib": uses custom joblib backend, set via `joblib_backend` tag
         #  "dask": uses `dask`, requires `dask` package in environment
         "backend:parallel:params": None,  # params for parallelization backend
     }
 
     _config_doc = {
-        "backend:parallel": """
-        backend:parallel : str, optional, default="None"
-            backend to use for parallelization when broadcasting/vectorizing, one of
-
-            - "None": executes loop sequentally, simple list comprehension
-            - "loky", "multiprocessing" and "threading": uses ``joblib.Parallel``
-            - "joblib": custom and 3rd party ``joblib`` backends, e.g., ``spark``
-            - "dask": uses ``dask``, requires ``dask`` package in environment
-        """,
-        "backend:parallel:params": """
-        backend:parallel:params : dict, optional, default={} (no parameters passed)
-            additional parameters passed to the parallelization backend as config.
-            Valid keys depend on the value of ``backend:parallel``:
-
-            - "None": no additional parameters, ``backend_params`` is ignored
-            - "loky", "multiprocessing" and "threading": default ``joblib`` backends
-              any valid keys for ``joblib.Parallel`` can be passed here, e.g.,
-              ``n_jobs``, with the exception of ``backend`` which is directly
-              controlled by ``backend``.
-              If ``n_jobs`` is not passed, it will default to ``-1``, other parameters
-              will default to ``joblib`` defaults.
-            - "joblib": custom and 3rd party ``joblib`` backends,
-              e.g., ``spark``. Any valid keys for ``joblib.Parallel``
-              can be passed here, e.g., ``n_jobs``,
-            ``backend`` must be passed as a key of ``backend_params`` in this case.
-              If ``n_jobs`` is not passed, it will default to ``-1``, other parameters
-              will default to ``joblib`` defaults.
-            - "dask": any valid keys for ``dask.compute``
-              can be passed, e.g., ``scheduler``
-        """,
         "input_conversion": """
         input_conversion : str, one of "on", "off", valid mtype string
             controls input checks and conversions,


Test patch:
diff --git a/sktime/classification/tests/test_all_classifiers.py b/sktime/classification/tests/test_all_classifiers.py
index e8f362266..cda9a0245 100644
--- a/sktime/classification/tests/test_all_classifiers.py
+++ b/sktime/classification/tests/test_all_classifiers.py
@@ -208,7 +208,7 @@ class TestAllClassifiers(ClassifierFixtureGenerator, QuickTester):
         X, y = make_classification_problem()
         y[:] = 42
 
-        error_msg = "single class label"
+        error_msg = "single label"
 
         with pytest.warns(UserWarning, match=error_msg):
             estimator_instance.fit(X, y)
diff --git a/sktime/classification/tests/test_base.py b/sktime/classification/tests/test_base.py
index 525c29125..33d0409f1 100644
--- a/sktime/classification/tests/test_base.py
+++ b/sktime/classification/tests/test_base.py
@@ -170,36 +170,41 @@ def test_check_capabilities(missing, multivariate, unequal):
     handle it and that cannot. Obvs could loop, but I think its clearer to just
     explicitly test;
     """
+    X_metadata = {
+        "has_nans": missing,
+        "is_univariate": not multivariate,
+        "is_equal_length": not unequal,
+    }
     handles_none = _DummyClassifier()
     handles_none_composite = _DummyComposite(_DummyClassifier())
 
     # checks that errors are raised
     if missing:
         with pytest.raises(ValueError, match=missing_message):
-            handles_none._check_capabilities(missing, multivariate, unequal)
+            handles_none._check_capabilities(X_metadata)
     if multivariate:
         with pytest.raises(ValueError, match=multivariate_message):
-            handles_none._check_capabilities(missing, multivariate, unequal)
+            handles_none._check_capabilities(X_metadata)
     if unequal:
         with pytest.raises(ValueError, match=unequal_message):
-            handles_none._check_capabilities(missing, multivariate, unequal)
+            handles_none._check_capabilities(X_metadata)
     if not missing and not multivariate and not unequal:
-        handles_none._check_capabilities(missing, multivariate, unequal)
+        handles_none._check_capabilities(X_metadata)
 
     if missing:
         with pytest.warns(UserWarning, match=missing_message):
-            handles_none_composite._check_capabilities(missing, multivariate, unequal)
+            handles_none_composite._check_capabilities(X_metadata)
     if multivariate:
         with pytest.warns(UserWarning, match=multivariate_message):
-            handles_none_composite._check_capabilities(missing, multivariate, unequal)
+            handles_none_composite._check_capabilities(X_metadata)
     if unequal:
         with pytest.warns(UserWarning, match=unequal_message):
-            handles_none_composite._check_capabilities(missing, multivariate, unequal)
+            handles_none_composite._check_capabilities(X_metadata)
     if not missing and not multivariate and not unequal:
-        handles_none_composite._check_capabilities(missing, multivariate, unequal)
+        handles_none_composite._check_capabilities(X_metadata)
 
     handles_all = _DummyHandlesAllInput()
-    handles_all._check_capabilities(missing, multivariate, unequal)
+    handles_all._check_capabilities(X_metadata)
 
 
 def test_convert_input():
@@ -253,9 +258,10 @@ def test__check_classifier_input():
     4. Test incorrect: y as a list
     5. Test incorrect: too few cases or too short a series
     """
+    clf = _DummyClassifier()
 
     def _check_classifier_input(X, y=None, enforce_min_instances=1):
-        return BaseClassifier._check_classifier_input(None, X, y, enforce_min_instances)
+        return clf._check_input(X, y, enforce_min_instances)
 
     # 1. Test correct: X: np.array of 2 and 3 dimensions vs y:np.array and np.Series
     test_X1 = np.random.uniform(-1, 1, size=(5, 10))