Repository: sktime/sktime

Issue:
[BUG] BATS/TBATS In-sample error
**Describe the bug**
Get reference error when insample predictions are done for BATS and TBATS.

**To Reproduce**
See [code gist here](https://gist.github.com/ngupta23/32856ac7116179fbfe95cea3c1e9ad25)

```python


/usr/local/lib/python3.7/dist-packages/sktime/forecasting/base/_base.py:220: FutureWarning: return_pred_int in predict() will be deprecated;please use predict_interval() instead to generate prediction intervals.
  FutureWarning,

---------------------------------------------------------------------------
UnboundLocalError                         Traceback (most recent call last)
<ipython-input-12-0d389f978f91> in <module>()
      1 import numpy as np
----> 2 forecaster.predict(fh=-np.arange(0, len(y)), return_pred_int=True)

/usr/local/lib/python3.7/dist-packages/sktime/forecasting/base/_base.py in predict(self, fh, X, return_pred_int, alpha, keep_old_return_type)
    227                     X=X_inner,
    228                     return_pred_int=return_pred_int,
--> 229                     alpha=alpha,
    230                 )
    231 

/usr/local/lib/python3.7/dist-packages/sktime/forecasting/base/adapters/_tbats.py in _predict(self, fh, X, return_pred_int, alpha)
    136 
    137         if return_pred_int:
--> 138             return y_pred, pred_int
    139         else:
    140             return y_pred

UnboundLocalError: local variable 'pred_int' referenced before assignment


```

**Expected behavior**
Out of sample works ok. Only insample seems to be an issue.

**Versions**

sktime = 0.8.1



[BUG] BATS/TBATS In-sample error
**Describe the bug**
Get reference error when insample predictions are done for BATS and TBATS.

**To Reproduce**
See [code gist here](https://gist.github.com/ngupta23/32856ac7116179fbfe95cea3c1e9ad25)

```python


/usr/local/lib/python3.7/dist-packages/sktime/forecasting/base/_base.py:220: FutureWarning: return_pred_int in predict() will be deprecated;please use predict_interval() instead to generate prediction intervals.
  FutureWarning,

---------------------------------------------------------------------------
UnboundLocalError                         Traceback (most recent call last)
<ipython-input-12-0d389f978f91> in <module>()
      1 import numpy as np
----> 2 forecaster.predict(fh=-np.arange(0, len(y)), return_pred_int=True)

/usr/local/lib/python3.7/dist-packages/sktime/forecasting/base/_base.py in predict(self, fh, X, return_pred_int, alpha, keep_old_return_type)
    227                     X=X_inner,
    228                     return_pred_int=return_pred_int,
--> 229                     alpha=alpha,
    230                 )
    231 

/usr/local/lib/python3.7/dist-packages/sktime/forecasting/base/adapters/_tbats.py in _predict(self, fh, X, return_pred_int, alpha)
    136 
    137         if return_pred_int:
--> 138             return y_pred, pred_int
    139         else:
    140             return y_pred

UnboundLocalError: local variable 'pred_int' referenced before assignment


```

**Expected behavior**
Out of sample works ok. Only insample seems to be an issue.

**Versions**

sktime = 0.8.1





Discussion:
I'm able to reproduce it on Windows (Python 3.8). Will be looking into it.
Since [_TbatsAdapter](https://github.com/alan-turing-institute/sktime/blob/main/sktime/forecasting/base/adapters/_tbats.py) has old interface. At the same time will refactor to respect current @kejsitake [template](https://github.com/alan-turing-institute/sktime/blob/f68f92e927a05e2e09cd03e01c9dcd78563ea9b2/extension_templates/forecasting.py#L222)
I'm able to reproduce it on Windows (Python 3.8). Will be looking into it.
Since [_TbatsAdapter](https://github.com/alan-turing-institute/sktime/blob/main/sktime/forecasting/base/adapters/_tbats.py) has old interface. At the same time will refactor to respect current @kejsitake [template](https://github.com/alan-turing-institute/sktime/blob/f68f92e927a05e2e09cd03e01c9dcd78563ea9b2/extension_templates/forecasting.py#L222)

Resolving patch:
diff --git a/sktime/forecasting/base/_base.py b/sktime/forecasting/base/_base.py
--- a/sktime/forecasting/base/_base.py
+++ b/sktime/forecasting/base/_base.py
@@ -792,7 +792,7 @@ def _check_X_y(self, X=None, y=None):
         TypeError if y is not compatible with self.get_tag("scitype:y")
             if tag value is "univariate", y must be univariate
             if tag value is "multivariate", y must be bi- or higher-variate
-            if tag vaule is "both", y can be either
+            if tag value is "both", y can be either
         TypeError if self.get_tag("X-y-must-have-same-index") is True
             and the index set of X is not a super-set of the index set of y
 
@@ -883,7 +883,7 @@ def _update_y_X(self, y, X=None, enforce_index_type=None):
 
         Parameters
         ----------
-        y : pd.Series, pd.DataFrame, or nd.nparray (1D or 2D)
+        y : pd.Series, pd.DataFrame, or np.ndarray (1D or 2D)
             Endogenous time series
         X : pd.DataFrame or 2D np.ndarray, optional (default=None)
             Exogeneous time series
@@ -1139,7 +1139,7 @@ def _fit(self, y, X=None, fh=None):
         """
         raise NotImplementedError("abstract method")
 
-    def _predict(self, fh, X=None, alpha=0.95):
+    def _predict(self, fh, X=None):
         """Forecast time series at future horizon.
 
             core logic
@@ -1235,7 +1235,7 @@ def _update_predict_single(
         self.update(y, X, update_params=update_params)
         return self.predict(fh, X, return_pred_int=return_pred_int, alpha=alpha)
 
-    def _predict_interval(self, fh=fh, X=None, coverage=0.95):
+    def _predict_interval(self, fh, X=None, coverage=0.90):
         """Compute/return prediction interval forecasts.
 
         If coverage is iterable, multiple intervals will be calculated.
diff --git a/sktime/forecasting/base/adapters/_tbats.py b/sktime/forecasting/base/adapters/_tbats.py
--- a/sktime/forecasting/base/adapters/_tbats.py
+++ b/sktime/forecasting/base/adapters/_tbats.py
@@ -3,7 +3,7 @@
 # copyright: sktime developers, BSD-3-Clause License (see LICENSE file)
 """Implements adapter for using tbats forecasters in sktime framework."""
 
-__author__ = ["mloning", "aiwalter"]
+__author__ = ["mloning", "aiwalter", "k1m190r"]
 __all__ = ["_TbatsAdapter"]
 
 import numpy as np
@@ -23,6 +23,7 @@ class _TbatsAdapter(BaseForecaster):
         "capability:pred_int": True,
         "requires-fh-in-fit": False,
         "handles-missing-data": False,
+        # "capability:predict_quantiles": True,
     }
 
     def __init__(
@@ -51,6 +52,7 @@ def __init__(
         self.context = context
         # custom sktime args
         self._forecaster = None
+        self._yname = None  # .fit(y) -> y.name
 
         super(_TbatsAdapter, self).__init__()
 
@@ -89,21 +91,23 @@ def _fit(self, y, X=None, fh=None):
         """
         self._forecaster = self._instantiate_model()
         self._forecaster = self._forecaster.fit(y)
+        self._yname = y.name
 
         return self
 
-    def _predict(self, fh, X=None, return_pred_int=False, alpha=DEFAULT_ALPHA):
+    def _predict(self, fh, X, return_pred_int=False, alpha=DEFAULT_ALPHA):
         """Forecast time series at future horizon.
 
         Parameters
         ----------
         fh : int, list, np.array or ForecastingHorizon
             Forecasting horizon
-        X : pd.DataFrame, optional (default=None)
-            Exogenous time series
+        X : (default=None)
+            NOT USED BY TBATS
         return_pred_int : bool, optional (default=False)
             If True, returns prediction intervals for given alpha values.
-        alpha : float or list, optional (default=0.95)
+        alpha : float, optional (default=0.05)
+            Interpreted as "Confidence Interval" = 1 - alpha
 
         Returns
         -------
@@ -112,32 +116,173 @@ def _predict(self, fh, X=None, return_pred_int=False, alpha=DEFAULT_ALPHA):
         y_pred_int : pd.DataFrame - only if return_pred_int=True
             Prediction intervals
         """
+        if return_pred_int:
+            return self._tbats_forecast_with_interval(fh, alpha)
+        else:
+            return self._tbats_forecast(fh)
+
+    def _tbats_forecast(self, fh):
+        """TBATS forecast without confidence interval.
+
+        Parameters
+        ----------
+        fh : int, list, np.array or ForecastingHorizon
+            Forecasting horizon
+
+        Returns
+        -------
+        y_pred : pd.Series
+            Prediction
+        """
+        fh = fh.to_relative(cutoff=self.cutoff)
+
+        if not fh.is_all_in_sample(cutoff=self.cutoff):
+            fh_out = fh.to_out_of_sample(cutoff=self.cutoff)
+            steps = fh_out.to_pandas().max()
+            y_out = self._forecaster.forecast(steps=steps, confidence_level=None)
+
+        else:
+            y_out = nans(len(fh))
+
+        # y_pred combine in and out samples
+        y_in_sample = pd.Series(self._forecaster.y_hat)
+        y_out_sample = pd.Series(y_out)
+        y_pred = self._get_y_pred(y_in_sample=y_in_sample, y_out_sample=y_out_sample)
+        y_pred.name = self._yname
+
+        return y_pred
+
+    def _tbats_forecast_with_interval(self, fh, conf_lev):
+        """TBATS forecast with confidence interval.
+
+        Parameters
+        ----------
+        fh : int, list, np.array or ForecastingHorizon
+            Forecasting horizon
+        conf_lev : float
+            confidence_level for TBATS
+
+        Returns
+        -------
+        y_pred : pd.Series
+            Prediction
+        y_pred_int : pd.DataFrame
+            Prediction intervals
+        """
         fh = fh.to_relative(cutoff=self.cutoff)
+        len_fh = len(fh)
 
         if not fh.is_all_in_sample(cutoff=self.cutoff):
             fh_out = fh.to_out_of_sample(cutoff=self.cutoff)
             steps = fh_out.to_pandas().max()
-            out = pd.DataFrame(
-                self._forecaster.forecast(steps=steps, confidence_level=1 - alpha)[1]
+            _, tbats_ci = self._forecaster.forecast(
+                steps=steps, confidence_level=conf_lev
             )
-            y_out = out["mean"]
+            out = pd.DataFrame(tbats_ci)
+            y_out = out["mean"]  # aka tbats y_hat out of sample
+
             # pred_int
             lower = pd.Series(out["lower_bound"])
             upper = pd.Series(out["upper_bound"])
             pred_int = self._get_pred_int(lower=lower, upper=upper)
 
+            if len(fh) != len(fh_out):
+                epred_int = pd.DataFrame({"lower": nans(len_fh), "upper": nans(len_fh)})
+                epred_int.index = fh.to_absolute(self.cutoff)
+
+                in_pred_int = epred_int.index.isin(pred_int.index)
+                epred_int[in_pred_int] = pred_int
+                pred_int = epred_int
+
         else:
-            y_out = np.array([])
+            y_out = nans(len_fh)
+            pred_int = pd.DataFrame({"lower": nans(len_fh), "upper": nans(len_fh)})
+            pred_int.index = fh.to_absolute(self.cutoff)
 
         # y_pred
         y_in_sample = pd.Series(self._forecaster.y_hat)
         y_out_sample = pd.Series(y_out)
         y_pred = self._get_y_pred(y_in_sample=y_in_sample, y_out_sample=y_out_sample)
+        y_pred.name = self._yname
 
-        if return_pred_int:
-            return y_pred, pred_int
-        else:
-            return y_pred
+        return y_pred, pred_int
+
+    def _predict_quantiles(self, fh, X, alpha):
+        """Compute/return quantile forecasts.
+
+        If alpha is iterable, multiple quantiles will be calculated.
+
+        In-sample is set to NaNs, ince TBATS does not support it.
+
+        Parameters
+        ----------
+        fh : int, list, np.array or ForecastingHorizon
+            Forecasting horizon, default = y.index (in-sample forecast)
+        X : (default=None)
+            Ignored, passed for interface compatibility
+        alpha : float or list of float, optional (default=[0.05, 0.95])
+            A probability or list of, at which quantile forecasts are computed.
+
+        Returns
+        -------
+        quantiles : pd.DataFrame
+            Column has multi-index: first level is variable name from y in fit,
+                second level being the values of alpha passed to the function.
+            Row index is fh. Entries are quantile forecasts, for var in col index,
+                at quantile probability in second col index, for the row index.
+        """
+        if isinstance(alpha, (int, float)):
+            alpha = [alpha]
+        # else assume iterative/ list
+
+        req_quant = np.asarray(alpha)  # requested quantiles
+
+        # accumulator of results
+        acc = pd.DataFrame([], fh.to_absolute(self.cutoff))
+
+        for q1 in req_quant:
+
+            if q1 in acc.columns:
+                # skip as this quantile is already computed by tbats
+                continue
+
+            # q = 0.5 -> conf_lev = 0 -> y_pred = pred_int[lower] = pred_int[upper]
+            # so don't compute CI intervals simply save y_hat = predictions
+            if q1 == 0.5:
+                acc[q1] = self._tbats_forecast(fh)
+                continue
+
+            # otherwise compute CI intervals
+
+            # tbats with CI intervals
+            conf_level = np.abs(1 - q1 * 2)
+            y_pred, pred_int = self._tbats_forecast_with_interval(fh, conf_level)
+
+            # preserve q = 0.5, which is calculated and returned anyway
+            acc[0.5] = y_pred
+
+            q2 = 1 - q1  # the other quantile
+
+            # _get_pred_int() returns DataFrame with "lower" "upper" columns
+            # rename them to quantiles
+            colnames = {
+                "lower": q1 if q1 < q2 else q2,
+                "upper": q2 if q2 > q1 else q1,
+            }
+            pred_int = pred_int.rename(columns=colnames)
+
+            # add to acc
+            for q in [q1, q2]:
+                acc[q] = pred_int[q]
+
+        # order as requested and drop un-requested
+        quantiles = acc.reindex(columns=req_quant)
+
+        # the y.name for multi-index or "quantiles"
+        col_name = "Quantiles" if (self._yname in {None, ""}) else self._yname
+        quantiles.columns = pd.MultiIndex.from_product([[col_name], req_quant])
+
+        return quantiles
 
     def get_fitted_params(self):
         """Get fitted parameters.
@@ -169,7 +314,7 @@ def _get_pred_int(self, lower, upper):
         Returns
         -------
         pd.DataFrame
-            pred_int, predicion intervalls (out-sample, sliced by fh)
+            pred_int, prediction intervals (out-sample, sliced by fh)
         """
         pred_int = pd.DataFrame({"lower": lower, "upper": upper})
         # Out-sample fh
@@ -188,3 +333,8 @@ def _get_pred_int(self, lower, upper):
         pred_int.index = fh_out.to_absolute(self.cutoff)
         pred_int = pred_int.drop(columns=["idx"])
         return pred_int
+
+
+def nans(length):
+    """Return a vector of NaNs, of length `length`."""
+    return np.full(length, np.nan)


Test patch: