Repository: alan-turing-institute/sktime
Pull request #3377 (2022-09-03 08:21:24)

[DOC] updated extension templates - transformer tags explained
This PR adds higher detail in the documentation of the tags in the transformer extension templates. Towards https://github.com/alan-turing-institute/sktime/issues/3280

FYI @lmmentel, @ltsaprounis


Patch:
diff --git a/docs/source/api_reference/transformations.rst b/docs/source/api_reference/transformations.rst
index e0e3174d7..0a0727261 100644
--- a/docs/source/api_reference/transformations.rst
+++ b/docs/source/api_reference/transformations.rst
@@ -382,8 +382,8 @@ Missing value imputation
 
     Imputer
 
-Datetime feature generation
-~~~~~~~~~~~~~~~~~~~~~~~~~~~
+Seasonality and Date-Time Features
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
 
 .. currentmodule:: sktime.transformations.series.date
 
@@ -393,6 +393,14 @@ Datetime feature generation
 
     DateTimeFeatures
 
+.. currentmodule:: sktime.transformations.series.fourier
+
+.. autosummary::
+    :toctree: auto_generated/
+    :template: class.rst
+
+    FourierFeatures
+
 Auto-correlation series
 ~~~~~~~~~~~~~~~~~~~~~~~
 
diff --git a/extension_templates/transformer.py b/extension_templates/transformer.py
index 3d08b09af..2f224060e 100644
--- a/extension_templates/transformer.py
+++ b/extension_templates/transformer.py
@@ -99,35 +99,150 @@ class MyTransformer(BaseTransformer):
     #   y_inner_mtype must be changed to one or a list of compatible sktime mtypes
     #  the other tags are "safe defaults" which can usually be left as-is
     _tags = {
+        # to list all valid tags with description, use sktime.registry.all_tags
+        #   all_tags(estimator_types="transformer", as_dataframe=True)
+        #
+        #
+        # behavioural tags: transformer type
+        # ----------------------------------
+        #
+        # scitype:transform-input, scitype:transform-output, scitype:transform-labels
+        # control the input/output type of transform, in terms of scitype
+        #
+        # scitype:transform-input, scitype:transform-output should be the
+        # simplest scitype that describes the mapping, taking into account vectorization
+        # a transform that produces Series when given Series, Panel when given Panel
+        #   should have both transform-input and transform-output as "Series"
+        # a transform that produces a tabular DataFrame (Table)
+        #   when given Series or Panel should have transform-input "Series"
+        #       and transform-output as "Primitives"
         "scitype:transform-input": "Series",
-        # what is the scitype of X: Series, or Panel
+        # valid values: "Series", "Panel"
         "scitype:transform-output": "Series",
-        # what scitype is returned: Primitives, Series, Panel
+        # valid values: "Series", "Panel", "Primitives"
+        #
+        # scitype:instancewise = is fit_transform an instance-wise operation?
+        # instance-wise = only values of a given series instance are used to transform
+        #   that instance. Example: Fourier transform; non-example: series PCA
+        "scitype:instancewise": True,
+        #
+        # scitype:transform-labels types the y used in transform
+        #   if y is not used in transform, this should be "None"
         "scitype:transform-labels": "None",
-        # what is the scitype of y: None (not needed), Primitives, Series, Panel
-        "scitype:instancewise": True,  # is this an instance-wise transform?
-        "capability:inverse_transform": False,  # can the transformer inverse transform?
-        "univariate-only": False,  # can the transformer handle multivariate X?
-        "X_inner_mtype": "pd.DataFrame",  # which mtypes do _fit/_predict support for X?
-        # this can be a Panel mtype even if transform-input is Series, vectorized
-        "y_inner_mtype": "None",  # which mtypes do _fit/_predict support for y?
-        "requires_y": False,  # does y need to be passed in fit?
-        "enforce_index_type": None,  # index type that needs to be enforced in X/y
-        "fit_is_empty": True,  # is fit empty and can be skipped? Yes = True
-        "X-y-must-have-same-index": False,  # can estimator handle different X/y index?
+        # valid values: "None" (not needed), "Primitives", "Series", "Panel"
+        #
+        #
+        # behavioural tags: internal type
+        # ----------------------------------
+        #
+        # X_inner_mtype, y_inner_mtype control which format X/y appears in
+        # in the inner functions _fit, _transform, etc
+        "X_inner_mtype": "pd.DataFrame",
+        "y_inner_mtype": "None",
+        # valid values: str and list of str
+        # if str, must be a valid mtype str, in sktime.datatypes.MTYPE_REGISTER
+        #   of scitype Series, Panel (panel data) or Hierarchical (hierarchical series)
+        #   y_inner_mtype can also be of scitype Table (one row/instance per series)
+        #   in that case, all inputs are converted to that one type
+        # if list of str, must be a list of valid str specifiers
+        #   in that case, X/y are passed through without conversion if on the list
+        #   if not on the list, converted to the first entry of the same scitype
+        #
+        # univariate-only controls whether internal X can be univariate/multivariate
+        # if True (only univariate), always applies vectorization over variables
+        "univariate-only": False,
+        # valid values: True = inner _fit, _transform receive only univariate serie
+        #   False = uni- and multivariate series are passed to inner methods
+        #
+        # requires_y = does y need to be passed in fit?
+        "requires_y": False,
+        # valid values: False (no), True = exception is raised if no y is seen in _fit
+        #   y can be passed or not in _transform for either value of requires_y
+        #
+        # remember_data = whether all data seen is remembered as self._X
+        "remember_data": False,
+        # valid vales: False (no), True = self._X is created/update in fit/update
+        #   self._X is all X passed via fit or update, updated via update_data
+        #   self._X is of mtype seen in fit, update adds more data to the same container
+        #   self._X can be used (readonly) by the estimator in _fit, _transform, _update
+        #   if set to True, fit-is-empty must be set to False
+        #
+        # capability tags: properties of the estimator
+        # --------------------------------------------
+        #
+        # fit_is_empty = is fit empty and can be skipped?
+        "fit_is_empty": True,
+        # valid values: True = _fit is considered empty and skipped, False = No
+        # CAUTION: default is "True", i.e., _fit will be skipped even if implemented
+        #
+        # X-y-must-have-same-index = can estimator handle different X/y index?
+        "X-y-must-have-same-index": False,
+        # valid values: boolean True (yes), False (no)
+        # if True, raises exception if X.index is not contained in y.index
+        #
+        # enforce_index_type = index type that needs to be enforced in X/y
+        "enforce_index_type": None,
+        # valid values: pd.Index subtype, or list of pd.Index subtype
+        # if not None, raises exception if X.index, y.index level -1 is not of that type
+        #
+        # transform-returns-same-time-index = does transform return same index as input?
         "transform-returns-same-time-index": False,
-        # does transform return have the same time index as input X
-        "skip-inverse-transform": False,  # is inverse-transform skipped when called?
+        # valid values: boolean True (yes), False (no)
+        # if True, transform and inverse_transform returns should have
+        #   same length and same index (if pandas) as inputs
+        # no exception is raised if this tag is incorrectly set
+        #
+        # capability:inverse_transform = is inverse_transform implemented?
+        "capability:inverse_transform": False,
+        # valid values: boolean True (yes), False (no)
+        # if True, _inverse_transform must be implemented
+        # if False, exception is raised if inverse_transform is called,
+        #   unless the skip-inverse-transform tag is set to True
+        #
+        # skip-inverse-transform = is inverse-transform skipped when called?
+        "skip-inverse-transform": False,
+        # if False, capability:inverse_transform tag behaviour is as per devault
+        # if True, inverse_transform is the identity transform and raises no exception
+        #   this is useful for transformers where inverse_transform
+        #   may be called but should behave as the identity, e.g., imputers
+        #
+        # capability:unequal_length = can the transformer handle unequal length panels,
+        #   i.e., when passed unequal length instances in Panel or Hierarchical data
         "capability:unequal_length": True,
-        # can the transformer handle unequal length time series (if passed Panel)?
+        # valid values: boolean True (yes), False (no)
+        # if False, may raise exception when passed unequal length Panel/Hierarchical
+        #
+        # capability:unequal_length:removes = if passed Panel/Hierarchical,
+        #   is transform result always guaranteed to be equal length (and series)?
         "capability:unequal_length:removes": False,
-        # is transform result always guaranteed to be equal length (and series)?
-        #   not relevant for transformers that return Primitives in transform-output
+        # valid values: boolean True (yes), False (no)
+        # applicable only if scitype:transform-output is not "Primitives"
+        # used for search index and validity checking, does not raise direct exception
+        #
+        # handles-missing-data = can the transformer handle missing data (np or pd.NA)?
         "handles-missing-data": False,  # can estimator handle missing data?
-        # todo: rename to capability:missing_values
+        # valid values: boolean True (yes), False (no)
+        # if False, may raise exception when passed time series with missing values
+        #
+        # capability:missing_values:removes = if passed time series
+        #   is transform result always guaranteed to contain no missing values?
         "capability:missing_values:removes": False,
-        # is transform result always guaranteed to contain no missing values?
-        "python_version": None,  # PEP 440 python version specifier to limit versions
+        # valid values: boolean True (yes), False (no)
+        # used for search index and validity checking, does not raise direct exception
+        #
+        #
+        # dependency tags: python version and soft dependencies
+        # -----------------------------------------------------
+        #
+        # python version requirement
+        "python_version": None,
+        # valid values: str, PEP 440 valid python version specifiers
+        # raises exception at construction if local python veresion is incompatible
+        #
+        # soft dependency requirement
+        "python_dependencies": None
+        # valid values: str or list of str
+        # raises exception at construction if modules at strings cannot be imported
     }
     # in case of inheritance, concrete class should typically set tags
     #  alternatively, descendants can set tags in __init__
