Repository: sktime/sktime

Issue:
[BUG] rise.fit fails with "x contains a constant. Adding a constant with trend='c' is not allowed."
**Describe the bug**
<!--
A clear and concise description of what the bug is.
-->
rise.fit fails with error.
**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.transformers.compose import RowwiseTransformer
from sktime.transformers.compose import Tabulariser
from sktime.transformers.segment import RandomIntervalSegmenter
from sktime.pipeline import Pipeline
from sktime.pipeline import FeatureUnion
from sktime.classifiers.compose import TimeSeriesForestClassifier
from sktime.datasets import load_gunpoint
from statsmodels.tsa.stattools import acf
from statsmodels.tsa.ar_model import AR
from sklearn.preprocessing import FunctionTransformer
from sklearn.tree import DecisionTreeClassifier
import numpy as np

X_train, y_train = load_gunpoint(split='TRAIN', return_X_y=True)
X_test, y_test = load_gunpoint(split='TEST', return_X_y=True)

def ar_coefs(x, maxlag=100):
    x = np.asarray(x).ravel()
    nlags = np.minimum(len(x) - 1, maxlag)
    model = AR(endog=x) 
    return model.fit(maxlag=nlags).params.ravel()

def acf_coefs(x, maxlag=100):
    x = np.asarray(x).ravel() 
    nlags = np.minimum(len(x) - 1, maxlag)
    return acf(x, nlags=nlags).ravel()

