pymc_forecast.model#

Model bodies are (covariates, data=None). Build the horizon with Horizon.from_data. Per-step latents use innovations; observations use predict.

Model-building core: the train/forecast Horizon and the primitives built on it.

The primitives here register time-series latents and observation variables against the Horizon.

The package’s central invariant (inherited from pyro / numpyro_forecast): one model definition both trains and forecasts. In-sample time latents live on variables dimmed "time"; the forecast horizon lives on separate {name}_future variables dimmed "time_future". Those future variables are absent from the fitted posterior, so pm.sample_posterior_predictive replays the posterior in-sample and draws the future from the prior — conditioned on the replayed parents (see tests/test_replay_mechanism.py).

A model is a callable (covariates, data) -> None executed inside a managed pm.Model whose coords carry real time coordinates. The horizon is derived from the coords: future = len(covariates.time) - len(data.time).

pymc_forecast.model.EXPECTED_OBSERVATION_FORECAST_VAR = 'expected_observation_future'#

Reserved name of the forecast-horizon expected observation registered by predict().

See EXPECTED_OBSERVATION_VAR.

pymc_forecast.model.EXPECTED_OBSERVATION_VAR = 'expected_observation'#

Reserved name of the in-sample expected observation registered by predict().

It is the conditional expected observation in observed outcome units, excluding observation noise.

pymc_forecast.model.FORECAST_VAR = 'forecast'#

Name of the forecast-horizon variable registered by predict().

Also registered by predict_mvn().

pymc_forecast.model.MU_FORECAST_VAR = 'mu_future'#

Reserved name of the forecast-horizon noise-free latent predictor from predict().

See MU_VAR.

pymc_forecast.model.MU_VAR = 'mu'#

Reserved name of the in-sample noise-free latent predictor registered by predict().

It is the latent passed to predict(), before observation noise (for GLM-style models this is the linear predictor, not the distribution mean).

pymc_forecast.model.OBS_VAR = 'obs'#

Name of the observed (in-sample) variable registered by predict().

Also registered by predict_mvn().

class pymc_forecast.model.ForecastingModel(priors=None)[source]#

Bases: PriorConfig, ABC

Object-oriented facade over the functional primitives.

Subclasses implement model() and use the bound helpers innovations(), predict() and markov_series(), which thread the current Horizon (available as horizon) automatically. An instance is a valid model function for build_model() and the forecaster classes.

Priors are user-injectable (see PriorConfig): a subclass declares its overridable defaults in default_priors and reads self.prior_config[...] in the model body; callers override any subset at construction time:

import pytensor.tensor as pt
from pymc_extras.prior import Prior

from pymc_forecast.model import ForecastingModel

class LocalLevel(ForecastingModel):
    default_priors = {
        "drift": Prior("Normal", mu=0, sigma=0.1),
        "noise": Prior("Normal", sigma=Prior("HalfNormal", sigma=1)),
    }

    def model(self, covariates, data=None):
        drift = self.innovations("drift", self.prior_config["drift"])
        self.predict(self.prior_config["noise"], pt.cumsum(drift))

LocalLevel(priors={"drift": Prior("StudentT", nu=4, mu=0, sigma=0.2)})
Parameters:

priors (mapping, optional) – Named prior overrides merged over default_priors. None keeps the defaults.

abstractmethod model(covariates, data=None)[source]#

Define the generative model.

The body must register the "obs" variable exactly once, normally by calling predict() once (a second call raises HorizonError).

Parameters:
  • covariates (xarray.DataArray) – Normalized (time-first) covariates spanning the full horizon.

  • data (xarray.DataArray, optional) – Normalized (time-first) observed data; None for prior-only builds.

property horizon#

The Horizon of the model build currently in progress.

Raises HorizonError when accessed outside a model build.

innovations(name, dist, *, dims=())[source]#

Bound innovations() using the current build’s horizon.

Parameters:
  • name (str) – Base variable name; the forecast segment is f"{name}_future".

  • dist (pymc_extras.prior.Prior or pytensor.tensor.TensorVariable) – A pymc-extras Prior or an unnamed .dist() tensor.

  • dims (tuple of str, default ()) – Extra (non-time) dims of the per-step latent; each must be a model coord.

Returns:

pytensor.tensor.TensorVariable – The latent over the full horizon, time on axis 0 (see innovations()).

Raises:

pymc_forecast.exceptions.HorizonError – If called outside a model build, or for the conditions listed in innovations().

predict(obs, latent, *, expected_observation=None, dims=None)[source]#

Bound predict() using the current build’s horizon.

Parameters:
  • obs (pymc_extras.prior.Prior, callable or pytensor.tensor.TensorVariable) – Observation specification (see predict()).

  • latent (pytensor.tensor.TensorVariable) – Full-horizon predictor with time on axis 0 and length horizon.duration.

  • expected_observation (pytensor.tensor.TensorVariable, optional) – Full-horizon conditional expected observation in observed outcome units. None registers no expected-observation variables.

  • dims (tuple of str, optional) – Extra (non-time) dims of the observation. None infers them from the data’s non-time dims (() for prior-only builds).

Raises:
markov_series(name, init, transition, *, params=(), xs=None, dims=())[source]#

Bound markov_series() using this build’s horizon.

Parameters:
  • name (str) – Base variable name; the forecast segment is f"{name}_future".

  • init (float, array_like or pytensor.tensor.TensorVariable) – Initial state fed to the first transition (cast to float64).

  • transition (callable) – (z_prev, *params) -> dist (with xs: (z_prev, x_t, *params) -> dist) returning a .dist().

  • params (sequence, default ()) – Random variables the transition uses, passed as explicit inputs.

  • xs (array_like, optional) – Exogenous inputs, positional with time on axis 0 and at least horizon.duration rows. None means no inputs.

  • dims (tuple of str, default ()) – Extra (non-time) dims of the per-step state.

Returns:

pytensor.tensor.TensorVariable – The latent over the full horizon, time on axis 0 (see markov_series()).

Raises:

pymc_forecast.exceptions.HorizonError – If called outside a model build, or for the conditions listed in markov_series().

class pymc_forecast.model.Horizon(data, time, time_future=<factory>)[source]#

Bases: object

The train/forecast split of a single model build, derived from coords.

Each parameter is available as an attribute of the same name.

Parameters:
  • data (xarray.DataArray or None) – Observed data, stored as given (time-first when built by build_model()), or None for prior-only builds.

  • time (numpy.ndarray) – Coordinate values of the in-sample window (length t_obs); in prior-only builds, the whole covariate span.

  • time_future (numpy.ndarray, optional) – Coordinate values of the forecast horizon. Empty whenever there is no horizon. Defaults to an empty array.

property t_obs#

Number of in-sample time steps (the full covariate span in prior-only builds).

property future#

Number of forecast time steps (0 when there is no forecast horizon).

property duration#

Total horizon length t_obs + future.

classmethod from_data(covariates, data)[source]#

Derive the horizon from normalized data/covariate time coords.

covariates span the full horizon; data (if given) covers the observed prefix. With data=None (prior-only builds) the whole covariate span counts as observed time. No normalization is applied here (build_model() normalizes with as_dataarray() first).

Parameters:
  • covariates (xarray.DataArray) – Normalized covariates with a "time" coord spanning the full horizon.

  • data (xarray.DataArray or None) – Normalized observed data with a "time" dim, or None for a prior-only build.

Returns:

Horizon – The derived horizon; data is stored as given.

Raises:

pymc_forecast.exceptions.AlignmentError – If data is given and is not aligned with covariates (see validate_alignment()).

pymc_forecast.model.build_model(model_fn, data, covariates, *, coords=None)[source]#

Build a pm.Model from a model function and (data, covariates).

The horizon is derived from the time coords: covariates span the full horizon, data covers the observed prefix. Registered coords: "time" (observed steps), "time_future" (forecast steps, only when forecasting), every non-time dim of data/covariates (data dims take precedence over covariate dims of the same name; unlabeled dims get integer coords), plus any user coords, which are applied last and override the derived coords.

Parameters:
  • model_fn (callable or ForecastingModel) – The model body (covariates, data) -> None or a ForecastingModel instance. It is called with the normalized (time-first) xarray.DataArray covariates and data (or None).

  • data (xarray.DataArray, pandas.Series, pandas.DataFrame or array_like) – Observed training series, normalized with as_dataarray() ("time" first; 2-D input gets a "series" dim). None gives a prior-only build over the whole covariate span (obs is then unobserved).

  • covariates (xarray.DataArray, pandas.Series, pandas.DataFrame or array_like) – Covariates spanning the full horizon, normalized with as_dataarray (2-D input gets a "covariate" dim). Rows past the end of data define the forecast horizon (use null_covariates() if the model has none).

  • coords (mapping, optional) – Extra coords to register on the model; they override derived coords of the same name. None adds none.

Returns:

pymc.Model – The built model (its context is no longer active on return).

Raises:
pymc_forecast.model.innovations(h, name, dist, *, dims=())[source]#

Sample a per-step latent over the full horizon.

dist is a pymc-extras Prior or an unnamed .dist() tensor. A Prior follows prior_rv_factory(), including one shared draw of nested hyper-priors. A .dist() is resized to ("time", *dims) and registered as name, plus ("time_future", *dims) as {name}_future when forecasting. Its parameters broadcast against the trailing dims (e.g. a per-series scale), and a multivariate dist’s support fills the last dims. The time axis belongs to this helper, so a .dist() with more axes than dims is rejected. So are a model variable (pass pm.Normal.dist(...), not pm.Normal(name, ...)) and a .dist() whose parameters are unnamed random variables: create a random scale as a model variable (sigma = pm.HalfNormal("sigma", 1)) and pass pm.Normal.dist(0, sigma).

