Skip to content

FourierFeatureTransformer

yohou.preprocessing.time_features.FourierFeatureTransformer

Bases: BaseTransformer

Generate Fourier harmonic features from the time column.

Creates sin/cos feature pairs at specified harmonics of a given seasonal period, useful for encoding cyclical patterns as inputs to reduction forecasters. Output columns are prefixed with fourier_.

For each harmonic \(k\) and seasonal period \(S\), the features at time step \(t\) are:

\[\text{sin}_k(t) = \sin\!\left(\frac{2\pi k\, t}{S}\right), \quad \text{cos}_k(t) = \cos\!\left(\frac{2\pi k\, t}{S}\right)\]

where \(t\) is the number of time steps since the first observed timestamp, \(S\) is the seasonality parameter, and \(k\) ranges over the selected harmonics. Harmonics must satisfy \(k \leq S / 2\) (Nyquist limit).

Parameters

Name Type Description Default
seasonality float

Seasonal period length in number of time steps. Can be non-integer (e.g., 365.25 for yearly on daily data).

7.0
harmonics list of int or None

Which Fourier harmonics to include. For example, [1, 2, 3] uses the first three harmonics. If None, defaults to [1].

None

Attributes

Name Type Description
harmonics_ list of int

Effective list of harmonics used for feature generation.

first_time_ datetime

First observed timestamp, used as reference for index computation.

See Also

Examples

>>> import polars as pl
>>> from datetime import datetime
>>> time = pl.datetime_range(
...     start=datetime(2020, 1, 1), end=datetime(2020, 1, 15), interval="1d", eager=True
... )
>>> X = pl.DataFrame({"time": time, "value": range(len(time))})
>>> transformer = FourierFeatureTransformer(seasonality=7.0, harmonics=[1, 2])
>>> transformer.fit(X)
FourierFeatureTransformer(harmonics=[1, 2])
>>> X_t = transformer.transform(X)
>>> "fourier_7.0_sin_1" in X_t.columns
True

Source Code

