Repository: sktime/sktime

Issue:
Implement Automatic Exponential Smoothing
**Is your feature request related to a problem? Please describe.**

Add the Automatic Exponential Smoothing feature to BaseSktimeForecaster.

**Describe the solution you'd like**

Translate [the fable version on R](https://github.com/tidyverts/fable/blob/master/R/ets.R) to Python and add to sktime.

**Describe alternatives you've considered**

There might be existing Python packages that implement this feature.

**Additional context**

R documentation of the [ETS function](https://www.rdocumentation.org/packages/fable/versions/0.0.0.9100/topics/ETS).

[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/setup.py b/setup.py
--- a/setup.py
+++ b/setup.py
@@ -23,7 +23,7 @@
     "numpy": "1.18.0",
     "pandas": "1.0.0",
     "scikit-learn": "0.23.0",
-    "statsmodels": "0.11.0"
+    "statsmodels": "0.12.0"
 }
 
 HERE = os.path.abspath(os.path.dirname(__file__))
@@ -148,7 +148,6 @@ def run(self):
 try:
     from numpy.distutils.command.build_ext import build_ext  # noqa
 
-
     class build_ext_subclass(build_ext):
 
         def build_extensions(self):
@@ -163,7 +162,6 @@ def build_extensions(self):
 
             build_ext.build_extensions(self)
 
-
     cmdclass['build_ext'] = build_ext_subclass
 
 except ImportError:
diff --git a/sktime/datasets/base.py b/sktime/datasets/base.py
--- a/sktime/datasets/base.py
+++ b/sktime/datasets/base.py
@@ -1,3 +1,4 @@
+# -*- coding: utf-8 -*-
 """
 Utilities for loading datasets
 """
@@ -19,12 +20,12 @@
     "load_shampoo_sales",
     "load_longley",
     "load_lynx",
-    "load_acsf1"
+    "load_acsf1",
 ]
 
-__author__ = ['Markus Löning', 'Sajay Ganesh', '@big-o']
+__author__ = ["Markus Löning", "Sajay Ganesh", "@big-o"]
 
-DIRNAME = 'data'
+DIRNAME = "data"
 MODULE = os.path.dirname(__file__)
 
 
@@ -35,7 +36,7 @@ def _load_dataset(name, split, return_X_y):
     """
 
     if split in ("train", "test"):
-        fname = name + '_' + split.upper() + '.ts'
+        fname = name + "_" + split.upper() + ".ts"
         abspath = os.path.join(MODULE, DIRNAME, name, fname)
         X, y = load_from_tsfile_to_dataframe(abspath)
 
@@ -44,7 +45,7 @@ def _load_dataset(name, split, return_X_y):
         X = pd.DataFrame(dtype="object")
         y = pd.Series(dtype="object")
         for split in ("train", "test"):
-            fname = name + '_' + split.upper() + '.ts'
+            fname = name + "_" + split.upper() + ".ts"
             abspath = os.path.join(MODULE, DIRNAME, name, fname)
             result = load_from_tsfile_to_dataframe(abspath)
             X = pd.concat([X, pd.DataFrame(result[0])])
@@ -56,7 +57,7 @@ def _load_dataset(name, split, return_X_y):
     if return_X_y:
         return X, y
     else:
-        X['class_val'] = pd.Series(y)
+        X["class_val"] = pd.Series(y)
         return X
 
 
@@ -104,7 +105,7 @@ def load_gunpoint(split=None, return_X_y=False):
     Dataset details: http://timeseriesclassification.com/description.php
     ?Dataset=GunPoint
     """
-    name = 'GunPoint'
+    name = "GunPoint"
     return _load_dataset(name, split, return_X_y)
 
 
@@ -147,7 +148,7 @@ def load_osuleaf(split=None, return_X_y=False):
     Dataset details: http://www.timeseriesclassification.com/description.php
     ?Dataset=OSULeaf
     """
-    name = 'OSULeaf'
+    name = "OSULeaf"
     return _load_dataset(name, split, return_X_y)
 
 
@@ -193,63 +194,63 @@ def load_italy_power_demand(split=None, return_X_y=False):
     ?Dataset=ItalyPowerDemand
     """
 
-    name = 'ItalyPowerDemand'
+    name = "ItalyPowerDemand"
     return _load_dataset(name, split, return_X_y)
 
 
 def load_japanese_vowels(split=None, return_X_y=False):
     """
-        Loads the JapaneseVowels time series classification problem and
-        returns X and y.
+    Loads the JapaneseVowels time series classification problem and
+    returns X and y.
 
-        Parameters
-        ----------
-        split: None or str{"train", "test"}, optional (default=None)
-            Whether to load the train or test partition of the problem. By
-        default it loads both.
-        return_X_y: bool, optional (default=False)
-            If True, returns (features, target) separately instead of a
-            single dataframe with columns for
-            features and the target.
-
-        Returns
-        -------
-        X: pandas DataFrame with m rows and c columns
-            The time series data for the problem with m cases and c dimensions
-        y: numpy array
-            The class labels for each case in X
-
-        Details
-        -------
-        Dimensionality:     multivariate, 12
-        Series length:      29
-        Train cases:        270
-        Test cases:         370
-        Number of classes:  9
-
-        A UCI Archive dataset. 9 Japanese-male speakers were recorded saying
-        the vowels 'a' and 'e'. A '12-degree
-        linear prediction analysis' is applied to the raw recordings to
-        obtain time-series with 12 dimensions, a
-        originally a length between 7 and 29. In this dataset, instances
-        have been padded to the longest length,
-        29. The classification task is to predict the speaker. Therefore,
-        each instance is a transformed utterance,
-        12*29 values with a single class label attached, [1...9]. The given
-        training set is comprised of 30
-        utterances for each speaker, however the test set has a varied
-        distribution based on external factors of
-        timing and experimental availability, between 24 and 88 instances per
-        speaker. Reference: M. Kudo, J. Toyama
-        and M. Shimbo. (1999). "Multidimensional Curve Classification Using
-        Passing-Through Regions". Pattern
-        Recognition Letters, Vol. 20, No. 11--13, pages 1103--1111.
-
-        Dataset details: http://timeseriesclassification.com/description.php
-        ?Dataset=JapaneseVowels
+    Parameters
+    ----------
+    split: None or str{"train", "test"}, optional (default=None)
+        Whether to load the train or test partition of the problem. By
+    default it loads both.
+    return_X_y: bool, optional (default=False)
+        If True, returns (features, target) separately instead of a
+        single dataframe with columns for
+        features and the target.
+
+    Returns
+    -------
+    X: pandas DataFrame with m rows and c columns
+        The time series data for the problem with m cases and c dimensions
+    y: numpy array
+        The class labels for each case in X
+
+    Details
+    -------
+    Dimensionality:     multivariate, 12
+    Series length:      29
+    Train cases:        270
+    Test cases:         370
+    Number of classes:  9
+
+    A UCI Archive dataset. 9 Japanese-male speakers were recorded saying
+    the vowels 'a' and 'e'. A '12-degree
+    linear prediction analysis' is applied to the raw recordings to
+    obtain time-series with 12 dimensions, a
+    originally a length between 7 and 29. In this dataset, instances
+    have been padded to the longest length,
+    29. The classification task is to predict the speaker. Therefore,
+    each instance is a transformed utterance,
+    12*29 values with a single class label attached, [1...9]. The given
+    training set is comprised of 30
+    utterances for each speaker, however the test set has a varied
+    distribution based on external factors of
+    timing and experimental availability, between 24 and 88 instances per
+    speaker. Reference: M. Kudo, J. Toyama
+    and M. Shimbo. (1999). "Multidimensional Curve Classification Using
+    Passing-Through Regions". Pattern
+    Recognition Letters, Vol. 20, No. 11--13, pages 1103--1111.
+
+    Dataset details: http://timeseriesclassification.com/description.php
+    ?Dataset=JapaneseVowels
     """
 
-    name = 'JapaneseVowels'
+    name = "JapaneseVowels"
     return _load_dataset(name, split, return_X_y)
 
 
@@ -298,7 +299,7 @@ def load_arrow_head(split=None, return_X_y=False):
     ?Dataset=ArrowHead
     """
 
-    name = 'ArrowHead'
+    name = "ArrowHead"
     return _load_dataset(name, split, return_X_y)
 
 
@@ -345,7 +346,7 @@ def load_acsf1(split=None, return_X_y=False):
     ?Dataset=ACSF1
     """
 
-    name = 'ACSF1'
+    name = "ACSF1"
     return _load_dataset(name, split, return_X_y)
 
 
@@ -394,7 +395,7 @@ def load_basic_motions(split=None, return_X_y=False):
     ?Dataset=ArrowHead
     """
 
-    name = 'BasicMotions'
+    name = "BasicMotions"
     return _load_dataset(name, split, return_X_y)
 
 
@@ -427,8 +428,8 @@ def load_shampoo_sales():
         John Wiley & Sons: New York. Chapter 3.
     """
 
-    name = 'ShampooSales'
-    fname = name + '.csv'
+    name = "ShampooSales"
+    fname = name + ".csv"
     path = os.path.join(MODULE, DIRNAME, name, fname)
     data = pd.read_csv(path, index_col=0, squeeze=True)
 
@@ -488,11 +489,11 @@ def load_longley(return_X_y=False):
         the American Statistical Association.  62.319, 819-41.
         (https://www.itl.nist.gov/div898/strd/lls/data/LINKS/DATA/Longley.dat)
     """
-    name = 'Longley'
-    fname = name + '.csv'
+    name = "Longley"
+    fname = name + ".csv"
     path = os.path.join(MODULE, DIRNAME, name, fname)
     data = pd.read_csv(path, index_col=0)
-    data = data.set_index('YEAR')
+    data = data.set_index("YEAR")
 
     # change period index to simple numeric index
     # TODO add support for period/datetime indexing
@@ -500,13 +501,12 @@ def load_longley(return_X_y=False):
     data = data.reset_index(drop=True)
 
     # Get target series
-    yname = 'TOTEMP'
+    yname = "TOTEMP"
     y = data.pop(yname)
     y = pd.Series([y], name=yname)
 
     # Get exogeneous series
-    X = pd.DataFrame(
-        [pd.Series([data.iloc[:, i]]) for i in range(data.shape[1])]).T
+    X = pd.DataFrame([pd.Series([data.iloc[:, i]]) for i in range(data.shape[1])]).T
     X.columns = data.columns
 
     if return_X_y:
@@ -557,8 +557,8 @@ def load_lynx():
     series A, 140, 411–431.
     """
 
-    name = 'Lynx'
-    fname = name + '.csv'
+    name = "Lynx"
+    fname = name + ".csv"
     path = os.path.join(MODULE, DIRNAME, name, fname)
     data = pd.read_csv(path, index_col=0, squeeze=True)
 
@@ -578,7 +578,7 @@ def load_airline():
     Returns
     -------
     y : pandas Series
-        Lynx sales dataset
+        Airline passenger numbers dataset
 
     Details
     -------
@@ -602,10 +602,10 @@ def load_airline():
           Series G.
     """
 
-    name = 'Airline'
-    fname = name + '.csv'
+    name = "Airline"
+    fname = name + ".csv"
     path = os.path.join(MODULE, DIRNAME, name, fname)
-    data = pd.read_csv(path, index_col=0, squeeze=True)
+    data = pd.read_csv(path, index_col=0, squeeze=True, dtype={1: "float64"})
 
     # change period index to simple numeric index
     # TODO add support for period/datetime indexing
diff --git a/sktime/forecasting/ets.py b/sktime/forecasting/ets.py
new file mode 100644
--- /dev/null
+++ b/sktime/forecasting/ets.py
@@ -0,0 +1,338 @@
+# -*- coding: utf-8 -*-
+__all__ = ["AutoETS"]
+__author__ = ["Hongyi Yang"]
+
+from sktime.forecasting.base._statsmodels import _StatsModelsAdapter
+from statsmodels.tsa.exponential_smoothing.ets import ETSModel as _ETSModel
+from itertools import product
+from joblib import delayed, Parallel
+import numpy as np
+
+
+class AutoETS(_StatsModelsAdapter):
+    """
+    ETS models with both manual and automatic fitting capabilities.
+
+    Manual fitting is adapted from statsmodels' version,
+    while automatic fitting is adapted from R version of ets.
+
+    The first few parameters are the same as the ones on statsmodels
+    (from ``error`` to ``return_params``, link:
+    https://www.statsmodels.org/stable/_modules/statsmodels/tsa/exponential_smoothing/ets.html#ETSModel).
+
+    The next few parameters are adapted from the ones on R
+    (``auto`` to ``additive_only``, link:
+    https://www.rdocumentation.org/packages/forecast/versions/8.12/topics/ets),
+    and are used for automatic model selection.
+
+    Parameters
+    ----------
+    error : str, optional
+        The error model. "add" (default) or "mul".
+    trend : str or None, optional
+        The trend component model. "add", "mul", or None (default).
+    damped : bool, optional
+        Whether or not an included trend component is damped. Default is
+        False.
+    seasonal : str, optional
+        The seasonality model. "add", "mul", or None (default).
+    sp : int, optional
+        The number of periods in a complete seasonal cycle for seasonal
+        (Holt-Winters) models. For example, 4 for quarterly data with an
+        annual cycle or 7 for daily data with a weekly cycle. Required if
+        `seasonal` is not None.
+    initialization_method : str, optional
+        Method for initialization of the state space model. One of:
+
+        * 'estimated' (default)
+        * 'heuristic'
+        * 'known'
+
+        If 'known' initialization is used, then `initial_level` must be
+        passed, as well as `initial_trend` and `initial_seasonal` if
+        applicable.
+        'heuristic' uses a heuristic based on the data to estimate initial
+        level, trend, and seasonal state. 'estimated' uses the same heuristic
+        as initial guesses, but then estimates the initial states as part of
+        the fitting process.  Default is 'estimated'.
+    initial_level : float, optional
+        The initial level component. Only used if initialization is 'known'.
+    initial_trend : float, optional
+        The initial trend component. Only used if initialization is 'known'.
+    initial_seasonal : array_like, optional
+        The initial seasonal component. An array of length `seasonal_periods`.
+        Only used if initialization is 'known'.
+    bounds : dict or None, optional
+        A dictionary with parameter names as keys and the respective bounds
+        intervals as values (lists/tuples/arrays).
+        The available parameter names are, depending on the model and
+        initialization method:
+
+        * "smoothing_level"
+        * "smoothing_trend"
+        * "smoothing_seasonal"
+        * "damping_trend"
+        * "initial_level"
+        * "initial_trend"
+        * "initial_seasonal.0", ..., "initial_seasonal.<m-1>"
+
+        The default option is ``None``, in which case the traditional
+        (nonlinear) bounds as described in [1]_ are used.
+    start_params : array_like, optional
+        Initial values for parameters that will be optimized. If this is
+        ``None``, default values will be used.
+        The length of this depends on the chosen model. This should contain
+        the parameters in the following order, skipping parameters that do
+        not exist in the chosen model.
+
+        * `smoothing_level` (alpha)
+        * `smoothing_trend` (beta)
+        * `smoothing_seasonal` (gamma)
+        * `damping_trend` (phi)
+
+        If ``initialization_method`` was set to ``'estimated'`` (the
+        default), additionally, the parameters
+
+        * `initial_level` (:math:`l_{-1}`)
+        * `initial_trend` (:math:`l_{-1}`)
+        * `initial_seasonal.0` (:math:`s_{-1}`)
+        * ...
+        * `initial_seasonal.<m-1>` (:math:`s_{-m}`)
+
+        also have to be specified.
+    maxiter : int, optional
+        The maximum number of iterations to perform.
+    full_output : bool, optional
+        Set to True to have all available output in the Results object's
+        mle_retvals attribute. The output is dependent on the solver.
+        See LikelihoodModelResults notes section for more information.
+    disp : bool, optional
+        Set to True to print convergence messages.
+    callback : callable callback(xk), optional
+        Called after each iteration, as callback(xk), where xk is the
+        current parameter vector.
+    return_params : bool, optional
+        Whether or not to return only the array of maximizing parameters.
+        Default is False.
+    auto : bool, optional
+        Set True to enable automatic model selection.
+        Default is False.
+    information_criterion : str, optional
+        Information criterion to be used in model selection. One of:
+
+        * "aic"
+        * "bic"
+        * "aicc"
+
+        Default is "aic".
+    allow_multiplicative_trend : bool, optional
+        If True, models with multiplicative trend are allowed when
+        searching for a model. Otherwise, the model space excludes them.
+        Default is False.
+    restrict : bool, optional
+        If True, the models with infinite variance will not be allowed.
+        Default is True.
+    additive_only : bool, optional
+        If True, will only consider additive models.
+        Default is False.
+    n_jobs : int or None, optional (default=None)
+        The number of jobs to run in parallel for automatic model fitting.
+        ``None`` means 1 unless in a :obj:`joblib.parallel_backend` context.
+        ``-1`` means using all processors.
+
+    References
+    ----------
+    [1] Hyndman, R.J., & Athanasopoulos, G. (2019) *Forecasting:
+        principles and practice*, 3rd edition, OTexts: Melbourne,
+        Australia. OTexts.com/fpp3. Accessed on April 19th 2020.
+    """
+
+    def __init__(
+        self,
+        error="add",
+        trend=None,
+        damped=False,
+        seasonal=None,
+        sp=None,
+        initialization_method="estimated",
+        initial_level=None,
+        initial_trend=None,
+        initial_seasonal=None,
+        bounds=None,
+        dates=None,
+        freq=None,
+        missing="none",
+        start_params=None,
+        maxiter=1000,
+        full_output=True,
+        disp=True,
+        callback=None,
+        return_params=False,
+        auto=False,
+        information_criterion="aic",
+        allow_multiplicative_trend=False,
+        restrict=True,
+        additive_only=False,
+        n_jobs=None,
+        **kwargs
+    ):
+
+        # Model params
+        self.error = error
+        self.trend = trend
+        self.damped = damped
+        self.seasonal = seasonal
+        self.sp = sp
+        self.initialization_method = initialization_method
+        self.initial_level = initial_level
+        self.initial_trend = initial_trend
+        self.initial_seasonal = initial_seasonal
+        self.bounds = bounds
+        self.dates = dates
+        self.freq = freq
+        self.missing = missing
+
+        # Fit params
+        self.start_params = start_params
+        self.maxiter = maxiter
+        self.full_output = full_output
+        self.disp = disp
+        self.callback = callback
+        self.return_params = return_params
+        self.information_criterion = information_criterion
+        self.auto = auto
+        self.allow_multiplicative_trend = allow_multiplicative_trend
+        self.restrict = restrict
+        self.additive_only = additive_only
+        self.n_jobs = n_jobs
+
+        super(AutoETS, self).__init__()
+
+    def _fit_forecaster(self, y, X_train=None):
+
+        # Select model automatically
+        if self.auto:
+            # Initialise parameter ranges
+            error_range = ["add", "mul"]
+            if self.allow_multiplicative_trend:
+                trend_range = ["add", "mul", None]
+            else:
+                trend_range = ["add", None]
+            seasonal_range = ["add", "mul", None]
+            damped_range = [True, False]
+
+            # Check information criterion input
+            if (
+                self.information_criterion != "aic"
+                and self.information_criterion != "bic"
+                and self.information_criterion != "aicc"
+            ):
+                raise ValueError(
+                    "information criterion must " "either be aic, bic or aicc"
+                )
+
+            # Fit model, adapted from:
+            # https://github.com/robjhyndman/forecast/blob/master/R/ets.R
+
+            # Initialise iterator
+            def _iter(error_range, trend_range, seasonal_range, damped_range):
+                for error, trend, seasonal, damped in product(
+                    error_range, trend_range, seasonal_range, damped_range
+                ):
+                    if trend is None and damped:
+                        continue
+
+                    if self.restrict:
+                        if error == "add" and (trend == "mul" or seasonal == "mul"):
+                            continue
+                        if error == "mul" and trend == "mul" and seasonal == "add":
+                            continue
+                        if self.additive_only and (
+                            error == "mul" or trend == "mul" or seasonal == "mul"
+                        ):
+                            continue
+
+                    yield error, trend, seasonal, damped
+
+            # Fit function
+            def _fit(error, trend, seasonal, damped):
+                _forecaster = _ETSModel(
+                    y,
+                    error=error,
+                    trend=trend,
+                    damped_trend=damped,
+                    seasonal=seasonal,
+                    seasonal_periods=self.sp,
+                    initialization_method=self.initialization_method,
+                    initial_level=self.initial_level,
+                    initial_trend=self.initial_trend,
+                    initial_seasonal=self.initial_seasonal,
+                    bounds=self.bounds,
+                    dates=self.dates,
+                    freq=self.freq,
+                    missing=self.missing,
+                )
+                _fitted_forecaster = _forecaster.fit(
+                    start_params=self.start_params,
+                    maxiter=self.maxiter,
+                    full_output=self.full_output,
+                    disp=self.disp,
+                    callback=self.callback,
+                    return_params=self.return_params,
+                )
+                return _forecaster, _fitted_forecaster
+
+            # Fit models
+            _fitted_results = Parallel(n_jobs=self.n_jobs)(
+                delayed(_fit)(error, trend, seasonal, damped)
+                for error, trend, seasonal, damped in _iter(
+                    error_range, trend_range, seasonal_range, damped_range
+                )
+            )
+
+            # Select best model based on information criterion
+            # Get index of best model
+            _index = np.argmin(
+                [
+                    getattr(result[1], self.information_criterion)
+                    for result in _fitted_results
+                ]
+            )
+            # Update best model
+            self._forecaster = _fitted_results[_index][0]
+            self._fitted_forecaster = _fitted_results[_index][1]
+
+        else:
+            self._forecaster = _ETSModel(
+                y,
+                error=self.error,
+                trend=self.trend,
+                damped_trend=self.damped,
+                seasonal=self.seasonal,
+                seasonal_periods=self.sp,
+                initialization_method=self.initialization_method,
+                initial_level=self.initial_level,
+                initial_trend=self.initial_trend,
+                initial_seasonal=self.initial_seasonal,
+                bounds=self.bounds,
+                dates=self.dates,
+                freq=self.freq,
+                missing=self.missing,
+            )
+
+            self._fitted_forecaster = self._forecaster.fit(
+                start_params=self.start_params,
+                maxiter=self.maxiter,
+                full_output=self.full_output,
+                disp=self.disp,
+                callback=self.callback,
+                return_params=self.return_params,
+            )
+
+    def summary(self):
+        """
+        Get a summary of the fitted forecaster,
+        same as the implementation in statsmodels:
+        https://www.statsmodels.org/dev/examples/notebooks/generated/ets.html
+        """
+        return self._fitted_forecaster.summary()


Test patch:
diff --git a/sktime/forecasting/tests/test_ets.py b/sktime/forecasting/tests/test_ets.py
new file mode 100644
--- /dev/null
+++ b/sktime/forecasting/tests/test_ets.py
@@ -0,0 +1,49 @@
+# -*- coding: utf-8 -*-
+__author__ = ["Hongyi Yang"]
+
+from numpy.testing import assert_array_equal
+from sktime.forecasting.ets import AutoETS
+from sktime.datasets import load_airline
+
+
+# test results against R implementation on airline dataset
+y = load_airline()
+
+
+# Default condition
+# fit <- ets(AirPassengers, model = "ZZZ")
+# components: "M" "A" "M" "TRUE" (error, trend, season, damped)
+# discrepancy lies in damped (True in R but False in statsmodels)
+def test_airline_default():
+    fit_result_R = ["mul", "add", "mul"]
+
+    forecaster = AutoETS(auto=True, sp=12, n_jobs=-1)
+    forecaster.fit(y)
+    fitted_forecaster = forecaster._fitted_forecaster
+    fit_result = [
+        fitted_forecaster.error,
+        fitted_forecaster.trend,
+        fitted_forecaster.seasonal,
+    ]
+
+    assert_array_equal(fit_result_R, fit_result)
+
+
+# Allow multiplicative trend
+# fit <- ets(AirPassengers, model = "ZZZ",
+# allow.multiplicative.trend = TRUE)
+# components: "M" "M" "M" "TRUE"
+# discrepancy lies in damped (True in R but False in statsmodels)
+def test_airline_allow_multiplicative_trend():
+    fit_result_R = ["mul", "mul", "mul"]
+
+    forecaster = AutoETS(auto=True, sp=12, n_jobs=-1, allow_multiplicative_trend=True)
+    forecaster.fit(y)
+    fitted_forecaster = forecaster._fitted_forecaster
+    fit_result = [
+        fitted_forecaster.error,
+        fitted_forecaster.trend,
+        fitted_forecaster.seasonal,
+    ]
+
+    assert_array_equal(fit_result_R, fit_result)