Repository: alan-turing-institute/sktime
Pull request #3264 (2022-08-14 10:07:23)

[ENH] arithmetic combinations of distances/kernel transformers
Partially implements #1888 - arithmetics over variables is separated in this implementation, due to different implementation requirements (sub-setting over variables, etc) which would over-complicate the signature if exposed in a single class.
This can also be leveraged towards dunders, see #1886.

Implements a `CombinedDistance` which takes multiple distances and produces the distance that is obtained from combining them via a numpy reducer style operation.


Patch:
diff --git a/.all-contributorsrc b/.all-contributorsrc
index b641b5a29..b4cc47ab2 100644
--- a/.all-contributorsrc
+++ b/.all-contributorsrc
@@ -589,7 +589,9 @@
       "avatar_url": "https://avatars.githubusercontent.com/u/54197164?v=4",
       "profile": "https://github.com/achieveordie",
       "contributions": [
-        "code"
+        "bug",
+        "code",
+        "doc"
       ]
     },
     {
diff --git a/pyproject.toml b/pyproject.toml
index f46fdb20d..cdc9ebec3 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -59,7 +59,7 @@ all_extras = [
     "gluonts>=0.9.0",
     "matplotlib>=3.3.2",
     "mne",
-    "pmdarima>=1.8.0,!=1.8.1",
+    "pmdarima>=1.8.0,!=1.8.1,<2.0.0",
     "prophet>=1.1",
     "pykalman>=0.9.5",
     "pyod>=0.8.0",
diff --git a/sktime/datasets/_data_io.py b/sktime/datasets/_data_io.py
index 0d82a1c50..4106cf731 100644
--- a/sktime/datasets/_data_io.py
+++ b/sktime/datasets/_data_io.py
@@ -44,6 +44,17 @@ DIRNAME = "data"
 MODULE = os.path.dirname(__file__)
 
 
+# Return appropriate return_type in case an alias was used
+def _alias_mtype_check(return_type):
+    if return_type is None:
+        return_type = "nested_univ"
+    if return_type in ["numpy2d", "numpy2D", "np2d", "np2D"]:
+        return_type = "numpyflat"
+    if return_type in ["numpy3d", "np3d", "np3D"]:
+        return_type = "numpy3D"
+    return return_type
+
+
 # time series classification data sets
 def _download_and_extract(url, extract_path=None):
     """
@@ -182,7 +193,7 @@ def _load_provided_dataset(
         name : string, file name
         split : string, default = None, or one of "TRAIN" or "TEST".
         return_X_y : default = True, if true, returns X and y separately.
-        return_type : default = None,
+        return_type : string or None, default = None.
         local_module: default = os.path.dirname(__file__),
         local_dirname: default = "data"
     """
@@ -192,36 +203,31 @@ def _load_provided_dataset(
     if split in ("TRAIN", "TEST"):
         fname = name + "_" + split + ".ts"
         abspath = os.path.join(local_module, local_dirname, name, fname)
-        X, y = load_from_tsfile(abspath, return_data_type=return_type)
+        X, y = load_from_tsfile(abspath, return_data_type="nested_univ")
     # if split is None, load both train and test set
     elif split is None:
         fname = name + "_TRAIN.ts"
         abspath = os.path.join(local_module, local_dirname, name, fname)
-        X_train, y_train = load_from_tsfile(abspath, return_data_type=return_type)
+        X_train, y_train = load_from_tsfile(abspath, return_data_type="nested_univ")
 
         fname = name + "_TEST.ts"
         abspath = os.path.join(local_module, local_dirname, name, fname)
-        X_test, y_test = load_from_tsfile(abspath, return_data_type=return_type)
+        X_test, y_test = load_from_tsfile(abspath, return_data_type="nested_univ")
 
-        if isinstance(X_train, np.ndarray):
-            X = np.concatenate([X_train, X_test])
-        elif isinstance(X_train, pd.DataFrame):
-            X = pd.concat([X_train, X_test])
-            X = X.reset_index(drop=True)
-        else:
-            raise IOError(
-                f"Invalid data structure type {type(X_train)} for loading "
-                f"classification problem "
-            )
+        X = pd.concat([X_train, X_test])
+        X = X.reset_index(drop=True)
         y = np.concatenate([y_train, y_test])
 
     else:
         raise ValueError("Invalid `split` value =", split)
-    # Return appropriately
+
+    return_type = _alias_mtype_check(return_type)
     if return_X_y:
+        X = convert(X, from_type="nested_univ", to_type=return_type)
         return X, y
     else:
         X["class_val"] = pd.Series(y)
+        X = convert(X, from_type="nested_univ", to_type=return_type)
         return X
 
 
@@ -324,7 +330,7 @@ def load_from_tsfile(
             "numpy3D"/"numpy3d"/"np3D": 3D np.ndarray (instance, variable, time index)
             "numpy2d"/"np2d"/"numpyflat": 2D np.ndarray (instance, time index)
             "pd-multiindex": pd.DataFrame with 2-level (instance, time) MultiIndex
-        Exception is raised if the data cannot be stored in therequested type.
+        Exception is raised if the data cannot be stored in the requested type.
 
     Returns
     -------
@@ -343,12 +349,7 @@ def load_from_tsfile(
     ValueError if return_data_type = numpy2d but the data are multivariate and/
     or unequal length series
     """
-    if return_data_type is None:
-        return_data_type = "nested_univ"
-    if return_data_type in ["numpy2d", "numpy2D", "np2d", "np2D"]:
-        return_data_type = "numpyflat"
-    if return_data_type in ["numpy3d", "np3d", "np3D"]:
-        return_data_type = "numpy3D"
+    return_data_type = _alias_mtype_check(return_data_type)
 
     if not isinstance(return_data_type, str):
         raise TypeError(
diff --git a/sktime/datasets/_single_problem_loaders.py b/sktime/datasets/_single_problem_loaders.py
index 4f391d526..e1d014cd3 100644
--- a/sktime/datasets/_single_problem_loaders.py
+++ b/sktime/datasets/_single_problem_loaders.py
@@ -53,7 +53,9 @@ DIRNAME = "data"
 MODULE = os.path.dirname(__file__)
 
 
-def load_UCR_UEA_dataset(name, split=None, return_X_y=True, extract_path=None):
+def load_UCR_UEA_dataset(
+    name, split=None, return_X_y=True, return_type=None, extract_path=None
+):
     """Load dataset from UCR UEA time series archive.
 
     Downloads and extracts dataset if not already downloaded. Data is assumed to be
@@ -76,6 +78,18 @@ def load_UCR_UEA_dataset(name, split=None, return_X_y=True, extract_path=None):
         format <name>_TRAIN.ts or <name>_TEST.ts.
     return_X_y : bool, optional (default=False)
         it returns two objects, if False, it appends the class labels to the dataframe.
+    return_type: None or str, optional (default=None)
+        Memory data format specification to return X in, if None then return the default
+        "nested_univ" type.
+        str can be any other supported Panel mtype,
+            for list of mtypes, see datatypes.SCITYPE_REGISTER
+            for specifications, see examples/AA_datatypes_and_datasets.ipynb
+        commonly used specifications:
+            "nested_univ: nested pd.DataFrame, pd.Series in cells
+            "numpy3D"/"numpy3d"/"np3D": 3D np.ndarray (instance, variable, time index)
+            "numpy2d"/"np2d"/"numpyflat": 2D np.ndarray (instance, time index)
+            "pd-multiindex": pd.DataFrame with 2-level (instance, time) MultiIndex
+        Exception is raised if the data cannot be stored in the requested type.
     extract_path : str, optional (default=None)
         the path to look for the data. If no path is provided, the function
         looks in `sktime/datasets/data/`.
@@ -96,7 +110,7 @@ def load_UCR_UEA_dataset(name, split=None, return_X_y=True, extract_path=None):
     >>> from sktime.datasets import load_UCR_UEA_dataset
     >>> X, y = load_UCR_UEA_dataset(name="ArrowHead")
     """
-    return _load_dataset(name, split, return_X_y, extract_path)
+    return _load_dataset(name, split, return_X_y, return_type, extract_path)
 
 
 def load_plaid(split=None, return_X_y=True):
diff --git a/sktime/datatypes/_panel/_convert.py b/sktime/datatypes/_panel/_convert.py
index 597b175bb..959fe2984 100644
--- a/sktime/datatypes/_panel/_convert.py
+++ b/sktime/datatypes/_panel/_convert.py
@@ -770,14 +770,17 @@ def from_nested_to_multi_index(X, instance_index=None, time_index=None):
     -------
     X_mi : pd.DataFrame
         The multi-indexed pandas DataFrame
-
     """
     # this contains the right values, but does not have the right index
     #   need convert_dtypes or dtypes will always be object
     # explode by column to ensure we deal with unequal length series properly
     X_mi = pd.DataFrame()
 
-    for c in X.columns:
+    X_cols = X.columns
+    nested_cols = [c for c in X_cols if isinstance(X[[c]].iloc[0, 0], pd.Series)]
+    non_nested_cols = list(set(X_cols).difference(nested_cols))
+
+    for c in nested_cols:
         X_col = X[[c]].explode(c)
         X_col = X_col.infer_objects()
 
@@ -788,6 +791,11 @@ def from_nested_to_multi_index(X, instance_index=None, time_index=None):
 
         X_mi[[c]] = X_col
 
+    for c in non_nested_cols:
+        for ix in X.index:
+            X_mi.loc[ix, c] = X[[c]].loc[ix].iloc[0]
+        X_mi[[c]] = X_mi[[c]].convert_dtypes()
+
     return X_mi
 
 
diff --git a/sktime/dists_kernels/__init__.py b/sktime/dists_kernels/__init__.py
index 88438b0d5..292301f36 100644
--- a/sktime/dists_kernels/__init__.py
+++ b/sktime/dists_kernels/__init__.py
@@ -1,18 +1,21 @@
 # -*- coding: utf-8 -*-
-"""
-module exports for dist_kernels module
-"""
+"""Module exports for dist_kernels module."""
 
 from sktime.dists_kernels._base import (
     BasePairwiseTransformer,
     BasePairwiseTransformerPanel,
 )
-from sktime.dists_kernels.compose_tab_to_panel import AggrDist
+from sktime.dists_kernels.compose_tab_to_panel import AggrDist, FlatDist
+from sktime.dists_kernels.dtw import DtwDist
+from sktime.dists_kernels.edit_dist import EditDist
 from sktime.dists_kernels.scipy_dist import ScipyDist
 
 __all__ = [
     "BasePairwiseTransformer",
     "BasePairwiseTransformerPanel",
     "AggrDist",
+    "DtwDist",
+    "EditDist",
+    "FlatDist",
     "ScipyDist",
 ]
diff --git a/sktime/dists_kernels/_base.py b/sktime/dists_kernels/_base.py
index 8a2805496..3087f4c05 100644
--- a/sktime/dists_kernels/_base.py
+++ b/sktime/dists_kernels/_base.py
@@ -52,6 +52,8 @@ class BasePairwiseTransformer(BaseEstimator):
         "symmetric": False,  # is the transformer symmetric, i.e., t(x,y)=t(y,x) always?
         "X_inner_mtype": "numpy2D",  # which mtype is used internally in _transform?
         "fit_is_empty": True,  # is "fit" empty? Yes, for all pairwise transforms
+        "capability:missing_values": True,  # can estimator handle missing data?
+        "capability:multivariate": True,  # can estimator handle multivariate data?
     }
 
     def __init__(self):
@@ -183,6 +185,9 @@ class BasePairwiseTransformerPanel(BaseEstimator):
         "symmetric": False,  # is the transformer symmetric, i.e., t(x,y)=t(y,x) always?
         "X_inner_mtype": "df-list",  # which mtype is used internally in _transform?
         "fit_is_empty": True,  # is "fit" empty? Yes, for all pairwise transforms
+        "capability:missing_values": True,  # can estimator handle missing data?
+        "capability:multivariate": True,  # can estimator handle multivariate data?
+        "capability:unequal_length": False,  # can dist handle unequal length panels?
     }
 
     def __init__(self):
diff --git a/sktime/dists_kernels/algebra.py b/sktime/dists_kernels/algebra.py
new file mode 100644
index 000000000..394aee6ef
--- /dev/null
+++ b/sktime/dists_kernels/algebra.py
@@ -0,0 +1,189 @@
+# -*- coding: utf-8 -*-
+"""Arithmetics with distances/kernels, e.g., addition, multiplication."""
+
+__author__ = ["fkiraly"]
+
+import numpy as np
+
+from sktime.base import _HeterogenousMetaEstimator
+from sktime.dists_kernels._base import BasePairwiseTransformerPanel
+
+SUPPORTED_MTYPES = ["pd-multiindex", "nested_univ", "df-list", "numpy3D"]
+
+
+class CombinedDistance(BasePairwiseTransformerPanel, _HeterogenousMetaEstimator):
+    """Distances combined via arithmetic operation, e.g., addition, multiplication.
+
+    `CombinedDistance` creates a pairwise trafo from multiple other pairwise trafos,
+    by performing an arithmetic operation (np.ufunc) on the multiple distance matrices.
+
+    For a list of transformers `trafo1`, `trafo2`, ..., `trafoN`, ufunc `operation`,
+    this compositor behaves as follows:
+    `transform(X, X2)` - computes `dist1 = trafo1.transform(X, X2)`,
+        `dist2 = trafo2.transform(X, X2)`, ..., distN =  trafoN.transform(X, X2)`,
+        all of shape `(len(X), len(X2)`, then applies `operation` entry-wise,
+        to obtain a single matrix `dist` of shape `(len(X), len(X2)`
+        Example: if `operation = np.sum`, then `dist` is
+        the entry-wise sum of `dist1`, `dist2`, ..., `distN`
+
+    Parameters
+    ----------
+    pw_trafos : list of sktime pairwise panel distances, or
+        list of tuples (str, transformer) of sktime pairwise panel distances
+        distances combined to a single distance using the operation
+    operation : None, str, function, or numpy ufunc, optional, default = None = mean
+        if str, must be one of "mean", "+" (add), "*" (multiply), "max", "min"
+        if func, must be of signature (1D iterable) -> float
+        operation carried out on the distance matrices distances
+
+    Examples
+    --------
+    >>> from sktime.dists_kernels.algebra import CombinedDistance
+    >>> from sktime.dists_kernels.dtw import DtwDist
+    >>> from sktime.datasets import load_unit_test
+    >>>
+    >>> X, _ = load_unit_test()
+    >>> X = X[0:3]
+    >>> sum_dist = CombinedDistance([DtwDist(), DtwDist(weighted=True)], "+")
+    >>> dist_mat = sum_dist.transform(X)
+    """
+
+    _tags = {
+        "X_inner_mtype": SUPPORTED_MTYPES,
+        "capability:missing_values": True,  # can estimator handle missing data?
+        "capability:multivariate": True,  # can estimator handle multivariate data?
+        "capability:unequal_length": True,  # can dist handle unequal length panels?
+    }
+
+    def __init__(self, pw_trafos, operation=None):
+
+        self.pw_trafos = pw_trafos
+        self.pw_trafos_ = self._check_estimators(
+            self.pw_trafos, cls_type=BasePairwiseTransformerPanel
+        )
+        self.operation = operation
+        self._operation = self._resolve_operation(operation)
+
+        super(CombinedDistance, self).__init__()
+
+        # abbreviate for readability
+        ests = self.pw_trafos_
+
+        # set property tags based on tags of components
+        self._anytagis_then_set("fit_is_empty", False, True, ests)
+        self._anytagis_then_set("capability:missing_values", False, True, ests)
+        self._anytagis_then_set("capability:multivariate", False, True, ests)
+        self._anytagis_then_set("capability:unequal_length", False, True, ests)
+
+    def _resolve_operation(self, operation):
+        """Coerce operation to a numpy.ufunc."""
+        alias_dict = {
+            None: np.mean,
+            "mean": np.mean,
+            "+": np.add,
+            "add": np.add,
+            "*": np.multiply,
+            "mult": np.multiply,
+            "multiply": np.multiply,
+            "min": np.min,
+            "max": np.max,
+        }
+
+        if operation in alias_dict.keys():
+            return alias_dict[operation]
+        else:
+            return operation
+
+    @property
+    def _pw_trafos(self):
+        return self._get_estimator_tuples(self.pw_trafos, clone_ests=False)
+
+    @_pw_trafos.setter
+    def _pw_trafos(self, value):
+        self.pw_trafos = value
+
+    def _transform(self, X, X2=None):
+        """Compute distance/kernel matrix.
+
+        private _transform containing core logic, called from public transform
+
+        Parameters
+        ----------
+        X: sktime Panel data container
+        X2: sktime Panel data container
+
+        Returns
+        -------
+        distmat: np.array of shape [n, m]
+            (i,j)-th entry contains distance/kernel between X.iloc[i] and X2.iloc[j]
+        """
+        ests = self._get_estimator_list(self.pw_trafos_)
+
+        distmats = [est.transform(X=X, X2=X2) for est in ests]
+        distmat_stack = np.stack(distmats)
+
+        operation = self._operation
+        if isinstance(operation, np.ufunc):
+            distmat = operation.reduce(distmat_stack)
+        else:
+            distmat = np.apply_over_axes(operation, distmat_stack, 0)
+            distmat = distmat.squeeze(axis=0)
+
+        return distmat
+
+    def get_params(self, deep=True):
+        """Get parameters of estimator in `steps`.
+
+        Parameters
+        ----------
+        deep : boolean, optional, default=True
+            If True, will return the parameters for this estimator and
+            contained sub-objects that are estimators.
+
+        Returns
+        -------
+        params : mapping of string to any
+            Parameter names mapped to their values.
+        """
+        return self._get_params("_pw_trafos", deep=deep)
+
+    def set_params(self, **kwargs):
+        """Set the parameters of estimator in `steps`.
+
+        Valid parameter keys can be listed with ``get_params()``.
+
+        Returns
+        -------
+        self : returns an instance of self.
+        """
+        self._set_params("_pw_trafos", **kwargs)
+        return self
+
+    @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`.
+        """
+        from sktime.dists_kernels.compose_tab_to_panel import AggrDist
+        from sktime.dists_kernels.dtw import DtwDist
+
+        params1 = {"pw_trafos": [AggrDist.create_test_instance()]}
+        params2 = {
+            "pw_trafos": [AggrDist.create_test_instance(), DtwDist()],
+            "operation": "+",
+        }
+
+        return [params1, params2]
diff --git a/sktime/dists_kernels/compose.py b/sktime/dists_kernels/compose.py
new file mode 100644
index 000000000..9e613f3d2
--- /dev/null
+++ b/sktime/dists_kernels/compose.py
@@ -0,0 +1,220 @@
+# -*- coding: utf-8 -*-
+"""Pipelines for pairwise panel transformers."""
+
+__author__ = ["fkiraly"]
+
+from sktime.base import _HeterogenousMetaEstimator
+from sktime.dists_kernels._base import BasePairwiseTransformerPanel
+from sktime.transformations.base import BaseTransformer
+from sktime.transformations.compose import TransformerPipeline
+
+SUPPORTED_MTYPES = ["pd-multiindex", "nested_univ", "df-list", "numpy3D"]
+
+
+class PwTrafoPanelPipeline(BasePairwiseTransformerPanel, _HeterogenousMetaEstimator):
+    """Pipeline of transformers and a pairwise panel transformer.
+
+    `PwTrafoPanelPipeline` chains transformers and a pairwise transformer at the end.
+    The pipeline is constructed with a list of sktime transformers (BaseTransformer),
+        plus a pairwise panel transformer, following BasePairwiseTransformerPanel.
+    The transformer list can be unnamed - a simple list of transformers -
+        or string named - a list of pairs of string, estimator.
+
+    For a list of transformers `trafo1`, `trafo2`, ..., `trafoN` and an estimator `est`,
+        the pipeline behaves as follows:
+    `transform(X)` - running `trafo1.fit_transform` on `X`,
+        them `trafo2.fit_transform` on the output of `trafo1.fit_transform`, etc
+        sequentially, with `trafo[i]` receiving the output of `trafo[i-1]`.
+        Then passes output of `trafo[N]` to `pw_trafo.transform`, as `X`.
+        Same chain of transformers is run on `X2` and passed, if not `None`.
+
+    `PwTrafoPanelPipeline` can also be created by using the magic multiplication
+        on any parameter estimator: if `pw_t` is `BasePairwiseTransformerPanel`,
+            and `my_trafo1`, `my_trafo2` inherit from `BaseTransformer`, then,
+            for instance, `my_trafo1 * my_trafo2 * pw_t`
+            will result in the same object as  obtained from the constructor
+            `PwTrafoPanelPipeline(pw_trafo=pw_t, transformers=[my_trafo1, my_trafo2])`
+        magic multiplication can also be used with (str, transformer) pairs,
+            as long as one element in the chain is a transformer
+
+    Parameters
+    ----------
+    pw_trafo : pairwise panel transformer,
+        i.e., estimator inheriting from BasePairwiseTransformerPane
+        this is a "blueprint" estimator, state does not change when `fit` is called
+    transformers : list of sktime transformers, or
+        list of tuples (str, transformer) of sktime transformers
+        these are "blueprint" transformers, states do not change when `fit` is called
+
+    Examples
+    --------
+    >>> from sktime.dists_kernels.compose import PwTrafoPanelPipeline
+    >>> from sktime.dists_kernels.dtw import DtwDist
+    >>> from sktime.transformations.series.exponent import ExponentTransformer
+    >>> from sktime.datasets import load_unit_test
+    >>>
+    >>> X, _ = load_unit_test()
+    >>> X = X[0:3]
+    >>> pipeline = PwTrafoPanelPipeline(DtwDist(), [ExponentTransformer()])
+    >>> dist_mat = pipeline.transform(X)
+    """
+
+    _tags = {
+        "X_inner_mtype": SUPPORTED_MTYPES,
+        "capability:missing_values": True,  # can estimator handle missing data?
+        "capability:multivariate": True,  # can estimator handle multivariate data?
+        "capability:unequal_length": True,  # can dist handle unequal length panels?
+    }
+
+    def __init__(self, pw_trafo, transformers):
+
+        self.pw_trafo = pw_trafo
+        self.transformers = transformers
+        self.transformers_ = TransformerPipeline(transformers)
+
+        super(PwTrafoPanelPipeline, self).__init__()
+
+        # can handle multivariate iff: both classifier and all transformers can
+        multivariate = pw_trafo.get_tag("capability:multivariate", False)
+        multivariate = multivariate and not self.transformers_.get_tag(
+            "univariate-only", True
+        )
+        # can handle missing values iff: both classifier and all transformers can,
+        #   *or* transformer chain removes missing data
+        missing = pw_trafo.get_tag("capability:missing_values", False)
+        missing = missing and self.transformers_.get_tag("handles-missing-data", False)
+        missing = missing or self.transformers_.get_tag(
+            "capability:missing_values:removes", False
+        )
+
+        # set the pipeline tags to the above
+        tags_to_set = {
+            "capability:multivariate": multivariate,
+            "capability:missing_values": missing,
+        }
+        self.set_tags(**tags_to_set)
+
+    @property
+    def _transformers(self):
+        return self.transformers_._steps
+
+    @_transformers.setter
+    def _transformers(self, value):
+        self.transformers_._steps = value
+
+    def __rmul__(self, other):
+        """Magic * method, return concatenated PwTrafoPanelPipeline, trafos on left.
+
+        Implemented for `other` being a transformer, otherwise returns `NotImplemented`.
+
+        Parameters
+        ----------
+        other: `sktime` transformer, must inherit from BaseTransformer
+            otherwise, `NotImplemented` is returned
+
+        Returns
+        -------
+        ParamFitterPipeline object, concatenation of `other` (first) with `self` (last).
+        """
+        if isinstance(other, BaseTransformer):
+            # use the transformers dunder to get a TransformerPipeline
+            trafo_pipeline = other * self.transformers_
+            # then stick the expanded pipeline in a PwTrafoPanelPipeline
+            new_pipeline = PwTrafoPanelPipeline(
+                pw_trafo=self.pw_trafo,
+                transformers=trafo_pipeline.steps,
+            )
+            return new_pipeline
+        else:
+            return NotImplemented
+
+    def _transform(self, X, X2=None):
+        """Compute distance/kernel matrix.
+
+        private _transform containing core logic, called from public transform
+
+        Behaviour: returns pairwise distance/kernel matrix
+            between samples in X and X2
+                if X2 is not passed, is equal to X
+                if X/X2 is a pd.DataFrame and contains non-numeric columns,
+                    these are removed before computation
+
+        Parameters
+        ----------
+        X: sktime Panel data container
+        X2: sktime Panel data container
+
+        Returns
+        -------
+        distmat: np.array of shape [n, m]
+            (i,j)-th entry contains distance/kernel between X.iloc[i] and X2.iloc[j]
+        """
+        trafos = self.transformers_.clone()
+        pw_trafo = self.pw_trafo
+
+        Xt = trafos.fit_transform(X)
+
+        # find out whether we know that the resulting matrix is symmetric
+        #   since aligner distances are always symmetric,
+        #   we know it's the case for sure if X equals X2
+        if X2 is None:
+            X2t = None
+        else:
+            X2t = trafos.fit_transform(X2)
+
+        distmat = pw_trafo.transform(Xt, X2t)
+        return distmat
+
+    def get_params(self, deep=True):
+        """Get parameters of estimator in `transformers`.
+
+        Parameters
+        ----------
+        deep : boolean, optional, default=True
+            If True, will return the parameters for this estimator and
+            contained sub-objects that are estimators.
+
+        Returns
+        -------
+        params : mapping of string to any
+            Parameter names mapped to their values.
+        """
+        params = dict()
+        trafo_params = self._get_params("_transformers", deep=deep)
+        params.update(trafo_params)
+
+        return params
+
+    def set_params(self, **kwargs):
+        """Set the parameters of estimator in `transformers`.
+
+        Valid parameter keys can be listed with ``get_params()``.
+
+        Returns
+        -------
+        self : returns an instance of self.
+        """
+        if "pw_trafo" in kwargs.keys():
+            if not isinstance(kwargs["pw_trafo"], BasePairwiseTransformerPanel):
+                raise TypeError('"pw_trafo" arg must be a pairwise panel transformer')
+        trafo_keys = self._get_params("_transformers", deep=True).keys()
+        pw_keys = self.pw_trafo.get_params(deep=True).keys()
+        trafo_args = self._subset_dict_keys(dict_to_subset=kwargs, keys=trafo_keys)
+        pw_args = self._subset_dict_keys(dict_to_subset=kwargs, keys=pw_keys)
+        if len(pw_args) > 0:
+            self.pw_trafo.set_params(**pw_args)
+        if len(trafo_args) > 0:
+            self._set_params("_transformers", **trafo_args)
+        return self
+
+    @classmethod
+    def get_test_params(cls, parameter_set="default"):
+        """Test parameters for DistFromAligner."""
+        from sktime.dists_kernels.compose_tab_to_panel import AggrDist
+        from sktime.transformations.series.exponent import ExponentTransformer
+
+        params = {
+            "pw_trafo": AggrDist.create_test_instance(),
+            "transformers": [ExponentTransformer()],
+        }
+        return params
diff --git a/sktime/dists_kernels/compose_tab_to_panel.py b/sktime/dists_kernels/compose_tab_to_panel.py
index 046a2220d..51511b36b 100644
--- a/sktime/dists_kernels/compose_tab_to_panel.py
+++ b/sktime/dists_kernels/compose_tab_to_panel.py
@@ -5,6 +5,7 @@ Composers that create panel pairwise transformers from table pairwise transforme
 Currently implemented composers in this module:
 
     AggrDist - panel distance from aggregation of tabular distance matrix entries
+    FlatDist - panel distance from applying tabular distance to flattened panel matrix
 """
 
 __author__ = ["fkiraly"]
@@ -16,26 +17,38 @@ from sktime.utils._testing.deep_equals import deep_equals
 
 
 class AggrDist(BasePairwiseTransformerPanel):
-    """Panel distance from tabular distance aggregation.
+    r"""Panel distance from tabular distance aggregation.
 
     panel distance obtained by applying aggregation function to tabular distance matrix
         example: AggrDist(ScipyDist()) is mean Euclidean distance between series
 
-    Components
+    Formal details (for real valued objects, mixed typed rows in analogy):
+    Let :math:`d: \mathbb{R}^k \times \mathbb{R}^{k}\rightarrow \mathbb{R}`
+    be the pairwise function in `transformer`, when applied to `k`-vectors.
+    Let :math:`f:\mathbb{R}^{n \ times m}` be the function `aggfunc` when applied to
+    an :math:`(n \times m)` matrix.
+    Let :math:`x_1, \dots, x_N\in \mathbb{R}^{n \times k}`,
+    :math:`y_1, \dots y_M \in \mathbb{R}^{m \times k}` be collections of matrices,
+    representing time series panel valued inputs `X` and `X2`, as follows:
+    :math:`x_i` is the `i`-th instance in `X`, and :math:`x_{i, j\ell}` is the
+    `j`-th time point, `\ell`-th variable of `X`. Analogous for :math:`y` and `X2`.
+
+    Then, `transform(X, X2)` returns the :math:`(N \times M)` matrix
+    with :math:`(i, j)`-th entry :math:`f \left((d(x_{i, a}, y_{j, b}))_{a, b}\right)`,
+    where :math:`x_{i, a}` denotes the :math:`a`-th row of :math:`x_i`, and
+    :math:`y_{j, b}` denotes the :math:`b`-th row of :math:`x_j`.
+
+    Parameters
     ----------
     transformer: pairwise transformer of BasePairwiseTransformer scitype
-
-    Hyper-parameters
-    ----------------
-    aggfunc: aggregation function 2D np.array -> float
-        default = None, however, if transform is called then defaults to np.mean
-    aggfunc_is_symm: bool - whether aggregation function is symmetric
-                i.e., invariant under transposing argument, it always holds that
-                    aggfunc(matrix) = aggfunc(np.transpose(matrix))
+    aggfunc: aggregation function (2D np.array) -> float or None, optional
+        default = None = np.mean
+    aggfunc_is_symm: bool, optional, default=False
+        whether aggregation function is symmetric (should be set according to aggfunc)
+            i.e., invariant under transposing argument, it always holds that
+                aggfunc(matrix) = aggfunc(np.transpose(matrix))
             used for fast computation of the resultant matrix (if symmetric)
             if unknown, False is the "safe" option that ensures correctness
-        default = True if aggfunc default is used
-            False otherwise (should be set according to choice of aggfunc)
     """
 
     def __init__(
@@ -51,6 +64,9 @@ class AggrDist(BasePairwiseTransformerPanel):
 
         super(AggrDist, self).__init__()
 
+        if self.aggfunc_is_symm:
+            self.set_tag("symmetric", True)
+
     def _transform(self, X, X2=None):
         """Compute distance/kernel matrix.
 
@@ -110,3 +126,81 @@ class AggrDist(BasePairwiseTransformerPanel):
         from sktime.dists_kernels import ScipyDist
 
         return {"transformer": ScipyDist()}
+
+
+class FlatDist(BasePairwiseTransformerPanel):
+    r"""Panel distance from applying tabular distance to flattened time series.
+
+    Applies the wrapped tabular distance to flattened series.
+    Flattening is done to a 2D numpy array of shape (n_instances, (n_vars, n_timepts))
+
+    Formal details (for real valued objects, mixed typed rows in analogy):
+    Let :math:`d:\mathbb{R}^k \times \mathbb{R}^{k}\rightarrow \mathbb{R}`
+    be the pairwise function in `transformer`, when applied to `k`-vectors.
+    Let :math:`x_1, \dots, x_N\in \mathbb{R}^{n \times \ell}`,
+    :math:`y_1, \dots y_M \in \mathbb{R}^{n \times \ell}` be collections of matrices,
+    representing time series panel valued inputs `X` and `X2`, as follows:
+    :math:`x_i` is the `i`-th instance in `X`, and :math:`x_{i, j\ell}` is the
+    `j`-th time point, `\ell`-th variable of `X`. Analogous for :math:`y` and `X2`.
+    Let :math:`f:\mathbb{R}^{n \times \ell} \rightarrow \mathbb{R}^{n \cdot \ell}`
+    be the mapping that flattens matrices by column-first lexicographical ordering,
+    and assume :math:`k = n \cdot \ell`.
+
+    Then, `transform(X, X2)` returns the :math:`(N \times M)` matrix
+    with :math:`(i, j)`-th entry :math:`d\left(f(x_i), f(y_j)\right)`.
+
+    Parameters
+    ----------
+    transformer: pairwise transformer of BasePairwiseTransformer scitype
+    """
+
+    _tags = {
+        "X_inner_mtype": "numpy3D",  # which mtype is used internally in _transform?
+    }
+
+    def __init__(self, transformer):
+
+        self.transformer = transformer
+
+        super(FlatDist, self).__init__()
+
+    def _transform(self, X, X2=None):
+        """Compute distance/kernel matrix.
+
+            Core logic.
+
+        Behaviour: returns pairwise distance/kernel matrix
+            between samples in X and X2
+                if X2 is not passed, is equal to X
+                if X/X2 is a pd.DataFrame and contains non-numeric columns,
+                    these are removed before computation
+
+        Parameters
+        ----------
+        X: pd.DataFrame of length n, or 2D np.array with n rows
+        X2: pd.DataFrame of length m, or 2D np.array with m rows, optional
+            default X2 = X
+
+        Returns
+        -------
+        distmat: np.array of shape [n, m]
+            (i,j)-th entry contains distance/kernel between X.iloc[i] and X2.iloc[j]
+        """
+        n_inst, n_vars, n_tts = X.shape
+        X = X.reshape(n_inst, n_vars * n_tts)
+
+        n_inst2, n_vars2, n_tts2 = X2.shape
+        X2 = X2.reshape(n_inst2, n_vars2 * n_tts2)
+
+        if deep_equals(X, X2):
+            return self.transformer.transform(X)
+        else:
+            return self.transformer.transform(X, X2)
+
+    @classmethod
+    def get_test_params(cls, parameter_set="default"):
+        """Test parameters for FlatDist."""
+        # importing inside to avoid circular dependencies
+        from sktime.dists_kernels import ScipyDist
+
+        return {"transformer": ScipyDist()}
diff --git a/sktime/dists_kernels/edit_dist.py b/sktime/dists_kernels/edit_dist.py
new file mode 100644
index 000000000..383ce5a5e
--- /dev/null
+++ b/sktime/dists_kernels/edit_dist.py
@@ -0,0 +1,199 @@
+# -*- coding: utf-8 -*-
+"""BaseEstimator interface to sktime edit distances in distances module."""
+
+__author__ = ["fkiraly"]
+
+from typing import Union
+
+import numpy as np
+
+from sktime.distances import pairwise_distance
+from sktime.dists_kernels._base import BasePairwiseTransformerPanel
+
+
+class EditDist(BasePairwiseTransformerPanel):
+    r"""Interface to sktime native edit distances.
+
+    Interface to the following edit distances:
+    LCSS - longest common subsequence distance
+    ERP - Edit distance for real penalty
+    EDR - Edit distance for real sequences
+    TWE - Time warp edit distance
+
+    LCSS [1]_ attempts to find the longest common sequence between two time series and
+    returns a value that is the percentage that longest common sequence assumes.
+    LCSS is computed by matching indexes that are
+    similar up until a defined threshold (epsilon).
+
+    The value returned will be between 0.0 and 1.0, where 0.0 means the two time series
+    are exactly the same and 1.0 means they are complete opposites.
+
+    EDR [2]_ computes the minimum number of elements (as a percentage) that must be
+    removed from x and y so that the sum of the distance between the remaining
+    signal elements lies within the tolerance (epsilon).
+
+    The value returned will be between 0 and 1 per time series. The value will
+    represent as a percentage of elements that must be removed for the time series to
+    be an exact match.
+
+    ERP [3]_ attempts align time series
+    by better considering how indexes are carried forward through the cost matrix.
+    Usually in the dtw cost matrix, if an alignment can't be found the previous value
+    is carried forward. ERP instead proposes the idea of gaps or sequences of points
+    that have no matches. These gaps are then punished based on their distance from 'g'.
+
+    TWE [4]_ is a distance measure for discrete time series
+    matching with time 'elasticity'. In comparison to other distance measures, (e.g.
+    DTW (Dynamic Time Warping) or LCS (Longest Common Subsequence Problem)), TWE is a
+    metric. Its computational time complexity is O(n^2), but can be drastically reduced
+    in some specific situation by using a corridor to reduce the search space. Its
+    memory space complexity can be reduced to O(n).
+
+    Parameters
+    ----------
+    distance: str, one of ["lcss", "edr", "erp", "twe"], optional, default = "lcss"
+        name of the distance that is calculated
+    window: float, default = None
+        Float that is the radius of the sakoe chiba window (if using Sakoe-Chiba
+        lower bounding). Value must be between 0. and 1.
+    itakura_max_slope: float, default = None
+        Gradient of the slope for itakura parallelogram (if using Itakura
+        Parallelogram lower bounding)
+    bounding_matrix: 2D np.ndarray, optional, default = None
+        if passed, must be of shape (len(X), len(X2)) for X, X2 in `transform`
+        Custom bounding matrix to use. If defined then other lower_bounding params
+        are ignored. The matrix should be structure so that indexes considered in
+        bound should be the value 0. and indexes outside the bounding matrix should
+        be infinity.
+    epsilon : float, defaults = 1.
+        Used in LCSS, EDR, ERP, otherwise ignored
+        Matching threshold to determine if two subsequences are considered close
+        enough to be considered 'common'.
+    g: float, defaults = 0.
+        Used in ERP, otherwise ignored.
+        The reference value to penalise gaps.
+    lmbda: float, optional, default = 1.0
+        Used in TWE, otherwise ignored.
+        A constant penalty that punishes the editing efforts. Must be >= 1.0.
+    nu: float optional, default = 0.001
+        Used in TWE, otherwise ignored.
+        A non-negative constant which characterizes the stiffness of the elastic
+        twe measure. Must be > 0.
+    p: int optional, default = 2
+        Used in TWE, otherwise ignored.
+        Order of the p-norm for local cost.
+
+    References
+    ----------
+    .. [1] M. Vlachos, D. Gunopoulos, and G. Kollios. 2002. "Discovering
+        Similar Multidimensional Trajectories", In Proceedings of the
+        18th International Conference on Data Engineering (ICDE '02).
+        IEEE Computer Society, USA, 673.
+    .. [2] Lei Chen, M. Tamer Özsu, and Vincent Oria. 2005. Robust and fast similarity
+        search for moving object trajectories. In Proceedings of the 2005 ACM SIGMOD
+        international conference on Management of data (SIGMOD '05). Association for
+        Computing Machinery, New York, NY, USA, 491–502.
+        DOI:https://doi.org/10.1145/1066157.1066213
+    .. [3] Lei Chen and Raymond Ng. 2004. On the marriage of Lp-norms and edit distance.
+        In Proceedings of the Thirtieth international conference on Very large data
+        bases - Volume 30 (VLDB '04). VLDB Endowment, 792–803.
+    .. [4] Marteau, P.; F. (2009). "Time Warp Edit Distance with Stiffness Adjustment
+        for Time Series Matching". IEEE Transactions on Pattern Analysis and Machine
+        Intelligence. 31 (2): 306–318.
+    """
+
+    _tags = {
+        "symmetric": True,  # all the distances are symmetric
+        "X_inner_mtype": "numpy3D",
+    }
+
+    ALLOWED_DISTANCE_STR = ["lcss", "edr", "erp", "twe"]
+
+    def __init__(
+        self,
+        distance: str = "lcss",
+        window: Union[int, None] = None,
+        itakura_max_slope: Union[float, None] = None,
+        bounding_matrix: np.ndarray = None,
+        epsilon: float = 1.0,
+        g: float = 0.0,
+        lmbda: float = 1.0,
+        nu: float = 0.001,
+        p: int = 2,
+    ):
+        self.distance = distance
+        self.window = window
+        self.itakura_max_slope = itakura_max_slope
+        self.bounding_matrix = bounding_matrix
+        self.epsilon = epsilon
+        self.g = g
+        self.lmbda = lmbda
+        self.nu = nu
+        self.p = p
+
+        super(EditDist, self).__init__()
+
+        kwargs = {
+            "window": window,
+            "itakura_max_slope": itakura_max_slope,
+            "bounding_matrix": bounding_matrix,
+        }
+
+        if distance not in self.ALLOWED_DISTANCE_STR:
+            raise ValueError(
+                "distance must be one of the strings"
+                f"{self.ALLOWED_DISTANCE_STR}, but found"
+                f" {distance}"
+            )
+
+        # epsilon is used only for lcss, edr, erp
+        if distance in ["lcss", "edr", "erp"]:
+            kwargs["epsilon"] = epsilon
+
+        # g is used only for erp
+        if distance == "erp":
+            kwargs["g"] = g
+
+        # twe has three unique params
+        if distance == "twe":
+            kwargs["lmbda"] = lmbda
+            kwargs["nu"] = nu
+            kwargs["p"] = p
+
+        self.kwargs = kwargs
+
+    def _transform(self, X, X2=None):
+        """Compute distance/kernel matrix.
+
+        private _transform containing core logic, called from public transform
+
+        Behaviour: returns pairwise distance/kernel matrix
+            between samples in X and X2
+                if X2 is not passed, is equal to X
+                if X/X2 is a pd.DataFrame and contains non-numeric columns,
+                    these are removed before computation
+
+        Parameters
+        ----------
+        X: 3D np.array of shape [num_instances, num_vars, num_time_points]
+        X2: 3D np.array of shape [num_instances, num_vars, num_time_points], optional
+            default X2 = X
+
+        Returns
+        -------
+        distmat: np.array of shape [n, m]
+            (i,j)-th entry contains distance/kernel between X[i] and X2[j]
+        """
+        metric_key = self.distance
+        kwargs = self.kwargs
+
+        distmat = pairwise_distance(X, X2, metric=metric_key, **kwargs)
+
+        return distmat
+
+    @classmethod
+    def get_test_params(cls, parameter_set="default"):
+        """Test parameters for EditDist."""
+        param_list = [{"distance": x} for x in cls.ALLOWED_DISTANCE_STR]
+
+        return param_list
diff --git a/sktime/forecasting/base/_base.py b/sktime/forecasting/base/_base.py
index e7b31d5a2..812501070 100644
--- a/sktime/forecasting/base/_base.py
+++ b/sktime/forecasting/base/_base.py
@@ -88,7 +88,7 @@ class BaseForecaster(BaseEstimator):
     # default tag values - these typically make the "safest" assumption
     _tags = {
         "scitype:y": "univariate",  # which y are fine? univariate/multivariate/both
-        "ignores-exogeneous-X": True,  # does estimator ignore the exogeneous X?
+        "ignores-exogeneous-X": False,  # does estimator ignore the exogeneous X?
         "capability:pred_int": False,  # can the estimator produce prediction intervals?
         "handles-missing-data": False,  # can estimator handle missing data?
         "y_inner_mtype": "pd.Series",  # which types do _fit/_predict, support for y?
diff --git a/sktime/param_est/compose.py b/sktime/param_est/compose.py
index af717fabc..ccb14faff 100644
--- a/sktime/param_est/compose.py
+++ b/sktime/param_est/compose.py
@@ -25,11 +25,11 @@ class ParamFitterPipeline(BaseParamFitter, _HeterogenousMetaEstimator):
 
     For a list of transformers `trafo1`, `trafo2`, ..., `trafoN` and an estimator `est`,
         the pipeline behaves as follows:
-    `fit(X)` - changes styte by running `trafo1.fit_transform` on `X`,
+    `fit(X)` - changes state by running `trafo1.fit_transform` on `X`,
         them `trafo2.fit_transform` on the output of `trafo1.fit_transform`, etc
         sequentially, with `trafo[i]` receiving the output of `trafo[i-1]`,
         and then running `est.fit` with `X` being the output of `trafo[N]`
-    `update(X)` - changes styte by running `trafo1.update.transform` on `X`,
+    `update(X)` - changes state by running `trafo1.update.transform` on `X`,
         them `trafo2.update.transform` on the output of `trafo1.update.transform`, etc
         sequentially, with `trafo[i]` receiving the output of `trafo[i-1]`,
         and then running `est.update` with `X` being the output of `trafo[N]`
@@ -45,7 +45,7 @@ class ParamFitterPipeline(BaseParamFitter, _HeterogenousMetaEstimator):
             and `my_trafo1`, `my_trafo2` inherit from `BaseTransformer`, then,
             for instance, `my_trafo1 * my_trafo2 * est`
             will result in the same object as  obtained from the constructor
-            `ParamFitterPipeline(classifier=est, transformers=[my_trafo1, my_trafo2])`
+            `ParamFitterPipeline(param_est=est, transformers=[my_trafo1, my_trafo2])`
         magic multiplication can also be used with (str, transformer) pairs,
             as long as one element in the chain is a transformer
 
@@ -105,12 +105,12 @@ class ParamFitterPipeline(BaseParamFitter, _HeterogenousMetaEstimator):
 
         super(ParamFitterPipeline, self).__init__()
 
-        # can handle multivariate iff: both classifier and all transformers can
+        # can handle multivariate iff: both estimator and all transformers can
         multivariate = param_est.get_tag("capability:multivariate", False)
         multivariate = multivariate and not self.transformers_.get_tag(
             "univariate-only", True
         )
-        # can handle missing values iff: both classifier and all transformers can,
+        # can handle missing values iff: both estimator and all transformers can,
         #   *or* transformer chain removes missing data
         missing = param_est.get_tag("capability:missing_values", False)
         missing = missing and self.transformers_.get_tag("handles-missing-data", False)
@@ -150,9 +150,9 @@ class ParamFitterPipeline(BaseParamFitter, _HeterogenousMetaEstimator):
         if isinstance(other, BaseTransformer):
             # use the transformers dunder to get a TransformerPipeline
             trafo_pipeline = other * self.transformers_
-            # then stick the expanded pipeline in a ClassifierPipeline
+            # then stick the expanded pipeline in a ParamFitterPipeline
             new_pipeline = ParamFitterPipeline(
-                classifier=self.classifier,
+                param_est=self.param_est,
                 transformers=trafo_pipeline.steps,
             )
             return new_pipeline
@@ -250,7 +250,7 @@ class ParamFitterPipeline(BaseParamFitter, _HeterogenousMetaEstimator):
         -------
         self : returns an instance of self.
         """
-        if "classifier" in kwargs.keys():
+        if "param_est" in kwargs.keys():
             if not isinstance(kwargs["param_est"], BaseParamFitter):
                 raise TypeError('"param_est" arg must be an sktime parameter fitter')
         trafo_keys = self._get_params("_transformers", deep=True).keys()
@@ -272,10 +272,6 @@ class ParamFitterPipeline(BaseParamFitter, _HeterogenousMetaEstimator):
         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.
-            For classifiers, a "default" set of parameters should be provided for
-            general testing, and a "results_comparison" set for comparing against
-            previously recorded results if the general set does not produce suitable
-            probabilities to compare against.
 
         Returns
         -------
diff --git a/sktime/registry/_tags.py b/sktime/registry/_tags.py
index 53b3f8db6..f5726beb6 100644
--- a/sktime/registry/_tags.py
+++ b/sktime/registry/_tags.py
@@ -201,13 +201,19 @@ ESTIMATOR_TAG_REGISTER = [
     ),
     (
         "capability:multivariate",
-        ["classifier", "early_classifier", "param_est"],
+        [
+            "classifier",
+            "early_classifier",
+            "param_est",
+            "transformer-pairwise",
+            "transformer-pairwise-panel",
+        ],
         "bool",
         "can the classifier classify time series with 2 or more variables?",
     ),
     (
         "capability:unequal_length",
-        ["classifier", "early_classifier", "transformer"],
+        ["classifier", "early_classifier", "transformer", "transformer-pairwise-panel"],
         "bool",
         "can the estimator handle unequal length time series?",
     ),
@@ -216,7 +222,13 @@ ESTIMATOR_TAG_REGISTER = [
     # Will be merged after refactor completion.
     (
         "capability:missing_values",
-        ["classifier", "early_classifier", "param_est"],
+        [
+            "classifier",
+            "early_classifier",
+            "param_est",
+            "transformer-pairwise",
+            "transformer-pairwise-panel",
+        ],
         "bool",
         "can the classifier handle missing data (NA, np.nan) in inputs?",
     ),


Test patch:
diff --git a/sktime/datasets/tests/test_data_io.py b/sktime/datasets/tests/test_data_io.py
index 584d99f1e..7e74c0546 100644
--- a/sktime/datasets/tests/test_data_io.py
+++ b/sktime/datasets/tests/test_data_io.py
@@ -1,12 +1,7 @@
 # -*- coding: utf-8 -*-
 """Test functions for data input and output."""
 
-__author__ = [
-    "SebasKoel",
-    "Emiliathewolf",
-    "TonyBagnall",
-    "jasonlines",
-]
+__author__ = ["SebasKoel", "Emiliathewolf", "TonyBagnall", "jasonlines", "achieveordie"]
 
 __all__ = []
 
@@ -30,8 +25,33 @@ from sktime.datasets import (
     load_uschange,
     write_dataframe_to_tsfile,
 )
-from sktime.datasets._data_io import MODULE, _convert_tsf_to_hierarchical
-from sktime.datatypes import check_is_mtype
+from sktime.datasets._data_io import (
+    MODULE,
+    _convert_tsf_to_hierarchical,
+    _load_provided_dataset,
+)
+from sktime.datatypes import MTYPE_LIST_PANEL, check_is_mtype
+
+# Disabling test for these mtypes since they don't support certain functionality yet
+_TO_DISABLE = ["pd-long", "pd-wide", "numpyflat"]
+
+
+@pytest.mark.parametrize("return_X_y", [True, False])
+@pytest.mark.parametrize(
+    "return_type", [mtype for mtype in MTYPE_LIST_PANEL if mtype not in _TO_DISABLE]
+)
+def test_load_provided_dataset(return_X_y, return_type):
+    """Test function to check for proper loading.
+
+    Check this via permutating between all possibilities of return_X_y and return_type.
+    """
+    if return_X_y:
+        X, y = _load_provided_dataset("UnitTest", "TRAIN", return_X_y, return_type)
+    else:
+        X = _load_provided_dataset("UnitTest", "TRAIN", return_X_y, return_type)
+
+    # Check whether object is same mtype or not, via bool
+    assert check_is_mtype(X, return_type)
 
 
 def test_load_from_tsfile():