diff --git a/extension_templates/transformer_simple.py b/extension_templates/transformer_simple.py
index d79cf511f..b52c03c64 100644
--- a/extension_templates/transformer_simple.py
+++ b/extension_templates/transformer_simple.py
@@ -87,33 +87,92 @@ class MyTransformer(BaseTransformer):
     #   y_inner_mtype must be changed to one or a list of compatible sktime mtypes
     #  the other tags are "safe defaults" which can usually be left as-is
     _tags = {
+        # to list all valid tags with description, use sktime.registry.all_tags
+        #   all_tags(estimator_types="transformer", as_dataframe=True)
+        #
+        #
+        # behavioural tags: transformer type
+        # ----------------------------------
+        #
+        # scitype:transform-input, scitype:transform-output, scitype:transform-labels
+        # control the input/output type of transform, in terms of scitype
+        #
+        # scitype:transform-input, scitype:transform-output should be the
+        # simplest scitype that describes the mapping, taking into account vectorization
+        # a transform that produces Series when given Series, Panel when given Panel
+        #   should have both transform-input and transform-output as "Series"
+        # a transform that produces a tabular DataFrame (Table)
+        #   when given Series or Panel should have transform-input "Series"
+        #       and transform-output as "Primitives"
         "scitype:transform-input": "Series",
-        # what is the scitype of X: Series, or Panel
+        # valid values: "Series", "Panel"
         "scitype:transform-output": "Series",
-        # what scitype is returned: Primitives, Series, Panel
+        # valid values: "Series", "Panel", "Primitives"
+        #
+        # scitype:instancewise = is fit_transform an instance-wise operation?
+        # instance-wise = only values of a given series instance are used to transform
+        #   that instance. Example: Fourier transform; non-example: series PCA
+        "scitype:instancewise": True,
+        #
+        # scitype:transform-labels types the y used in transform
+        #   if y is not used in transform, this should be "None"
         "scitype:transform-labels": "None",
-        # what is the scitype of y: None (not needed), Primitives, Series, Panel
-        "scitype:instancewise": True,  # is this an instance-wise transform?
-        "capability:inverse_transform": False,  # can the transformer inverse transform?
-        "univariate-only": False,  # can the transformer handle multivariate X?
-        "X_inner_mtype": "pd.DataFrame",  # which mtypes do _fit/_predict support for X?
-        # this can be a Panel mtype even if transform-input is Series, vectorized
-        "y_inner_mtype": "None",  # which mtypes do _fit/_predict support for y?
-        "requires_y": False,  # does y need to be passed in fit?
-        "enforce_index_type": None,  # index type that needs to be enforced in X/y
-        "fit_is_empty": True,  # is fit empty and can be skipped? Yes = True
-        "X-y-must-have-same-index": False,  # can estimator handle different X/y index?
-        "transform-returns-same-time-index": False,
-        # does transform return have the same time index as input X
-        "skip-inverse-transform": False,  # is inverse-transform skipped when called?
+        # valid values: "None" (not needed), "Primitives", "Series", "Panel"
+        #
+        #
+        # behavioural tags: internal type
+        # ----------------------------------
+        #
+        # X_inner_mtype, y_inner_mtype control which format X/y appears in
+        # in the inner functions _fit, _transform, etc
+        "X_inner_mtype": "pd.DataFrame",
+        "y_inner_mtype": "None",
+        # valid values: str and list of str
+        # if str, must be a valid mtype str, in sktime.datatypes.MTYPE_REGISTER
+        #   of scitype Series, Panel (panel data) or Hierarchical (hierarchical series)
+        #   y_inner_mtype can also be of scitype Table (one row/instance per series)
+        #   in that case, all inputs are converted to that one type
+        # if list of str, must be a list of valid str specifiers
+        #   in that case, X/y are passed through without conversion if on the list
+        #   if not on the list, converted to the first entry of the same scitype
+        #
+        # univariate-only controls whether internal X can be univariate/multivariate
+        # if True (only univariate), always applies vectorization over variables
+        "univariate-only": False,
+        # valid values: True = inner _fit, _transform receive only univariate serie
+        #   False = uni- and multivariate series are passed to inner methods
+        #
+        # requires_y = does y need to be passed in fit?
+        "requires_y": False,
+        # valid values: False (no), True = exception is raised if no y is seen in _fit
+        #   y can be passed or not in _transform for either value of requires_y
+        #
+        #
+        # capability tags: properties of the estimator
+        # --------------------------------------------
+        #
+        # fit_is_empty = is fit empty and can be skipped?
+        "fit_is_empty": True,
+        # valid values: True = _fit is considered empty and skipped, False = No
+        # CAUTION: default is "True", i.e., _fit will be skipped even if implemented
+        #
+        # capability:inverse_transform = is inverse_transform implemented?
+        "capability:inverse_transform": False,
+        # valid values: boolean True (yes), False (no)
+        # if True, _inverse_transform must be implemented
+        # if False, exception is raised if inverse_transform is called,
+        #   unless the skip-inverse-transform tag is set to True
+        #
+        # capability:unequal_length = can the transformer handle unequal length panels,
+        #   i.e., when passed unequal length instances in Panel or Hierarchical data
         "capability:unequal_length": True,
-        # can the transformer handle unequal length time series (if passed Panel)?
-        "capability:unequal_length:removes": False,
-        # is transform result always guaranteed to be equal length (and series)?
-        #   not relevant for transformers that return Primitives in transform-output
+        # valid values: boolean True (yes), False (no)
+        # if False, may raise exception when passed unequal length Panel/Hierarchical
+        #
+        # handles-missing-data = can the transformer handle missing data (np or pd.NA)?
         "handles-missing-data": False,  # can estimator handle missing data?
-        "capability:missing_values:removes": False,
-        # is transform result always guaranteed to contain no missing values?
+        # valid values: boolean True (yes), False (no)
+        # if False, may raise exception when passed time series with missing values
     }
 
     # todo: add any hyper-parameters and components to constructor
