StudentT#

class impulso.observation.StudentT(*, name='student_t', is_heavy_tailed=True, nu='infer', prior_alpha=2.0, prior_beta=0.1)[source]#

Bases: ImpulsoModel

Student-t observation errors — y_t ~ MvT_nu(μ_t, Ω).

Heavy-tailed innovations for samples with outliers (crises, pandemic quarters, data revisions). Estimation stays fully Bayesian: the t likelihood downweights extreme observations automatically rather than requiring them to be dummied out by hand, because the t score with respect to the location is bounded and redescending — ∂ log p/∂μ = w · Ω⁻¹(y - μ) with w = (nu + n)/(nu + q) and q the squared Mahalanobis distance, so w → 0 as an observation moves far into the tail.

Ω = L Lᵀ is the scale matrix, not the covariance: E[y_t] = μ_t (nu > 1) and Cov[y_t] = nu/(nu-2) · Ω (nu > 2). The consequences of that convention — which downstream quantities change and which are exactly invariant — are set out in docs/adr/0007-student-t-errors-use-the-scale-matrix-convention.md.

Parameters:
name#

Discriminator key for the registry (always “student_t”).

Type:

Literal[‘student_t’]

is_heavy_tailed#

Always True.

Type:

bool

nu#

Degrees of freedom. A float strictly greater than 2 fixes nu; the default “infer” estimates it from the data. Small values mean fat tails (nu ≈ 4 is aggressively robust); large values approach the Gaussian.

Type:

float | Literal[‘infer’]

prior_alpha#

Shape of the Gamma prior on the excess degrees of freedom nu - 2. Must be strictly greater than 1 so the prior density vanishes at the origin — see PRIOR_ALPHA_LOWER. Only used when nu=”infer”.

Type:

float

prior_beta#

Rate of that Gamma prior. Only used when nu=”infer”.

Type:

float

Expand for references to impulso.observation.StudentT

Heavy-Tailed Observation Errors / Fitting

NU_LOWER: ClassVar[float] = 2.0#

below it the t has infinite variance, which would make innovation_covariance, density-forecast bands, and variance decompositions meaningless. Users who genuinely want infinite-variance innovations should build the PyMC model directly.

Type:

Hard lower bound on nu. Not configurable

PRIOR_ALPHA_LOWER: ClassVar[float] = 1.0#

Hard lower bound on prior_alpha. Gamma(alpha, ·) has zero density at the origin only for alpha > 1: at alpha = 1 the density there is the rate itself and for alpha < 1 it is unbounded. Since the prior on nu is that Gamma shifted by NU_LOWER, any alpha <= 1 piles mass against the nu = 2 infinite-variance boundary the shift exists to avoid.

build_likelihood(name, mu, chol, observed, dims=None)[source]#

Register the multivariate Student-t likelihood in the active PyMC model.

Registers nu as a Deterministic named “nu” in both modes, so the posterior always carries the degrees of freedom and downstream code never has to know whether they were fixed or inferred. When nu=”infer”, the free random variable is the excess degrees of freedom nu_excess ~ Gamma(prior_alpha, prior_beta) and nu = 2 + nu_excess. The shift (rather than a truncation) is deliberate: Gamma(alpha, ·) has zero density at the origin for any alpha > 1 — which the validator enforces — so the prior vanishes exactly where nu/(nu-2) blows up, and the unconstrained NUTS transform is the standard positive-support one.

chol is passed straight through, so the manual Cholesky parameterisation the volatility seam builds carries over verbatim (pm.MvStudentT shares quaddist_matrix with pm.MvNormal).

Parameters:
  • name (str) – Name for the observed random variable (the pipeline uses “obs”).

  • mu (Any) – Conditional mean tensor, shape (T, n_vars).

  • chol (Any) – Lower-triangular Cholesky factor of the scale matrix Ω.

  • observed (ndarray) – Observed endogenous matrix, shape (T, n_vars).

  • dims (tuple[str, ...] | None) – PyMC dims for the observed variable.

Returns:

The registered PyMC random variable.

Return type:

Any

draw_standardised_innovations(shape, rng, posterior)[source]#

Draw standardised multivariate-t innovations ξ.

Reproduces PyMC’s own MvStudentT construction: ξ = z / sqrt(g / nu) with z ~ N(0, I) and g ~ χ²_nu drawn once per observation vector, not once per component. The shared mixing variable is what makes the components dependent (though uncorrelated) and the joint law genuinely multivariate-t; drawing one χ² per component would give a product of independent t marginals instead.

rng.standard_normal is consumed first, before the χ² draw, so the Gaussian adapter’s stream stays a strict prefix of this one and seeded Gaussian forecasts are unaffected by the seam.

nu is read per posterior draw, so a model with inferred degrees of freedom yields innovations whose tail weight varies across draws.

Parameters:
  • shape (tuple[int, ...]) – Draw shape, (chains, draws, n_vars).

  • rng (Generator) – Generator to consume.

  • posterior (xr.Dataset) – Posterior Dataset carrying nu with shape (chains, draws).

Returns:

Standardised t innovations of shape shape, with E[ξ] = 0 and Cov[ξ] = nu/(nu-2) · I. Each marginal is exactly a standard t_nu.

Raises:

ValueError – If the posterior carries no nu, or if its shape does not match the leading (chains, draws) of shape.

Return type:

ndarray

model_config = {'frozen': True}#

Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].

variance_inflation(posterior)[source]#

Factor converting the scale matrix Ω into the innovation covariance.

nu/(nu-2), evaluated per posterior draw. Finite by construction: nu is bounded below by 2 both by the validator (fixed mode) and by the shifted Gamma prior (inferred mode).

Parameters:

posterior (xr.Dataset) – Posterior Dataset carrying nu with shape (chains, draws).

Returns:

Array of shape (chains, draws).

Raises:

ValueError – If the posterior carries no nu.

Return type:

ndarray