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 : int or array_like Either a Python ``int`` duration (phases ``0..duration-1``) or an array of numeric time positions (e.g. ``np.arange(len(index))`` or fractional day-of-week positions). Only a built-in ``int`` takes the duration branch; anything else (including a NumPy integer scalar) is converted with ``np.asarray(time, dtype=float)``. The positions set the phase; the returned ``"time"`` coord carries them. period : float Seasonal period, in the same units as ``time``. num_terms : int Number of harmonics; the output has ``2 * num_terms`` columns. Returns ------- xarray.DataArray Dims ``("time", "fourier")`` with shape ``(n, 2 * num_terms)``. The ``"time"`` coord holds the positions (not the data's time coords) and the ``"fourier"`` coord labels each harmonic (``sin_1 .. sin_k, cos_1 .. cos_k``). Raises ------ ValueError If ``num_terms < 1``. """ 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 PyTensor variables the length along ``axis`` is always symbolic, so ``period`` must be passed explicitly. Parameters ---------- pattern : numpy.ndarray or pytensor.tensor.TensorVariable The seasonal pattern; its length along ``axis`` is the period unless ``period`` is given. duration : int Target length along ``axis``. axis : int, default 0 Axis to repeat along. period : int, optional Explicit period; only the first ``period`` entries of ``pattern`` along ``axis`` are repeated. ``None`` reads it from a NumPy ``pattern``'s shape; it is required for every PyTensor ``pattern``. Returns ------- numpy.ndarray or pytensor.tensor.TensorVariable The tiled pattern with length ``duration`` along ``axis``: a NumPy array for a NumPy ``pattern``, otherwise a symbolic tensor. Raises ------ ValueError If ``period`` is ``None`` and the length of ``pattern`` along ``axis`` is symbolic (any PyTensor ``pattern``). """ 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)