diff --git a/sktime/forecasting/conformal.py b/sktime/forecasting/conformal.py
index 7b99b6e6e..ca8809a0e 100644
--- a/sktime/forecasting/conformal.py
+++ b/sktime/forecasting/conformal.py
@@ -8,6 +8,7 @@ Code based partially on NaiveVariance by ilyasmoutawwakil.
 __all__ = ["ConformalIntervals"]
 __author__ = ["fkiraly", "bethrice44"]
 
+from math import floor
 from warnings import warn
 
 import numpy as np
@@ -53,8 +54,12 @@ class ConformalIntervals(BaseForecaster):
             Caveat: this does not give frequentist but conformal predictive intervals
         "conformal": as in Stankeviciute et al, but with H=1,
             i.e., no Bonferroni correction under number of indices in the horizon
-    initial_window : int, optional, default=1
-        number of minimum initial indices to use for fitting when computing residuals
+    initial_window : float, int or None, optional (default=max(10, 0.1*len(y)))
+        Defines the size of the initial training window
+        If float, should be between 0.0 and 1.0 and represent the proportion
+        of the dataset to include for the initial window for the train split.
+        If int, represents the relative number of train samples in the initial window.
+        If None, the value is set to the larger of 0.1*len(y) and 10
     sample_frac : float, optional, default=None
         value in range (0,1) corresponding to fraction of y index to calculate
         residuals matrix values for (for speeding up calculation)
@@ -273,6 +278,39 @@ class ConformalIntervals(BaseForecaster):
 
         return pred_int
 
+    def _parse_initial_window(self, y, initial_window=None):
+
+        n_samples = len(y)
+
+        if initial_window is None:
+            initial_window = max(10, 0.1 * n_samples)
+
+        initial_window_type = np.asarray(initial_window).dtype.kind
+
+        if (
+            initial_window_type == "i"
+            and (initial_window >= n_samples or initial_window <= 0)
+            or initial_window_type == "f"
+            and (initial_window <= 0 or initial_window >= 1)
+        ):
+            raise ValueError(
+                "initial_window={0} should be either positive and smaller"
+                " than the number of samples {1} or a float in the "
+                "(0, 1) range".format(initial_window, n_samples)
+            )
+
+        if initial_window is not None and initial_window_type not in ("i", "f"):
+            raise ValueError(
+                "Invalid value for initial_window: {}".format(initial_window)
+            )
+
+        if initial_window_type == "f":
+            n_initial_window = int(floor(initial_window * n_samples))
+        elif initial_window_type == "i":
+            n_initial_window = int(initial_window)
+
+        return n_initial_window
+
     def _compute_sliding_residuals(self, y, X, forecaster, initial_window, sample_frac):
         """Compute sliding residuals used in uncertainty estimates.
 
@@ -284,10 +322,15 @@ class ConformalIntervals(BaseForecaster):
             sktime compatible exogeneous time series to use in forecasts
         forecaster : sktime compatible forecaster
             forecaster to use in computing the sliding residuals
-        initial_window : int
-            minimum length of initial window to use in fitting
+        initial_window : float, int or None, optional (default=max(10, 0.1*len(y)))
+            Defines the size of the initial training window
+            If float, should be between 0.0 and 1.0 and represent the proportion
+            of the dataset to include for the initial window for the train split.
+            If int, represents the relative number of train samples in the
+            initial window.
+            If None, the value is set to the larger of 0.1*len(y) and 10
         sample_frac : float
-            for speeding up computing of residuals matrix.
+            For speeding up computing of residuals matrix.
             sample value in range (0, 1) to obtain a fraction of y indices to
             compute residuals matrix for
         Returns
@@ -300,7 +343,9 @@ class ConformalIntervals(BaseForecaster):
         """
         y = convert_to(y, "pd.Series")
 
