Source code for pymc_forecast.features

"""Seasonal feature builders: Fourier design matrices and periodic tiling."""

import numpy as np
import xarray as xr

from pymc_forecast.data import TIME_DIM

__all__ = ["fourier_features", "periodic_repeat"]


[docs] def fourier_features(time, *, period: float, num_terms: int) -> xr.DataArray: """Build a labeled Fourier seasonality design matrix. Parameters ---------- time Either an integer duration (phases ``0..duration-1``) or an array of numeric time positions (e.g. ``np.arange(len(index))`` or fractional day-of-week positions). The positions set the phase; the returned ``"time"`` coord carries them. period Seasonal period, in the same units as ``time``. num_terms Number of harmonics; the output has ``2 * num_terms`` columns. Returns ------- xarray.DataArray Dims ``("time", "fourier")`` with coords labeling each harmonic (``sin_1 .. sin_k, cos_1 .. cos_k``). """ if num_terms < 1: msg = f"num_terms must be >= 1, got {num_terms}" raise ValueError(msg) positions = np.arange(time) if isinstance(time, int) else np.asarray(time, dtype=float) angles = 2.0 * np.pi * np.arange(1, num_terms + 1)[None, :] * positions[:, None] / period values = np.concatenate([np.sin(angles), np.cos(angles)], axis=-1) labels = [f"sin_{k}" for k in range(1, num_terms + 1)] + [ f"cos_{k}" for k in range(1, num_terms + 1) ] return xr.DataArray( values, dims=(TIME_DIM, "fourier"), coords={TIME_DIM: positions, "fourier": labels}, )
[docs] def periodic_repeat(pattern, duration: int, *, axis: int = 0, period: int | None = None): """Tile a seasonal pattern to cover ``duration`` time steps. Works on numpy arrays and on PyTensor variables (e.g. a sampled seasonal latent inside a model). For symbolic tensors whose length along ``axis`` is not statically known, pass ``period`` explicitly. Parameters ---------- pattern The seasonal pattern; its length along ``axis`` is the period. duration Target length along ``axis``. axis Axis to repeat along (defaults to ``0``, the package's time axis). period Explicit period, required only when it cannot be read from ``pattern``'s static shape. """ if period is None: size = pattern.shape[axis] try: period = int(size) except TypeError as err: # symbolic dimension msg = ( "pattern length along the repeat axis is not statically known; " "pass period= explicitly" ) raise ValueError(msg) from err indices = np.arange(duration) % period if isinstance(pattern, np.ndarray): return np.take(pattern, indices, axis=axis) import pytensor.tensor as pt return pt.take(pattern, indices, axis=axis)