Repository: sktime/sktime

Issue:
[BUG] `DynamicFactor` cannot predict quantiles in presence of exogenous variables
**Describe the bug**
<!--
A clear and concise description of what the bug is.
-->
`DynamicFactor` has `"ignores-exogeneous-X": False` and `"capability:pred_int": True`, but fails to predict quantiles when `X` is present.

**To Reproduce**
<!--
Add a Minimal, Complete, and Verifiable example (for more details, see e.g. https://stackoverflow.com/help/mcve

If the code is too long, feel free to put it in a public gist and link it in the issue: https://gist.github.com
-->

```python
from sktime.datasets import load_longley
_, df = load_longley()

y = df[["GNPDEFL", "GNP"]]
X = df[["UNEMP", "POP"]]

from sktime.forecasting.dynamic_factor import DynamicFactor
forecaster = DynamicFactor().fit(y[:10], X=X[:10])

forecaster.predict_quantiles(fh=[1, 2, 3, 4, 5, 6], X=X[10:])
```

**Expected behavior**
<!--
A clear and concise description of what you expected to happen.
-->
No error. Currently it fails with this:

> ValueError: Out-of-sample operations in a model with a regression component require additional exogenous values via the `exog` argument.

**Additional context**
<!--
Add any other context about the problem here.
-->
This is likely failing due to some issue in check of `exog`. Since `predict` works by checking on `self._forecaster`, failure in `_predict_interval` can be because of using `self._fitted_forecaster`.

**Versions**
<details>

<!--
Please run the following code snippet and paste the output here:

from sktime import show_versions; show_versions()
-->

```pycon
>>> from sktime import show_versions; show_versions()

System:
    python: 3.9.16 (main, Mar  8 2023, 14:00:05)  [GCC 11.2.0]
executable: /path/to/venv/bin/python
   machine: Linux-5.15.90.1-microsoft-standard-WSL2-x86_64-with-glibc2.35

Python dependencies:
          pip: 23.1.2
       sktime: 0.19.2
      sklearn: 1.2.2
       skbase: 0.4.0
        numpy: 1.24.3
        scipy: 1.10.1
       pandas: 1.5.3
   matplotlib: 3.7.1
       joblib: 1.2.0
  statsmodels: 0.13.5
        numba: 0.57.1
     pmdarima: 2.0.3
      tsfresh: 0.20.0
   tensorflow: 2.12.0
tensorflow_probability: 0.19.0
>>> 
```

</details>

<!-- Thanks for contributing! -->


Discussion:

Patch:
diff --git a/.all-contributorsrc b/.all-contributorsrc
index b50330f23..883b4266b 100644
--- a/.all-contributorsrc
+++ b/.all-contributorsrc
@@ -2190,6 +2190,24 @@
       "contributions": [
         "code"
       ]
+    },
+    {
+      "login": "CTFallon",
+      "name": "Colin Fallon",
+      "avatar_url": "https://avatars.githubusercontent.com/u/19725980?v=4",
+      "profile": "https://github.com/CTFallon",
+      "contributions": [
+        "doc"
+      ]
+    },
+    {
+      "login": "mgazian000",
+      "name": "Michael Gaziani",
+      "avatar_url": "https://avatars.githubusercontent.com/mgazian000",
+      "profile": "https://github.com/mgazian000",
+      "contributions": [
+        "doc"
+      ]
     }
   ]
 }
diff --git a/README.md b/README.md
index 5f2232b43..8753066fb 100644
--- a/README.md
+++ b/README.md
@@ -4,7 +4,7 @@
 
 > A unified interface for machine learning with time series
 
-:rocket: **Version 0.19.2 out now!** [Check out the release notes here](https://www.sktime.net/en/latest/changelog.html).
+:rocket: **Version 0.20.0 out now!** [Check out the release notes here](https://www.sktime.net/en/latest/changelog.html).
 
 sktime is a library for time series analysis in Python. It provides a unified interface for multiple time series learning tasks. Currently, this includes time series classification, regression, clustering, annotation and forecasting. It comes with [time series algorithms](https://www.sktime.net/en/stable/estimator_overview.html) and [scikit-learn] compatible tools to build, tune and validate time series models.
 
diff --git a/docs/source/changelog.rst b/docs/source/changelog.rst
index 52389bf02..f85607a0b 100644
--- a/docs/source/changelog.rst
+++ b/docs/source/changelog.rst
@@ -13,6 +13,64 @@ All notable changes to this project will be documented in this file. We keep tra
 For upcoming changes and next releases, see our `milestones <https://github.com/sktime/sktime/milestones?direction=asc&sort=due_date&state=open>`_.
 For our long-term plan, see our :ref:`roadmap`.
 
+Version 0.20.0 - 2023-06-21
+---------------------------
+
+Maintenance release - python 3.7 end-of-life maintenance update,
+scheduled deprecations.
+
+For last non-maintenance content updates, see 0.19.2 and 0.19.1.
+
+Contents
+~~~~~~~~
+
+* python 3.7 is no longer supported by ``sktime``, as python 3.7 end-of-life is
+  imminent (June 27), with ``sktime`` dependencies already having dropped support.
+* pre-commit and coding style upgrades (3.8 plus)
+* scheduled 0.20.0 deprecation actions
+
+Dependency changes
+~~~~~~~~~~~~~~~~~~
+
+* ``numpy`` version bounds now allow versions ``1.25.X``
+
+Deprecations and removals
+~~~~~~~~~~~~~~~~~~~~~~~~~
+
+Python 3.7 end-of-life
+^^^^^^^^^^^^^^^^^^^^^^
+
+``sktime`` no longer supports python 3.7 with ``sktime`` 0.20.0 and later.
+
+python reaches end-of-life on Jun 27, 2023, and core dependencies of ``sktime``
+have already dropped support for python 3.7 with their most recent versions
+(e.g., ``scikit-learn``).
+
+Time Series Classification
+^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+``ComposableTimeSeriesClassifier`` and ``WeightedEnsembleClassifier``
+have finished their move to ``classification.ensemble``, they are no longer
+importable in their original locations.
+
+List of PR
+~~~~~~~~~~
+
+* [MNT] 0.20.0 deprecation actions (:pr:`4733`) :user:`fkiraly`
+* [MNT] 0.20.0 release action - remove python 3.7 support (:pr:`4717`) :user:`fkiraly`
+* [MNT] 0.20.0 release action - increase ``scikit-base`` bound to ``<0.6.0`` (:pr:`4735`) :user:`fkiraly`
+* [MNT] 0.20.0 release action - support for ``numpy 1.25`` (:pr:`4720`) :user:`jorenham`
+* [MNT] 0.20.0 release action - remove initial utf comments in all python modules which are unnecessary in python 3 (:pr:`4725`) :user:`yarnabrina`
+* [MNT] 0.20.0 release action - upgrade to coding style of python 3.8 and above using ``pyupgrade`` (:pr:`4726`) :user:`yarnabrina`
+
+Contributors
+~~~~~~~~~~~~
+
+:user:`fkiraly`,
+:user:`jorenham`,
+:user:`yarnabrina`
+
+
 Version 0.19.2 - 2023-06-19
 ---------------------------
 
@@ -230,7 +288,6 @@ Time series classification
 ^^^^^^^^^^^^^^^^^^^^^^^^^^
 
 * [ENH] `SimpleRNN` DL time series regressor, migrated from ``sktime-dl`` (:pr:`4185`) :user:`ArushikaBansal`
-
 * [ENH] move classification ensembles to ``classification.ensembles`` (:pr:`4532`) :user:`fkiraly`
 * [ENH] better documentation and test coverage for custom estimators and parameters in ``DrCIF`` (:pr:`4621`) :user:`Taise228`
 * [ENH] Add MACNN classifier and network (:pr:`4636`) :user:`achieveordie`
diff --git a/docs/source/developer_guide/continuous_integration.rst b/docs/source/developer_guide/continuous_integration.rst
index d4ee0e951..826954a4b 100644
--- a/docs/source/developer_guide/continuous_integration.rst
+++ b/docs/source/developer_guide/continuous_integration.rst
@@ -125,19 +125,19 @@ the following command:
 (for ``UNIX`` based OS)
   .. code:: bash
 
-      find . -name "*.py" -print0 | xargs -0 python -m doctest -v
+      find . -name "*.py" -print0 | xargs -0 python -m doctest -v -o=ELLIPSIS
 
 (for windows)
   .. code:: bash
 
-      for /r %G in (*.py) do python -m doctest -v "%G"
+      for /r %G in (*.py) do python -m doctest -v "%G" -o=ELLIPSIS
 
 To run doctest on a specific module, navigate to the directory where the
 module is located and execute the following command:
 
    .. code:: bash
 
-      python -m doctest -v {filename}
+      python -m doctest -v -o=ELLIPSIS {filename}
 
 
 Executing this command will display the test results for all the docstrings
diff --git a/pyproject.toml b/pyproject.toml
index c1d73ff97..9f31dc096 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -1,6 +1,6 @@
 [project]
 name = "sktime"
-version = "0.19.2"
+version = "0.20.0"
 description = "A unified framework for machine learning with time series"
 authors = [
     {name = "sktime developers", email = "sktime.toolbox@gmail.com"},
@@ -50,7 +50,7 @@ dependencies = [
     "packaging",
     "scikit-base<0.6.0",
     "scikit-learn>=0.24.0,<1.3.0",
-    "scipy<2.0.0,>=1.2.0",
+    "scipy<1.11.0,>=1.2.0",
 ]
 
 [project.optional-dependencies]
diff --git a/sktime/__init__.py b/sktime/__init__.py
index aa91af740..07d9419be 100644
--- a/sktime/__init__.py
+++ b/sktime/__init__.py
@@ -1,6 +1,6 @@
 """sktime."""
 
-__version__ = "0.19.2"
+__version__ = "0.20.0"
 
 __all__ = ["show_versions"]
 
diff --git a/sktime/clustering/k_means.py b/sktime/clustering/k_means.py
index 2ab38b987..a763875f4 100644
--- a/sktime/clustering/k_means.py
+++ b/sktime/clustering/k_means.py
@@ -63,6 +63,17 @@ class TimeSeriesKMeans(TimeSeriesLloyds):
         the sample weights if provided.
     n_iter_: int
         Number of iterations run.
+
+    Examples
+    --------
+    >>> from sktime.datasets import load_arrow_head
+    >>> from sktime.clustering.k_means import TimeSeriesKMeans
+    >>> X_train, y_train = load_arrow_head(split="train")
+    >>> X_test, y_test = load_arrow_head(split="test")
+    >>> clusterer = TimeSeriesKMeans(n_clusters=3)  # doctest: +SKIP
+    >>> clusterer.fit(X_train)  # doctest: +SKIP
+    TimeSeriesKMeans(n_clusters=3)
+    >>> y_pred = clusterer.predict(X_test)  # doctest: +SKIP
     """
 
     _tags = {"python_dependencies": "numba"}
diff --git a/sktime/clustering/k_medoids.py b/sktime/clustering/k_medoids.py
index 86453f940..c7b1e8c52 100755
--- a/sktime/clustering/k_medoids.py
+++ b/sktime/clustering/k_medoids.py
@@ -57,6 +57,17 @@ class TimeSeriesKMedoids(TimeSeriesLloyds):
         the sample weights if provided.
     n_iter_: int
         Number of iterations run.
+
+    Examples
+    --------
+    >>> from sktime.datasets import load_arrow_head
+    >>> from sktime.clustering.k_medoids import TimeSeriesKMedoids
+    >>> X_train, y_train = load_arrow_head(split="train")
+    >>> X_test, y_test = load_arrow_head(split="test")
+    >>> clusterer = TimeSeriesKMedoids(n_clusters=3)  # doctest: +SKIP
+    >>> clusterer.fit(X_train)  # doctest: +SKIP
+    TimeSeriesKMedoids(n_clusters=3)
+    >>> y_pred = clusterer.predict(X_test)  # doctest: +SKIP
     """
 
     _tags = {"python_dependencies": "numba"}
diff --git a/sktime/forecasting/dynamic_factor.py b/sktime/forecasting/dynamic_factor.py
index 0d32b43d9..0469e6201 100644
--- a/sktime/forecasting/dynamic_factor.py
+++ b/sktime/forecasting/dynamic_factor.py
@@ -1,14 +1,13 @@
 # copyright: sktime developers, BSD-3-Clause License (see LICENSE file)
 """Implements DynamicFactor Model as interface to statsmodels."""
-
-import inspect
+from typing import List, Union
 
 import numpy as np
 import pandas as pd
 
 from sktime.forecasting.base.adapters import _StatsModelsAdapter
 
-_all_ = ["DynamicFactor"]
+__all__ = ["DynamicFactor"]
 __author__ = ["Ris-Bali", "lbventura"]
 
 
@@ -211,10 +210,8 @@ class DynamicFactor(_StatsModelsAdapter):
         # beginning of the training series when passing integers
         start, end = fh.to_absolute_int(self._y.index[0], self.cutoff)[[0, -1]]
 
-        if "exog" in inspect.signature(self._forecaster.__init__).parameters.keys():
-            y_pred = self._fitted_forecaster.predict(start=start, end=end, exog=X)
-        else:
-            y_pred = self._fitted_forecaster.predict(start=start, end=end)
+        y_pred = self._fitted_forecaster.predict(start=start, end=end, exog=X)
+
         # statsmodels forecasts all periods from start to end of forecasting
         # horizon, but only return given time points in forecasting horizon
 
@@ -224,7 +221,7 @@ class DynamicFactor(_StatsModelsAdapter):
             )
         return y_pred.loc[fh.to_absolute_index(self.cutoff)]
 
-    def _predict_interval(self, fh, X=None, coverage: [float] = None):
+    def _predict_interval(self, fh, X=None, coverage: Union[float, List[float]] = None):
         """Compute/return prediction quantiles for a forecast.
 
         private _predict_interval containing the core logic,
@@ -278,10 +275,7 @@ class DynamicFactor(_StatsModelsAdapter):
         for coverage in coverage_list:
             alpha = 1 - coverage
 
-            if "exog" in inspect.signature(model.__init__).parameters.keys():
-                y_pred = model.get_forecast(steps=steps, exog=X).conf_int(alpha=alpha)
-            else:
-                y_pred = model.get_forecast(steps=steps).conf_int(alpha=alpha)
+            y_pred = model.get_forecast(steps=steps, exog=X).conf_int(alpha=alpha)
 
             y_pred = y_pred.iloc[ix]
 
diff --git a/sktime/regression/deep_learning/resnet.py b/sktime/regression/deep_learning/resnet.py
index 774ffe373..684d96357 100644
--- a/sktime/regression/deep_learning/resnet.py
+++ b/sktime/regression/deep_learning/resnet.py
@@ -54,7 +54,7 @@ class ResNetRegressor(BaseDeepRegressor):
     >>> from sktime.regression.deep_learning.resnet import ResNetRegressor
     >>> from sktime.datasets import load_unit_test
     >>> X_train, y_train = load_unit_test(split="train")
-    >>> clf = ResNetRegressor(n_epochs=20, bacth_size=4) # doctest: +SKIP
+    >>> clf = ResNetRegressor(n_epochs=20, batch_size=4) # doctest: +SKIP
     >>> clf.fit(X_train, Y_train) # doctest: +SKIP
     ResNetRegressor(...)
     """
diff --git a/sktime/regression/dummy/_dummy.py b/sktime/regression/dummy/_dummy.py
index 91d099f9b..f6864f51c 100644
--- a/sktime/regression/dummy/_dummy.py
+++ b/sktime/regression/dummy/_dummy.py
@@ -44,6 +44,17 @@ class DummyRegressor(BaseRegressor):
         The quantile to predict using the "quantile" strategy. A quantile of
         0.5 corresponds to the median, while 0.0 to the minimum and 1.0 to the
         maximum.
+
+    Examples
+    --------
+    >>> from sktime.regression.dummy import DummyRegressor
+    >>> from sktime.datasets import load_unit_test
+    >>> X_train, y_train = load_unit_test(split="train")
+    >>> X_test, y_test = load_unit_test(split="test")
+    >>> regressor = DummyRegressor(strategy="median") # doctest: +SKIP
+    >>> regressor.fit(X_train,y_train) # doctest: +SKIP
+    DummyRegressor(strategy='median')
+    >>> y_pred = regressor.predict(X_test) # doctest: +SKIP
     """
 
     _tags = {
diff --git a/sktime/regression/interval_based/_tsf.py b/sktime/regression/interval_based/_tsf.py
index 7f5c595e6..597fba1f8 100644
--- a/sktime/regression/interval_based/_tsf.py
+++ b/sktime/regression/interval_based/_tsf.py
@@ -63,6 +63,17 @@ class TimeSeriesForestRegressor(BaseTimeSeriesForest, ForestRegressor, BaseRegre
        classification and feature extraction", Information Sciences, 239, 2013
     .. [2] Java implementation https://github.com/uea-machine-learning/tsml
     .. [3] Arxiv paper: https://arxiv.org/abs/1302.2277
+
+    Examples
+    --------
+    >>> from sktime.regression.interval_based import TimeSeriesForestRegressor
+    >>> from sktime.datasets import load_unit_test
+    >>> X_train, y_train = load_unit_test(split="train")
+    >>> X_test, y_test = load_unit_test(split="test")
+    >>> regressor = TimeSeriesForestRegressor(n_estimators=150) # doctest: +SKIP
+    >>> regressor.fit(X_train, y_train) # doctest: +SKIP
+    TimeSeriesForestRegressor(n_estimators=150)
+    >>> y_pred = regressor.predict(X_test) # doctest: +SKIP
     """
 
     _tags = {


Test patch:
diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml
index 28d25cb20..91f3c616b 100644
--- a/.github/workflows/test.yml
+++ b/.github/workflows/test.yml
@@ -196,7 +196,7 @@ jobs:
           mkdir -p testdir/
           cp .coveragerc testdir/
           cp setup.cfg testdir/
-          python -m pytest -v
+          python -m pytest
 
       - name: Publish code coverage
         uses: codecov/codecov-action@v3
diff --git a/sktime/forecasting/tests/test_dynamic_factor.py b/sktime/forecasting/tests/test_dynamic_factor.py
new file mode 100644
index 000000000..cfe2d8efa
--- /dev/null
+++ b/sktime/forecasting/tests/test_dynamic_factor.py
@@ -0,0 +1,131 @@
+"""Tests the DynamicFactor model."""
+import pytest
+from pandas.testing import assert_frame_equal
+
+from sktime.datasets import load_longley
+from sktime.forecasting.dynamic_factor import DynamicFactor
+from sktime.utils.validation._dependencies import _check_soft_dependencies
+
+__author__ = ["yarnabrina"]
+
+HISTORY_LENGTH = 10
+PREDICTION_LENGTH = 6
+
+K_FACTORS = 1
+FACTOR_ORDER = 1
+
+COVERAGES = [0.95, 0.99]
+
+_, MULTIVARIATE_DF = load_longley()
+
+TARGET_COLUMNS = ["GNPDEFL", "GNP"]
+FEATURE_COLUMNS = ["UNEMP", "POP"]
+
+ENDOGENOUS_DF = MULTIVARIATE_DF[TARGET_COLUMNS]
+EXOGENOUS_DF = MULTIVARIATE_DF[FEATURE_COLUMNS]
+
+TRAIN_Y = ENDOGENOUS_DF[:HISTORY_LENGTH]
+TRAIN_X = EXOGENOUS_DF[:HISTORY_LENGTH]
+
+PREDICT_X = EXOGENOUS_DF[HISTORY_LENGTH : (HISTORY_LENGTH + PREDICTION_LENGTH)]
+
+
+def compare_predictions_against_statsmodels(
+    sktime_point_predictions, sktime_interval_predictions, statsmodels_predictions
+) -> None:
+    """Compare predictions from ``sktime`` wrapper against ``statsmodels`` estimator.
+
+    Notes
+    -----
+    * compare point predictions - predictive mean
+    * compare confidence intervals for multiple coverage values, viz. ``COVERAGES``
+    """
+    statsmodels_point_predictions = statsmodels_predictions.predicted_mean
+    assert_frame_equal(sktime_point_predictions, statsmodels_point_predictions)
+
+    for coverage in COVERAGES:
+        statsmodels_interval_predictions = statsmodels_predictions.conf_int(
+            alpha=(1 - coverage)
+        )
+
+        for target in TARGET_COLUMNS:
+            sktime_results = sktime_interval_predictions.xs(
+                (target, coverage), axis="columns"
+            )
+
+            statsmodels_results = statsmodels_interval_predictions.filter(
+                regex=f"{target}$"
+            )
+            statsmodels_results.columns = ["lower", "upper"]
+
+            assert_frame_equal(sktime_results, statsmodels_results)
+
+
+@pytest.mark.skipif(
+    not _check_soft_dependencies("statsmodels", severity="none"),
+    reason="skip test if required soft dependency not available",
+)
+def test_DynamicFactor_without_exogenous_variables():
+    """Test ``DynamicFactor`` in absence of exogenous variables."""
+    from statsmodels.tsa.statespace.dynamic_factor import (
+        DynamicFactor as _DynamicFactor,
+    )
+
+    unfitted_sktime_model = DynamicFactor(
+        k_factors=K_FACTORS, factor_order=FACTOR_ORDER
+    )
+    fitted_sktime_model = unfitted_sktime_model.fit(TRAIN_Y)
+
+    sktime_point_predictions = fitted_sktime_model.predict(
+        fh=range(1, PREDICTION_LENGTH + 1)
+    )
+    sktime_interval_predictions = fitted_sktime_model.predict_interval(
+        fh=range(1, PREDICTION_LENGTH + 1), coverage=COVERAGES
+    )
+
+    unfitted_statsmodels_model = _DynamicFactor(TRAIN_Y, K_FACTORS, FACTOR_ORDER)
+    fitted_statsmodels_model = unfitted_statsmodels_model.fit()
+
+    statsmodels_predictions = fitted_statsmodels_model.get_prediction(
+        start=HISTORY_LENGTH, end=HISTORY_LENGTH + PREDICTION_LENGTH - 1
+    )
+
+    compare_predictions_against_statsmodels(
+        sktime_point_predictions, sktime_interval_predictions, statsmodels_predictions
+    )
+
+
+@pytest.mark.skipif(
+    not _check_soft_dependencies("statsmodels", severity="none"),
+    reason="skip test if required soft dependency not available",
+)
+def test_DynamicFactor_with_exogenous_variables():
+    """Test ``DynamicFactor`` in presence of exogenous variables."""
+    from statsmodels.tsa.statespace.dynamic_factor import (
+        DynamicFactor as _DynamicFactor,
+    )
+
+    unfitted_sktime_model = DynamicFactor(
+        k_factors=K_FACTORS, factor_order=FACTOR_ORDER
+    )
+    fitted_sktime_model = unfitted_sktime_model.fit(TRAIN_Y, X=TRAIN_X)
+
+    sktime_point_predictions = fitted_sktime_model.predict(
+        fh=range(1, PREDICTION_LENGTH + 1), X=PREDICT_X
+    )
+    sktime_interval_predictions = fitted_sktime_model.predict_interval(
+        fh=range(1, PREDICTION_LENGTH + 1), X=PREDICT_X, coverage=COVERAGES
+    )
+
+    unfitted_statsmodels_model = _DynamicFactor(
+        TRAIN_Y, K_FACTORS, FACTOR_ORDER, exog=TRAIN_X
+    )
+    fitted_statsmodels_model = unfitted_statsmodels_model.fit()
+
+    statsmodels_predictions = fitted_statsmodels_model.get_prediction(
+        start=HISTORY_LENGTH, end=HISTORY_LENGTH + PREDICTION_LENGTH - 1, exog=PREDICT_X
+    )
+
+    compare_predictions_against_statsmodels(
+        sktime_point_predictions, sktime_interval_predictions, statsmodels_predictions
+    )
diff --git a/sktime/tests/_config.py b/sktime/tests/_config.py
index b92e957db..6bab2ab5e 100644
--- a/sktime/tests/_config.py
+++ b/sktime/tests/_config.py
@@ -37,6 +37,13 @@ EXCLUDE_ESTIMATORS = [
     "SimpleRNNClassifier",
     "SimpleRNNRegressor",
     "EditDist",
+    "CNNClassifier",
+    "FCNClassifier",
+    "InceptionTimeClassifer",
+    "LSTMFCNClassifier",
+    "MLPClassifier",
+    "CNNRegressor",
+    "ResNetRegressor",
 ]