# Buggy code
def _fit_forecaster(self, y, X=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,
        )


# Traceback (AttributeError)
---------------------------------------------------------------------------
AttributeError                            Traceback (most recent call last)
<ipython-input-80-03aaf6ca0732> in <module>
----> 1 hw_auto_model.fit(y_train)

~/miniconda3/lib/python3.7/site-packages/sktime/forecasting/base/_statsmodels.py in fit(self, y_train, fh, X_train)
39         self._set_y_X(y_train, X_train)
40         self._set_fh(fh)
---> 41         self._fit_forecaster(y_train, X_train=X_train)
42         self._is_fitted = True
43         return self

~/miniconda3/lib/python3.7/site-packages/sktime/forecasting/ets.py in _fit_forecaster(self, y, X_train)
287                 delayed(_fit)(error, trend, seasonal, damped)
288                 for error, trend, seasonal, damped in _iter(
--> 289                     error_range, trend_range, seasonal_range, damped_range
290                 )
291             )

~/miniconda3/lib/python3.7/site-packages/joblib/parallel.py in __call__(self, iterable)
1027             # remaining jobs.
1028             self._iterating = False
-> 1029             if self.dispatch_one_batch(iterator):
1030                 self._iterating = self._original_iterator is not None
1031

~/miniconda3/lib/python3.7/site-packages/joblib/parallel.py in dispatch_one_batch(self, iterator)
845                 return False
846             else:
--> 847                 self._dispatch(tasks)
848                 return True
849

~/miniconda3/lib/python3.7/site-packages/joblib/parallel.py in _dispatch(self, batch)
763         with self._lock:
764             job_idx = len(self._jobs)
--> 765             job = self._backend.apply_async(batch, callback=cb)
766             # A job can complete so quickly than its callback is
767             # called before we get here, causing self._jobs to

~/miniconda3/lib/python3.7/site-packages/joblib/_parallel_backends.py in apply_async(self, func, callback)
206     def apply_async(self, func, callback=None):
207         """Schedule a func to be run"""
--> 208         result = ImmediateResult(func)
209         if callback:
210             callback(result)

~/miniconda3/lib/python3.7/site-packages/joblib/_parallel_backends.py in __init__(self, batch)
570         # Don't delay the application, to avoid keeping the input
571         # arguments in memory
--> 572         self.results = batch()
573
574     def get(self):

~/miniconda3/lib/python3.7/site-packages/joblib/parallel.py in __call__(self)
251         with parallel_backend(self._backend, n_jobs=self._n_jobs):
252             return [func(*args, **kwargs)
--> 253                     for func, args, kwargs in self.items]
254
255     def __reduce__(self):

~/miniconda3/lib/python3.7/site-packages/joblib/parallel.py in <listcomp>(.0)
251         with parallel_backend(self._backend, n_jobs=self._n_jobs):
252             return [func(*args, **kwargs)
--> 253                     for func, args, kwargs in self.items]
254
255     def __reduce__(self):

~/miniconda3/lib/python3.7/site-packages/sktime/forecasting/ets.py in _fit(error, trend, seasonal, damped)
271                     dates=self.dates,
272                     freq=self.freq,
--> 273                     missing=self.missing,
274                 )
275                 _fitted_forecaster = _forecaster.fit(

~/miniconda3/lib/python3.7/site-packages/statsmodels/tsa/exponential_smoothing/ets.py in __init__(self, endog, error, trend, damped_trend, seasonal, seasonal_periods, initialization_method, initial_level, initial_trend, initial_seasonal, bounds, dates, freq, missing)
454             )
455             if seasonal_periods is None:
--> 456                 self.seasonal_periods = freq_to_period(self._index_freq)
457             if self.seasonal_periods <= 1:
458                 raise ValueError("seasonal_periods must be larger than 1.")

~/miniconda3/lib/python3.7/site-packages/statsmodels/tsa/tsatools.py in freq_to_period(freq)
810     if not isinstance(freq, offsets.DateOffset):
811         freq = to_offset(freq)  # go ahead and standardize
--> 812     freq = freq.rule_code.upper()
813
814     if freq == 'A' or freq.startswith(('A-', 'AS-')):

AttributeError: 'NoneType' object has no attribute 'rule_code'

# Fixed code
def _fit_forecaster(self, y, X=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]
        if self.sp <= 1 or self.sp is None:
            seasonal_range = [None]
        else:
            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,
        )