Parameters:
  • h (Horizon) – The horizon of the current model build.

  • name (str) – Base variable name of the in-sample latent; the forecast segment is registered as f"{name}_future".

  • dist (pymc_extras.prior.Prior or pytensor.tensor.TensorVariable) – A pymc-extras Prior or an unnamed .dist() tensor.

  • dims (tuple of str, default ()) – Extra (non-time) dims of the per-step latent; each must be a model coord.

Returns:

pytensor.tensor.TensorVariable – The latent over the full horizon, time on axis 0. When h.future == 0 this is the registered model variable name itself; otherwise it is an unnamed concatenation of name and {name}_future with shape (h.duration, *dim lengths).

Raises:

pymc_forecast.exceptions.HorizonError – If dist is neither a Prior nor an unnamed .dist(), is a model variable, depends on unnamed random variables, or has more axes than dims; if a .dist() latent needs a dim that is not a model coord; or if a Prior has a time-dimmed hyper-prior.

pymc_forecast.model.predict(h, obs, latent, *, expected_observation=None, dims=None)[source]#

Register the observation and forecast variables of the model.

latent is the deterministic full-horizon predictor (time on axis 0). The observed prefix becomes the likelihood ("obs", dims ("time", *dims)); when forecasting, the suffix becomes the unobserved "forecast" variable (dims ("time_future", *dims)) that pm.sample_posterior_predictive draws.

The latent itself is also recorded, noise-free, as "mu" (in-sample) and "mu_future" (forecast horizon) Deterministics — the documented way to separate parameter/latent uncertainty from observation noise (see docs/schema.md). For GLM-style models this is the linear predictor passed to predict, not the distribution mean. The names "mu" and "mu_future" are therefore reserved: a model body must not define variables with these names.

Model authors may additionally pass expected_observation to record the conditional expected observation in observed outcome units, excluding observation noise. It is emitted as "expected_observation" and "expected_observation_future" without changing the meaning of "mu" / "mu_future". This value is explicit because predict does not infer inverse links or distribution means from obs.

This single primitive covers both upstream predict (location-family noise: pass lambda name, mu, dims, observed: pm.Normal(name, mu, sigma, dims=dims, observed=observed)) and upstream predict_glm (any link, e.g. lambda name, eta, dims, observed: pm.Poisson(name, pt.exp(eta), dims=dims, observed=observed)) — PyMC likelihoods take their parameters directly, so no distribution surgery is needed.

Parameters:
  • h (Horizon) – The horizon of the current model build.

  • obs (pymc_extras.prior.Prior, callable or pytensor.tensor.TensorVariable) –

    Observation specification, dispatched in order:

    • a pymc-extras Prior (mu must be left unset; nested hyper-priors are shared across segments);

    • a 4-argument factory (name, latent, dims, observed) -> RV: a callable with at least four positional parameters, or one that binds four positional arguments but not one (e.g. a functools.partial). Parameter names do not matter;

    • any other callable, called once per segment with the segment latent as its first positional argument and returning an unnamed .dist(). A bare .dist classmethod is accepted only when that parameter is mu (e.g. pm.Poisson.dist); pm.StudentT.dist takes nu first and is rejected. The callable must not create model variables: create them in the model body and close over them;

    • a zero-centered pm.Normal.dist / pm.StudentT.dist whose location is replaced by the segment latent.

  • latent (pytensor.tensor.TensorVariable) – Full-horizon predictor with time on axis 0; its length along axis 0 must be h.duration.

  • expected_observation (pytensor.tensor.TensorVariable, optional) – Full-horizon conditional expected observation in observed outcome units, with time on axis 0. For a Poisson log-link model whose latent is eta, pass pt.exp(eta). None registers no expected-observation variables; their names are reserved either way.

  • dims (tuple of str, optional) – Extra (non-time) dims of the observation. None infers them from the data’s non-time dims (() for prior-only builds).

Raises:
  • pymc_forecast.exceptions.HorizonError – If the model already defines "mu", "mu_future", "expected_observation" or "expected_observation_future" (including a second predict call in one model); or if obs is invalid: any other dist, a non-zero location, a bare .dist classmethod whose first parameter is not mu, a callable that creates model variables or does not return a .dist(), a model variable instead of a .dist(), a .dist() depending on unnamed random variables, or a Prior with a time-dimmed hyper-prior.

  • ValueError – If obs is a Prior with mu set.

  • KeyError – If an entry of dims is not a model coord.