def powerspectrum(x, **kwargs):
    x = np.asarray(x).ravel()
    fft = np.fft.fft(x)
    ps = fft.real * fft.real + fft.imag * fft.imag
    return ps[:ps.shape[0] // 2].ravel()

steps = [
    ('segment', RandomIntervalSegmenter(n_intervals=1, min_length=5)),
    ('transform', FeatureUnion([
        ('ar', RowwiseTransformer(FunctionTransformer(func=ar_coefs, validate=False))),
        ('acf', RowwiseTransformer(FunctionTransformer(func=acf_coefs, validate=False))),
        ('ps', RowwiseTransformer(FunctionTransformer(func=powerspectrum, validate=False)))
    ])),
    ('tabularise', Tabulariser()),
    ('clf', DecisionTreeClassifier())
]
base_estimator = Pipeline(steps)

rise = TimeSeriesForestClassifier(base_estimator=base_estimator,
                                  n_estimators=50, 
                                  bootstrap=True,
                                  oob_score=True)

rise.fit(X_train, y_train)
if rise.oob_score:
    print(rise.oob_score_)

# ---------------------------------------------------------------------------
# ValueError                                Traceback (most recent call last)
# ~/anaconda3/envs/sktime/lib/python3.7/site-packages/sktime/transformers/compose.py in _apply_rowwise(self, func, X, y)
#     235             Xt = pd.concat([pd.Series(col.apply(func))
# --> 236                             for _, col in X.items()], axis=1)
#     237 

# ~/anaconda3/envs/sktime/lib/python3.7/site-packages/sktime/transformers/compose.py in <listcomp>(.0)
#     235             Xt = pd.concat([pd.Series(col.apply(func))
# --> 236                             for _, col in X.items()], axis=1)
#     237 

# ~/anaconda3/envs/sktime/lib/python3.7/site-packages/pandas/core/series.py in apply(self, func, convert_dtype, args, **kwds)
#    4044                 values = self.astype(object).values
# -> 4045                 mapped = lib.map_infer(values, f, convert=convert_dtype)
#    4046 

# pandas/_libs/lib.pyx in pandas._libs.lib.map_infer()

# ~/anaconda3/envs/sktime/lib/python3.7/site-packages/sklearn/base.py in fit_transform(self, X, y, **fit_params)
#     552             # fit method of arity 1 (unsupervised transformation)
# --> 553             return self.fit(X, **fit_params).transform(X)
#     554         else:

# ~/anaconda3/envs/sktime/lib/python3.7/site-packages/sklearn/preprocessing/_function_transformer.py in transform(self, X)
#     152         """
# --> 153         return self._transform(X, func=self.func, kw_args=self.kw_args)
#     154 

# ~/anaconda3/envs/sktime/lib/python3.7/site-packages/sklearn/preprocessing/_function_transformer.py in _transform(self, X, func, kw_args)
#     178 
# --> 179         return func(X, **(kw_args if kw_args else {}))
#     180 

# <ipython-input-5-9af4e0007a3b> in ar_coefs(x, maxlag)
#      20     model = AR(endog=x)
# ---> 21     return model.fit(maxlag=nlags).params.ravel()
#      22 

# ~/anaconda3/envs/sktime/lib/python3.7/site-packages/statsmodels/tsa/ar_model.py in fit(self, maxlag, method, ic, trend, transparams, start_params, solver, maxiter, full_output, disp, callback, **kwargs)
#     546         # make lagged RHS
# --> 547         X = self._stackX(k_ar, trend)  # sets self.k_trend
#     548         k_trend = self.k_trend

# ~/anaconda3/envs/sktime/lib/python3.7/site-packages/statsmodels/tsa/ar_model.py in _stackX(self, k_ar, trend)
#     390         if k_trend:
# --> 391             X = add_trend(X, prepend=True, trend=trend, has_constant="raise")
#     392         self.k_trend = k_trend

# ~/anaconda3/envs/sktime/lib/python3.7/site-packages/statsmodels/tsa/tsatools.py in add_trend(x, trend, prepend, has_constant)
#     107                       "trend='{0}' is not allowed.".format(trend)
# --> 108                 raise ValueError(msg)
#     109             elif has_constant == 'skip':

# ValueError: x contains a constant. Adding a constant with trend='c' is not allowed.

# During handling of the above exception, another exception occurred:

# ValueError                                Traceback (most recent call last)
# <ipython-input-5-9af4e0007a3b> in <module>
#      49                                   oob_score=True)
#      50 
# ---> 51 rise.fit(X_train, y_train)
#      52 if rise.oob_score:
#      53     print(rise.oob_score_)

# ~/anaconda3/envs/sktime/lib/python3.7/site-packages/sktime/classifiers/compose/ensemble.py in fit(self, X, y, sample_weight)
#     354                     t, self, X, y, sample_weight, i, len(trees),
#     355                     verbose=self.verbose, class_weight=self.class_weight)
# --> 356                 for i, t in enumerate(trees))
#     357 
#     358             # Collect newly grown trees

# ~/anaconda3/envs/sktime/lib/python3.7/site-packages/joblib/parallel.py in __call__(self, iterable)
#    1002             # remaining jobs.
#    1003             self._iterating = False
# -> 1004             if self.dispatch_one_batch(iterator):
#    1005                 self._iterating = self._original_iterator is not None
#    1006 

# ~/anaconda3/envs/sktime/lib/python3.7/site-packages/joblib/parallel.py in dispatch_one_batch(self, iterator)
#     833                 return False
#     834             else:
# --> 835                 self._dispatch(tasks)
#     836                 return True
#     837 

# ~/anaconda3/envs/sktime/lib/python3.7/site-packages/joblib/parallel.py in _dispatch(self, batch)
#     752         with self._lock:
#     753             job_idx = len(self._jobs)
# --> 754             job = self._backend.apply_async(batch, callback=cb)
#     755             # A job can complete so quickly than its callback is
#     756             # called before we get here, causing self._jobs to

# ~/anaconda3/envs/sktime/lib/python3.7/site-packages/joblib/_parallel_backends.py in apply_async(self, func, callback)
#     207     def apply_async(self, func, callback=None):
#     208         """Schedule a func to be run"""
# --> 209         result = ImmediateResult(func)
#     210         if callback:
#     211             callback(result)

# ~/anaconda3/envs/sktime/lib/python3.7/site-packages/joblib/_parallel_backends.py in __init__(self, batch)
#     588         # Don't delay the application, to avoid keeping the input
#     589         # arguments in memory
# --> 590         self.results = batch()
#     591 
#     592     def get(self):

# ~/anaconda3/envs/sktime/lib/python3.7/site-packages/joblib/parallel.py in __call__(self)
#     254         with parallel_backend(self._backend, n_jobs=self._n_jobs):
#     255             return [func(*args, **kwargs)
# --> 256                     for func, args, kwargs in self.items]
#     257 
#     258     def __len__(self):

# ~/anaconda3/envs/sktime/lib/python3.7/site-packages/joblib/parallel.py in <listcomp>(.0)
#     254         with parallel_backend(self._backend, n_jobs=self._n_jobs):
#     255             return [func(*args, **kwargs)
# --> 256                     for func, args, kwargs in self.items]
#     257 
#     258     def __len__(self):

# ~/anaconda3/envs/sktime/lib/python3.7/site-packages/sktime/classifiers/compose/ensemble.py in _parallel_build_trees(tree, forest, X, y, sample_weight, tree_idx, n_trees, verbose, class_weight)
#     494         fit_params = {f'{estimator}__sample_weight': curr_sample_weight,
#     495                       f'{estimator}__check_input': True}
# --> 496         tree.fit(X, y, **fit_params)
#     497 
#     498     else:

# ~/anaconda3/envs/sktime/lib/python3.7/site-packages/sklearn/pipeline.py in fit(self, X, y, **fit_params)
#     350             This estimator
#     351         """
# --> 352         Xt, fit_params = self._fit(X, y, **fit_params)
#     353         with _print_elapsed_time('Pipeline',
#     354                                  self._log_message(len(self.steps) - 1)):

# ~/anaconda3/envs/sktime/lib/python3.7/site-packages/sklearn/pipeline.py in _fit(self, X, y, **fit_params)
#     315                 message_clsname='Pipeline',
#     316                 message=self._log_message(step_idx),
# --> 317                 **fit_params_steps[name])
#     318             # Replace the transformer of the step with the fitted
#     319             # transformer. This is necessary when loading the transformer

# ~/anaconda3/envs/sktime/lib/python3.7/site-packages/joblib/memory.py in __call__(self, *args, **kwargs)
#     353 
#     354     def __call__(self, *args, **kwargs):
# --> 355         return self.func(*args, **kwargs)
#     356 
#     357     def call_and_shelve(self, *args, **kwargs):

# ~/anaconda3/envs/sktime/lib/python3.7/site-packages/sklearn/pipeline.py in _fit_transform_one(transformer, X, y, weight, message_clsname, message, **fit_params)
#     714     with _print_elapsed_time(message_clsname, message):
#     715         if hasattr(transformer, 'fit_transform'):
# --> 716             res = transformer.fit_transform(X, y, **fit_params)
#     717         else:
#     718             res = transformer.fit(X, y, **fit_params).transform(X)

# ~/anaconda3/envs/sktime/lib/python3.7/site-packages/sktime/pipeline.py in fit_transform(self, X, y, **fit_params)
#     182             delayed(_fit_transform_one)(trans, X, y, weight,
#     183                                         **fit_params)
# --> 184             for name, trans, weight in self._iter())
#     185 
#     186         if not result:

# ~/anaconda3/envs/sktime/lib/python3.7/site-packages/joblib/parallel.py in __call__(self, iterable)
#    1002             # remaining jobs.
#    1003             self._iterating = False
# -> 1004             if self.dispatch_one_batch(iterator):
#    1005                 self._iterating = self._original_iterator is not None
#    1006 

# ~/anaconda3/envs/sktime/lib/python3.7/site-packages/joblib/parallel.py in dispatch_one_batch(self, iterator)
#     833                 return False
#     834             else:
# --> 835                 self._dispatch(tasks)
#     836                 return True
#     837 

# ~/anaconda3/envs/sktime/lib/python3.7/site-packages/joblib/parallel.py in _dispatch(self, batch)
#     752         with self._lock:
#     753             job_idx = len(self._jobs)
# --> 754             job = self._backend.apply_async(batch, callback=cb)
#     755             # A job can complete so quickly than its callback is
#     756             # called before we get here, causing self._jobs to

# ~/anaconda3/envs/sktime/lib/python3.7/site-packages/joblib/_parallel_backends.py in apply_async(self, func, callback)
#     207     def apply_async(self, func, callback=None):
#     208         """Schedule a func to be run"""
# --> 209         result = ImmediateResult(func)
#     210         if callback:
#     211             callback(result)

# ~/anaconda3/envs/sktime/lib/python3.7/site-packages/joblib/_parallel_backends.py in __init__(self, batch)
#     588         # Don't delay the application, to avoid keeping the input
#     589         # arguments in memory
# --> 590         self.results = batch()
#     591 
#     592     def get(self):

# ~/anaconda3/envs/sktime/lib/python3.7/site-packages/joblib/parallel.py in __call__(self)
#     254         with parallel_backend(self._backend, n_jobs=self._n_jobs):
#     255             return [func(*args, **kwargs)
# --> 256                     for func, args, kwargs in self.items]
#     257 
#     258     def __len__(self):

# ~/anaconda3/envs/sktime/lib/python3.7/site-packages/joblib/parallel.py in <listcomp>(.0)
#     254         with parallel_backend(self._backend, n_jobs=self._n_jobs):
#     255             return [func(*args, **kwargs)
# --> 256                     for func, args, kwargs in self.items]
#     257 
#     258     def __len__(self):

# ~/anaconda3/envs/sktime/lib/python3.7/site-packages/sklearn/pipeline.py in _fit_transform_one(transformer, X, y, weight, message_clsname, message, **fit_params)
#     714     with _print_elapsed_time(message_clsname, message):
#     715         if hasattr(transformer, 'fit_transform'):
# --> 716             res = transformer.fit_transform(X, y, **fit_params)
#     717         else:
#     718             res = transformer.fit(X, y, **fit_params).transform(X)

# ~/anaconda3/envs/sktime/lib/python3.7/site-packages/sktime/transformers/base.py in fit_transform(self, X, y, **fit_params)
#      40             return self.fit(X, **fit_params).transform(X)
#      41         # fit method of arity 2 (supervised transformation)
# ---> 42         return self.fit(X, y, **fit_params).transform(X)

# ~/anaconda3/envs/sktime/lib/python3.7/site-packages/sktime/transformers/compose.py in transform(self, X, y)
#     201         """
#     202         func = self.transformer.fit_transform
# --> 203         return self._apply_rowwise(func, X, y)
#     204 
#     205     def inverse_transform(self, X, y=None):

# ~/anaconda3/envs/sktime/lib/python3.7/site-packages/sktime/transformers/compose.py in _apply_rowwise(self, func, X, y)
#     244                 for row in col:  # loop over rows in each column
#     245                     row_2d = pd.DataFrame(row)  # convert into 2d dataframe
# --> 246                     row_t = func(row_2d).ravel()  # apply transform
#     247                     rows_t.append(row_t)  # append transformed rows
#     248                 cols_t.append(rows_t)  # append transformed columns

# ~/anaconda3/envs/sktime/lib/python3.7/site-packages/sklearn/base.py in fit_transform(self, X, y, **fit_params)
#     551         if y is None:
#     552             # fit method of arity 1 (unsupervised transformation)
# --> 553             return self.fit(X, **fit_params).transform(X)
#     554         else:
#     555             # fit method of arity 2 (supervised transformation)

# ~/anaconda3/envs/sktime/lib/python3.7/site-packages/sklearn/preprocessing/_function_transformer.py in transform(self, X)
#     151             Transformed input.
#     152         """
# --> 153         return self._transform(X, func=self.func, kw_args=self.kw_args)
#     154 
#     155     def inverse_transform(self, X):

# ~/anaconda3/envs/sktime/lib/python3.7/site-packages/sklearn/preprocessing/_function_transformer.py in _transform(self, X, func, kw_args)
#     177             func = _identity
#     178 
# --> 179         return func(X, **(kw_args if kw_args else {}))
#     180 
#     181     def _more_tags(self):

# <ipython-input-5-9af4e0007a3b> in ar_coefs(x, maxlag)
#      19     nlags = np.minimum(len(x) - 1, maxlag)
#      20     model = AR(endog=x)
# ---> 21     return model.fit(maxlag=nlags).params.ravel()
#      22 
#      23 def acf_coefs(x, maxlag=100):

# ~/anaconda3/envs/sktime/lib/python3.7/site-packages/statsmodels/tsa/ar_model.py in fit(self, maxlag, method, ic, trend, transparams, start_params, solver, maxiter, full_output, disp, callback, **kwargs)
#     545         Y = endog[k_ar:, :]
#     546         # make lagged RHS
# --> 547         X = self._stackX(k_ar, trend)  # sets self.k_trend
#     548         k_trend = self.k_trend
#     549         self.exog_names = util.make_lag_names(self.endog_names, k_ar, k_trend)

# ~/anaconda3/envs/sktime/lib/python3.7/site-packages/statsmodels/tsa/ar_model.py in _stackX(self, k_ar, trend)
#     389         k_trend = util.get_trendorder(trend)
#     390         if k_trend:
# --> 391             X = add_trend(X, prepend=True, trend=trend, has_constant="raise")
#     392         self.k_trend = k_trend
#     393         return X

# ~/anaconda3/envs/sktime/lib/python3.7/site-packages/statsmodels/tsa/tsatools.py in add_trend(x, trend, prepend, has_constant)
#     106                 msg = "x contains a constant. Adding a constant with " \
#     107                       "trend='{0}' is not allowed.".format(trend)
# --> 108                 raise ValueError(msg)
#     109             elif has_constant == 'skip':
#     110                 columns = columns[1:]

# ValueError: x contains a constant. Adding a constant with trend='c' is not allowed.
```

**Expected behavior**
<!--
A clear and concise description of what you expected to happen.
-->
run without error


**Additional context**
<!--
Add any other context about the problem here.
-->

**Versions**
<details>

Linux-5.3.0-26-generic-x86_64-with-debian-buster-sid
Python 3.7.6 (default, Jan  8 2020, 19:59:22) 
[GCC 7.3.0]
NumPy 1.18.1
SciPy 1.3.2
sktime 0.3.1

<!--
Please run the following snippet and paste the output here.
import platform; print(platform.platform())
import sys; print("Python", sys.version)
import numpy; print("NumPy", numpy.__version__)
import scipy; print("SciPy", scipy.__version__)
import sktime; print("sktime", sktime.__version__)
-->

</details>

<!-- Thanks for contributing! -->



Discussion:

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
@@ -55,6 +55,7 @@ def predict(self, fh=None, X=None, return_pred_int=False,
         X : pd.DataFrame, optional (default=None)
         return_pred_int : bool, optional (default=False)
         alpha : float or list, optional (default=0.95)
+            A significance level or list of significance levels.
 
         Returns
         -------
@@ -65,6 +66,31 @@ def predict(self, fh=None, X=None, return_pred_int=False,
         """
         raise NotImplementedError("abstract method")
 
+    def compute_pred_int(self, y_pred, alpha=DEFAULT_ALPHA):
+        """
+        Get the prediction intervals for a forecast.
+
+        If alpha is iterable, multiple intervals will be calculated.
+
+        Parameters
+        ----------
+
+        y_pred : pd.Series
+            Point predictions.
+
+        alpha : float or list, optional (default=0.95)
+            A significance level or list of significance levels.
+
+        Returns
+        -------
+
+        intervals : pd.DataFrame
+            A table of upper and lower bounds for each point prediction in
+            ``y_pred``. If ``alpha`` was iterable, then ``intervals`` will be a
+            list of such tables.
+        """
+        raise NotImplementedError("abstract method")
+
     def update(self, y_new, X_new=None, update_params=False):
         """Update fitted parameters
 
diff --git a/sktime/forecasting/base/_sktime.py b/sktime/forecasting/base/_sktime.py
--- a/sktime/forecasting/base/_sktime.py
+++ b/sktime/forecasting/base/_sktime.py
@@ -2,7 +2,7 @@
 # coding: utf-8
 # copyright: sktime developers, BSD-3-Clause License (see LICENSE file)
 
-__author__ = ["Markus Löning"]
+__author__ = ["Markus Löning", "@big-o"]
 __all__ = ["BaseSktimeForecaster", "BaseWindowForecaster"]
 
 from contextlib import contextmanager
@@ -14,6 +14,7 @@
 from sktime.forecasting.base._base import DEFAULT_ALPHA
 from sktime.forecasting.model_selection import CutoffSplitter
 from sktime.forecasting.model_selection import SlidingWindowSplitter
+from sktime.utils.validation.forecasting import check_alpha
 from sktime.utils.validation.forecasting import check_cv
 from sktime.utils.validation.forecasting import check_fh
 from sktime.utils.validation.forecasting import check_y
@@ -158,6 +159,68 @@ def predict(self, fh=None, X=None, return_pred_int=False,
         return self._predict(self.fh, X=X, return_pred_int=return_pred_int,
                              alpha=alpha)
 
+    def compute_pred_int(self, y_pred, alpha=DEFAULT_ALPHA):
+        """
+        Get the prediction intervals for a forecast. Must be run *after* the
+        forecaster has been fitted.
+
+        If alpha is iterable, multiple intervals will be calculated.
+
+        Parameters
+        ----------
+
+        y_pred : pd.Series
+            Point predictions.
+
+        alpha : float or list, optional (default=0.95)
+            A significance level or list of significance levels.
+
+        Returns
+        -------
+
+        intervals : pd.DataFrame
+            A table of upper and lower bounds for each point prediction in
+            ``y_pred``. If ``alpha`` was iterable, then ``intervals`` will be a
+            list of such tables.
+        """
+
+        alphas = check_alpha(alpha)
+        errors = self._compute_pred_err(alphas)
+
+        # compute prediction intervals
+        pred_int = [
+            pd.DataFrame({
+                "lower": y_pred - error,
+                "upper": y_pred + error
+            })
+            for error in errors
+        ]
+
+        # for a single alpha, return single pd.DataFrame
+        if isinstance(alpha, float):
+            return pred_int[0]
+
+        # otherwise return list of pd.DataFrames
+        return pred_int
+
+    def _compute_pred_err(self, alphas):
+        """Calculate the prediction errors for each point.
+
+        Parameters
+        ----------
+
+        alpha : float or list, optional (default=0.95)
+            A significance level or list of significance levels.
+
+        Returns
+        -------
+
+        errors : list of pd.Series
+            Each series in the list will contain the errors for each point in
+            the forecast for the corresponding alpha.
+        """
+        raise NotImplementedError("abstract method")
+
     def update_predict_single(self, y_new, fh=None, X=None,
                               update_params=False, return_pred_int=False,
                               alpha=DEFAULT_ALPHA):
diff --git a/sktime/forecasting/theta.py b/sktime/forecasting/theta.py
--- a/sktime/forecasting/theta.py
+++ b/sktime/forecasting/theta.py
@@ -10,7 +10,6 @@
 from sktime.transformers.single_series.detrend import Deseasonalizer
 from sktime.utils.confidence import zscore
 from sktime.utils.time_series import fit_trend
-from sktime.utils.validation.forecasting import check_alpha
 from sktime.utils.validation.forecasting import check_sp
 
 
@@ -193,12 +192,11 @@ def _compute_drift(self):
 
         return drift
 
-    def _compute_pred_errors(self, alpha=DEFAULT_ALPHA):
+    def _compute_pred_err(self, alphas):
         """
         Get the prediction errors for the forecast.
         """
         self.check_is_fitted()
-        alpha = check_alpha(alpha)
 
         n_timepoints = len(self._y)
 
@@ -206,17 +204,12 @@ def _compute_pred_errors(self, alpha=DEFAULT_ALPHA):
         sem = self.sigma_ * np.sqrt(self._fh * self.smoothing_level_ ** 2 + 1)
 
         errors = []
-        for a in alpha:
-            z = zscore(1 - a)
+        for alpha in alphas:
+            z = zscore(1 - alpha)
             error = z * sem
             errors.append(
                 pd.Series(error, index=self.fh.absolute(self.cutoff)))
 
-        # for a single alpha value, unwrap list
-        if len(errors) == 1:
-            return errors[0]
-
-        # otherwise, return list of errors
         return errors
 
     def update(self, y_new, X_new=None, update_params=True):
@@ -229,34 +222,3 @@ def update(self, y_new, X_new=None, update_params=True):
                 "smoothing_level"]
             self.trend_ = self._compute_trend(y_new)
         return self
-
-    def compute_pred_int(self, y_pred, alpha=DEFAULT_ALPHA):
-        """
-        Get the prediction intervals for the forecast. If alpha is iterable,
-        multiple
-        intervals will be calculated.
-        """
-        errors = self._compute_pred_errors(alpha=alpha)
-
-        # for multiple alphas, errors come in a list;
-        # for single alpha, they come as a single pd.Series,
-        # wrap it here into a list to make it iterable,
-        # to avoid code duplication
-        if isinstance(errors, pd.Series):
-            errors = [errors]
-
-        # compute prediction intervals
-        pred_int = [
-            pd.DataFrame({
-                "lower": y_pred - error,
-                "upper": y_pred + error
-            })
-            for error in errors
-        ]
-
-        # for a single alpha, return single pd.DataFrame
-        if len(pred_int) == 1:
-            return pred_int[0]
-
-        # otherwise return list of pd.DataFrames
-        return pred_int


Test patch:
diff --git a/sktime/forecasting/tests/test_theta.py b/sktime/forecasting/tests/test_theta.py
--- a/sktime/forecasting/tests/test_theta.py
+++ b/sktime/forecasting/tests/test_theta.py
@@ -1,7 +1,6 @@
 __author__ = ["@big-o"]
 
 import numpy as np
-import pandas as pd
 import pytest
 from sktime.datasets import load_airline
 from sktime.forecasting.model_selection import temporal_train_test_split
@@ -27,13 +26,18 @@ def test_predictive_performance_on_airline():
 def test_pred_errors_against_y_test(fh):
     y = load_airline()
     y_train, y_test = temporal_train_test_split(y)
+
     f = ThetaForecaster()
     f.fit(y_train, fh)
+
     y_pred = f.predict(return_pred_int=False)
-    errors = f._compute_pred_errors(alpha=0.1)
-    if isinstance(errors, pd.Series):
-        errors = [errors]  # make iterable
+
+    intervals = f.compute_pred_int(y_pred, [0.1])
+
     y_test = y_test.iloc[check_fh(fh) - 1]
-    for error in errors:
-        assert np.all(y_test > y_pred - error)
-        assert np.all(y_test < y_pred + error)
+
+    # Performance should be good enough that all point forecasts lie within the
+    # prediction intervals.
+    for ints in intervals:
+        assert np.all(y_test > ints["lower"])
+        assert np.all(y_test < ints["upper"])