Show/Hide source
class FourierFeatureTransformer(BaseTransformer):
    r"""Generate Fourier harmonic features from the time column.

    Creates sin/cos feature pairs at specified harmonics of a given
    seasonal period, useful for encoding cyclical patterns as inputs
    to reduction forecasters. Output columns are prefixed with
    ``fourier_``.

    For each harmonic $k$ and seasonal period $S$, the features at
    time step $t$ are:

    $$\text{sin}_k(t) = \sin\!\left(\frac{2\pi k\, t}{S}\right), \quad \text{cos}_k(t) = \cos\!\left(\frac{2\pi k\, t}{S}\right)$$

    where $t$ is the number of time steps since the first observed
    timestamp, $S$ is the ``seasonality`` parameter, and $k$ ranges
    over the selected ``harmonics``. Harmonics must satisfy
    $k \leq S / 2$ (Nyquist limit).

    Parameters
    ----------
    seasonality : float, default=7.0
        Seasonal period length in number of time steps. Can be
        non-integer (e.g., ``365.25`` for yearly on daily data).
    harmonics : list of int or None, default=None
        Which Fourier harmonics to include. For example,
        ``[1, 2, 3]`` uses the first three harmonics. If ``None``,
        defaults to ``[1]``.

    Attributes
    ----------
    harmonics_ : list of int
        Effective list of harmonics used for feature generation.
    first_time_ : datetime
        First observed timestamp, used as reference for index
        computation.

    See Also
    --------
    - [`CalendarFeatureTransformer`][yohou.preprocessing.calendar.CalendarFeatureTransformer] : Calendar features (month, day of week, etc.).
    - [`HolidayFeatureTransformer`][yohou.preprocessing.calendar.HolidayFeatureTransformer] : Binary holiday indicator.
    - [`TimeIndexTransformer`][yohou.preprocessing.time_features.TimeIndexTransformer] : Numeric time index for trend features.
    - [`FourierSeasonalityForecaster`][yohou.stationarity.seasonality.FourierSeasonalityForecaster] : Forecaster-level Fourier seasonality.

    Examples
    --------
    >>> import polars as pl
    >>> from datetime import datetime
    >>> time = pl.datetime_range(
    ...     start=datetime(2020, 1, 1), end=datetime(2020, 1, 15), interval="1d", eager=True
    ... )
    >>> X = pl.DataFrame({"time": time, "value": range(len(time))})
    >>> transformer = FourierFeatureTransformer(seasonality=7.0, harmonics=[1, 2])
    >>> transformer.fit(X)
    FourierFeatureTransformer(harmonics=[1, 2])
    >>> X_t = transformer.transform(X)
    >>> "fourier_7.0_sin_1" in X_t.columns
    True

    """

    _parameter_constraints: dict = {
        "seasonality": [Interval(numbers.Real, 0, None, closed="neither")],
        "harmonics": [list, None],
    }

    def __init__(
        self,
        seasonality: float = 7.0,
        harmonics: list[int] | None = None,
    ):
        self.seasonality = seasonality
        self.harmonics = harmonics

    def _fit(self, X: pl.DataFrame, y: pl.DataFrame | None = None) -> None:
        """Fit the internal model."""
        self.harmonics_ = self.harmonics if self.harmonics is not None else [1]

        if not self.harmonics_:
            raise ValueError("harmonics list cannot be empty")
        if any(h < 1 for h in self.harmonics_):
            raise ValueError("All harmonics must be positive integers")

        max_harmonic = max(self.harmonics_)
        if max_harmonic > self.seasonality / 2:
            raise ValueError(
                f"Maximum harmonic ({max_harmonic}) cannot exceed seasonality/2 "
                f"({self.seasonality / 2:.1f}) due to Nyquist sampling theorem."
            )

        self.first_time_ = X["time"][0]

        s = f"{self.seasonality}"
        generated_names = []
        for k in self.harmonics_:
            generated_names.append(f"fourier_{s}_sin_{k}")
            generated_names.append(f"fourier_{s}_cos_{k}")
        existing = set(X.columns) - {"time"}
        conflicts = set(generated_names) & existing
        if conflicts:
            raise ValueError(f"Generated column names {sorted(conflicts)} conflict with existing columns in X.")

    def _transform(self, X: pl.DataFrame) -> pl.DataFrame:
        """Generate Fourier harmonic features from the time column.

        Parameters
        ----------
        X : pl.DataFrame
            Validated input time series.

        Returns
        -------
        pl.DataFrame
            DataFrame with ``"time"`` column and Fourier feature columns.

        """
        interval = self.interval_
        time_diff = X["time"] - self.first_time_
        if interval.endswith("mo") or interval.endswith("y"):
            t = np.arange(len(X), dtype=np.float64)
        else:
            t = time_diff.dt.total_seconds().to_numpy().astype(np.float64)
            first_diff = (X["time"][1] - X["time"][0]).total_seconds() if len(X) > 1 else 1.0
            if first_diff != 0:
                t = t / first_diff

        feature_cols = []
        s = f"{self.seasonality}"
        for k in self.harmonics_:
            angle = 2.0 * math.pi * k * t / self.seasonality
            sin_vals = np.sin(angle)
            cos_vals = np.cos(angle)
            feature_cols.append(pl.Series(f"fourier_{s}_sin_{k}", sin_vals, dtype=pl.Float64))
            feature_cols.append(pl.Series(f"fourier_{s}_cos_{k}", cos_vals, dtype=pl.Float64))

        return X.select(pl.col("time")).with_columns(*feature_cols)

    def get_feature_names_out(self, input_features=None) -> list[str]:
        """Get output feature names for transformation.

        Parameters
        ----------
        input_features : array-like of str or None, default=None
            Input feature names (unused, for API compatibility).

        Returns
        -------
        list of str
            Generated Fourier feature column names.

        """
        check_is_fitted(self, ["harmonics_"])
        s = f"{self.seasonality}"
        generated = []
        for k in self.harmonics_:
            generated.append(f"fourier_{s}_sin_{k}")
            generated.append(f"fourier_{s}_cos_{k}")
        return generated

Methods

get_feature_names_out(input_features=None)

Get output feature names for transformation.

Parameters
Name Type Description Default
input_features array-like of str or None

Input feature names (unused, for API compatibility).

None
Returns
Type Description
list of str

Generated Fourier feature column names.

Source Code
Show/Hide source
def get_feature_names_out(self, input_features=None) -> list[str]:
    """Get output feature names for transformation.

    Parameters
    ----------
    input_features : array-like of str or None, default=None
        Input feature names (unused, for API compatibility).

    Returns
    -------
    list of str
        Generated Fourier feature column names.

    """
    check_is_fitted(self, ["harmonics_"])
    s = f"{self.seasonality}"
    generated = []
    for k in self.harmonics_:
        generated.append(f"fourier_{s}_sin_{k}")
        generated.append(f"fourier_{s}_cos_{k}")
    return generated

Tutorials

The following example notebooks use this component:

  • How to Add Calendar, Fourier, and Holiday Features


    Data-Features

    Enrich your feature matrix with time-derived signals using CalendarFeatureTransformer, FourierFeatureTransformer, and HolidayFeatureTransformer.

    View · Open in marimo