We are currently solving the following issue within our repository. Here is the issue text:
--- BEGIN ISSUE ---
[BUG] `SARIMAX` fails when `X` is passed in `predict` but not used in `fit`
## Minimal Reproducible Example

```pycon
>>> 
>>> from sktime.datasets import load_longley
>>> from sktime.forecasting.sarimax import SARIMAX
>>> from sktime.split import temporal_train_test_split
>>> 
>>> y, X = load_longley()
>>> 
>>> y_train, _, _, X_test = temporal_train_test_split(y, X)
>>> 
>>> forecaster = SARIMAX()
>>> 
>>> forecaster.fit(y_train)
SARIMAX()
>>> 
>>> # works
>>> forecaster.predict(fh=[1, 2, 3, 4])
1959    66061.176439
1960    65682.034815
1961    65363.883253
1962    65096.910677
Freq: A-DEC, Name: TOTEMP, dtype: float64
>>> 
>>> # fails
>>> forecaster.predict(fh=[1, 2, 3, 4], X=X_test)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "/home/anirban/sktime-fork/sktime/forecasting/base/_base.py", line 412, in predict
    y_pred = self._predict(fh=fh, X=X_inner)
  File "/home/anirban/sktime-fork/sktime/forecasting/base/adapters/_statsmodels.py", line 108, in _predict
    ind_drop = self._X.index
AttributeError: 'NoneType' object has no attribute 'index'
>>> 
```

## Expectation

I was expecting no failures, and identical behaviour in both cases. I do get that behaviour if I try with `sktime.forecasting.arima.ARIMA`.

## Version

Operating System: Ubuntu 22.04.3 LTS (WSL)
Python: 3.10.12
Sktime: e51ec2472a


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

{"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@@ -110,13 +110,15 @@\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+        if X is not None and self._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+            if self._X is None:\n+                X = None  # change X passed in predict to None if X wasn't passed to fit\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", "issue": "[BUG] `SARIMAX` fails when `X` is passed in `predict` but not used in `fit`\n## Minimal Reproducible Example\r\n\r\n```pycon\r\n>>> \r\n>>> from sktime.datasets import load_longley\r\n>>> from sktime.forecasting.sarimax import SARIMAX\r\n>>> from sktime.split import temporal_train_test_split\r\n>>> \r\n>>> y, X = load_longley()\r\n>>> \r\n>>> y_train, _, _, X_test = temporal_train_test_split(y, X)\r\n>>> \r\n>>> forecaster = SARIMAX()\r\n>>> \r\n>>> forecaster.fit(y_train)\r\nSARIMAX()\r\n>>> \r\n>>> # works\r\n>>> forecaster.predict(fh=[1, 2, 3, 4])\r\n1959    66061.176439\r\n1960    65682.034815\r\n1961    65363.883253\r\n1962    65096.910677\r\nFreq: A-DEC, Name: TOTEMP, dtype: float64\r\n>>> \r\n>>> # fails\r\n>>> forecaster.predict(fh=[1, 2, 3, 4], X=X_test)\r\nTraceback (most recent call last):\r\n  File \"<stdin>\", line 1, in <module>\r\n  File \"/home/anirban/sktime-fork/sktime/forecasting/base/_base.py\", line 412, in predict\r\n    y_pred = self._predict(fh=fh, X=X_inner)\r\n  File \"/home/anirban/sktime-fork/sktime/forecasting/base/adapters/_statsmodels.py\", line 108, in _predict\r\n    ind_drop = self._X.index\r\nAttributeError: 'NoneType' object has no attribute 'index'\r\n>>> \r\n```\r\n\r\n## Expectation\r\n\r\nI was expecting no failures, and identical behaviour in both cases. I do get that behaviour if I try with `sktime.forecasting.arima.ARIMA`.\r\n\r\n## Version\r\n\r\nOperating System: Ubuntu 22.04.3 LTS (WSL)\r\nPython: 3.10.12\r\nSktime: e51ec2472a\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\", \"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        # packaging info\n        # --------------\n        \"authors\": [\"mloning\", \"ciaran-g\"],\n        \"maintainers\": [\"ciaran-g\"],\n        \"python_dependencies\": \"statsmodels\",\n        # estimator type\n        # --------------\n        \"ignores-exogeneous-X\": True,\n        \"requires-fh-in-fit\": False,\n        \"handles-missing-data\": False,\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"}], "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        # packaging info\n        # --------------\n        \"authors\": [\"mloning\", \"ciaran-g\"],\n        \"maintainers\": [\"ciaran-g\"],\n        \"python_dependencies\": \"statsmodels\",\n        # estimator type\n        # --------------\n        \"ignores-exogeneous-X\": True,\n        \"requires-fh-in-fit\": False,\n        \"handles-missing-data\": False,\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 and self._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            if self._X is None:\n                X = None  # change X passed in predict to None if X wasn't passed to fit\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"}]}