-        y_index = y.index[initial_window:]
+        n_initial_window = self._parse_initial_window(y, initial_window=initial_window)
+
+        y_index = y.iloc[n_initial_window:].index
 
         residuals_matrix = pd.DataFrame(columns=y_index, index=y_index, dtype="float")
 
diff --git a/sktime/transformations/series/fourier.py b/sktime/transformations/series/fourier.py
new file mode 100644
index 000000000..8bfa17d62
--- /dev/null
+++ b/sktime/transformations/series/fourier.py
@@ -0,0 +1,212 @@
+# -*- coding: utf-8 -*-
+# copyright: sktime developers, BSD-3-Clause License (see LICENSE file)
+"""Fourier features for time series with long/complex seasonality."""
+
+__author__ = ["ltsaprounis"]
+
+import warnings
+from copy import deepcopy
+from typing import List, Union
+
+import numpy as np
+import pandas as pd
+
+from sktime.transformations.base import BaseTransformer
+
+
+class FourierFeatures(BaseTransformer):
+    r"""Fourier Features for time series seasonality.
+
+    Fourier Series terms can be used as explanatory variables for the cases of multiple
+    seasonal periods and or complex / long seasonal periods [1]_, [2]_. For every
+    seasonal period, :math:`sp` and fourier term :math:`k` pair there are 2 fourier
+    terms sin_sp_k and cos_sp_k:
+        - sin_sp_k = :math:`sin(\frac{2 \pi k t}{sp})`
+        - cos_sp_k = :math:`cos(\frac{2 \pi k t}{sp})`
+
+    Where :math:`t` is the number of time steps elapsed from the beginning of the time
+    series.
+
+    The output of the trasform is a pandas DataFrame that includes the fourier terms as
+    additional columns with the naming convention stated above (sin_sp_k and cos_sp_k).
+    For instance for sp_list = [12, 3] and fourier_terms_list = [2, 1] the transformed
+    series will have the additional columns:
+    "cos_12_1", "sin_12_1", "cos_12_2", "sin_12_2", "cos_3_1", "sin_3_1"
+
+    The implementation is based on the fourier function from the R forecast package [3]_
+
+    Parameters
+    ----------
+    sp_list : List[Union[int, float]]
+        list of seasonal periods
+    fourier_terms_list : List[int]
+        list of number of fourier terms (K) for each seasonal period.
+        Each K matches to the sp (seasonal period) of the sp_list.
+        For example, if sp_list = [7, 365] and fourier_terms_list = [3, 9], the seasonal
+        frequency of 7 will have 3 fourier terms and the seasonal frequency of 365
+        will have 9 fourier terms.
+
+    References
+    ----------
+    .. [1] Hyndsight - Forecasting with long seasonal periods:
+        https://robjhyndman.com/hyndsight/longseasonality/
+    .. [2] Hyndman, R.J., & Athanasopoulos, G. (2021) Forecasting: principles and
+        practice, 3rd edition, OTexts: Melbourne, Australia. OTexts.com/fpp3.
+        Accessed on August 14th 2022.
+    .. [3] https://pkg.robjhyndman.com/forecast/reference/fourier.html
+
+    Examples
+    --------
+    >>> from sktime.transformations.series.fourier import FourierFeatures
+    >>> from sktime.datasets import load_airline
+    >>> y = load_airline()
+    >>> transformer = FourierFeatures(sp_list=[12], fourier_terms_list=[4])
+    >>> y_hat = transformer.fit_transform(y)
+    """
+
+    _tags = {
+        "scitype:transform-input": "Series",
+        # what is the scitype of X: Series, or Panel
+        "scitype:transform-output": "Series",
+        # what scitype is returned: Primitives, Series, Panel
+        "scitype:transform-labels": "None",
+        # what is the scitype of y: None (not needed), Primitives, Series, Panel
+        "scitype:instancewise": True,  # is this an instance-wise transform?
+        "capability:inverse_transform": False,  # can the transformer inverse transform?
+        "univariate-only": False,  # can the transformer handle multivariate X?
+        "X_inner_mtype": "pd.DataFrame",  # which mtypes do _fit/_predict support for X?
+        # this can be a Panel mtype even if transform-input is Series, vectorized
+        "y_inner_mtype": "None",  # which mtypes do _fit/_predict support for y?
+        "requires_y": False,  # does y need to be passed in fit?
+        "enforce_index_type": [pd.PeriodIndex],  # index type that needs to be enforced
+        # in X/y
+        "fit_is_empty": False,  # is fit empty and can be skipped? Yes = True
+        "X-y-must-have-same-index": False,  # can estimator handle different X/y index?
+        "transform-returns-same-time-index": True,
+        # does transform return have the same time index as input X
+        "skip-inverse-transform": True,  # is inverse-transform skipped when called?
+        "capability:unequal_length": False,
+        # can the transformer handle unequal length time series (if passed Panel)?
+        "capability:unequal_length:removes": False,
+        # is transform result always guaranteed to be equal length (and series)?
+        #   not relevant for transformers that return Primitives in transform-output
+        "handles-missing-data": False,  # can estimator handle missing data?
+        # todo: rename to capability:missing_values
+        "capability:missing_values:removes": False,
+        # is transform result always guaranteed to contain no missing values?
+        "python_version": None,  # PEP 440 python version specifier to limit versions
+    }
+
+    def __init__(self, sp_list: List[Union[int, float]], fourier_terms_list: List[int]):
+        self.sp_list = sp_list
+        self.fourier_terms_list = fourier_terms_list
+
+        if len(self.sp_list) != len(self.fourier_terms_list):
+            raise ValueError(
+                "In FourierFeatures the length of the sp_list needs to be equal "
+                "to the length of fourier_terms_list."
+            )
+
+        if np.any(np.array(self.sp_list) / np.array(self.fourier_terms_list) < 1):
+            raise ValueError(
+                "In FourierFeatures the number of each element of fourier_terms_list"
+                "needs to be lower from the corresponding element of the sp_list"
+            )
+
+        super(FourierFeatures, self).__init__()
+
+    def _fit(self, X, y=None):
+        """Fit transformer to X and y.
+
+        private _fit containing the core logic, called from fit
+
+        Parameters
+        ----------
+        X : Series or Panel of mtype X_inner_mtype
+            if X_inner_mtype is list, _fit must support all types in it
+            Data to fit transform to
+        y : Series or Panel of mtype y_inner_mtype, default=None
+            Additional data, e.g., labels for transformation
+
+        Returns
+        -------
+        self: reference to self
+        """
+        # Create the sp, k pairs
+        # Don't add pairs where the coefficient k/sp already exists
+        self.sp_k_pairs_list_ = []
+        coefficient_list = []
+        for i, sp in enumerate(self.sp_list):
+            for k in range(1, self.fourier_terms_list[i] + 1):
+                coef = k / sp
+                if coef not in coefficient_list:
+                    coefficient_list.append(coef)
+                    self.sp_k_pairs_list_.append((sp, k))
+                else:
+                    warnings.warn(
+                        f"The terms sin_{sp}_{k} and cos_{sp}_{k} from FourierFeatures "
+                        "will be skipped because the resulting coefficient already "
+                        "exists from other seasonal period, fourier term pairs."
+                    )
+
+        # store the integer form of the minimum date in the prediod index
+        # this is used to make sure that time t is calculated with reference to
+        # the data passed on fit
+        self.min_t_ = np.min(X.index.astype(int))
+
+        return self
+
+    def _transform(self, X, y=None):
+        """Transform X and return a transformed version.
+
+        private _transform containing core logic, called from transform
+
+        Parameters
+        ----------
+        X : Series or Panel of mtype X_inner_mtype
+            if X_inner_mtype is list, _transform must support all types in it
+            Data to be transformed
+        y : Series or Panel of mtype y_inner_mtype, default=None
+            Additional data, e.g., labels for transformation
+
+        Returns
+        -------
+        transformed version of X
+        """
+        X_transformed = deepcopy(X)
+        # get the integer form of the PeriodIndex
+        int_index = X_transformed.index.astype(int) - self.min_t_
+
+        for sp_k in self.sp_k_pairs_list_:
+            sp = sp_k[0]
+            k = sp_k[1]
+
+            X_transformed[f"sin_{sp}_{k}"] = np.sin(int_index * 2 * k * np.pi / sp)
+            X_transformed[f"cos_{sp}_{k}"] = np.cos(int_index * 2 * k * np.pi / sp)
+
+        return X_transformed
+
+    @classmethod
+    def get_test_params(cls, parameter_set="default"):
+        """Return testing parameter settings for the estimator.
+
+        Parameters
+        ----------
+        parameter_set : str, default="default"
+            Name of the set of test parameters to return, for use in tests. If no
+            special parameters are defined for a value, will return `"default"` set.
+            There are currently no reserved values for transformers.
+
+        Returns
+        -------
+        params : dict or list of dict, default = {}
+            Parameters to create testing instances of the class
+            Each dict are parameters to construct an "interesting" test instance, i.e.,
+            `MyClass(**params)` or `MyClass(**params[i])` creates a valid test instance.
+            `create_test_instance` uses the first (or only) dictionary in `params`
+        """
+        params = [
+            {"sp_list": [12], "fourier_terms_list": [4]},
+            {"sp_list": [12, 6.2], "fourier_terms_list": [3, 4]},
+        ]
+        return params


