Repository: sktime/sktime

Issue:
[BUG] Non informative Exception when fitting TimeSeriesKMeans with pd.Dataframe 
**Describe the bug**
The fitting of TimeSeriesKMeans with pd.Dataframe produces an TypeError that is not informative. 

**To Reproduce**
<!--
Add a Minimal, Complete, and Verifiable example (for more details, see e.g. https://stackoverflow.com/help/mcve

If the code is too long, feel free to put it in a public gist and link it in the issue: https://gist.github.com
-->

```python
from sktime.clustering.k_means import TimeSeriesKMeans
import numpy as np
import pandas as pd
data = np.random.normal(0,1, (50, 12))
data_df = pd.DataFrame(data)
TimeSeriesKMeans().fit(data)
TimeSeriesKMeans().fit(data_df) # <- This line raises the TypeError
```

The resulting error message is:

```
TypeError: X is not of a supported input data type.X must be of type np.ndarray or pd.DataFrame, found <class 'pandas.core.frame.DataFrame'>Use datatypes.check_is_mtype to check conformance with specifications.
```

**Expected behavior**
Based on the discussion in Discord: The problem is that the the error message from `check_is_mtype` is not passed on. The problem is not that the python type is wrong, but that no panel specification is complied with (e.g., `pd-multiindex` needs 2 `pd.MultiIndex` levels). The error message should be more specific about the problem, e.g., by passing on the `check_is_mtype` error messages, such as in forecasting.



Discussion:
FYI, I updated the problem description with details on `check_is_mtype`, by editing the post - please change or revert if not happy.
Based on my preliminary research on this bug, here are a few observations:
- The error message is actually arising from a failed `check_is_scitype()` against the type `Panel` rather than checking for any specific `mtype`. In fact, there are no checks for any particular `mtype`.
- The error arises from the base clusterer rather than `TimeSeriesKMeans`, which means this will be reproducible using other clusterers.
- Another interesting point that I noted was the lack of a "clusterer" label in the registry. I suppose at the very least `X_inner_mtype` should also have a clusterer tag appended in the second element.

Based on the observations, our current clusterer requires a more extensive validating method (and hence a more extensive failure message) and to make sure that they can be searched and indexed using tags, we also need to add them in the registry, wherever appropriate.

Resolving patch:
diff --git a/sktime/clustering/base.py b/sktime/clustering/base.py
--- a/sktime/clustering/base.py
+++ b/sktime/clustering/base.py
@@ -1,5 +1,5 @@
 """Base class for clustering."""
-__author__ = ["chrisholder", "TonyBagnall"]
+__author__ = ["chrisholder", "TonyBagnall", "achieveordie"]
 __all__ = ["BaseClusterer"]
 
 import time
@@ -9,7 +9,7 @@
 import pandas as pd
 
 from sktime.base import BaseEstimator
-from sktime.datatypes import check_is_scitype, convert_to
+from sktime.datatypes import check_is_scitype, convert_to, scitype_to_mtype
 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
@@ -370,6 +370,17 @@ def _check_clusterer_input(
         """
         X = self._initial_conversion(X)
 
+        ALLOWED_SCITYPES = [
+            "Panel",
+        ]
+        FORBIDDEN_MTYPES = []
+
+        mtypes_messages = []
+        for scitype in ALLOWED_SCITYPES:
+            mtypes = set(scitype_to_mtype(scitype))
+            mtypes = list(mtypes.difference(FORBIDDEN_MTYPES))
+            mtypes_messages.append(f"For {scitype} scitype: {mtypes}")
+
         X_metadata_required = [
             "n_instances",
             "has_nans",
@@ -377,15 +388,22 @@ def _check_clusterer_input(
             "is_equal_length",
         ]
         X_valid, _, X_metadata = check_is_scitype(
-            X, scitype="Panel", return_metadata=X_metadata_required
+            X, scitype=ALLOWED_SCITYPES, return_metadata=X_metadata_required
         )
         if not X_valid:
             raise TypeError(
-                f"X is not of a supported input data type."
-                f"X must be of type np.ndarray or pd.DataFrame, found {type(X)}"
-                f"Use datatypes.check_is_mtype to check conformance with "
-                f"specifications."
+                "X must be in a sktime compatible format, of scitype: "
+                f"{', '.join(ALLOWED_SCITYPES)}. "
+                "For instance a pandas.DataFrame must have a 2-level MultiIndex. "
+                "In case of numpy array, it must be "
+                "a 3D array as (num_instance, num_vars, series). "
+                "If you think X is already is an sktime supported input format, "
+                "run `sktime.datatypes.check_raise(X, MTYPE)` to diagnose the error, "
+                "where MTYPE is the string of the type specification you want for X. "
+                "Possible mtype specification strings are as follows: "
+                f"{', '.join(mtypes_messages)}"
             )
+
         n_cases = X_metadata["n_instances"]
         if n_cases < enforce_min_instances:
             raise ValueError(
diff --git a/sktime/forecasting/base/_base.py b/sktime/forecasting/base/_base.py
--- a/sktime/forecasting/base/_base.py
+++ b/sktime/forecasting/base/_base.py
@@ -1410,10 +1410,11 @@ def _check_missing(metadata, obj_name):
         ALLOWED_SCITYPES = ["Series", "Panel", "Hierarchical"]
         FORBIDDEN_MTYPES = ["numpyflat", "pd-wide"]
 
+        mtypes_messages = []
         for scitype in ALLOWED_SCITYPES:
             mtypes = set(scitype_to_mtype(scitype))
             mtypes = list(mtypes.difference(FORBIDDEN_MTYPES))
-            mtypes_msg = f'"For {scitype} scitype: {mtypes}. '
+            mtypes_messages.append(f'"For {scitype} scitype: {mtypes}. ')
 
         # checking y
         if y is not None:
@@ -1432,7 +1433,7 @@ def _check_missing(metadata, obj_name):
             )
             msg = (
                 "y must be in an sktime compatible format, "
-                "of scitype Series, Panel or Hierarchical, "
+                f"of scitype {', '.join(ALLOWED_SCITYPES)}, "
                 "for instance a pandas.DataFrame with sktime compatible time indices, "
                 "or with MultiIndex and last(-1) level an sktime compatible time index."
                 " See the forecasting tutorial examples/01_forecasting.ipynb, or"
@@ -1440,10 +1441,10 @@ def _check_missing(metadata, obj_name):
                 "If you think y is already in an sktime supported input format, "
                 "run sktime.datatypes.check_raise(y, mtype) to diagnose the error, "
                 "where mtype is the string of the type specification you want for y. "
-                "Possible mtype specification strings are as follows. "
+                "Possible mtype specification strings are as follows: "
             )
             if not y_valid:
-                raise TypeError(msg + mtypes_msg)
+                raise TypeError(msg + ", ".join(mtypes_messages))
 
             y_scitype = y_metadata["scitype"]
             self._y_mtype_last_seen = y_metadata["mtype"]
@@ -1498,7 +1499,7 @@ def _check_missing(metadata, obj_name):
                 "Possible mtype specification strings are as follows. "
             )
             if not X_valid:
-                raise TypeError(msg + mtypes_msg)
+                raise TypeError(msg + ", ".join(mtypes_messages))
 
             X_scitype = X_metadata["scitype"]
             X_requires_vectorization = X_scitype not in X_inner_scitype
@@ -1520,7 +1521,7 @@ def _check_missing(metadata, obj_name):
         if X is not None and y is not None:
             if self.get_tag("X-y-must-have-same-index"):
                 # currently, check_equal_time_index only works for Series
-                # todo: fix this so the check is general, using get_time_index
+                # TODO: fix this so the check is general, using get_time_index
                 if not self.get_tag("ignores-exogeneous-X") and X_scitype == "Series":
                     check_equal_time_index(X, y, mode="contains")
 
@@ -1528,7 +1529,7 @@ def _check_missing(metadata, obj_name):
                 raise TypeError("X and y must have the same scitype")
         # end compatibility checking X and y
 
-        # todo: add tests that :
+        # TODO: add tests that :
         #   y_inner_scitype are same as X_inner_scitype
         #   y_inner_scitype always includes "less index" scitypes
 
diff --git a/sktime/registry/_tags.py b/sktime/registry/_tags.py
--- a/sktime/registry/_tags.py
+++ b/sktime/registry/_tags.py
@@ -134,7 +134,13 @@
     ),
     (
         "X_inner_mtype",
-        ["forecaster", "transformer", "transformer-pairwise-panel", "param_est"],
+        [
+            "clusterer",
+            "forecaster",
+            "transformer",
+            "transformer-pairwise-panel",
+            "param_est",
+        ],
         (
             "list",
             [


Test patch: