We are currently solving the following issue within our repository. Here is the issue text:
--- BEGIN ISSUE ---
[BUG] convert to period bug in `_StatsModelsAdapter`
**Describe the bug**
<!--
A clear and concise description of what the bug is.
-->

For panel data with datetime indexes, converting datetimes to period index causes an error in the predict stage in the `_StatsModelsAdapter` models

**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_airline
from sktime.forecasting.exp_smoothing import ExponentialSmoothing
import pandas as pd
y = load_airline()

# create dummy index with hourly timestamps and panel data by hour of day
y.index = pd.date_range(start='1960-01-01', periods=len(y.index), freq='H')
y.index.names = ["datetime"]
y.name = "passengers"
y = y.to_frame()
y['hour_of_day'] = y.index.hour
y = y.reset_index().set_index(['hour_of_day', 'datetime']).sort_index()

forecaster = ExponentialSmoothing(
    trend='add', sp=1
)  
forecaster.fit(y)
forecaster.predict(fh=[1])

```

**Expected behavior**
<!--
A clear and concise description of what you expected to happen.
-->

For each panel predict one step ahead

**Additional context**
<!--
Add any other context about the problem here.
-->

I will address this issue 👍 

**Versions**
<details>

<!--
Please run the following code snippet and paste the output here:

from sktime import show_versions; show_versions()
-->

Python dependencies:
          pip: 23.1.2
       sktime: 0.24.1
      sklearn: 1.2.2
       skbase: 0.6.1
        numpy: 1.24.3
        scipy: 1.10.1
       pandas: 2.0.2
   matplotlib: None
       joblib: 1.2.0
        numba: None
  statsmodels: 0.14.0
     pmdarima: 2.0.3
statsforecast: None
      tsfresh: None
      tslearn: None
        torch: None
   tensorflow: None
tensorflow_probability: None

<!-- Thanks for contributing! -->


--- END ISSUE ---

Below are some code segments, each from a relevant file. One or more of these files may contain bugs.
--- BEGIN FILES ---
Path: `sktime/forecasting/base/adapters/_statsmodels.py`
Content:
```
1 # !/usr/bin/env python3 -u
2 # copyright: sktime developers, BSD-3-Clause License (see LICENSE file)
3 """Implements adapter for statsmodels forecasters to be used in sktime framework."""
4 
5 __author__ = ["mloning"]
6 __all__ = ["_StatsModelsAdapter"]
7 
8 import inspect
9 
10 import numpy as np
11 import pandas as pd
12 
13 from sktime.forecasting.base import BaseForecaster
14 from sktime.utils.warnings import warn
15 
16 
17 class _StatsModelsAdapter(BaseForecaster):
18     """Base class for interfacing statsmodels forecasting algorithms."""
19 
20     _fitted_param_names = ()
21     _tags = {
22         "ignores-exogeneous-X": True,
23         "requires-fh-in-fit": False,
24         "handles-missing-data": False,
25         "python_dependencies": "statsmodels",
26     }
27 
28     def __init__(self, random_state=None):
29         self._forecaster = None
30         self.random_state = random_state
31         self._fitted_forecaster = None
32         super().__init__()
33 
34     def _fit(self, y, X, fh):
35         """Fit to training data.
36 
37         Parameters
38         ----------
39         y : pd.Series
40             Target time series to which to fit the forecaster.
41         fh : int, list or np.array, optional (default=None)
42             The forecasters horizon with the steps ahead to to predict.
43         X : pd.DataFrame, optional (default=None)
44             Exogenous variables are ignored
45 
46         Returns
47         -------
48         self : returns an instance of self.
49         """
50         # statsmodels does not support the pd.Int64Index as required,
51         # so we coerce them here to pd.RangeIndex
52         if isinstance(y, pd.Series) and pd.api.types.is_integer_dtype(y.index):
53             y, X = _coerce_int_to_range_index(y, X)
54         self._fit_forecaster(y, X)
55         return self
56 
57     def _fit_forecaster(self, y_train, X_train=None):
58         """Log used internally in fit."""
59         raise NotImplementedError("abstract method")
60 
61     def _update(self, y, X=None, update_params=True):
62         """Update used internally in update."""
63         if update_params or self.is_composite():
64             super()._update(y, X, update_params=update_params)
65         else:
66             if not hasattr(self._fitted_forecaster, "append"):
67                 warn(
68                     f"NotImplementedWarning: {self.__class__.__name__} "
69                     f"can not accept new data when update_params=False. "
70                     f"Call with update_params=True to refit with new data.",
71                     obj=self,
72                 )
73             else:
74                 # only append unseen data to fitted forecaster
75                 index_diff = y.index.difference(
76                     self._fitted_forecaster.fittedvalues.index
77                 )
78                 if index_diff.isin(y.index).all():
79                     y = y.loc[index_diff]
80                 self._fitted_forecaster = self._fitted_forecaster.append(y)
81 
82     def _predict(self, fh, X):
83         """Make forecasts.
84 
85         Parameters
86         ----------
87         fh : ForecastingHorizon
88             The forecasters horizon with the steps ahead to to predict.
89             Default is one-step ahead forecast,
90             i.e. np.array([1])
91         X : pd.DataFrame, optional (default=None)
92             Exogenous variables are ignored.
93 
94         Returns
95         -------
96         y_pred : pd.Series
97             Returns series of predicted values.
98         """
99         # statsmodels requires zero-based indexing starting at the
100         # beginning of the training series when passing integers
101         start, end = fh.to_absolute_int(self._y.index[0], self.cutoff)[[0, -1]]
102         fh_abs = fh.to_absolute_index(self.cutoff)
103 
104         # bug fix for evaluate function as test_plus_train indices are passed
105         # statsmodels exog must contain test indices only.
106         # For discussion see https://github.com/sktime/sktime/issues/3830
107         if X is not None:
108             ind_drop = self._X.index
109             X = X.loc[~X.index.isin(ind_drop)]
110             # Entire range of the forecast horizon is required
111             X = X[: fh_abs[-1]]
112 
113         if "exog" in inspect.signature(self._forecaster.__init__).parameters.keys():
114             y_pred = self._fitted_forecaster.predict(start=start, end=end, exog=X)
115         else:
116             y_pred = self._fitted_forecaster.predict(start=start, end=end)
117 
118         # statsmodels forecasts all periods from start to end of forecasting
119         # horizon, but only return given time points in forecasting horizon
120         y_pred = y_pred.loc[fh_abs]
121         # ensure that name is not added nor removed
122         # otherwise this may upset conversion to pd.DataFrame
123         y_pred.name = self._y.name
124         return y_pred
125 
126     @staticmethod
127     def _extract_conf_int(prediction_results, alpha) -> pd.DataFrame:
128         """Construct confidence interval at specified `alpha` for each timestep.
129 
130         Parameters
131         ----------
132         prediction_results : PredictionResults
133             results class, as returned by ``self._fitted_forecaster.get_prediction``
134         alpha : float
135             one minus nominal coverage
136 
137         Returns
138         -------
139         pd.DataFrame
140             confidence intervals at each timestep
141 
142             The dataframe must have at least two columns ``lower`` and ``upper``, and
143             the row indices must be integers relative to ``self.cutoff``. Order of
144             columns do not matter, and row indices must be a superset of relative
145             integer horizon of ``fh``.
146         """
147         del prediction_results, alpha  # tools like ``vulture`` may complain as unused
148 
149         raise NotImplementedError("abstract method")
150 
151     def _predict_interval(self, fh, X, coverage):
152         """Compute/return prediction interval forecasts.
153 
154         private _predict_interval containing the core logic,
155             called from predict_interval and default _predict_quantiles
156 
157         Parameters
158         ----------
159         fh : guaranteed to be ForecastingHorizon
160             The forecasting horizon with the steps ahead to to predict.
161         X : optional (default=None)
162             guaranteed to be of a type in self.get_tag("X_inner_mtype")
163             Exogeneous time series to predict from.
164         coverage : float or list of float, optional (default=0.95)
165            nominal coverage(s) of predictive interval(s)
166 
167         Returns
168         -------
169         pred_int : pd.DataFrame
170             Column has multi-index: first level is variable name from y in fit,
171                 second level coverage fractions for which intervals were computed.
172                     in the same order as in input `coverage`.
173                 Third level is string "lower" or "upper", for lower/upper interval end.
174             Row index is fh, with additional (upper) levels equal to instance levels,
175                 from y seen in fit, if y_inner_mtype is Panel or Hierarchical.
176             Entries are forecasts of lower/upper interval end,
177                 for var in col index, at nominal coverage in second col index,
178                 lower/upper depending on third col index, for the row index.
179                 Upper/lower interval end forecasts are equivalent to
180                 quantile forecasts at alpha = 0.5 - c/2, 0.5 + c/2 for c in coverage.
181         """
182         implements_interval_adapter = self._has_implementation_of("_extract_conf_int")
183         implements_quantiles = self._has_implementation_of("_predict_quantiles")
184 
185         if not implements_interval_adapter and implements_quantiles:
186             return BaseForecaster._predict_interval(self, fh, X=X, coverage=coverage)
187 
188         start, end = fh.to_absolute_int(self._y.index[0], self.cutoff)[[0, -1]]
189         valid_indices = fh.to_absolute(self.cutoff).to_pandas()
190 
191         get_prediction_arguments = {"start": start, "end": end}
192 
193         if hasattr(self, "random_state"):
194             get_prediction_arguments["random_state"] = self.random_state
195 
196         if inspect.signature(self._fitted_forecaster.get_prediction).parameters.get(
197             "exog"
198         ):
199             get_prediction_arguments["exog"] = X
200 
201         prediction_results = self._fitted_forecaster.get_prediction(
202             **get_prediction_arguments
203         )
204 
205         var_names = self._get_varnames()
206         var_name = var_names[0]
207         columns = pd.MultiIndex.from_product([var_names, coverage, ["lower", "upper"]])
208         pred_int = pd.DataFrame(index=valid_indices, columns=columns)
209 
210         for c in coverage:
211             pred_statsmodels = self._extract_conf_int(prediction_results, (1 - c))
212 
213             pred_int[(var_name, c, "lower")] = pred_statsmodels.loc[
214                 valid_indices, "lower"
215             ]
216             pred_int[(var_name, c, "upper")] = pred_statsmodels.loc[
217                 valid_indices, "upper"
218             ]
219 
220         return pred_int
221 
222     def _get_fitted_params(self):
223         """Get fitted parameters.
224 
225         Returns
226         -------
227         fitted_params : dict
228         """
229         fitted_params = {}
230         for name in self._get_fitted_param_names():
231             if name in ["aic", "aicc", "bic", "hqic"]:
232                 fitted_params[name] = getattr(self._fitted_forecaster, name, None)
233             else:
234                 fitted_params[name] = self._fitted_forecaster.params.get(name)
235         return fitted_params
236 
237     def _get_fitted_param_names(self):
238         """Get names of fitted parameters."""
239         return self._fitted_param_names
240 
241 
242 def _coerce_int_to_range_index(y, X=None):
243     new_index = pd.RangeIndex(y.index[0], y.index[-1] + 1)
244     try:
245         np.testing.assert_array_equal(y.index, new_index)
246     except AssertionError:
247         raise ValueError(
248             "Coercion of integer pd.Index to pd.RangeIndex "
249             "failed. Please provide `y_train` with a "
250             "pd.RangeIndex."
251         )
252     y.index = new_index
253     if X is not None:
254         X.index = new_index
255     return y, X
256
```
--- END FILES ---

Please first localize the bug based on the issue statement, and then generate a patch according to the `git diff` format fenced by three backticks.

Here is an example:

```diff
diff --git a/examples/server_async.py b/examples/server_async.py
--- a/examples/server_async.py
+++ b/examples/server_async.py
@@ -313,4 +313,4 @@
 
 
 if __name__ == "__main__":
-    asyncio.run(run_async_server("."), debug=True)
+    asyncio.run(run_async_server(), debug=True)
diff --git a/examples/server_sync.py b/examples/server_sync.py
--- a/examples/server_sync.py
+++ b/examples/server_sync.py
@@ -313,5 +313,5 @@
 
 
 if __name__ == "__main__":
-    server = run_sync_server(".")
+    server = run_sync_server()
     server.shutdown()

```

diff --git a/sktime/forecasting/base/adapters/_statsmodels.py b/sktime/forecasting/base/adapters/_statsmodels.py
--- a/sktime/forecasting/base/adapters/_statsmodels.py
+++ b/sktime/forecasting/base/adapters/_statsmodels.py
@@ -2,7 +2,7 @@
 # copyright: sktime developers, BSD-3-Clause License (see LICENSE file)
 """Implements adapter for statsmodels forecasters to be used in sktime framework."""
 
-__author__ = ["mloning"]
+__author__ = ["mloning", "ciaran-g"]
 __all__ = ["_StatsModelsAdapter"]
 
 import inspect
@@ -99,7 +99,7 @@
         # statsmodels requires zero-based indexing starting at the
         # beginning of the training series when passing integers
         start, end = fh.to_absolute_int(self._y.index[0], self.cutoff)[[0, -1]]
-        fh_abs = fh.to_absolute_index(self.cutoff)
+        fh_int = fh.to_absolute_int(self._y.index[0], self.cutoff) - len(self._y)
 
         # bug fix for evaluate function as test_plus_train indices are passed
         # statsmodels exog must contain test indices only.
@@ -108,7 +108,7 @@
             ind_drop = self._X.index
             X = X.loc[~X.index.isin(ind_drop)]
             # Entire range of the forecast horizon is required
-            X = X[: fh_abs[-1]]
+            X = X.iloc[: (fh_int[-1] + 1)]  # include end point
 
         if "exog" in inspect.signature(self._forecaster.__init__).parameters.keys():
             y_pred = self._fitted_forecaster.predict(start=start, end=end, exog=X)
@@ -117,7 +117,9 @@
 
         # statsmodels forecasts all periods from start to end of forecasting
         # horizon, but only return given time points in forecasting horizon
-        y_pred = y_pred.loc[fh_abs]
+        # if fh[0] > 1 steps ahead of cutoff then make relative to `start`
+        fh_int = fh_int - fh_int[0]
+        y_pred = y_pred.iloc[fh_int]
         # ensure that name is not added nor removed
         # otherwise this may upset conversion to pd.DataFrame
         y_pred.name = self._y.name
@@ -186,7 +188,9 @@
             return BaseForecaster._predict_interval(self, fh, X=X, coverage=coverage)
 
         start, end = fh.to_absolute_int(self._y.index[0], self.cutoff)[[0, -1]]
-        valid_indices = fh.to_absolute(self.cutoff).to_pandas()
+        fh_int = fh.to_absolute_int(self._y.index[0], self.cutoff) - len(self._y)
+        # if fh > 1 steps ahead of cutoff
+        fh_int = fh_int - fh_int[0]
 
         get_prediction_arguments = {"start": start, "end": end}
 
@@ -205,17 +209,15 @@
         var_names = self._get_varnames()
         var_name = var_names[0]
         columns = pd.MultiIndex.from_product([var_names, coverage, ["lower", "upper"]])
-        pred_int = pd.DataFrame(index=valid_indices, columns=columns)
+        preds_index = self._extract_conf_int(prediction_results, (1 - coverage[0]))
+        preds_index = preds_index.iloc[fh_int].index
+        pred_int = pd.DataFrame(index=preds_index, columns=columns)
 
         for c in coverage:
             pred_statsmodels = self._extract_conf_int(prediction_results, (1 - c))
 
-            pred_int[(var_name, c, "lower")] = pred_statsmodels.loc[
-                valid_indices, "lower"
-            ]
-            pred_int[(var_name, c, "upper")] = pred_statsmodels.loc[
-                valid_indices, "upper"
-            ]
+            pred_int[(var_name, c, "lower")] = pred_statsmodels.iloc[fh_int]["lower"]
+            pred_int[(var_name, c, "upper")] = pred_statsmodels.iloc[fh_int]["upper"]
 
         return pred_int

{"golden_diff": "diff --git a/sktime/forecasting/base/adapters/_statsmodels.py b/sktime/forecasting/base/adapters/_statsmodels.py\n--- a/sktime/forecasting/base/adapters/_statsmodels.py\n+++ b/sktime/forecasting/base/adapters/_statsmodels.py\n@@ -2,7 +2,7 @@\n # copyright: sktime developers, BSD-3-Clause License (see LICENSE file)\n \"\"\"Implements adapter for statsmodels forecasters to be used in sktime framework.\"\"\"\n \n-__author__ = [\"mloning\"]\n+__author__ = [\"mloning\", \"ciaran-g\"]\n __all__ = [\"_StatsModelsAdapter\"]\n \n import inspect\n@@ -99,7 +99,7 @@\n         # statsmodels requires zero-based indexing starting at the\n         # beginning of the training series when passing integers\n         start, end = fh.to_absolute_int(self._y.index[0], self.cutoff)[[0, -1]]\n-        fh_abs = fh.to_absolute_index(self.cutoff)\n+        fh_int = fh.to_absolute_int(self._y.index[0], self.cutoff) - len(self._y)\n \n         # bug fix for evaluate function as test_plus_train indices are passed\n         # statsmodels exog must contain test indices only.\n@@ -108,7 +108,7 @@\n             ind_drop = self._X.index\n             X = X.loc[~X.index.isin(ind_drop)]\n             # Entire range of the forecast horizon is required\n-            X = X[: fh_abs[-1]]\n+            X = X.iloc[: (fh_int[-1] + 1)]  # include end point\n \n         if \"exog\" in inspect.signature(self._forecaster.__init__).parameters.keys():\n             y_pred = self._fitted_forecaster.predict(start=start, end=end, exog=X)\n@@ -117,7 +117,9 @@\n \n         # statsmodels forecasts all periods from start to end of forecasting\n         # horizon, but only return given time points in forecasting horizon\n-        y_pred = y_pred.loc[fh_abs]\n+        # if fh[0] > 1 steps ahead of cutoff then make relative to `start`\n+        fh_int = fh_int - fh_int[0]\n+        y_pred = y_pred.iloc[fh_int]\n         # ensure that name is not added nor removed\n         # otherwise this may upset conversion to pd.DataFrame\n         y_pred.name = self._y.name\n@@ -186,7 +188,9 @@\n             return BaseForecaster._predict_interval(self, fh, X=X, coverage=coverage)\n \n         start, end = fh.to_absolute_int(self._y.index[0], self.cutoff)[[0, -1]]\n-        valid_indices = fh.to_absolute(self.cutoff).to_pandas()\n+        fh_int = fh.to_absolute_int(self._y.index[0], self.cutoff) - len(self._y)\n+        # if fh > 1 steps ahead of cutoff\n+        fh_int = fh_int - fh_int[0]\n \n         get_prediction_arguments = {\"start\": start, \"end\": end}\n \n@@ -205,17 +209,15 @@\n         var_names = self._get_varnames()\n         var_name = var_names[0]\n         columns = pd.MultiIndex.from_product([var_names, coverage, [\"lower\", \"upper\"]])\n-        pred_int = pd.DataFrame(index=valid_indices, columns=columns)\n+        preds_index = self._extract_conf_int(prediction_results, (1 - coverage[0]))\n+        preds_index = preds_index.iloc[fh_int].index\n+        pred_int = pd.DataFrame(index=preds_index, columns=columns)\n \n         for c in coverage:\n             pred_statsmodels = self._extract_conf_int(prediction_results, (1 - c))\n \n-            pred_int[(var_name, c, \"lower\")] = pred_statsmodels.loc[\n-                valid_indices, \"lower\"\n-            ]\n-            pred_int[(var_name, c, \"upper\")] = pred_statsmodels.loc[\n-                valid_indices, \"upper\"\n-            ]\n+            pred_int[(var_name, c, \"lower\")] = pred_statsmodels.iloc[fh_int][\"lower\"]\n+            pred_int[(var_name, c, \"upper\")] = pred_statsmodels.iloc[fh_int][\"upper\"]\n \n         return pred_int\n", "issue": "[BUG] convert to period bug in `_StatsModelsAdapter`\n**Describe the bug**\r\n<!--\r\nA clear and concise description of what the bug is.\r\n-->\r\n\r\nFor panel data with datetime indexes, converting datetimes to period index causes an error in the predict stage in the `_StatsModelsAdapter` models\r\n\r\n**To Reproduce**\r\n<!--\r\nAdd a Minimal, Complete, and Verifiable example (for more details, see e.g. https://stackoverflow.com/help/mcve\r\n\r\nIf the code is too long, feel free to put it in a public gist and link it in the issue: https://gist.github.com\r\n-->\r\n\r\n```python\r\nfrom sktime.datasets import load_airline\r\nfrom sktime.forecasting.exp_smoothing import ExponentialSmoothing\r\nimport pandas as pd\r\ny = load_airline()\r\n\r\n# create dummy index with hourly timestamps and panel data by hour of day\r\ny.index = pd.date_range(start='1960-01-01', periods=len(y.index), freq='H')\r\ny.index.names = [\"datetime\"]\r\ny.name = \"passengers\"\r\ny = y.to_frame()\r\ny['hour_of_day'] = y.index.hour\r\ny = y.reset_index().set_index(['hour_of_day', 'datetime']).sort_index()\r\n\r\nforecaster = ExponentialSmoothing(\r\n    trend='add', sp=1\r\n)  \r\nforecaster.fit(y)\r\nforecaster.predict(fh=[1])\r\n\r\n```\r\n\r\n**Expected behavior**\r\n<!--\r\nA clear and concise description of what you expected to happen.\r\n-->\r\n\r\nFor each panel predict one step ahead\r\n\r\n**Additional context**\r\n<!--\r\nAdd any other context about the problem here.\r\n-->\r\n\r\nI will address this issue \ud83d\udc4d \r\n\r\n**Versions**\r\n<details>\r\n\r\n<!--\r\nPlease run the following code snippet and paste the output here:\r\n\r\nfrom sktime import show_versions; show_versions()\r\n-->\r\n\r\nPython dependencies:\r\n          pip: 23.1.2\r\n       sktime: 0.24.1\r\n      sklearn: 1.2.2\r\n       skbase: 0.6.1\r\n        numpy: 1.24.3\r\n        scipy: 1.10.1\r\n       pandas: 2.0.2\r\n   matplotlib: None\r\n       joblib: 1.2.0\r\n        numba: None\r\n  statsmodels: 0.14.0\r\n     pmdarima: 2.0.3\r\nstatsforecast: None\r\n      tsfresh: None\r\n      tslearn: None\r\n        torch: None\r\n   tensorflow: None\r\ntensorflow_probability: None\r\n\r\n<!-- Thanks for contributing! -->\r\n\n", "before_files": [{"content": "# !/usr/bin/env python3 -u\n# copyright: sktime developers, BSD-3-Clause License (see LICENSE file)\n\"\"\"Implements adapter for statsmodels forecasters to be used in sktime framework.\"\"\"\n\n__author__ = [\"mloning\"]\n__all__ = [\"_StatsModelsAdapter\"]\n\nimport inspect\n\nimport numpy as np\nimport pandas as pd\n\nfrom sktime.forecasting.base import BaseForecaster\nfrom sktime.utils.warnings import warn\n\n\nclass _StatsModelsAdapter(BaseForecaster):\n    \"\"\"Base class for interfacing statsmodels forecasting algorithms.\"\"\"\n\n    _fitted_param_names = ()\n    _tags = {\n        \"ignores-exogeneous-X\": True,\n        \"requires-fh-in-fit\": False,\n        \"handles-missing-data\": False,\n        \"python_dependencies\": \"statsmodels\",\n    }\n\n    def __init__(self, random_state=None):\n        self._forecaster = None\n        self.random_state = random_state\n        self._fitted_forecaster = None\n        super().__init__()\n\n    def _fit(self, y, X, fh):\n        \"\"\"Fit to training data.\n\n        Parameters\n        ----------\n        y : pd.Series\n            Target time series to which to fit the forecaster.\n        fh : int, list or np.array, optional (default=None)\n            The forecasters horizon with the steps ahead to to predict.\n        X : pd.DataFrame, optional (default=None)\n            Exogenous variables are ignored\n\n        Returns\n        -------\n        self : returns an instance of self.\n        \"\"\"\n        # statsmodels does not support the pd.Int64Index as required,\n        # so we coerce them here to pd.RangeIndex\n        if isinstance(y, pd.Series) and pd.api.types.is_integer_dtype(y.index):\n            y, X = _coerce_int_to_range_index(y, X)\n        self._fit_forecaster(y, X)\n        return self\n\n    def _fit_forecaster(self, y_train, X_train=None):\n        \"\"\"Log used internally in fit.\"\"\"\n        raise NotImplementedError(\"abstract method\")\n\n    def _update(self, y, X=None, update_params=True):\n        \"\"\"Update used internally in update.\"\"\"\n        if update_params or self.is_composite():\n            super()._update(y, X, update_params=update_params)\n        else:\n            if not hasattr(self._fitted_forecaster, \"append\"):\n                warn(\n                    f\"NotImplementedWarning: {self.__class__.__name__} \"\n                    f\"can not accept new data when update_params=False. \"\n                    f\"Call with update_params=True to refit with new data.\",\n                    obj=self,\n                )\n            else:\n                # only append unseen data to fitted forecaster\n                index_diff = y.index.difference(\n                    self._fitted_forecaster.fittedvalues.index\n                )\n                if index_diff.isin(y.index).all():\n                    y = y.loc[index_diff]\n                self._fitted_forecaster = self._fitted_forecaster.append(y)\n\n    def _predict(self, fh, X):\n        \"\"\"Make forecasts.\n\n        Parameters\n        ----------\n        fh : ForecastingHorizon\n            The forecasters horizon with the steps ahead to to predict.\n            Default is one-step ahead forecast,\n            i.e. np.array([1])\n        X : pd.DataFrame, optional (default=None)\n            Exogenous variables are ignored.\n\n        Returns\n        -------\n        y_pred : pd.Series\n            Returns series of predicted values.\n        \"\"\"\n        # statsmodels requires zero-based indexing starting at the\n        # beginning of the training series when passing integers\n        start, end = fh.to_absolute_int(self._y.index[0], self.cutoff)[[0, -1]]\n        fh_abs = fh.to_absolute_index(self.cutoff)\n\n        # bug fix for evaluate function as test_plus_train indices are passed\n        # statsmodels exog must contain test indices only.\n        # For discussion see https://github.com/sktime/sktime/issues/3830\n        if X is not None:\n            ind_drop = self._X.index\n            X = X.loc[~X.index.isin(ind_drop)]\n            # Entire range of the forecast horizon is required\n            X = X[: fh_abs[-1]]\n\n        if \"exog\" in inspect.signature(self._forecaster.__init__).parameters.keys():\n            y_pred = self._fitted_forecaster.predict(start=start, end=end, exog=X)\n        else:\n            y_pred = self._fitted_forecaster.predict(start=start, end=end)\n\n        # statsmodels forecasts all periods from start to end of forecasting\n        # horizon, but only return given time points in forecasting horizon\n        y_pred = y_pred.loc[fh_abs]\n        # ensure that name is not added nor removed\n        # otherwise this may upset conversion to pd.DataFrame\n        y_pred.name = self._y.name\n        return y_pred\n\n    @staticmethod\n    def _extract_conf_int(prediction_results, alpha) -> pd.DataFrame:\n        \"\"\"Construct confidence interval at specified `alpha` for each timestep.\n\n        Parameters\n        ----------\n        prediction_results : PredictionResults\n            results class, as returned by ``self._fitted_forecaster.get_prediction``\n        alpha : float\n            one minus nominal coverage\n\n        Returns\n        -------\n        pd.DataFrame\n            confidence intervals at each timestep\n\n            The dataframe must have at least two columns ``lower`` and ``upper``, and\n            the row indices must be integers relative to ``self.cutoff``. Order of\n            columns do not matter, and row indices must be a superset of relative\n            integer horizon of ``fh``.\n        \"\"\"\n        del prediction_results, alpha  # tools like ``vulture`` may complain as unused\n\n        raise NotImplementedError(\"abstract method\")\n\n    def _predict_interval(self, fh, X, coverage):\n        \"\"\"Compute/return prediction interval forecasts.\n\n        private _predict_interval containing the core logic,\n            called from predict_interval and default _predict_quantiles\n\n        Parameters\n        ----------\n        fh : guaranteed to be ForecastingHorizon\n            The forecasting horizon with the steps ahead to to predict.\n        X : optional (default=None)\n            guaranteed to be of a type in self.get_tag(\"X_inner_mtype\")\n            Exogeneous time series to predict from.\n        coverage : float or list of float, optional (default=0.95)\n           nominal coverage(s) of predictive interval(s)\n\n        Returns\n        -------\n        pred_int : pd.DataFrame\n            Column has multi-index: first level is variable name from y in fit,\n                second level coverage fractions for which intervals were computed.\n                    in the same order as in input `coverage`.\n                Third level is string \"lower\" or \"upper\", for lower/upper interval end.\n            Row index is fh, with additional (upper) levels equal to instance levels,\n                from y seen in fit, if y_inner_mtype is Panel or Hierarchical.\n            Entries are forecasts of lower/upper interval end,\n                for var in col index, at nominal coverage in second col index,\n                lower/upper depending on third col index, for the row index.\n                Upper/lower interval end forecasts are equivalent to\n                quantile forecasts at alpha = 0.5 - c/2, 0.5 + c/2 for c in coverage.\n        \"\"\"\n        implements_interval_adapter = self._has_implementation_of(\"_extract_conf_int\")\n        implements_quantiles = self._has_implementation_of(\"_predict_quantiles\")\n\n        if not implements_interval_adapter and implements_quantiles:\n            return BaseForecaster._predict_interval(self, fh, X=X, coverage=coverage)\n\n        start, end = fh.to_absolute_int(self._y.index[0], self.cutoff)[[0, -1]]\n        valid_indices = fh.to_absolute(self.cutoff).to_pandas()\n\n        get_prediction_arguments = {\"start\": start, \"end\": end}\n\n        if hasattr(self, \"random_state\"):\n            get_prediction_arguments[\"random_state\"] = self.random_state\n\n        if inspect.signature(self._fitted_forecaster.get_prediction).parameters.get(\n            \"exog\"\n        ):\n            get_prediction_arguments[\"exog\"] = X\n\n        prediction_results = self._fitted_forecaster.get_prediction(\n            **get_prediction_arguments\n        )\n\n        var_names = self._get_varnames()\n        var_name = var_names[0]\n        columns = pd.MultiIndex.from_product([var_names, coverage, [\"lower\", \"upper\"]])\n        pred_int = pd.DataFrame(index=valid_indices, columns=columns)\n\n        for c in coverage:\n            pred_statsmodels = self._extract_conf_int(prediction_results, (1 - c))\n\n            pred_int[(var_name, c, \"lower\")] = pred_statsmodels.loc[\n                valid_indices, \"lower\"\n            ]\n            pred_int[(var_name, c, \"upper\")] = pred_statsmodels.loc[\n                valid_indices, \"upper\"\n            ]\n\n        return pred_int\n\n    def _get_fitted_params(self):\n        \"\"\"Get fitted parameters.\n\n        Returns\n        -------\n        fitted_params : dict\n        \"\"\"\n        fitted_params = {}\n        for name in self._get_fitted_param_names():\n            if name in [\"aic\", \"aicc\", \"bic\", \"hqic\"]:\n                fitted_params[name] = getattr(self._fitted_forecaster, name, None)\n            else:\n                fitted_params[name] = self._fitted_forecaster.params.get(name)\n        return fitted_params\n\n    def _get_fitted_param_names(self):\n        \"\"\"Get names of fitted parameters.\"\"\"\n        return self._fitted_param_names\n\n\ndef _coerce_int_to_range_index(y, X=None):\n    new_index = pd.RangeIndex(y.index[0], y.index[-1] + 1)\n    try:\n        np.testing.assert_array_equal(y.index, new_index)\n    except AssertionError:\n        raise ValueError(\n            \"Coercion of integer pd.Index to pd.RangeIndex \"\n            \"failed. Please provide `y_train` with a \"\n            \"pd.RangeIndex.\"\n        )\n    y.index = new_index\n    if X is not None:\n        X.index = new_index\n    return y, X\n", "path": "sktime/forecasting/base/adapters/_statsmodels.py"}], "after_files": [{"content": "# !/usr/bin/env python3 -u\n# copyright: sktime developers, BSD-3-Clause License (see LICENSE file)\n\"\"\"Implements adapter for statsmodels forecasters to be used in sktime framework.\"\"\"\n\n__author__ = [\"mloning\", \"ciaran-g\"]\n__all__ = [\"_StatsModelsAdapter\"]\n\nimport inspect\n\nimport numpy as np\nimport pandas as pd\n\nfrom sktime.forecasting.base import BaseForecaster\nfrom sktime.utils.warnings import warn\n\n\nclass _StatsModelsAdapter(BaseForecaster):\n    \"\"\"Base class for interfacing statsmodels forecasting algorithms.\"\"\"\n\n    _fitted_param_names = ()\n    _tags = {\n        \"ignores-exogeneous-X\": True,\n        \"requires-fh-in-fit\": False,\n        \"handles-missing-data\": False,\n        \"python_dependencies\": \"statsmodels\",\n    }\n\n    def __init__(self, random_state=None):\n        self._forecaster = None\n        self.random_state = random_state\n        self._fitted_forecaster = None\n        super().__init__()\n\n    def _fit(self, y, X, fh):\n        \"\"\"Fit to training data.\n\n        Parameters\n        ----------\n        y : pd.Series\n            Target time series to which to fit the forecaster.\n        fh : int, list or np.array, optional (default=None)\n            The forecasters horizon with the steps ahead to to predict.\n        X : pd.DataFrame, optional (default=None)\n            Exogenous variables are ignored\n\n        Returns\n        -------\n        self : returns an instance of self.\n        \"\"\"\n        # statsmodels does not support the pd.Int64Index as required,\n        # so we coerce them here to pd.RangeIndex\n        if isinstance(y, pd.Series) and pd.api.types.is_integer_dtype(y.index):\n            y, X = _coerce_int_to_range_index(y, X)\n        self._fit_forecaster(y, X)\n        return self\n\n    def _fit_forecaster(self, y_train, X_train=None):\n        \"\"\"Log used internally in fit.\"\"\"\n        raise NotImplementedError(\"abstract method\")\n\n    def _update(self, y, X=None, update_params=True):\n        \"\"\"Update used internally in update.\"\"\"\n        if update_params or self.is_composite():\n            super()._update(y, X, update_params=update_params)\n        else:\n            if not hasattr(self._fitted_forecaster, \"append\"):\n                warn(\n                    f\"NotImplementedWarning: {self.__class__.__name__} \"\n                    f\"can not accept new data when update_params=False. \"\n                    f\"Call with update_params=True to refit with new data.\",\n                    obj=self,\n                )\n            else:\n                # only append unseen data to fitted forecaster\n                index_diff = y.index.difference(\n                    self._fitted_forecaster.fittedvalues.index\n                )\n                if index_diff.isin(y.index).all():\n                    y = y.loc[index_diff]\n                self._fitted_forecaster = self._fitted_forecaster.append(y)\n\n    def _predict(self, fh, X):\n        \"\"\"Make forecasts.\n\n        Parameters\n        ----------\n        fh : ForecastingHorizon\n            The forecasters horizon with the steps ahead to to predict.\n            Default is one-step ahead forecast,\n            i.e. np.array([1])\n        X : pd.DataFrame, optional (default=None)\n            Exogenous variables are ignored.\n\n        Returns\n        -------\n        y_pred : pd.Series\n            Returns series of predicted values.\n        \"\"\"\n        # statsmodels requires zero-based indexing starting at the\n        # beginning of the training series when passing integers\n        start, end = fh.to_absolute_int(self._y.index[0], self.cutoff)[[0, -1]]\n        fh_int = fh.to_absolute_int(self._y.index[0], self.cutoff) - len(self._y)\n\n        # bug fix for evaluate function as test_plus_train indices are passed\n        # statsmodels exog must contain test indices only.\n        # For discussion see https://github.com/sktime/sktime/issues/3830\n        if X is not None:\n            ind_drop = self._X.index\n            X = X.loc[~X.index.isin(ind_drop)]\n            # Entire range of the forecast horizon is required\n            X = X.iloc[: (fh_int[-1] + 1)]  # include end point\n\n        if \"exog\" in inspect.signature(self._forecaster.__init__).parameters.keys():\n            y_pred = self._fitted_forecaster.predict(start=start, end=end, exog=X)\n        else:\n            y_pred = self._fitted_forecaster.predict(start=start, end=end)\n\n        # statsmodels forecasts all periods from start to end of forecasting\n        # horizon, but only return given time points in forecasting horizon\n        # if fh[0] > 1 steps ahead of cutoff then make relative to `start`\n        fh_int = fh_int - fh_int[0]\n        y_pred = y_pred.iloc[fh_int]\n        # ensure that name is not added nor removed\n        # otherwise this may upset conversion to pd.DataFrame\n        y_pred.name = self._y.name\n        return y_pred\n\n    @staticmethod\n    def _extract_conf_int(prediction_results, alpha) -> pd.DataFrame:\n        \"\"\"Construct confidence interval at specified `alpha` for each timestep.\n\n        Parameters\n        ----------\n        prediction_results : PredictionResults\n            results class, as returned by ``self._fitted_forecaster.get_prediction``\n        alpha : float\n            one minus nominal coverage\n\n        Returns\n        -------\n        pd.DataFrame\n            confidence intervals at each timestep\n\n            The dataframe must have at least two columns ``lower`` and ``upper``, and\n            the row indices must be integers relative to ``self.cutoff``. Order of\n            columns do not matter, and row indices must be a superset of relative\n            integer horizon of ``fh``.\n        \"\"\"\n        del prediction_results, alpha  # tools like ``vulture`` may complain as unused\n\n        raise NotImplementedError(\"abstract method\")\n\n    def _predict_interval(self, fh, X, coverage):\n        \"\"\"Compute/return prediction interval forecasts.\n\n        private _predict_interval containing the core logic,\n            called from predict_interval and default _predict_quantiles\n\n        Parameters\n        ----------\n        fh : guaranteed to be ForecastingHorizon\n            The forecasting horizon with the steps ahead to to predict.\n        X : optional (default=None)\n            guaranteed to be of a type in self.get_tag(\"X_inner_mtype\")\n            Exogeneous time series to predict from.\n        coverage : float or list of float, optional (default=0.95)\n           nominal coverage(s) of predictive interval(s)\n\n        Returns\n        -------\n        pred_int : pd.DataFrame\n            Column has multi-index: first level is variable name from y in fit,\n                second level coverage fractions for which intervals were computed.\n                    in the same order as in input `coverage`.\n                Third level is string \"lower\" or \"upper\", for lower/upper interval end.\n            Row index is fh, with additional (upper) levels equal to instance levels,\n                from y seen in fit, if y_inner_mtype is Panel or Hierarchical.\n            Entries are forecasts of lower/upper interval end,\n                for var in col index, at nominal coverage in second col index,\n                lower/upper depending on third col index, for the row index.\n                Upper/lower interval end forecasts are equivalent to\n                quantile forecasts at alpha = 0.5 - c/2, 0.5 + c/2 for c in coverage.\n        \"\"\"\n        implements_interval_adapter = self._has_implementation_of(\"_extract_conf_int\")\n        implements_quantiles = self._has_implementation_of(\"_predict_quantiles\")\n\n        if not implements_interval_adapter and implements_quantiles:\n            return BaseForecaster._predict_interval(self, fh, X=X, coverage=coverage)\n\n        start, end = fh.to_absolute_int(self._y.index[0], self.cutoff)[[0, -1]]\n        fh_int = fh.to_absolute_int(self._y.index[0], self.cutoff) - len(self._y)\n        # if fh > 1 steps ahead of cutoff\n        fh_int = fh_int - fh_int[0]\n\n        get_prediction_arguments = {\"start\": start, \"end\": end}\n\n        if hasattr(self, \"random_state\"):\n            get_prediction_arguments[\"random_state\"] = self.random_state\n\n        if inspect.signature(self._fitted_forecaster.get_prediction).parameters.get(\n            \"exog\"\n        ):\n            get_prediction_arguments[\"exog\"] = X\n\n        prediction_results = self._fitted_forecaster.get_prediction(\n            **get_prediction_arguments\n        )\n\n        var_names = self._get_varnames()\n        var_name = var_names[0]\n        columns = pd.MultiIndex.from_product([var_names, coverage, [\"lower\", \"upper\"]])\n        preds_index = self._extract_conf_int(prediction_results, (1 - coverage[0]))\n        preds_index = preds_index.iloc[fh_int].index\n        pred_int = pd.DataFrame(index=preds_index, columns=columns)\n\n        for c in coverage:\n            pred_statsmodels = self._extract_conf_int(prediction_results, (1 - c))\n\n            pred_int[(var_name, c, \"lower\")] = pred_statsmodels.iloc[fh_int][\"lower\"]\n            pred_int[(var_name, c, \"upper\")] = pred_statsmodels.iloc[fh_int][\"upper\"]\n\n        return pred_int\n\n    def _get_fitted_params(self):\n        \"\"\"Get fitted parameters.\n\n        Returns\n        -------\n        fitted_params : dict\n        \"\"\"\n        fitted_params = {}\n        for name in self._get_fitted_param_names():\n            if name in [\"aic\", \"aicc\", \"bic\", \"hqic\"]:\n                fitted_params[name] = getattr(self._fitted_forecaster, name, None)\n            else:\n                fitted_params[name] = self._fitted_forecaster.params.get(name)\n        return fitted_params\n\n    def _get_fitted_param_names(self):\n        \"\"\"Get names of fitted parameters.\"\"\"\n        return self._fitted_param_names\n\n\ndef _coerce_int_to_range_index(y, X=None):\n    new_index = pd.RangeIndex(y.index[0], y.index[-1] + 1)\n    try:\n        np.testing.assert_array_equal(y.index, new_index)\n    except AssertionError:\n        raise ValueError(\n            \"Coercion of integer pd.Index to pd.RangeIndex \"\n            \"failed. Please provide `y_train` with a \"\n            \"pd.RangeIndex.\"\n        )\n    y.index = new_index\n    if X is not None:\n        X.index = new_index\n    return y, X\n", "path": "sktime/forecasting/base/adapters/_statsmodels.py"}]}