Test patch:
diff --git a/sktime/transformations/series/tests/test_fourier.py b/sktime/transformations/series/tests/test_fourier.py
new file mode 100644
index 000000000..cbca0520b
--- /dev/null
+++ b/sktime/transformations/series/tests/test_fourier.py
@@ -0,0 +1,74 @@
+# -*- coding: utf-8 -*-
+"""Tests for the FourierFeatures transformer."""
+
+import numpy as np
+import pandas as pd
+import pytest
+from pandas.util.testing import assert_frame_equal
+
+from sktime.datasets import load_airline
+from sktime.transformations.series.fourier import FourierFeatures
+
+Y = load_airline()
+
+
+def test_fourier_list_length_missmatch():
+    """Tests exception raised when sp_list & fourier_terms_list lengths don't match."""
+    with pytest.raises(ValueError) as ex:
+        FourierFeatures(sp_list=[365, 52], fourier_terms_list=[1])
+        assert ex.value == (
+            "In FourierFeatures the length of the sp_list needs to be equal "
+            "to the length of fourier_terms_list."
+        )
+
+
+def test_fourier_k_larger_than_sp():
+    """Tests exception raised when fourier_terms_list elements larger than sp_list."""
+    with pytest.raises(ValueError) as ex:
+        FourierFeatures(sp_list=[2], fourier_terms_list=[3])
+        assert ex.value == (
+            "In FourierFeatures the number of each element of fourier_terms_list"
+            "needs to be lower from the corresponding element of the sp_list"
+        )
+
+
+def test_fourier_redundant_terms_dropped():
+    """Tests redundant sp and k pairs are dropped if their equivalents exist."""
+    transformer = FourierFeatures(sp_list=[12, 9, 3], fourier_terms_list=[4, 3, 1])
+    transformer.fit(Y)
+    assert transformer.sp_k_pairs_list_ == [
+        (12, 1),
+        (12, 2),
+        (12, 3),
+        (12, 4),
+        (9, 1),
+        (9, 2),
+    ]
+
+
+def test_fit_transform_outputs():
+    """Tests that we get the expected outputs."""
+    y = Y.iloc[:3]
+    y_transformed = FourierFeatures(sp_list=[12], fourier_terms_list=[2]).fit_transform(
+        y
+    )
+    expected = (
+        y.to_frame()
+        .assign(sin_12_1=[np.sin(2 * np.pi * i / 12) for i in range(3)])
+        .assign(cos_12_1=[np.cos(2 * np.pi * i / 12) for i in range(3)])
+        .assign(sin_12_2=[np.sin(4 * np.pi * i / 12) for i in range(3)])
+        .assign(cos_12_2=[np.cos(4 * np.pi * i / 12) for i in range(3)])
+    )
+    assert_frame_equal(y_transformed, expected)
+
+
+def test_fit_transform_behaviour():
+    """Tests that the transform method evaluates time steps passed based on X in fit."""
+    transformer = FourierFeatures(sp_list=[12], fourier_terms_list=[2])
+    # fit_transform on one part of the dataset
+    y_tr_1 = transformer.fit_transform(Y.iloc[:100])
+    # transform the rest
+    y_tr_2 = transformer.transform(Y.iloc[100:])
+    # fit_transform the entire dataset
+    y_tr_complete = transformer.fit_transform(Y)
+    assert_frame_equal(pd.concat([y_tr_1, y_tr_2]), y_tr_complete)