Repository: sktime/sktime
Pull request #5566 (2023-11-21 20:45:00)

[MNT] [Dependabot](deps-dev): Update sphinx-gallery requirement from <0.15.0 to <0.16.0
Updates the requirements on [sphinx-gallery](https://github.com/sphinx-gallery/sphinx-gallery) to permit the latest version.
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a href="https://github.com/sphinx-gallery/sphinx-gallery/releases">sphinx-gallery's releases</a>.</em></p>
<blockquote>
<h2>Release v0.15.0</h2>
<p><a href="https://github.com/sphinx-gallery/sphinx-gallery/compare/v0.14.0...v0.15.0">Full Changelog</a></p>
<p>Support for Python 3.7 dropped in this release. Requirement is now Python &gt;=3.8.
Pillow added as a dependency.</p>
<p><strong>Implemented enhancements:</strong></p>
<ul>
<li>ENH: Improve logging visibility of errors and filenames <a href="https://redirect.github.com/sphinx-gallery/sphinx-gallery/pull/1225">#1225</a> (<a href="https://github.com/larsoner">larsoner</a>)</li>
<li>ENH: Improve API usage graph <a href="https://redirect.github.com/sphinx-gallery/sphinx-gallery/pull/1203">#1203</a> (<a href="https://github.com/larsoner">larsoner</a>)</li>
<li>ENH: Always write sg_execution_times and make DataTable <a href="https://redirect.github.com/sphinx-gallery/sphinx-gallery/pull/1198">#1198</a> (<a href="https://github.com/larsoner">larsoner</a>)</li>
<li>ENH: Write all computation times <a href="https://redirect.github.com/sphinx-gallery/sphinx-gallery/pull/1197">#1197</a> (<a href="https://github.com/larsoner">larsoner</a>)</li>
<li>ENH: Support source files in any language <a href="https://redirect.github.com/sphinx-gallery/sphinx-gallery/pull/1192">#1192</a> (<a href="https://github.com/speth">speth</a>)</li>
<li>FEA Add examples recommender system <a href="https://redirect.github.com/sphinx-gallery/sphinx-gallery/pull/1125">#1125</a> (<a href="https://github.com/ArturoAmorQ">ArturoAmorQ</a>)</li>
</ul>
<p><strong>Fixed bugs:</strong></p>
<ul>
<li>FIX Copy JupyterLite contents early so it runs before jupyterlite_sphinx build-finished <a href="https://redirect.github.com/sphinx-gallery/sphinx-gallery/pull/1213">#1213</a> (<a href="https://github.com/lesteve">lesteve</a>)</li>
<li>BUG: Fix bug with orphan sg_api_usage <a href="https://redirect.github.com/sphinx-gallery/sphinx-gallery/pull/1207">#1207</a> (<a href="https://github.com/larsoner">larsoner</a>)</li>
<li>MAINT Fix check for mismatched &quot;ignore&quot; blocks <a href="https://redirect.github.com/sphinx-gallery/sphinx-gallery/pull/1193">#1193</a> (<a href="https://github.com/speth">speth</a>)</li>
<li>Avoid importing new modules in backrefs <a href="https://redirect.github.com/sphinx-gallery/sphinx-gallery/pull/1177">#1177</a> (<a href="https://github.com/aganders3">aganders3</a>)</li>
</ul>
<p><strong>Documentation</strong></p>
<ul>
<li>DOC Put configuration list under headings <a href="https://redirect.github.com/sphinx-gallery/sphinx-gallery/pull/1230">#1230</a> (<a href="https://github.com/lucyleeow">lucyleeow</a>)</li>
<li>DOC: contributing guide <a href="https://redirect.github.com/sphinx-gallery/sphinx-gallery/pull/1223">#1223</a> (<a href="https://github.com/story645">story645</a>)</li>
<li>DOC Note support for python 3.7 dropped in release notes <a href="https://redirect.github.com/sphinx-gallery/sphinx-gallery/pull/1199">#1199</a> (<a href="https://github.com/lucyleeow">lucyleeow</a>)</li>
</ul>
<p><strong>Project maintenance</strong></p>
<ul>
<li>[pre-commit.ci] pre-commit autoupdate <a href="https://redirect.github.com/sphinx-gallery/sphinx-gallery/pull/1231">#1231</a> (<a href="https://github.com/apps/pre-commit-ci">pre-commit-ci[bot]</a>)</li>
<li>MAINT Add <code>extras_require</code> in <code>setup.py</code> <a href="https://redirect.github.com/sphinx-gallery/sphinx-gallery/pull/1229">#1229</a> (<a href="https://github.com/lucyleeow">lucyleeow</a>)</li>
<li>[pre-commit.ci] pre-commit autoupdate <a href="https://redirect.github.com/sphinx-gallery/sphinx-gallery/pull/1227">#1227</a> (<a href="https://github.com/apps/pre-commit-ci">pre-commit-ci[bot]</a>)</li>
<li>[pre-commit.ci] pre-commit autoupdate <a href="https://redirect.github.com/sphinx-gallery/sphinx-gallery/pull/1224">#1224</a> (<a href="https://github.com/apps/pre-commit-ci">pre-commit-ci[bot]</a>)</li>
<li>[pre-commit.ci] pre-commit autoupdate <a href="https://redirect.github.com/sphinx-gallery/sphinx-gallery/pull/1219">#1219</a> (<a href="https://github.com/apps/pre-commit-ci">pre-commit-ci[bot]</a>)</li>
<li>MAINT: pydata-sphinx-theme <a href="https://redirect.github.com/sphinx-gallery/sphinx-gallery/pull/1218">#1218</a> (<a href="https://github.com/larsoner">larsoner</a>)</li>
<li>MAINT: Improve CircleCI time <a href="https://redirect.github.com/sphinx-gallery/sphinx-gallery/pull/1216">#1216</a> (<a href="https://github.com/larsoner">larsoner</a>)</li>
<li>[pre-commit.ci] pre-commit autoupdate <a href="https://redirect.github.com/sphinx-gallery/sphinx-gallery/pull/1215">#1215</a> (<a href="https://github.com/apps/pre-commit-ci">pre-commit-ci[bot]</a>)</li>
<li>MAINT: Move to GHA <a href="https://redirect.github.com/sphinx-gallery/sphinx-gallery/pull/1214">#1214</a> (<a href="https://github.com/larsoner">larsoner</a>)</li>
<li>[pre-commit.ci] pre-commit autoupdate <a href="https://redirect.github.com/sphinx-gallery/sphinx-gallery/pull/1206">#1206</a> (<a href="https://github.com/apps/pre-commit-ci">pre-commit-ci[bot]</a>)</li>
<li>[pre-commit.ci] pre-commit autoupdate <a href="https://redirect.github.com/sphinx-gallery/sphinx-gallery/pull/1201">#1201</a> (<a href="https://github.com/apps/pre-commit-ci">pre-commit-ci[bot]</a>)</li>
<li>[pre-commit.ci] pre-commit autoupdate <a href="https://redirect.github.com/sphinx-gallery/sphinx-gallery/pull/1196">#1196</a> (<a href="https://github.com/apps/pre-commit-ci">pre-commit-ci[bot]</a>)</li>
<li>[pre-commit.ci] pre-commit autoupdate <a href="https://redirect.github.com/sphinx-gallery/sphinx-gallery/pull/1194">#1194</a> (<a href="https://github.com/apps/pre-commit-ci">pre-commit-ci[bot]</a>)</li>
<li>[pre-commit.ci] pre-commit autoupdate <a href="https://redirect.github.com/sphinx-gallery/sphinx-gallery/pull/1191">#1191</a> (<a href="https://github.com/apps/pre-commit-ci">pre-commit-ci[bot]</a>)</li>
<li>[pre-commit.ci] pre-commit autoupdate <a href="https://redirect.github.com/sphinx-gallery/sphinx-gallery/pull/1189">#1189</a> (<a href="https://github.com/apps/pre-commit-ci">pre-commit-ci[bot]</a>)</li>
<li>[pre-commit.ci] pre-commit autoupdate <a href="https://redirect.github.com/sphinx-gallery/sphinx-gallery/pull/1187">#1187</a> (<a href="https://github.com/apps/pre-commit-ci">pre-commit-ci[bot]</a>)</li>
<li>MAINT: Bump ver <a href="https://redirect.github.com/sphinx-gallery/sphinx-gallery/pull/1185">#1185</a> (<a href="https://github.com/larsoner">larsoner</a>)</li>
</ul>
</blockquote>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a href="https://github.com/sphinx-gallery/sphinx-gallery/commit/2c9e803bd0299ef8f587117edc9245664bf5d39d"><code>2c9e803</code></a> DOC Minor config format fix (<a href="https://redirect.github.com/sphinx-gallery/sphinx-gallery/issues/1233">#1233</a>)</li>
<li><a href="https://github.com/sphinx-gallery/sphinx-gallery/commit/1adafe82dee9e2a84094761d2fe3d38581321ec0"><code>1adafe8</code></a> MAINT Version 0.15.0 and changelog (<a href="https://redirect.github.com/sphinx-gallery/sphinx-gallery/issues/1232">#1232</a>)</li>
<li><a href="https://github.com/sphinx-gallery/sphinx-gallery/commit/847ed2af7e08050e44eda5c3611a867e07eca10b"><code>847ed2a</code></a> [pre-commit.ci] pre-commit autoupdate (<a href="https://redirect.github.com/sphinx-gallery/sphinx-gallery/issues/1231">#1231</a>)</li>
<li><a href="https://github.com/sphinx-gallery/sphinx-gallery/commit/161aa2a73a70c170a7f13d91292bd3edefe6160f"><code>161aa2a</code></a> DOC Put configuration list under headings (<a href="https://redirect.github.com/sphinx-gallery/sphinx-gallery/issues/1230">#1230</a>)</li>
<li><a href="https://github.com/sphinx-gallery/sphinx-gallery/commit/959f29ec4039ad166ab9a9421da5e6d231dc5ef4"><code>959f29e</code></a> MAINT Add <code>extras_require</code> in <code>setup.py</code> (<a href="https://redirect.github.com/sphinx-gallery/sphinx-gallery/issues/1229">#1229</a>)</li>
<li><a href="https://github.com/sphinx-gallery/sphinx-gallery/commit/13b9bef732da6cc12314d90b20bc59904fc4c9b1"><code>13b9bef</code></a> FEA Add examples recommender system (<a href="https://redirect.github.com/sphinx-gallery/sphinx-gallery/issues/1125">#1125</a>)</li>
<li><a href="https://github.com/sphinx-gallery/sphinx-gallery/commit/83f8c3a4e21174fa3281ce54733d23bdec0ae9c4"><code>83f8c3a</code></a> [pre-commit.ci] pre-commit autoupdate (<a href="https://redirect.github.com/sphinx-gallery/sphinx-gallery/issues/1227">#1227</a>)</li>
<li><a href="https://github.com/sphinx-gallery/sphinx-gallery/commit/b9f3b82389e4ad20484aedccc866c32c62e65ce8"><code>b9f3b82</code></a> ENH: Improve logging visibility of errors and filenames (<a href="https://redirect.github.com/sphinx-gallery/sphinx-gallery/issues/1225">#1225</a>)</li>
<li><a href="https://github.com/sphinx-gallery/sphinx-gallery/commit/f5cc4613b17e22ed593b8f30fcd5f879b33d53d5"><code>f5cc461</code></a> [pre-commit.ci] pre-commit autoupdate (<a href="https://redirect.github.com/sphinx-gallery/sphinx-gallery/issues/1224">#1224</a>)</li>
<li><a href="https://github.com/sphinx-gallery/sphinx-gallery/commit/7149c249e2a32483518cadb92f16e915555f9b77"><code>7149c24</code></a> DOC: contributing guide (<a href="https://redirect.github.com/sphinx-gallery/sphinx-gallery/issues/1223">#1223</a>)</li>
<li>Additional commits viewable in <a href="https://github.com/sphinx-gallery/sphinx-gallery/compare/v0.0.3...v0.15.0">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/pyproject.toml b/pyproject.toml
index 91438fffa..3025f7873 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -116,7 +116,7 @@ all_extras_pandas2 = [
   "arch<6.3.0,>=5.6",
   "cloudpickle",
   "dash!=2.9.0",
-  "dask<2023.7.1",
+  "dask<2023.11.1",
   "dtw-python",
   'esig==0.9.7; python_version < "3.10"',
   'filterpy>=1.4.5; python_version < "3.11"',
@@ -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.37",
+  "holidays>=0.29,<0.38",
   "mne<1.6,>=1.5",
   'numba<0.59,>=0.53; python_version < "3.12"',
   "pycatch22<0.4.4,>=0.4",
@@ -236,7 +236,7 @@ docs = [
   "Sphinx!=7.2.0,<8.0.0",
   "sphinx-copybutton",
   "sphinx-design<0.6.0",
-  "sphinx-gallery<0.15.0",
+  "sphinx-gallery<0.16.0",
   "sphinx-issues<4.0.0",
   "tabulate",
 ]
diff --git a/sktime/datatypes/_panel/_examples.py b/sktime/datatypes/_panel/_examples.py
index e7c1503cc..d2f4ef0f3 100644
--- a/sktime/datatypes/_panel/_examples.py
+++ b/sktime/datatypes/_panel/_examples.py
@@ -68,7 +68,7 @@ example_dict[("pd-multiindex", "Panel", 0)] = X
 example_dict_lossy[("pd-multiindex", "Panel", 0)] = False
 
 cols = [f"var_{i}" for i in range(2)]
-X = pd.DataFrame(columns=cols, index=[0, 1, 2])
+X = pd.DataFrame(columns=cols, index=pd.RangeIndex(3))
 X["var_0"] = pd.Series(
     [pd.Series([1, 2, 3]), pd.Series([1, 2, 3]), pd.Series([1, 2, 3])]
 )
@@ -143,7 +143,7 @@ example_dict[("pd-multiindex", "Panel", 1)] = X
 example_dict_lossy[("pd-multiindex", "Panel", 1)] = False
 
 cols = [f"var_{i}" for i in range(1)]
-X = pd.DataFrame(columns=cols, index=[0, 1, 2])
+X = pd.DataFrame(columns=cols, index=pd.RangeIndex(3))
 X["var_0"] = pd.Series(
     [pd.Series([4, 5, 6]), pd.Series([4, 55, 6]), pd.Series([42, 5, 6])]
 )
@@ -211,7 +211,7 @@ example_dict[("pd-multiindex", "Panel", 2)] = X
 example_dict_lossy[("pd-multiindex", "Panel", 2)] = False
 
 cols = [f"var_{i}" for i in range(1)]
-X = pd.DataFrame(columns=cols, index=[0])
+X = pd.DataFrame(columns=cols, index=pd.RangeIndex(1))
 X["var_0"] = pd.Series([pd.Series([4, 5, 6])])
 
 example_dict[("nested_univ", "Panel", 2)] = X
diff --git a/sktime/forecasting/conformal.py b/sktime/forecasting/conformal.py
index 175e75437..b63176813 100644
--- a/sktime/forecasting/conformal.py
+++ b/sktime/forecasting/conformal.py
@@ -11,6 +11,7 @@ from math import floor
 
 import numpy as np
 import pandas as pd
+import scipy
 from joblib import Parallel, delayed
 from sklearn.base import clone
 
@@ -417,8 +418,10 @@ class ConformalIntervals(BaseForecaster):
 
         full_y_index = y.iloc[n_initial_window:].index
 
-        residuals_matrix = pd.DataFrame(
-            columns=full_y_index, index=full_y_index, dtype="float"
+        residuals_matrix = pd.DataFrame.sparse.from_spmatrix(
+            scipy.sparse.csr_array((len(full_y_index), len(full_y_index))),
+            index=full_y_index,
+            columns=full_y_index,
         )
 
         if update and hasattr(self, "residuals_matrix_") and not sample_frac:
@@ -427,9 +430,9 @@ class ConformalIntervals(BaseForecaster):
                 overlapping_index = pd.Index(
                     self.residuals_matrix_.index.intersection(full_y_index)
                 ).sort_values()
-                residuals_matrix.loc[
+                residuals_matrix = self.residuals_matrix_.loc[
                     overlapping_index, overlapping_index
-                ] = self.residuals_matrix_.loc[overlapping_index, overlapping_index]
+                ].combine_first(residuals_matrix)
             else:
                 overlapping_index = None
             y_index = remaining_y_index
@@ -466,8 +469,10 @@ class ConformalIntervals(BaseForecaster):
             delayed(_get_residuals_matrix_row)(forecaster.clone(), y, X, id)
             for id in y_index
         )
-        for idx, id in enumerate(y_index):
-            residuals_matrix.loc[id] = all_residuals[idx]
+        all_residuals = pd.concat(all_residuals, axis=1, ignore_index=True).T
+        all_residuals.index = y_index
+        all_residuals = all_residuals.astype(pd.SparseDtype("float", 0))
+        residuals_matrix = all_residuals.combine_first(residuals_matrix)
 
         if overlapping_index is not None:
 
@@ -494,9 +499,10 @@ class ConformalIntervals(BaseForecaster):
                 delayed(_extend_residuals_matrix_row)(y, X, id)
                 for id in overlapping_index
             )
-
-            for idx, id in enumerate(overlapping_index):
-                residuals_matrix.loc[id] = extend_residuals[idx]
+            extend_residuals = pd.concat(extend_residuals, axis=1, ignore_index=True).T
+            extend_residuals.index = overlapping_index
+            extend_residuals = extend_residuals.astype(pd.SparseDtype("float", 0))
+            residuals_matrix = extend_residuals.combine_first(residuals_matrix)
 
         return residuals_matrix
 
diff --git a/sktime/utils/sklearn/__init__.py b/sktime/utils/sklearn/__init__.py
new file mode 100644
index 000000000..5b1546d33
--- /dev/null
+++ b/sktime/utils/sklearn/__init__.py
@@ -0,0 +1,21 @@
+"""Sklearn related utility functionality."""
+
+from sktime.utils.sklearn._adapt_df import prep_skl_df
+from sktime.utils.sklearn._scitype import (
+    is_sklearn_classifier,
+    is_sklearn_clusterer,
+    is_sklearn_estimator,
+    is_sklearn_regressor,
+    is_sklearn_transformer,
+    sklearn_scitype,
+)
+
+__all__ = [
+    "prep_skl_df",
+    "is_sklearn_estimator",
+    "is_sklearn_transformer",
+    "is_sklearn_classifier",
+    "is_sklearn_regressor",
+    "is_sklearn_clusterer",
+    "sklearn_scitype",
+]
diff --git a/sktime/utils/sklearn/_adapt_df.py b/sktime/utils/sklearn/_adapt_df.py
new file mode 100644
index 000000000..300764c1b
--- /dev/null
+++ b/sktime/utils/sklearn/_adapt_df.py
@@ -0,0 +1,31 @@
+"""Utility functions for adapting to sklearn."""
+# copyright: sktime developers, BSD-3-Clause License (see LICENSE file)
+
+import numpy as np
+
+
+def prep_skl_df(df, copy_df=False):
+    """Make df compatible with sklearn input expectations.
+
+    Changes:
+    turns column index into a list of strings
+
+    Parameters
+    ----------
+    df : pd.DataFrame
+        list of indices to sample from
+    copy_df : bool, default=False
+        whether to mutate df or return a copy
+        if False, index of df is mutated
+        if True, original df is not mutated. If index is not a list of strings,
+        a copy is made and the copy is mutated. Otherwise, the original df is returned.
+    """
+    cols = df.columns
+    str_cols = cols.astype(str)
+
+    if not np.all(str_cols == cols):
+        if copy_df:
+            df = df.copy()
+        df.columns = str_cols
+
+    return df


Test patch:
diff --git a/sktime/utils/_testing/deep_equals.py b/sktime/utils/_testing/deep_equals.py
index 84494ad59..0616cc81a 100644
--- a/sktime/utils/_testing/deep_equals.py
+++ b/sktime/utils/_testing/deep_equals.py
@@ -98,11 +98,67 @@ def deep_equals(x, y, return_msg=False):
         else:
             return ret(x.equals(y), ".series_equals, x = {} != y = {}", [x, y])
     elif isinstance(x, pd.DataFrame):
+        # check column names for equality
         if not x.columns.equals(y.columns):
             return ret(
                 False, f".columns, x.columns = {x.columns} != y.columns = {y.columns}"
             )
-        # if columns are equal and at least one is object, recurse over Series
+        # check dtypes for equality
+        if not x.dtypes.equals(y.dtypes):
+            return ret(
+                False, f".dtypes, x.dtypes = {x.dtypes} != y.dtypes = {y.dtypes}"
+            )
+        # check index for equality
+        # we are not recursing due to ambiguity in integer index types
+        # which may differ from pandas version to pandas version
+        # and would upset the type check, e.g., RangeIndex(2) vs Index([0, 1])
+        xix = x.index
+        yix = y.index
+        if hasattr(xix, "dtype") and hasattr(xix, "dtype"):
+            if not xix.dtype == yix.dtype:
+                return ret(
+                    False,
+                    ".index.dtype, x.index.dtype = {} != y.index.dtype = {}",
+                    [xix.dtype, yix.dtype],
+                )
+        if hasattr(xix, "dtypes") and hasattr(yix, "dtypes"):
+            if not x.dtypes.equals(y.dtypes):
+                return ret(
+                    False,
+                    ".index.dtypes, x.dtypes = {} != y.index.dtypes = {}",
+                    [xix.dtypes, yix.dtypes],
+                )
+        ix_eq = xix.equals(yix)
+        if not ix_eq:
+            if not len(xix) == len(yix):
+                return ret(
+                    False,
+                    ".index.len, x.index.len = {} != y.index.len = {}",
+                    [len(xix), len(yix)],
+                )
+            if hasattr(xix, "name") and hasattr(yix, "name"):
+                if not xix.name == yix.name:
+                    return ret(
+                        False,
+                        ".index.name, x.index.name = {} != y.index.name = {}",
+                        [xix.name, yix.name],
+                    )
+            if hasattr(xix, "names") and hasattr(yix, "names"):
+                if not len(xix.names) == len(yix.names):
+                    return ret(
+                        False,
+                        ".index.names, x.index.names = {} != y.index.name = {}",
+                        [xix.names, yix.names],
+                    )
+                if not np.all(xix.names == yix.names):
+                    return ret(
+                        False,
+                        ".index.names, x.index.names = {} != y.index.name = {}",
+                        [xix.names, yix.names],
+                    )
+            elts_eq = np.all(xix == yix)
+            return ret(elts_eq, ".index.equals, x = {} != y = {}", [xix, yix])
+        # if columns, dtypes are equal and at least one is object, recurse over Series
         if sum(x.dtypes == "object") > 0:
             for c in x.columns:
                 is_equal, msg = deep_equals(x[c], y[c], return_msg=True)
@@ -112,6 +168,14 @@ def deep_equals(x, y, return_msg=False):
         else:
             return ret(x.equals(y), ".df_equals, x = {} != y = {}", [x, y])
     elif isinstance(x, pd.Index):
+        if hasattr(x, "dtype") and hasattr(y, "dtype"):
+            if not x.dtype == y.dtype:
+                return ret(False, f".dtype, x.dtype = {x.dtype} != y.dtype = {y.dtype}")
+        if hasattr(x, "dtypes") and hasattr(y, "dtypes"):
+            if not x.dtypes.equals(y.dtypes):
+                return ret(
+                    False, f".dtypes, x.dtypes = {x.dtypes} != y.dtypes = {y.dtypes}"
+                )
         return ret(x.equals(y), ".index_equals, x = {} != y = {}", [x, y])
     elif isinstance(x, np.ndarray):
         if x.dtype != y.dtype:
diff --git a/sktime/utils/sklearn.py b/sktime/utils/sklearn/_scitype.py
similarity index 100%
rename from sktime/utils/sklearn.py
rename to sktime/utils/sklearn/_scitype.py
diff --git a/sktime/utils/sklearn/tests/__init__.py b/sktime/utils/sklearn/tests/__init__.py
new file mode 100644
index 000000000..1d9b760fe
--- /dev/null
+++ b/sktime/utils/sklearn/tests/__init__.py
@@ -0,0 +1,1 @@
+"""Tests for sklearn related utility functionality."""
diff --git a/sktime/utils/sklearn/tests/test_sklearn_df_adapt.py b/sktime/utils/sklearn/tests/test_sklearn_df_adapt.py
new file mode 100644
index 000000000..57b36ebcd
--- /dev/null
+++ b/sktime/utils/sklearn/tests/test_sklearn_df_adapt.py
@@ -0,0 +1,33 @@
+"""Tests for sklearn dataframe coercion."""
+
+__author__ = ["fkiraly"]
+
+import numpy as np
+import pandas as pd
+import pytest
+
+from sktime.utils.sklearn._adapt_df import prep_skl_df
+
+
+@pytest.mark.parametrize("copy_df", [True, False])
+def test_prep_skl_df_coercion(copy_df):
+    """Test that prep_skl_df behaves correctly on the coercion case."""
+    mixed_example = pd.DataFrame({0: [1, 2, 3], "b": [1, 2, 3]})
+
+    res = prep_skl_df(mixed_example, copy_df=copy_df)
+
+    assert np.all(res.columns == ["0", "b"])
+
+    if not copy_df:
+        assert res is mixed_example
+
+
+@pytest.mark.parametrize("copy_df", [True, False])
+def test_prep_skl_df_non_coercion(copy_df):
+    """Test that prep_skl_df behaves correctly on the non-coercion case."""
+    mixed_example = pd.DataFrame({"a": [1, 2, 3], "b": [1, 2, 3]})
+
+    res = prep_skl_df(mixed_example, copy_df=copy_df)
+
+    assert np.all(res.columns == ["a", "b"])
+    assert res is mixed_example