PyMC Extras

repository·main·Indexed 40 days ago

https://github.com/pymc-devs/pymc-extras

An extension of the PyMC library providing specialized probability distributions, advanced inference methods, and model transformations. It includes tools for automatic marginalization, statespace models (SARIMAX, VARMAX, ETS), and alternative fitting methods such as Pathfinder, INLA, and DADVI. It also features VIP (Variationally Inferred Parametrization) for automatic hierarchical model reparametrization and a scikit-learn-like ModelBuilder interface.

Tokens
14.3K
Snippets
41
Records
67
Agent score
49%

What's inside PyMC Extras

  1. Overview of pymc-extras features

    main

    pymc-extras provides specialized functionality that extends the core PyMC library, including:

    • Automatic marginalization: Exact for finite discrete and conjugate variables; approximate via Laplace approximation.
    • Alternative inference methods: Pathfinder, DADVI, INLA, Laplace approximation, and improved MAP estimation.
    • Statespace models: SARIMAX, VARMAX, ETS, and structural time series with Kalman filtering.
    • Additional distributions: Such as DiscreteMarkovChain, GeneralizedPoisson, and GenExtreme.

    Functionality that becomes widely useful may eventually be graduated to the main pymc repository.

  2. Overview of pymc-extras capabilities

    main

    pymc-extras is an extension for PyMC that provides specialized functionality not included in the core library. It includes:

    • Automatic marginalization: Exact marginalization for finite discrete and conjugate variables, or approximate marginalization via Laplace approximation.
    • Alternative inference methods: Support for Pathfinder, DADVI, INLA, Laplace approximation, and improved MAP estimation.
    • Statespace models: Implementations for SARIMAX, VARMAX, ETS, and structural time series using Kalman filtering.
    • Additional distributions: Specialized distributions such as DiscreteMarkovChain, GeneralizedPoisson, and GenExtreme.
    • Model building tools: Utilities like the as_model decorator and the ModelBuilder base class to streamline model construction.
  3. Use Kalman Filters in pymc-extras

    main

    The pymc_extras.statespace.filters module provides implementations for state-space filtering and smoothing. It includes standard Kalman filters, univariate filters, square-root filters, and Kalman smoothers designed to work with linear Gaussian state-space models.

    Key components available in this module:

    • StandardFilter: A standard implementation of the Kalman filter.
    • UnivariateFilter: A specialized filter for univariate state-space models.
    • KalmanSmoother: An implementation for performing smoothing (estimating the state at all time points given the entire dataset).
    • SquareRootFilter: A numerically stable implementation using square-root filtering.
    • LinearGaussianStateSpace: The underlying model structure these filters operate on.
  4. Use Bayesian Statespace Models in pymc-extras

    main

    The pymc_extras.statespace.models module provides implementations for Bayesian versions of common statistical statespace models. These models are designed to handle time-series data within the PyMC probabilistic programming framework.

    Available high-level model classes include:

    • BayesianETS: Exponential Smoothing models.
    • BayesianSARIMAX: Seasonal AutoRegressive Integrated Moving Average models with eXogenous regressors.
    • BayesianVARMAX: Vector AutoRegressive Moving Average models with eXogenous regressors.
    from pymc_extras.statespace.models import BayesianETS, BayesianSARIMAX, BayesianVARMAX
  5. Use Linear Gaussian statespace models in pymc-extras

    main

    The pymc_extras.statespace module provides support for Linear Gaussian statespace models. These models are designed for time series analysis using Kalman filtering and smoothing.

    Supported model types include:

    • Classical time series models: Such as SARIMAX, VARMAX, and ETS.
    • Structural models: Models built from interpretable components like trend, seasonality, cycles, and autoregressive errors.
  6. How marginalization works in pymc-extras

    main

    The marginalize function operates on a PyTensor FunctionGraph in two distinct stages:

    1. Marking: For each variable to be marginalized, the subgraph connecting it to its dependent Random Variables (RVs) is wrapped in a MarginalSubgraph marker Op. This marker delimits the variable's Markov blanket: its children (dependent RVs) are the subgraph outputs, and its parents (plus the children's other parents) form the boundary inputs. The marker is type-agnostic.
    2. Resolution: The marginal_rewrites_db (a PyTensor EquilibriumDB) runs on the graph. Registered rewrites inspect MarginalSubgraph nodes. If a rewrite recognizes a pattern (e.g., a specific conjugate pair), it replaces the marker with a typed MarginalRV. If no rewrite matches, marginalize raises a NotImplementedError.

    A MarginalRV is a pytensor.compile.builders.OpFromGraph that also acts as a PyMC MeasurableOp. It preserves the original generative subgraph for forward sampling (e.g., pm.sample_prior_predictive), but implements a custom _logprob that returns the marginal log-probability (integrating out the marginalized variable).

  7. Use VIP for automatic reparametrization of hierarchical models

    main

    The pymc_extras.model.transforms module provides VIP (Variationally Inferred Parametrization) to handle hierarchical model reparametrization. Unlike manual centered or non-centered parametrizations, VIP makes the choice continuous, allowing the model to learn the optimal parametrization per variable during inference. This avoids the need for manual, all-or-nothing decisions between centered and non-centered forms.

    from pymc_extras.model.transforms import vip_reparametrize, VIP
    # Usage involves applying VIP to hierarchical model variables to learn optimal parametrization
  8. Define hierarchical priors using Prior specification

    main

    The pymc_extras.prior module provides a declarative way to define (hierarchical) prior distributions that can be serialized to and from JSON. This is particularly useful when priors are managed via configuration files rather than being hardcoded into a PyMC model (e.g., in pymc-marketing workflows).

    Key components include:

    • Prior: The base class for defining distributions.
    • Censored: For defining censored distributions.
    • Scaled: For defining scaled distributions.
    • sample_prior: Utility to sample from the defined priors.
    • VariableFactory: For managing variable creation.
    • create_dim_handler and handle_dims: For managing dimensions within priors.
  9. How marginalization works in pymc-extras

    main

    Marginalization in pymc_extras.marginal provides model transformations that integrate variables out of a model and allow for their recovery later.

    This is useful for:

    1. Enabling gradient-based sampling: Marginalizing discrete variables allows you to use samplers like NUTS that require gradients.
    2. Reducing dimensionality: Marginalizing conjugate pairs or using the Laplace approximation reduces the dimensionality of the posterior, potentially speeding up computation.

    The workflow typically involves:

    • marginalize: Returns a new model where the requested variables are integrated out, but the remaining variables maintain their original joint distribution (exactly, or approximately via Laplace approximation).
    • unmarginalize: Reverses the transformation.
    • conditional / recover: Reintroduces the marginalized variables conditioned on the posterior of the remaining variables.
  10. Add a new marginalization strategy

    main

    To add a new kind of marginalization (e.g., a new conjugate pair or approximation), you must implement a new module under pymc_extras/model/marginal/distributions/. You need to follow these four steps:

    1. Subclass MarginalRV

    Create a class inheriting from MarginalRV. The inner graph outputs must be laid out as [marginalized_rv, *dependent_rvs, *rng_updates].

    2. Write the Rewrite

    Create a function decorated with @node_rewriter(tracks=[MarginalSubgraph]). This function should:

    • Use extract_marginal_subgraph(node) to get inputs and outputs.
    • Inspect the nodes to see if they match your specific distribution pattern.
    • Return None if the pattern does not match (to allow other rewrites to run).
    • If it matches, return the new MarginalRV outputs.
    • Register the rewrite using marginal_rewrites_db.register(name, rewrite_func, priority).

    3. Register the Marginal Log-Probability

    Use the @_logprob.register(YourMarginalRVClass) decorator. Inside, use inline_ofg_outputs(op, inputs) to recover the inner generative graph and calculate the log-probability of the dependent values with the marginalized variable integrated out.

    4. (Optional) Register the Conditional

    To support conditional() and recover(), use the @marginalized_conditional.register(YourMarginalRVClass) decorator. This function receives the node's inputs and the dep_rvs (the variables the dependents are conditioned on) and must return a random variable representing $p( ext{marginalized} ext{ | } ext{dependents})$.

    # 1. Subclass MarginalRV
    from pymc_extras.model.marginal.distributions.core import MarginalRV
    
    class GammaPoissonMarginalRV(MarginalRV):
        def __init__(self, *args, marginalized_dims, **kwargs):
            self.marginalized_dims = marginalized_dims
            self.n_dependent_rvs = 1
            super().__init__(*args, **kwargs)
    
    # 2. Write the rewrite
    from pymc.distributions import Gamma, Poisson
    from pytensor.graph import node_rewriter
    from pymc_extras.model.marginal.rewrites import (
        MarginalSubgraph,
        extract_marginal_subgraph,
        marginal_rewrites_db,
    )
    
    @node_rewriter(tracks=[MarginalSubgraph])
    def gamma_poisson_marginal(fgraph, node):
        if node.op.n_dependent_rvs != 1:
            return None
    
        inputs, outputs = extract_marginal_subgraph(node)
        marginalized_rv, dependent_rv = outputs[:2]
    
        if not (
            isinstance(marginalized_rv.owner.op, Gamma)
            and isinstance(dependent_rv.owner.op, Poisson)
            and marginalized_rv.type.ndim == 0
        ):
            return None
    
        [poisson_mu] = dependent_rv.owner.op.dist_params(dependent_rv.owner)
        if poisson_mu is not marginalized_rv:
            return None
    
        typed_op = GammaPoissonMarginalRV(
            inputs=inputs,
            outputs=outputs,
            marginalized_dims=node.op.marginalized_dims,
        )
        new_outputs = typed_op(*inputs)
        return list(new_outputs)[: len(node.outputs)]
    
    marginal_rewrites_db.register("gamma_poisson_marginal", gamma_poisson_marginal, "basic")
    
    # 3. Register the marginal logp
    from pymc import NegativeBinomial
    from pymc.logprob.abstract import _logprob
    from pymc.logprob.basic import logp
    from pymc_extras.model.marginal.distributions.core import inline_ofg_outputs
    
    @_logprob.register(GammaPoissonMarginalRV)
    def gamma_poisson_marginal_logp(op, values, *inputs, **kwargs):
        [value] = values
        marginalized_rv, _ = inline_ofg_outputs(op, inputs)[:2]
        alpha, scale = marginalized_rv.owner.op.dist_params(marginalized_rv.owner)
        beta = 1 / scale
        return logp(NegativeBinomial.dist(n=alpha, p=beta / (beta + 1)), value)
    
    # 4. (Optional) Register the conditional
    from pymc_extras.model.marginal.distributions.core import marginalized_conditional
    
    @marginalized_conditional.register(GammaPoissonMarginalRV)
    def gamma_poisson_conditional(op, inputs, dep_rvs):
        marginalized = inline_ofg_outputs(op, inputs)[0]
        alpha, scale = marginalized.owner.op.dist_params(marginalized.owner)
        beta = 1 / scale
    
        [dep_rv] = dep_rvs
        return Gamma.dist(alpha + dep_rv, beta + 1)
  11. Analyze model stability via eigenvalues

    main

    To understand the dynamics and stability of a VAR model, you can analyze the eigenvalues of the transition matrix.

    Workflow:

    1. Sample the statespace matrices using bvar_mod.sample_statespace_matrices(idata, ["T"]). This extracts the transition matrix T from the posterior.
    2. Use numpy.linalg.eigvals (or xarray.apply_ufunc for batch processing) to compute the eigenvalues for each posterior sample.
    3. Inspect the eigenvalues: if they lie within the unit circle in the complex plane, the system is stable.
    # 1. Sample the transition matrix 'T'
    matrix_idata = bvar_mod.sample_statespace_matrices(idata, ["T"])
    
    # 2. Compute eigenvalues across all posterior samples
    import xarray as xr
    import numpy as np
    
    eigs = xr.apply_ufunc(
        np.linalg.eigvals,
        matrix_idata.posterior_predictive.to_dataset()["T"],
        input_core_dims=[["state", "state_aux"]],
        output_core_dims=[["state"]],
    )
    # eigs now contains the eigenvalues for each chain/draw
  12. Perform optimization-based and variational inference in pymc-extras

    main

    Beyond the standard pm.sample method in PyMC, pymc-extras provides specialized inference methods for faster or different types of estimation:

    Optimization-based Point Estimates

    • find_MAP: Finds the Maximum A Posteriori estimate.

    Gaussian Approximations

    • fit_laplace: Uses the Laplace approximation.
    • fit_INLA: Uses Integrated Nested Laplace Approximation.

    Fast Variational Methods

    • fit_pathfinder: Uses the Pathfinder algorithm.
    • fit_dadvi: Uses DADVI.