Repository: sktime/sktime
Pull request #5538 (2023-11-06 20:40:14)

[MNT] [Dependabot](deps-dev): Update holidays requirement from <0.36,>=0.29 to >=0.29,<0.37
Updates the requirements on [holidays](https://github.com/vacanza/python-holidays) to permit the latest version.
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a href="https://github.com/vacanza/python-holidays/releases">holidays's releases</a>.</em></p>
<blockquote>
<h2>v0.36</h2>
<h1>Version 0.36</h1>
<p>Released November 6, 2023</p>
<ul>
<li>Add special holidays per subdivisions support (<a href="https://redirect.github.com/vacanza/python-holidays/issues/1520">#1520</a> by <a href="https://github.com/KJhellico"><code>@​KJhellico</code></a>)</li>
<li>Add special_holidays_observed support (<a href="https://redirect.github.com/vacanza/python-holidays/issues/1535">#1535</a> by <a href="https://github.com/KJhellico"><code>@​KJhellico</code></a>)</li>
<li>Refactor Australia holidays (<a href="https://redirect.github.com/vacanza/python-holidays/issues/1531">#1531</a> by <a href="https://github.com/KJhellico"><code>@​KJhellico</code></a>)</li>
<li>Refactor Isle of Man holidays (Tynwald Day) (<a href="https://redirect.github.com/vacanza/python-holidays/issues/1532">#1532</a> by <a href="https://github.com/KJhellico"><code>@​KJhellico</code></a>)</li>
<li>Add Bahamas holidays (<a href="https://redirect.github.com/vacanza/python-holidays/issues/1517">#1517</a> by <a href="https://github.com/PPsyrius"><code>@​PPsyrius</code></a>)</li>
<li>Add Timor-Leste holidays (<a href="https://redirect.github.com/vacanza/python-holidays/issues/1516">#1516</a> by <a href="https://github.com/PPsyrius"><code>@​PPsyrius</code></a>)</li>
<li>Add Tonga holidays (<a href="https://redirect.github.com/vacanza/python-holidays/issues/1534">#1534</a> by <a href="https://github.com/PPsyrius"><code>@​PPsyrius</code></a>)</li>
<li>Update Portugal holidays: add holiday categories (<a href="https://redirect.github.com/vacanza/python-holidays/issues/1529">#1529</a> by <a href="https://github.com/KJhellico"><code>@​KJhellico</code></a>)</li>
<li>Update South Africa holidays: add holiday in honor of Winning the Rugby World Cup 2023 (Dec 15, 2023) (<a href="https://redirect.github.com/vacanza/python-holidays/issues/1538">#1538</a> by <a href="https://github.com/KJhellico"><code>@​KJhellico</code></a>)</li>
<li>Update snapshots generator (<a href="https://redirect.github.com/vacanza/python-holidays/issues/1530">#1530</a> by <a href="https://github.com/KJhellico"><code>@​KJhellico</code></a>)</li>
<li>Add Thailand's Bridge Public Holiday on Dec 29, 2023 (<a href="https://redirect.github.com/vacanza/python-holidays/issues/1528">#1528</a> by <a href="https://github.com/PPsyrius"><code>@​PPsyrius</code></a>)</li>
<li>Migrate the remaining <code>special_holidays</code> to <code>StaticHolidays</code> class (<a href="https://redirect.github.com/vacanza/python-holidays/issues/1519">#1519</a> by <a href="https://github.com/PPsyrius"><code>@​PPsyrius</code></a>)</li>
<li>Remove hard-coding observed_rule for Bahamas (<a href="https://redirect.github.com/vacanza/python-holidays/issues/1527">#1527</a> by <a href="https://github.com/PPsyrius"><code>@​PPsyrius</code></a>)</li>
<li>Remove redundant gettext assignment (<a href="https://redirect.github.com/vacanza/python-holidays/issues/1533">#1533</a> by <a href="https://github.com/arkid15r"><code>@​arkid15r</code></a>)</li>
<li>Set concurrency group for CI/CD jobs (<a href="https://redirect.github.com/vacanza/python-holidays/issues/1540">#1540</a> by <a href="https://github.com/arkid15r"><code>@​arkid15r</code></a>)</li>
<li>Set pyproject.toml project version dynamically (<a href="https://redirect.github.com/vacanza/python-holidays/issues/1525">#1525</a> by <a href="https://github.com/arkid15r"><code>@​arkid15r</code></a>)</li>
<li>Use OIDC (trusted publishers) for PyPI uploads (<a href="https://redirect.github.com/vacanza/python-holidays/issues/1523">#1523</a> by <a href="https://github.com/arkid15r"><code>@​arkid15r</code></a>)</li>
<li>Use setdefault instead of hard-coding observed_rule and observed_since (<a href="https://redirect.github.com/vacanza/python-holidays/issues/1521">#1521</a> by <a href="https://github.com/BossColo"><code>@​BossColo</code></a>)</li>
</ul>
<p><strong>New Contributors</strong></p>
<ul>
<li><a href="https://github.com/BossColo"><code>@​BossColo</code></a> made their first contribution in <a href="https://redirect.github.com/vacanza/python-holidays/pull/1521">vacanza/python-holidays#1521</a></li>
</ul>
<p><strong>Full Changelog</strong>: <a href="https://github.com/vacanza/python-holidays/compare/v0.35...v0.36">https://github.com/vacanza/python-holidays/compare/v0.35...v0.36</a></p>
</blockquote>
</details>
<details>
<summary>Changelog</summary>
<p><em>Sourced from <a href="https://github.com/vacanza/python-holidays/blob/beta/CHANGES">holidays's changelog</a>.</em></p>
<blockquote>
<h1>Version 0.36</h1>
<p>Released November 6, 2023</p>
<ul>
<li>Add special holidays per subdivisions support (<a href="https://redirect.github.com/vacanza/python-holidays/issues/1520">#1520</a> by <a href="https://github.com/KJhellico"><code>@​KJhellico</code></a>)</li>
<li>Add special_holidays_observed support (<a href="https://redirect.github.com/vacanza/python-holidays/issues/1535">#1535</a> by <a href="https://github.com/KJhellico"><code>@​KJhellico</code></a>)</li>
<li>Refactor Australia holidays (<a href="https://redirect.github.com/vacanza/python-holidays/issues/1531">#1531</a> by <a href="https://github.com/KJhellico"><code>@​KJhellico</code></a>)</li>
<li>Refactor Isle of Man holidays (Tynwald Day) (<a href="https://redirect.github.com/vacanza/python-holidays/issues/1532">#1532</a> by <a href="https://github.com/KJhellico"><code>@​KJhellico</code></a>)</li>
<li>Add Bahamas holidays (<a href="https://redirect.github.com/vacanza/python-holidays/issues/1517">#1517</a> by <a href="https://github.com/PPsyrius"><code>@​PPsyrius</code></a>)</li>
<li>Add Timor-Leste holidays (<a href="https://redirect.github.com/vacanza/python-holidays/issues/1516">#1516</a> by <a href="https://github.com/PPsyrius"><code>@​PPsyrius</code></a>)</li>
<li>Add Tonga holidays (<a href="https://redirect.github.com/vacanza/python-holidays/issues/1534">#1534</a> by <a href="https://github.com/PPsyrius"><code>@​PPsyrius</code></a>)</li>
<li>Update Portugal holidays: add holiday categories (<a href="https://redirect.github.com/vacanza/python-holidays/issues/1529">#1529</a> by <a href="https://github.com/KJhellico"><code>@​KJhellico</code></a>)</li>
<li>Update South Africa holidays: add holiday in honor of Winning the Rugby World Cup 2023 (Dec 15, 2023) (<a href="https://redirect.github.com/vacanza/python-holidays/issues/1538">#1538</a> by <a href="https://github.com/KJhellico"><code>@​KJhellico</code></a>)</li>
<li>Update snapshots generator (<a href="https://redirect.github.com/vacanza/python-holidays/issues/1530">#1530</a> by <a href="https://github.com/KJhellico"><code>@​KJhellico</code></a>)</li>
<li>Add Thailand's Bridge Public Holiday on Dec 29, 2023 (<a href="https://redirect.github.com/vacanza/python-holidays/issues/1528">#1528</a> by <a href="https://github.com/PPsyrius"><code>@​PPsyrius</code></a>)</li>
<li>Migrate the remaining <code>special_holidays</code> to <code>StaticHolidays</code> class (<a href="https://redirect.github.com/vacanza/python-holidays/issues/1519">#1519</a> by <a href="https://github.com/PPsyrius"><code>@​PPsyrius</code></a>)</li>
<li>Remove hard-coding observed_rule for Bahamas (<a href="https://redirect.github.com/vacanza/python-holidays/issues/1527">#1527</a> by <a href="https://github.com/PPsyrius"><code>@​PPsyrius</code></a>)</li>
<li>Remove redundant gettext assignment (<a href="https://redirect.github.com/vacanza/python-holidays/issues/1533">#1533</a> by <a href="https://github.com/arkid15r"><code>@​arkid15r</code></a>)</li>
<li>Set concurrency group for CI/CD jobs (<a href="https://redirect.github.com/vacanza/python-holidays/issues/1540">#1540</a> by <a href="https://github.com/arkid15r"><code>@​arkid15r</code></a>)</li>
<li>Set pyproject.toml project version dynamically (<a href="https://redirect.github.com/vacanza/python-holidays/issues/1525">#1525</a> by <a href="https://github.com/arkid15r"><code>@​arkid15r</code></a>)</li>
<li>Use OIDC (trusted publishers) for PyPI uploads (<a href="https://redirect.github.com/vacanza/python-holidays/issues/1523">#1523</a> by <a href="https://github.com/arkid15r"><code>@​arkid15r</code></a>)</li>
<li>Use setdefault instead of hard-coding observed_rule and observed_since (<a href="https://redirect.github.com/vacanza/python-holidays/issues/1521">#1521</a> by <a href="https://github.com/BossColo"><code>@​BossColo</code></a>)</li>
</ul>
<h1>Version 0.35</h1>
<p>Released October 16, 2023</p>
<ul>
<li>Refactor static holidays (<a href="https://redirect.github.com/vacanza/python-holidays/issues/1509">#1509</a> by <a href="https://github.com/arkid15r"><code>@​arkid15r</code></a>)</li>
<li>Add Maldives holidays (<a href="https://redirect.github.com/vacanza/python-holidays/issues/1508">#1508</a> by <a href="https://github.com/kasya"><code>@​kasya</code></a>)</li>
<li>Add Tanzania holidays (<a href="https://redirect.github.com/vacanza/python-holidays/issues/1505">#1505</a> by <a href="https://github.com/PPsyrius"><code>@​PPsyrius</code></a>)</li>
<li>Fix estimated label for multiple holiday group entities (<a href="https://redirect.github.com/vacanza/python-holidays/issues/1512">#1512</a> by <a href="https://github.com/arkid15r"><code>@​arkid15r</code></a>)</li>
<li>Update China holidays add non-statutory holidays support, l10n, optional half-day holidays (<a href="https://redirect.github.com/vacanza/python-holidays/issues/1491">#1491</a> by <a href="https://github.com/PPsyrius"><code>@​PPsyrius</code></a>)</li>
<li>Update Thailand holidays: name change for October 13 holiday (<a href="https://redirect.github.com/vacanza/python-holidays/issues/1493">#1493</a> by <a href="https://github.com/PPsyrius"><code>@​PPsyrius</code></a>)</li>
<li>Update United States holidays: Columbus Day adjustment (<a href="https://redirect.github.com/vacanza/python-holidays/issues/1503">#1503</a> by <a href="https://github.com/KJhellico"><code>@​KJhellico</code></a>)</li>
<li>Update documentation for holiday categories usage (<a href="https://redirect.github.com/vacanza/python-holidays/issues/1506">#1506</a> by <a href="https://github.com/KJhellico"><code>@​KJhellico</code></a>)</li>
<li>Update pre-commit hooks automatic update workflow (<a href="https://redirect.github.com/vacanza/python-holidays/issues/1507">#1507</a> by <a href="https://github.com/arkid15r"><code>@​arkid15r</code></a>)</li>
<li>Update project metadata: add <a href="https://github.com/KJhellico"><code>@​KJhellico</code></a> to maintainers (<a href="https://redirect.github.com/vacanza/python-holidays/issues/1501">#1501</a> by <a href="https://github.com/arkid15r"><code>@​arkid15r</code></a>)</li>
<li>Add py.typed to MANIFEST.in includes (<a href="https://redirect.github.com/vacanza/python-holidays/issues/1498">#1498</a> by <a href="https://github.com/arkid15r"><code>@​arkid15r</code></a>)</li>
<li>Implement Tanzania <code>StaticHolidays</code>  (<a href="https://redirect.github.com/vacanza/python-holidays/issues/1513">#1513</a> by <a href="https://github.com/PPsyrius"><code>@​PPsyrius</code></a>)</li>
<li>Simplify PR template (<a href="https://redirect.github.com/vacanza/python-holidays/issues/1502">#1502</a> by <a href="https://github.com/arkid15r"><code>@​arkid15r</code></a>)</li>
<li>Standardize custom holiday classes use (<a href="https://redirect.github.com/vacanza/python-holidays/issues/1510">#1510</a> by <a href="https://github.com/arkid15r"><code>@​arkid15r</code></a>)</li>
</ul>
<h1>Version 0.34</h1>
<p>Released October 2, 2023</p>
<ul>
<li>Introduce holiday snapshots (<a href="https://redirect.github.com/vacanza/python-holidays/issues/1478">#1478</a> by <a href="https://github.com/arkid15r"><code>@​arkid15r</code></a>)</li>
</ul>
<!-- raw HTML omitted -->
</blockquote>
<p>... (truncated)</p>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a href="https://github.com/vacanza/python-holidays/commit/daa4b804a342368b21477636f7c04ca7d86088aa"><code>daa4b80</code></a> Merge pull request <a href="https://redirect.github.com/vacanza/python-holidays/issues/1542">#1542</a> from vacanza/beta</li>
<li><a href="https://github.com/vacanza/python-holidays/commit/cf3215c73624f2dedb6b7904b004c5716387b3f1"><code>cf3215c</code></a> Finalize v0.36</li>
<li><a href="https://github.com/vacanza/python-holidays/commit/208ea9769ae03832260351cc05bfe50f661712c5"><code>208ea97</code></a> Add Tonga holidays (<a href="https://redirect.github.com/vacanza/python-holidays/issues/1534">#1534</a>)</li>
<li><a href="https://github.com/vacanza/python-holidays/commit/2b1270212f4a3df47f71cb28df6557d2fc15aeb3"><code>2b12702</code></a> Update South Africa holidays: add holiday in honor of Winning the Rugby World...</li>
<li><a href="https://github.com/vacanza/python-holidays/commit/3e29bdc83e9361bdcea0c34fc01f6477a4f9036d"><code>3e29bdc</code></a> Set concurrency group for CI/CD jobs (<a href="https://redirect.github.com/vacanza/python-holidays/issues/1540">#1540</a>)</li>
<li><a href="https://github.com/vacanza/python-holidays/commit/ab495013ed87abbd1c819ccdc0700beb32c03ce3"><code>ab49501</code></a> Add special_holidays_observed support (<a href="https://redirect.github.com/vacanza/python-holidays/issues/1535">#1535</a>)</li>
<li><a href="https://github.com/vacanza/python-holidays/commit/142cbccdb1dc6cd9ca8ef7e8602df5fa5a348fce"><code>142cbcc</code></a> Refactor Isle of Man holidays (Tynwald Day) (<a href="https://redirect.github.com/vacanza/python-holidays/issues/1532">#1532</a>)</li>
<li><a href="https://github.com/vacanza/python-holidays/commit/7d737a9789650999add33c581fc42417ef0919f0"><code>7d737a9</code></a> Refactor Australia holidays (<a href="https://redirect.github.com/vacanza/python-holidays/issues/1531">#1531</a>)</li>
<li><a href="https://github.com/vacanza/python-holidays/commit/68867a99631942c10a1abd74ef0a0dde1822240d"><code>68867a9</code></a> Update snapshots generator (<a href="https://redirect.github.com/vacanza/python-holidays/issues/1530">#1530</a>)</li>
<li><a href="https://github.com/vacanza/python-holidays/commit/1ba2709b39a49f8ebff1d58ad2873e93b99677bf"><code>1ba2709</code></a> Update Portugal holidays: add holiday categories (<a href="https://redirect.github.com/vacanza/python-holidays/issues/1529">#1529</a>)</li>
<li>Additional commits viewable in <a href="https://github.com/vacanza/python-holidays/compare/v.0.29...v0.36">compare view</a></li>
</ul>
</details>
<br />


Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`.

[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)

---

<details>
<summary>Dependabot commands and options</summary>
<br />

You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it
- `@dependabot merge` will merge this PR after your CI passes on it
- `@dependabot squash and merge` will squash and merge this PR after your CI passes on it
- `@dependabot cancel merge` will cancel a previously requested merge and block automerging
- `@dependabot reopen` will reopen this PR if it is closed
- `@dependabot close` will close this PR and stop Dependabot recreating it. You can achieve the same result by closing it manually
- `@dependabot show <dependency name> ignore conditions` will show all of the ignore conditions of the specified dependency
- `@dependabot ignore this major version` will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself)
- `@dependabot ignore this minor version` will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself)
- `@dependabot ignore this dependency` will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself)


</details>


Patch:
diff --git a/.github/workflows/validate_datasets.yml b/.github/workflows/validate_datasets.yml
index ece655708..d7eabb9b3 100644
--- a/.github/workflows/validate_datasets.yml
+++ b/.github/workflows/validate_datasets.yml
@@ -1,12 +1,8 @@
 name: datasets validation
 on:
-  pull_request:
-    branches:
-      - main
-    paths:
-      - sktime/datasets/**
   schedule:
     - cron: 0 0 1 * *
+  workflow_call:
   workflow_dispatch:
 jobs:
   test_internal_data:
diff --git a/.github/workflows/validate_extras.yml b/.github/workflows/validate_extras.yml
index 1f0f82259..69d205af7 100644
--- a/.github/workflows/validate_extras.yml
+++ b/.github/workflows/validate_extras.yml
@@ -1,12 +1,8 @@
 name: extras validation
 on:
-  pull_request:
-    branches:
-      - main
-    paths:
-      - pyproject.toml
   schedule:
     - cron: 0 0 1 * *
+  workflow_call:
   workflow_dispatch:
 jobs:
   test_installations:
diff --git a/pyproject.toml b/pyproject.toml
index dd7fe360f..2c486f1e3 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -200,7 +200,7 @@ regression = [
 transformations = [
   'esig<0.10,>=0.9.7; python_version < "3.11"',
   "filterpy<1.5,>=1.4.5",
-  "holidays>=0.29,<0.36",
+  "holidays>=0.29,<0.37",
   "mne<1.6,>=1.5",
   'numba<0.59,>=0.53; python_version < "3.12"',
   "pycatch22<0.4.4,>=0.4",
diff --git a/sktime/base/_meta.py b/sktime/base/_meta.py
index 78901df76..6e49c88e6 100644
--- a/sktime/base/_meta.py
+++ b/sktime/base/_meta.py
@@ -97,29 +97,31 @@ class _HeterogenousMetaEstimator:
 
     def _get_params(self, attr, deep=True, fitted=False):
         if fitted:
-            method = "_get_fitted_params"
-            methodd = "get_fitted_params"
+            private_method = "_get_fitted_params"
+            public_method = "get_fitted_params"
             deepkw = {}
         else:
-            method = "get_params"
-            methodd = "get_params"
+            private_method = "get_params"
+            public_method = "get_params"
             deepkw = {"deep": deep}
 
-        out = getattr(super(), method)(**deepkw)
+        out = getattr(super(), private_method)(**deepkw)
         if deep and hasattr(self, attr):
             estimators = getattr(self, attr)
             estimators = [(x[0], x[1]) for x in estimators]
             out.update(estimators)
             for name, estimator in estimators:
                 # checks estimator has the method we want to call
-                cond1 = hasattr(estimator, methodd)
+                cond1 = hasattr(estimator, public_method)
                 # checks estimator is fitted if calling get_fitted_params
                 is_fitted = hasattr(estimator, "is_fitted") and estimator.is_fitted
                 # if we call get_params and not get_fitted_params, this is True
                 cond2 = not fitted or is_fitted
                 # check both conditions together
                 if cond1 and cond2:
-                    for key, value in getattr(estimator, methodd)(**deepkw).items():
+                    for key, value in getattr(estimator, public_method)(
+                        **deepkw
+                    ).items():
                         out[f"{name}__{key}"] = value
         return out
 
diff --git a/sktime/classification/base.py b/sktime/classification/base.py
index b5167b993..7d8110f49 100644
--- a/sktime/classification/base.py
+++ b/sktime/classification/base.py
@@ -29,7 +29,12 @@ import numpy as np
 import pandas as pd
 
 from sktime.base import BaseEstimator
-from sktime.datatypes import check_is_scitype, convert_to
+from sktime.datatypes import (
+    MTYPE_LIST_PANEL,
+    check_is_error_msg,
+    check_is_scitype,
+    convert_to,
+)
 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
@@ -714,16 +719,21 @@ class BaseClassifier(BaseEstimator, ABC):
             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, _, X_metadata = check_is_scitype(
+        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:
-            raise TypeError(
-                f"X is not of a supported input data type."
-                f"X must be in a supported mtype format for Panel, found {type(X)}"
-                f"Use datatypes.check_is_mtype to check conformance "
-                "with specifications."
+            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(
diff --git a/sktime/datatypes/__init__.py b/sktime/datatypes/__init__.py
index 1c90739d0..a866eb0ea 100644
--- a/sktime/datatypes/__init__.py
+++ b/sktime/datatypes/__init__.py
@@ -3,6 +3,7 @@
 __author__ = ["fkiraly"]
 
 from sktime.datatypes._check import (
+    check_is_error_msg,
     check_is_mtype,
     check_is_scitype,
     check_raise,
@@ -31,6 +32,7 @@ __all__ = [
     "ALL_TIME_SERIES_MTYPES",
     "check_is_mtype",
     "check_is_scitype",
+    "check_is_error_msg",
     "check_raise",
     "convert",
     "convert_to",
diff --git a/sktime/datatypes/_check.py b/sktime/datatypes/_check.py
index 90d9ffb5c..0853fd25d 100644
--- a/sktime/datatypes/_check.py
+++ b/sktime/datatypes/_check.py
@@ -35,6 +35,7 @@ from sktime.datatypes._proba import check_dict_Proba
 from sktime.datatypes._registry import AMBIGUOUS_MTYPES, SCITYPE_LIST, mtype_to_scitype
 from sktime.datatypes._series import check_dict_Series
 from sktime.datatypes._table import check_dict_Table
+from sktime.utils.warnings import warn
 
 # pool convert_dict-s
 check_dict = dict()
@@ -86,12 +87,14 @@ def _coerce_list_of_str(obj, var_name="obj"):
     return obj
 
 
+# todo 0.26.0: change default for msg_return_dict to "dict", update docstring
 def check_is_mtype(
     obj,
     mtype: Union[str, List[str]],
     scitype: str = None,
     return_metadata=False,
     var_name="obj",
+    msg_return_dict=None,
 ):
     """Check object for compliance with mtype specification, return metadata.
 
@@ -107,14 +110,23 @@ def check_is_mtype(
         if False, returns only "valid" return
         if True, returns all three return objects
         if str, list of str, metadata return dict is subset to keys in return_metadata
-    var_name: str, optional, default="obj" - name of input in error messages
+    var_name: str, optional, default="obj"
+        name of input in error messages
+    msg_return_dict: str, "list" or "dict", optional, default="list"
+        whether returned msg, if returned is a str, dict or list
+        if "list", msg is str if mtype is str, list of str if mtype is list
+        if "dict", msg is dict if mtype is str, list of str if mtype is list,
+        if dict, has with mtype as key and error message for mtype as value
 
     Returns
     -------
     valid: bool - whether obj is a valid object of mtype/scitype
-    msg: str or list of str - error messages if object is not valid, otherwise None
-            str if mtype is str; list of len(mtype) with message per mtype if list
-            returned only if return_metadata is True or str, list of str
+    msg: str or list/dict of str - error messages if object is not valid, otherwise None
+        list or dict type is controlled via msg_return_dict
+        if str: error message for tested mtype
+        it list: list of len(mtype) with message per mtype if list, same order as mtype
+        if dict: dict with mtype as key and error message for mtype as value
+        returned only if return_metadata is True or str, list of str
     metadata: dict - metadata about obj if valid, otherwise None
             returned only if return_metadata is True or str, list of str
         Keys populated depend on (assumed, otherwise identified) scitype of obj.
@@ -153,7 +165,24 @@ def check_is_mtype(
 
     # we loop through individual mtypes in mtype and see whether they pass the check
     #  for each check we remember whether it passed and what it returned
-    msg = []
+
+    # initialize loop variables
+    if msg_return_dict is None:
+        # todo 0.26.0: remove this warning, and change default to "dict"
+        warn(
+            "From sktime 0.26.0 onwards, msg return of check_is_mtype "
+            "will default to dict if mtype is a list. "
+            "To retain the old behaviour, set msg_return_dict='list' explicitly. "
+            "To move to the new behaviour, set msg_return_dict='dict' explicitly. "
+            "Setting msg_return_dict explicitly will silence the warning."
+        )
+        msg = []
+        msg_return_dict = "list"
+    elif msg_return_dict == "list":
+        msg = []
+    elif msg_return_dict == "dict":
+        msg = dict()
+
     found_mtype = []
     found_scitype = []
 
@@ -179,7 +208,10 @@ def check_is_mtype(
             found_scitype.append(scitype_of_m)
             final_result = res
         elif _metadata_requested(return_metadata):
-            msg.append(res[1])
+            if msg_return_dict == "list":
+                msg.append(res[1])
+            else:
+                msg[m] = res[1]
 
     # there are three options on the result of check_is_mtype:
     # a. two or more mtypes are found - this is unexpected and an error with checks
@@ -437,6 +469,46 @@ def check_is_scitype(
         return _ret(False, msg, None, return_metadata)
 
 
+def check_is_error_msg(msg, var_name="obj", allowed_msg=None, raise_exception=False):
+    """Format and possibly raise error message from check_is_mtype or check_is_scitype.
+
+    Parameters
+    ----------
+    msg: dict[str, str]
+        error message from check_is_scitype, or from check_is_mtype with dict return
+    var_name: str, optional, default="obj"
+        name of input in error messages
+    allowed_msg: str, optional, default=None
+        message component detailing allowed mtypes or scitype combinations
+    raise_exception: bool or Exception, optional, default=False
+        whether to raise exception or return error message
+        if False, returns formatted error message
+        if True, raises TypeError with formatted error message
+        if Exception, raises that Exception with formatted error message
+
+    Returns
+    -------
+    str - formatted error message
+    """
+    msg_invalid_input = (
+        f"{var_name} must be in an sktime compatible format. {allowed_msg}"
+        f" See the data format tutorial examples/AA_datatypes_and_datasets.ipynb. "
+        f"If you think the data is already in an sktime supported input format, "
+        f"run sktime.datatypes.check_raise(data, mtype) to diagnose the error, "
+        f"where mtype is the string of the type specification you want. "
+        f"Error message for checked mtypes, in format [mtype: message], as follows:"
+    )
+    for mtype, err in msg.items():
+        msg_invalid_input += f" [{mtype}: {err}] "
+
+    if raise_exception is True:
+        raise TypeError(msg_invalid_input)
+    elif raise_exception is False:
+        return msg_invalid_input
+    else:
+        raise raise_exception(msg_invalid_input)
+
+
 def scitype(obj, candidate_scitypes=SCITYPE_LIST, exclude_mtypes=AMBIGUOUS_MTYPES):
     """Infer the scitype of an object.
 
diff --git a/sktime/datatypes/_panel/_check.py b/sktime/datatypes/_panel/_check.py
index 8203aadcf..eabef8e99 100644
--- a/sktime/datatypes/_panel/_check.py
+++ b/sktime/datatypes/_panel/_check.py
@@ -233,7 +233,8 @@ def check_pdmultiindex_panel(obj, return_metadata=False, var_name="obj", panel=T
         ):
             msg = (
                 f"The (time) index of {var_name} must be sorted monotonically "
-                f"increasing, but found: {index}"
+                f"increasing. Use {var_name}.sort_index() to sort the index, or "
+                f"{var_name}.duplicated() to find duplicates."
             )
             return _ret(False, msg, None, return_metadata)
 
@@ -396,10 +397,10 @@ def is_nested_dataframe(obj, return_metadata=False, var_name="obj"):
 
     # Check instance index is unique
     if not obj.index.is_unique:
-        duplicates = obj.index[obj.index.duplicated()].unique().to_list()
         msg = (
             f"The instance index of {var_name} must be unique, "
-            f"but found duplicates: {duplicates}"
+            f"but found duplicates. Use {var_name}.duplicated() "
+            f"to find the duplicates."
         )
         return _ret(False, msg, None, return_metadata)
 
diff --git a/sktime/datatypes/_vectorize.py b/sktime/datatypes/_vectorize.py
index 5d6d73e3b..f8755d435 100644
--- a/sktime/datatypes/_vectorize.py
+++ b/sktime/datatypes/_vectorize.py
@@ -511,6 +511,7 @@ class VectorizedDF:
 
             - "None": executes loop sequentally, simple list comprehension
             - "loky", "multiprocessing" and "threading": uses ``joblib.Parallel`` loops
+            - "joblib": custom and 3rd party ``joblib`` backends, e.g., ``spark``
             - "dask": uses ``dask``, requires ``dask`` package in environment
             - "dask_lazy": same as "dask", but returns delayed object instead
 
diff --git a/sktime/forecasting/base/_base.py b/sktime/forecasting/base/_base.py
index 46d226617..136c43a8d 100644
--- a/sktime/forecasting/base/_base.py
+++ b/sktime/forecasting/base/_base.py
@@ -115,7 +115,8 @@ class BaseForecaster(BaseEstimator):
             backend to use for parallelization when broadcasting/vectorizing, one of
 
             - "None": executes loop sequentally, simple list comprehension
-            - "loky", "multiprocessing" and "threading": uses ``joblib`` ``Parallel``
+            - "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": """
@@ -124,12 +125,20 @@ class BaseForecaster(BaseEstimator):
             Valid keys depend on the value of ``backend:parallel``:
 
             - "None": no additional parameters, ``backend_params`` is ignored
-            - "loky", "multiprocessing" and "threading":
-              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:parallel``
-            - "dask": any valid keys for ``dask.compute``
-              can be passed, e.g., ``scheduler``
+            - "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``
         """,
     }
 
diff --git a/sktime/forecasting/base/_fh.py b/sktime/forecasting/base/_fh.py
index 4754cf6d5..01cbfca43 100644
--- a/sktime/forecasting/base/_fh.py
+++ b/sktime/forecasting/base/_fh.py
@@ -674,6 +674,84 @@ class ForecastingHorizon:
             relative = self.to_relative(cutoff)
             return relative - relative.to_pandas()[0]
 
+    def get_expected_pred_idx(self, y=None, cutoff=None, sort_by_time=False):
+        """Construct DataFrame Index expected in y_pred, return of _predict.
+
+        Parameters
+        ----------
+        y : pd.DataFrame, pd.Series, pd.Index, or None
+            data to compute fh relative to,
+            assumed in sktime pandas based mtype or index thereof
+        cutoff : pd.Period, pd.Timestamp, int, optional (default=None)
+            Cutoff value to use in computing resulting index.
+            If cutoff is not provided, is computed from ``y`` via ``get_cutoff``.
+        sort_by_time : bool, optional (default=False)
+            for MultiIndex returns, whether to sort by time index (level -1)
+            - If True, result Index is sorted sort by time index (level -1)
+            - If False, result Index is sorted overall
+
+        Returns
+        -------
+        fh_idx : pd.Index, expected index of y_pred returned by predict
+            assumes pandas based return mtype
+        """
+        from sktime.datatypes import get_cutoff
+
+        def _make_y_pred(y_single):
+            """Make y_pred from single instance y, used in list comprehension."""
+            cutoff = get_cutoff(y_single)
+            return pd.Index(self.to_absolute_index(cutoff))
+
+        if hasattr(y, "index"):
+            y_index = y.index
+        elif isinstance(y, pd.Index):
+            y_index = y
+            y = pd.DataFrame(index=y_index)
+        else:
+            y_index = pd.Index(y)
+            y = pd.DataFrame(index=y_index)
+
+        if cutoff is None and not isinstance(y_index, pd.MultiIndex):
+            _cutoff = get_cutoff(y)
+        else:
+            _cutoff = cutoff
+
+        if cutoff is not None or not isinstance(y_index, pd.MultiIndex):
+            fh_idx = pd.Index(self.to_absolute_index(_cutoff))
+
+        if cutoff is not None and isinstance(y_index, pd.MultiIndex):
+            y_inst_idx = y_index.droplevel(-1).unique()
+            if isinstance(y_inst_idx, pd.MultiIndex) and sort_by_time:
+                fh_list = [x + (y,) for x in y_inst_idx for y in fh_idx]
+            elif isinstance(y_inst_idx, pd.MultiIndex) and not sort_by_time:
+                fh_list = [x + (y,) for y in fh_idx for x in y_inst_idx]
+            elif sort_by_time:  # and not isinstance(y_inst_idx, pd.MultiIndex):
+                fh_list = [(x, y) for x in y_inst_idx for y in fh_idx]
+            else:  # not sort_by_time and not isinstance(y_inst_idx, pd.MultiIndex):
+                fh_list = [(x, y) for y in fh_idx for x in y_inst_idx]
+
+            fh_idx = pd.Index(fh_list)
+
+        elif isinstance(y_index, pd.MultiIndex):
+            y_inst_idx = y_index.droplevel(-1).unique()
+
+            if isinstance(y_inst_idx, pd.MultiIndex):
+                fh_list = [x + (z,) for x in y_inst_idx for z in _make_y_pred(y.loc[x])]
+            else:
+                fh_list = [(x, z) for x in y_inst_idx for z in _make_y_pred(y.loc[x])]
+
+            fh_idx = pd.Index(fh_list)
+
+            if sort_by_time:
+                fh_df = pd.DataFrame(index=fh_idx)
+                fh_idx = fh_df.sort_index(level=-1).index
+
+        # replicating index names
+        if hasattr(y_index, "names") and y_index.names is not None:
+            fh_idx.names = y_index.names
+
+        return fh_idx
+
     def __repr__(self):
         """Generate repr based on wrapped index repr."""
         class_name = self.__class__.__name__
diff --git a/sktime/forecasting/compose/_reduce.py b/sktime/forecasting/compose/_reduce.py
index cde9fe614..9f179fc26 100644
--- a/sktime/forecasting/compose/_reduce.py
+++ b/sktime/forecasting/compose/_reduce.py
@@ -901,15 +901,27 @@ class _RecursiveReducer(_Reducer):
             fh_max = fh.to_relative(self.cutoff)[-1]
 
             y_pred = np.zeros(fh_max)
+
+            # Array with input data for prediction.
             last = np.zeros((1, n_columns, window_length + fh_max))
 
             # Fill pre-allocated arrays with available data.
             last[:, 0, :window_length] = y_last
             if X is not None:
-                last[:, 1:, :window_length] = X_last.T
-                last[:, 1:, window_length:] = X.iloc[
-                    -(last.shape[2] - window_length) :, :
-                ].T
+                X_to_use = np.concatenate(
+                    [X_last.T, X.iloc[-(last.shape[2] - window_length) :, :].T], axis=1
+                )
+                if X_to_use.shape[1] < window_length + fh_max:
+                    X_to_use = np.pad(
+                        X_to_use,
+                        ((0, 0), (0, window_length + fh_max - X_to_use.shape[1])),
+                        "edge",
+                    )
+                elif X_to_use.shape[1] > window_length + fh_max:
+                    X_to_use = X_to_use[:, : window_length + fh_max]
+                # else X_to_use.shape[1] == window_length + fh_max
+                # and there are no additional steps to take
+                last[:, 1:] = X_to_use
 
             # Recursively generate predictions by iterating over forecasting horizon.
             for i in range(fh_max):
@@ -1572,68 +1584,31 @@ def _create_fcst_df(target_date, origin_df, fill=None):
     -------
     A pandas dataframe or series
     """
-    oi = origin_df.index
-    if not isinstance(oi, pd.MultiIndex):
-        if isinstance(origin_df, pd.Series):
-            if fill is None:
-                template = pd.Series(np.zeros(len(target_date)), index=target_date)
-            else:
-                template = pd.Series(fill, index=target_date)
-            template.name = origin_df.name
-            return template
-        else:
-            if fill is None:
-                template = pd.DataFrame(
-                    np.zeros((len(target_date), len(origin_df.columns))),
-                    index=target_date,
-                    columns=origin_df.columns.to_list(),
-                )
-            else:
-                template = pd.DataFrame(
-                    fill, index=target_date, columns=origin_df.columns.to_list()
-                )
-            return template
+    if not isinstance(target_date, ForecastingHorizon):
+        ix = pd.Index(target_date)
+        fh = ForecastingHorizon(ix, is_relative=False)
     else:
-        idx = origin_df.index.to_frame(index=False)
-        instance_names = idx.columns[0:-1].to_list()
-        time_names = idx.columns[-1]
-        idx = idx[instance_names].drop_duplicates()
-
-        timeframe = pd.DataFrame(target_date, columns=[time_names])
-        target_frame = idx.merge(timeframe, how="cross")
-        if hasattr(target_date, "freq"):
-            freq_inferred = target_date.freq
-            mi = (
-                target_frame.groupby(instance_names, as_index=True)
-                .apply(
-                    lambda df: df.drop(instance_names, axis=1)
-                    .set_index(time_names)
-                    .asfreq(freq_inferred)
-                )
-                .index
-            )
-        else:
-            mi = (
-                target_frame.groupby(instance_names, as_index=True)
-                .apply(lambda df: df.drop(instance_names, axis=1).set_index(time_names))
-                .index
-            )
+        fh = target_date.to_absolute()
 
-        if fill is None:
-            template = pd.DataFrame(
-                np.zeros((len(target_date) * idx.shape[0], len(origin_df.columns))),
-                index=mi,
-                columns=origin_df.columns.to_list(),
-            )
-        else:
-            template = pd.DataFrame(
-                fill,
-                index=mi,
-                columns=origin_df.columns.to_list(),
-            )
+    index = fh.get_expected_pred_idx(origin_df)
+
+    if isinstance(origin_df, pd.Series):
+        columns = [origin_df.name]
+    else:
+        columns = origin_df.columns.to_list()
+
+    if fill is None:
+        values = 0
+    else:
+        values = fill
+
+    res = pd.DataFrame(values, index=index, columns=columns)
+
+    if isinstance(origin_df, pd.Series) and not isinstance(index, pd.MultiIndex):
+        res = res.iloc[:, 0]
+        res.name = origin_df.name
 
-        template = template.astype(origin_df.dtypes.to_dict())
-        return template
+    return res
 
 
 def _coerce_col_str(X):
diff --git a/sktime/forecasting/model_evaluation/_functions.py b/sktime/forecasting/model_evaluation/_functions.py
index 155caabd8..613360937 100644
--- a/sktime/forecasting/model_evaluation/_functions.py
+++ b/sktime/forecasting/model_evaluation/_functions.py
@@ -413,6 +413,7 @@ def evaluate(
 
         - "None": executes loop sequentally, simple list comprehension
         - "loky", "multiprocessing" and "threading": uses ``joblib.Parallel`` loops
+        - "joblib": custom and 3rd party ``joblib`` backends, e.g., ``spark``
         - "dask": uses ``dask``, requires ``dask`` package in environment
         - "dask_lazy": same as "dask",
           but changes the return to (lazy) ``dask.dataframe.DataFrame``.
@@ -435,12 +436,18 @@ def evaluate(
         Valid keys depend on the value of ``backend``:
 
         - "None": no additional parameters, ``backend_params`` is ignored
-        - "loky", "multiprocessing" and "threading":
-            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``
+        - "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``
+          e.g., ``scheduler``
 
     Returns
     -------
diff --git a/sktime/forecasting/model_selection/_tune.py b/sktime/forecasting/model_selection/_tune.py
index 7b70b6d1e..fd0fcd585 100644
--- a/sktime/forecasting/model_selection/_tune.py
+++ b/sktime/forecasting/model_selection/_tune.py
@@ -464,6 +464,7 @@ class ForecastingGridSearchCV(BaseGridSearch):
 
         - "None": executes loop sequentally, simple list comprehension
         - "loky", "multiprocessing" and "threading": uses ``joblib.Parallel`` loops
+        - "joblib": custom and 3rd party ``joblib`` backends, e.g., ``spark``
         - "dask": uses ``dask``, requires ``dask`` package in environment
 
         Recommendation: Use "dask" or "loky" for parallel evaluate.
@@ -498,10 +499,16 @@ class ForecastingGridSearchCV(BaseGridSearch):
         Valid keys depend on the value of ``backend``:
 
         - "None": no additional parameters, ``backend_params`` is ignored
-        - "loky", "multiprocessing" and "threading":
-            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``
+        - "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``
 
     Attributes
@@ -796,6 +803,7 @@ class ForecastingRandomizedSearchCV(BaseGridSearch):
 
         - "None": executes loop sequentally, simple list comprehension
         - "loky", "multiprocessing" and "threading": uses ``joblib.Parallel`` loops
+        - "joblib": custom and 3rd party ``joblib`` backends, e.g., ``spark``
         - "dask": uses ``dask``, requires ``dask`` package in environment
 
         Recommendation: Use "dask" or "loky" for parallel evaluate.
@@ -830,10 +838,16 @@ class ForecastingRandomizedSearchCV(BaseGridSearch):
         Valid keys depend on the value of ``backend``:
 
         - "None": no additional parameters, ``backend_params`` is ignored
-        - "loky", "multiprocessing" and "threading":
-            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``
+        - "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``
 
     Attributes
@@ -1055,6 +1069,7 @@ class ForecastingSkoptSearchCV(BaseGridSearch):
 
         - "None": executes loop sequentally, simple list comprehension
         - "loky", "multiprocessing" and "threading": uses ``joblib.Parallel`` loops
+        - "joblib": custom and 3rd party ``joblib`` backends, e.g., ``spark``
         - "dask": uses ``dask``, requires ``dask`` package in environment
 
         Recommendation: Use "dask" or "loky" for parallel evaluate.
@@ -1085,10 +1100,16 @@ class ForecastingSkoptSearchCV(BaseGridSearch):
         Valid keys depend on the value of ``backend``:
 
         - "None": no additional parameters, ``backend_params`` is ignored
-        - "loky", "multiprocessing" and "threading":
-            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``
+        - "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``
 
     Attributes
diff --git a/sktime/regression/base.py b/sktime/regression/base.py
index 1ac7cdad2..903cc0cf7 100644
--- a/sktime/regression/base.py
+++ b/sktime/regression/base.py
@@ -29,7 +29,12 @@ import numpy as np
 import pandas as pd
 
 from sktime.base import BaseEstimator
-from sktime.datatypes import check_is_scitype, convert_to
+from sktime.datatypes import (
+    MTYPE_LIST_PANEL,
+    check_is_error_msg,
+    check_is_scitype,
+    convert_to,
+)
 from sktime.utils.sklearn import is_sklearn_transformer
 from sktime.utils.validation import check_n_jobs
 from sktime.utils.warnings import warn
@@ -411,15 +416,21 @@ def _check_regressor_input(
         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, _, X_metadata = check_is_scitype(
+    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:
-        raise TypeError(
-            f"X is not of a supported input data type."
-            f"X must be in a supported mtype format for Panel, found {type(X)}"
-            f"Use datatypes.check_is_mtype to check conformance with specifications."
+        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(
diff --git a/sktime/transformations/base.py b/sktime/transformations/base.py
index 430037f06..126ce81f4 100644
--- a/sktime/transformations/base.py
+++ b/sktime/transformations/base.py
@@ -56,6 +56,7 @@ import pandas as pd
 from sktime.base import BaseEstimator
 from sktime.datatypes import (
     VectorizedDF,
+    check_is_error_msg,
     check_is_mtype,
     check_is_scitype,
     convert,
@@ -165,7 +166,8 @@ class BaseTransformer(BaseEstimator):
             backend to use for parallelization when broadcasting/vectorizing, one of
 
             - "None": executes loop sequentally, simple list comprehension
-            - "loky", "multiprocessing" and "threading": uses ``joblib`` ``Parallel``
+            - "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": """
@@ -174,10 +176,18 @@ class BaseTransformer(BaseEstimator):
             Valid keys depend on the value of ``backend:parallel``:
 
             - "None": no additional parameters, ``backend_params`` is ignored
-            - "loky", "multiprocessing" and "threading":
-              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:parallel``
+            - "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``
         """,
@@ -983,32 +993,25 @@ class BaseTransformer(BaseEstimator):
             var_name="X",
         )
 
-        msg_invalid_input = (
-            f"must be in an sktime compatible format, "
-            f"of scitype Series, Panel or Hierarchical, "
-            f"for instance a pandas.DataFrame with sktime compatible time indices, "
-            f"or with MultiIndex and last(-1) level an sktime compatible time index. "
-            f"Allowed compatible mtype format specifications are: {ALLOWED_MTYPES} ."
-            # f"See the transformers tutorial examples/05_transformers.ipynb, or"
-            f" See the data format tutorial examples/AA_datatypes_and_datasets.ipynb. "
-            f"If you think the data is already in an sktime supported input format, "
-            f"run sktime.datatypes.check_raise(data, mtype) to diagnose the error, "
-            f"where mtype is the string of the type specification you want. "
-            f"Error message for checked mtypes, in format [mtype: message], as follows:"
-        )
-        if not X_valid:
-            for mtype, err in msg.items():
-                msg_invalid_input += f" [{mtype}: {err}] "
-            raise TypeError("X " + msg_invalid_input)
-
         X_scitype = X_metadata["scitype"]
         X_mtype = X_metadata["mtype"]
         # remember these for potential back-conversion (in transform etc)
         metadata["_X_mtype_last_seen"] = X_mtype
         metadata["_X_input_scitype"] = X_scitype
 
-        if X_mtype not in ALLOWED_MTYPES:
-            raise TypeError("X " + msg_invalid_input)
+        # raise informative error message if X is in wrong format
+        allowed_msg = (
+            f"Allowed scitypes for X in transformations are "
+            f"Series, Panel or Hierarchical, "
+            f"for instance a pandas.DataFrame with sktime compatible time indices, "
+            f"or with MultiIndex and last(-1) level an sktime compatible time index. "
+            f"Allowed compatible mtype format specifications are: {ALLOWED_MTYPES} ."
+        )
+        if not X_valid or X_mtype not in ALLOWED_MTYPES:
+            msg = {k: v for k, v in msg.items() if k in ALLOWED_MTYPES}
+            check_is_error_msg(
+                msg, var_name="X", allowed_msg=allowed_msg, raise_exception=True
+            )
 
         if X_scitype in X_inner_scitype:
             case = "case 1: scitype supported"
@@ -1041,14 +1044,20 @@ class BaseTransformer(BaseEstimator):
             y_valid, msg, y_metadata = check_is_scitype(
                 y, scitype=y_possible_scitypes, return_metadata=[], var_name="y"
             )
-            if not y_valid:
-                for mtype, err in msg.items():
-                    msg_invalid_input += f" [{mtype}: {err}] "
-                raise TypeError("y " + msg_invalid_input)
-
             y_scitype = y_metadata["scitype"]
             y_mtype = y_metadata["mtype"]
 
+            # raise informative error message if y is is in wrong format
+            if not y_valid:
+                allowed_msg = (
+                    f"Allowed scitypes for y in transformations depend on X passed. "
+                    f"Passed X scitype was {X_scitype}, "
+                    f"so allowed scitypes for y are {y_possible_scitypes}. "
+                )
+                check_is_error_msg(
+                    msg, var_name="y", allowed_msg=allowed_msg, raise_exception=True
+                )
+
         else:
             # y_scitype is used below - set to None if y is None
             y_scitype = None
diff --git a/sktime/utils/parallel.py b/sktime/utils/parallel.py
index ab839145c..6a8fb4090 100644
--- a/sktime/utils/parallel.py
+++ b/sktime/utils/parallel.py
@@ -38,6 +38,7 @@ def parallelize(fun, iter, meta=None, backend=None, backend_params=None):
 
         - "None": executes loop sequentally, simple list comprehension
         - "loky", "multiprocessing" and "threading": uses ``joblib`` ``Parallel`` loops
+        - "joblib": custom and 3rd party ``joblib`` backends, e.g., ``spark``
         - "dask": uses ``dask``, requires ``dask`` package in environment
         - "dask_lazy": same as ``"dask"``, but returns delayed object instead of list
 
@@ -46,9 +47,16 @@ def parallelize(fun, iter, meta=None, backend=None, backend_params=None):
         Valid keys depend on the value of ``backend``:
 
         - "None": no additional parameters, ``backend_params`` is ignored
-        - "loky", "multiprocessing" and "threading":
+        - "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``
+          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``
     """
     if meta is None:
@@ -72,6 +80,7 @@ backend_dict = {
     "loky": "joblib",
     "multiprocessing": "joblib",
     "threading": "joblib",
+    "joblib": "joblib",
     "dask": "dask",
     "dask_lazy": "dask",
 }
@@ -92,7 +101,29 @@ def _parallelize_joblib(fun, iter, meta, backend, backend_params):
     from joblib import Parallel, delayed
 
     par_params = backend_params.copy()
-    par_params["backend"] = backend
+    if "backend" not in par_params:
+        # if user selects custom joblib backend but does not specify backend explicitly,
+        # raise a ValueError
+        if backend == "joblib":
+            raise ValueError(
+                '"joblib" was selected as first layer parallelization backend, '
+                "but no backend string was "
+                '"passed in the backend parameters dict, e.g., "spark". '
+                "Please specify a backend to joblib as a key-value pair "
+                "in the backend_params arg or the backend:parallel:params config "
+                'when using "joblib". '
+                'For clarity, "joblib" should only be used for two-layer '
+                "backend dispatch, where the first layer is joblib, "
+                "and the second layer is a custom backend of joblib, e.g., spark."
+                "For first-party joblib backends, please use the backend string "
+                'of sktime directly, e.g., by specifying "multiprocessing" or "loky".'
+            )
+        # in all other cases, we ensure the backend parameter is one of
+        # "loky", "multiprocessing" or "threading", as passed via backend
+        else:
+            par_params["backend"] = backend
+    elif backend != "joblib":
+        par_params["backend"] = backend
 
     if "n_jobs" not in par_params:
         par_params["n_jobs"] = -1


Test patch:
diff --git a/.github/workflows/test_all.yml b/.github/workflows/test_all.yml
index 2afe9d65a..e00cd1265 100644
--- a/.github/workflows/test_all.yml
+++ b/.github/workflows/test_all.yml
@@ -47,7 +47,7 @@ jobs:
           python-version-identifier: ${{ matrix.python-version }}
           sub-sample-estimators: "False"
           test-affected-estimators: "False"
-  test_components:
+  test_components_with_extras:
     needs: code_quality
     name: test module
     strategy:
@@ -84,3 +84,39 @@ jobs:
           python-version-identifier: ${{ matrix.python-version }}
           sub-sample-estimators: "False"
           test-affected-estimators: "False"
+  test_components_without_extras:
+    needs: code_quality
+    name: test all components without extras
+    strategy:
+      fail-fast: false
+      matrix:
+        python-version:
+          - "3.8"
+          - "3.9"
+          - "3.10"
+          - "3.11"
+          - "3.12"
+        operating-system:
+          - macos-latest
+          - ubuntu-latest
+          - windows-latest
+    runs-on: ${{ matrix.operating-system }}
+    steps:
+      - name: checkout pull request branch
+        uses: actions/checkout@v4
+      - name: update local git tracking reference
+        run: git remote set-branches origin main
+      - name: update local shallow clone
+        run: git fetch --depth 1
+      - name: create python virtual environment
+        uses: actions/setup-python@v4
+        with:
+          python-version: ${{ matrix.python-version }}
+      - name: install core, test and all soft dependencies
+        run: python3 -m pip install .[all_extras_pandas2,tests]
+      - name: run unit tests
+        run: python3 -m pytest sktime --ignore sktime/alignment --ignore sktime/annotation --ignore sktime/classification --ignore sktime/clustering --ignore sktime/datasets --ignore sktime/forecasting --ignore sktime/networks --ignore sktime/param_est --ignore sktime/regression --ignore sktime/transformations --matrixdesign False --only_changed_modules False
+      - name: test coverage
+        uses: codecov/codecov-action@v3
+        with:
+          flags: ${{ matrix.python-version }},components_without_extras
diff --git a/.github/workflows/test_code_quality.yml b/.github/workflows/test_code_quality.yml
index 20b2eb76b..8e2b60ee1 100644
--- a/.github/workflows/test_code_quality.yml
+++ b/.github/workflows/test_code_quality.yml
@@ -24,6 +24,28 @@ jobs:
       - name: check missing __init__ files
         run: build_tools/fail_on_missing_init_files.sh
         shell: bash
+  detect:
+    name: detect changes
+    runs-on: ubuntu-latest
+    permissions:
+      pull-requests: read
+    outputs:
+      components: ${{ steps.filter.outputs.changes }}
+    steps:
+      - uses: dorny/paths-filter@v2
+        id: filter
+        with:
+          filters: |
+            datasets:
+              - sktime/base/**
+              - sktime/datasets/**
+            pyproject:
+              - pyproject.toml
+  test_install_extras:
+    needs: detect
+    name: validate extra installs
+    if: ${{ needs.detect.outputs.pyproject == 'true' }}
+    uses: ./.github/workflows/validate_extras.yml
   unit_test_base:
     needs: code_quality
     name: run unit tests for base framework
@@ -32,3 +54,8 @@ jobs:
     needs: code_quality
     name: run unit tests for individual components
     uses: ./.github/workflows/test_components.yml
+  unit_test_datasets:
+    needs: detect
+    name: run unit tests for datasets
+    if: ${{ needs.detect.outputs.datasets == 'true' }}
+    uses: ./.github/workflows/validate_datasets.yml
diff --git a/.github/workflows/test_components.yml b/.github/workflows/test_components.yml
index ea91af5b9..417c34f03 100644
--- a/.github/workflows/test_components.yml
+++ b/.github/workflows/test_components.yml
@@ -15,22 +15,31 @@ jobs:
         with:
           filters: |
             alignment:
+              - sktime/base/**
               - sktime/alignment/**
             annotation:
+              - sktime/base/**
               - sktime/annotation/**
             classification:
+              - sktime/base/**
               - sktime/classification/**
             clustering:
+              - sktime/base/**
               - sktime/clustering/**
             forecasting:
+              - sktime/base/**
               - sktime/forecasting/**
             networks:
+              - sktime/base/**
               - sktime/networks/**
             param_est:
+              - sktime/base/**
               - sktime/param_est/**
             regression:
+              - sktime/base/**
               - sktime/regression/**
             transformations:
+              - sktime/base/**
               - sktime/transformations/**
   test:
     needs: detect
@@ -48,7 +57,7 @@ jobs:
           - macos-latest
           - ubuntu-latest
           - windows-latest
-        sktime-component: ${{ fromJSON(needs.detect.outputs.components) }}
+        sktime-component: ${{ fromJSON(needs.detect_components_with_extras.outputs.components) }}
     runs-on: ${{ matrix.operating-system }}
     steps:
       - name: checkout pull request branch
@@ -60,3 +69,67 @@ jobs:
           python-version-identifier: ${{ matrix.python-version }}
           sub-sample-estimators: "True"
           test-affected-estimators: "True"
+  detect_components_without_extras:
+    name: check for changes in individual components without extras
+    runs-on: ubuntu-latest
+    permissions:
+      pull-requests: read
+    outputs:
+      components_without_extras: ${{ steps.filter.outputs.components_without_extras }}
+    steps:
+      - uses: dorny/paths-filter@v2
+        id: filter
+        with:
+          filters: |
+            components_without_extras:
+              - sktime/base/**
+              - sktime/_contrib/**
+              - sktime/benchmarking/**
+              - sktime/datatypes/**
+              - sktime/distances/**
+              - sktime/dists_kernels/**
+              - sktime/performance_metrics/**
+              - sktime/pipeline/**
+              - sktime/proba/**
+              - sktime/registry/**
+              - sktime/series_as_features/**
+              - sktime/split/**
+              - sktime/tests/**
+              - sktime/utils/**
+  test_components_without_extras:
+    needs: detect_components_without_extras
+    name: test all components without extras
+    if: ${{ needs.detect.outputs.components_without_extras == 'true' }}
+    strategy:
+      fail-fast: false
+      matrix:
+        python-version:
+          - "3.8"
+          - "3.9"
+          - "3.10"
+          - "3.11"
+          - "3.12"
+        operating-system:
+          - macos-latest
+          - ubuntu-latest
+          - windows-latest
+    runs-on: ${{ matrix.operating-system }}
+    steps:
+      - name: checkout pull request branch
+        uses: actions/checkout@v4
+      - name: update local git tracking reference
+        run: git remote set-branches origin main
+      - name: update local shallow clone
+        run: git fetch --depth 1
+      - name: create python virtual environment
+        uses: actions/setup-python@v4
+        with:
+          python-version: ${{ matrix.python-version }}
+      - name: install core, test and all soft dependencies
+        run: python3 -m pip install .[all_extras_pandas2,tests]
+      - name: run unit tests
+        run: python3 -m pytest sktime --ignore sktime/alignment --ignore sktime/annotation --ignore sktime/classification --ignore sktime/clustering --ignore sktime/datasets --ignore sktime/forecasting --ignore sktime/networks --ignore sktime/param_est --ignore sktime/regression --ignore sktime/transformations --matrixdesign True --only_changed_modules True
+      - name: test coverage
+        uses: codecov/codecov-action@v3
+        with:
+          flags: ${{ matrix.python-version }},components_without_extras
diff --git a/sktime/classification/tests/test_base.py b/sktime/classification/tests/test_base.py
index 89dc59ad3..9713be6f0 100644
--- a/sktime/classification/tests/test_base.py
+++ b/sktime/classification/tests/test_base.py
@@ -287,9 +287,7 @@ def test__check_classifier_input():
         _check_classifier_input(test_X5, test_y1)
     # 4. Test incorrect data type: y is a List
     test_y3 = [1, 2, 3, 4, 5]
-    with pytest.raises(
-        TypeError, match=r".*X is not of a supported input data " r"type.*"
-    ):
+    with pytest.raises(TypeError, match="must be in an sktime compatible format"):
         _check_classifier_input(test_X1, test_y3)
     # 5. Test incorrect: too few cases or too short a series
     with pytest.raises(ValueError, match=r".*Minimum number of cases required*."):
diff --git a/sktime/forecasting/base/tests/test_fh.py b/sktime/forecasting/base/tests/test_fh.py
index 873313eda..5e5ddd9ef 100644
--- a/sktime/forecasting/base/tests/test_fh.py
+++ b/sktime/forecasting/base/tests/test_fh.py
@@ -728,3 +728,49 @@ def test_empty_range_in_fh():
     """Test when ``range`` has zero length."""
     empty_range = ForecastingHorizon(values=range(-5))
     assert (empty_range == ForecastingHorizon(values=[])).all()
+
+
+def test_fh_expected_pred():
+    """Test for expected prediction index method."""
+    fh = ForecastingHorizon([1, 2, 3])
+    y_pred_idx = fh.get_expected_pred_idx(pd.Index([2, 3, 4]))
+
+    assert y_pred_idx.equals(pd.Index([5, 6, 7]))
+
+    y_df = pd.DataFrame([1, 2, 3], index=[2, 3, 4])
+    y_pred_idx = fh.get_expected_pred_idx(y_df)
+
+    assert y_pred_idx.equals(pd.Index([5, 6, 7]))
+
+    # pd.MultiIndex case, 2 levels
+    idx = pd.MultiIndex.from_tuples([("a", 3), ("a", 5), ("b", 4), ("b", 5), ("b", 6)])
+    y_pred_idx = fh.get_expected_pred_idx(idx)
+
+    y_pred_idx_expected = pd.MultiIndex.from_tuples(
+        [("a", 6), ("a", 7), ("a", 8), ("b", 7), ("b", 8), ("b", 9)]
+    )
+    assert y_pred_idx.equals(y_pred_idx_expected)
+
+    y_pred_idx = fh.get_expected_pred_idx(idx, sort_by_time=True)
+    y_pred_idx_expected = pd.MultiIndex.from_tuples(
+        [("a", 6), ("a", 7), ("b", 7), ("a", 8), ("b", 8), ("b", 9)]
+    )
+    assert y_pred_idx.equals(y_pred_idx_expected)
+
+    # pd.MultiIndex case, 3 levels
+    idx = pd.MultiIndex.from_tuples(
+        [("a", 3, 4), ("a", 3, 5), ("b", 5, 4), ("b", 5, 5), ("b", 5, 6)]
+    )
+    y_pred_idx = fh.get_expected_pred_idx(idx)
+
+    y_pred_idx_expected = pd.MultiIndex.from_tuples(
+        [("a", 3, 6), ("a", 3, 7), ("a", 3, 8), ("b", 5, 7), ("b", 5, 8), ("b", 5, 9)]
+    )
+    assert y_pred_idx.equals(y_pred_idx_expected)
+
+    y_pred_idx = fh.get_expected_pred_idx(idx, sort_by_time=True)
+
+    y_pred_idx_expected = pd.MultiIndex.from_tuples(
+        [("a", 3, 6), ("a", 3, 7), ("b", 5, 7), ("a", 3, 8), ("b", 5, 8), ("b", 5, 9)]
+    )
+    assert y_pred_idx.equals(y_pred_idx_expected)
diff --git a/sktime/forecasting/compose/tests/test_reduce.py b/sktime/forecasting/compose/tests/test_reduce.py
index 13758a2e7..26b980e4f 100644
--- a/sktime/forecasting/compose/tests/test_reduce.py
+++ b/sktime/forecasting/compose/tests/test_reduce.py
@@ -588,3 +588,28 @@ def test_direct_vs_recursive():
     assert pred_dir_max.head(1).equals(pred_rec_max.head(1))
     assert pred_dir_max.head(1).equals(pred_rec_spec.head(1))
     assert not pred_dir_max.head(1).equals(pred_dir_spec.head(1))
+
+
+def test_recursive_reducer_X_not_fit_to_fh():
+    """Test recursive reducer with X that do not fit the fh.
+
+    I.e., either X is longer or smaller than max_fh
+    """
+    y = load_airline()
+    y_train, y_test = temporal_train_test_split(y)
+    X_train = y_train
+    X_test = y_test
+
+    forecaster = make_reduction(
+        LinearRegression(), window_length=2, strategy="recursive"
+    )
+    forecaster.fit(y_train, X_train)
+
+    pred1 = forecaster.predict(X=X_test[:1], fh=[1, 2, 3])
+    assert pred1.shape == (3,)
+    pred2 = forecaster.predict(X=X_test[:2], fh=[1, 2, 3])
+    assert pred2.shape == (3,)
+    pred3 = forecaster.predict(X=X_test[:3], fh=[1, 2, 3])
+    assert pred3.shape == (3,)
+    pred4 = forecaster.predict(X=X_test, fh=[1])
+    assert pred4.shape == (1,)
diff --git a/sktime/forecasting/compose/tests/test_reduce_global.py b/sktime/forecasting/compose/tests/test_reduce_global.py
index c332178dd..e2a78d278 100644
--- a/sktime/forecasting/compose/tests/test_reduce_global.py
+++ b/sktime/forecasting/compose/tests/test_reduce_global.py
@@ -106,6 +106,7 @@ def y_dict():
     # Create integer index data
     y_numeric = y_train.copy()
     y_numeric.index = pd.to_numeric(y_numeric.index)
+    y_numeric.index.names = [None]  # setting None to cover "no index name" case
     y_dict["y_numeric"] = y_numeric
 
     return y_dict
@@ -139,7 +140,7 @@ def check_eval(test_input, expected):
         ),
         (
             "y_train",
-            [None],
+            ["Period"],
         ),
         (
             "y_numeric",
@@ -186,7 +187,7 @@ def test_recursive_reduction(y, index_names, y_dict):
         ),
         (
             "y_train",
-            [None],
+            ["Period"],
         ),
         (
             "y_numeric",
@@ -232,7 +233,7 @@ def test_direct_reduction(y, index_names, y_dict):
         ),
         (
             "y_train",
-            [None],
+            ["Period"],
         ),
         (
             "y_numeric",