Skip to content

Working with results

Every engine — inla, tmb, hmc_laplace — returns a result that implements one accessor protocol. This page collects those accessors in one place. The examples use INLA, but the named-block and marginal accessors below work the same way on any engine's result.

Use the accessor functions, not the fields

Reach for the accessor functionslatent_marginals(result), linear_predictor_marginals(result), base_latent_marginals(result) — rather than the matching result.… fields.

Under the default (compact) latent parameterization the model does not materialize the augmented blocks, so result.linear_predictor_marginals and result.base_latent_marginals are nothing. The functions handle this: they return the stored block when it exists and derive it from the latent posterior otherwise. A compact result derives the linear predictor η = A·x from the latent posterior through the design map; an augmented result slices the stored η-block. Either way you get the same vector of marginals back, so code written against the functions keeps working if the parameterization changes.

julia
result = inla(model, y)

ηs = linear_predictor_marginals(result)   # works in both compact and augmented mode
xs = base_latent_marginals(result)        # the original model's latent components

Latent marginals by name

When the model is written with @latte (or converted with latte_from_dppl), each ~-bound latent term gets a name. Rather than indexing the flat marginal vector with hand-computed offsets, ask for a named block with latent_marginals(result, name::Symbol). It returns the slice of marginals for that term — a Vector of Distributions, length 1 for a scalar term:

julia
# Model with `β ~ MvNormal(...)` (regression coefficients) and a
# `u ~ BesagModel(W)(τ = τ)` spatial effect.
β_marginals = latent_marginals(result, )   # one marginal per coefficient
u_marginals = latent_marginals(result, :u)   # one marginal per spatial unit

β_means = [mean(m) for m in β_marginals]
β_stds  = [std(m)  for m in β_marginals]

This is the recommended way to pull out a specific term: it stays correct when the latent layout changes (extra covariates, a different number of spatial units) and reads more clearly than positional indexing. The available names are those of the ~-bound latent terms in the model; latent_groups(result) returns the full name => index-range map. The same name-keyed form works for hyperparameters via hyperparameter_marginals(result, name), with hyperparameter_groups(result) exposing their layout.

The returned marginals are Distributions.jl distributions, so mean, var, std, quantile, pdf, and rand work on them directly.

Hyperparameter marginals are in natural space

hyperparameter_marginals(result) returns the hyperparameter posteriors in natural space — the scale the prior was declared on — not the internal working (transformed) space the optimizer uses. So no back-transformation is needed before computing summaries:

julia
τ_marginal = hyperparameter_marginals(result, )[1]   # precision, natural space

precision_mean = mean(τ_marginal)
precision_ci   = quantile(τ_marginal, [0.025, 0.975])

hyperparameter_mode(result) likewise reports the posterior mode in natural space.

Derived quantities

A declared hyperparameter's transform is inferred from its prior's support, so declaring the prior on the parameter you care about is usually all you need. A positive parameter is cleanest as a positive prior — write α ~ LogNormal(0, 1) rather than log_α ~ Normal(0, 1) with α = exp(log_α) (an identical prior): then α is a declared hyperparameter and mean(hyperparameter_marginals(result, :α)[1]) is the natural-space E[α], directly.

For a quantity that genuinely can't be written as a single declared prior — a function of two or more hyperparameters, say — use pushforward to map a marginal through a transform. Its mean, quantile, etc. are computed by integration, so mean(pushforward(m, exp)) is the true E[exp X], not the Jensen-biased exp(E[X]):

julia
m = hyperparameter_marginals(result, :log_β)[1]
derived = pushforward(m, exp)
mean(derived)                          # true E[exp(log_β)], integrated
quantile(derived, [0.025, 0.975])

Fitted values on the response scale

observation_marginals(result) transforms the linear-predictor marginals through the inverse link to give marginals for the expected observation μ = g⁻¹(η), one per observation. For a Poisson log-link these are the rates λ; for a logit link they are the success probabilities p. The returned distributions support the full Distributions.jl interface:

julia
fitted = observation_marginals(result)
μ_mean = mean(fitted[1])
μ_ci   = (quantile(fitted[1], 0.025), quantile(fitted[1], 0.975))

This requires an ExponentialFamily observation model (possibly wrapped) whose link function can be extracted.

Convergence and timing

julia
converged(result)       # did the optimization / exploration converge?
time_elapsed(result)    # total wall-clock time, in seconds

log_marginal_likelihood(result) returns the engine's approximation to log p(y), or nothing when the method has no natural estimate.

Approximation quality

diagnose(result) runs a PSIS-k̂ check on the inner Laplace approximation q(x | θ) ≈ p(x | y, θ) at the hyperparameter mode and returns a NamedTuple with a relative effective sample size, the GPD shape pareto_k, and a qualitative interpretation (:excellent / :acceptable / :unreliable):

julia
d = diagnose(result)
d.rel_ess          # relative effective sample size, in (0, 1]
d.interpretation   # :excellent / :acceptable / :unreliable
d.obs_hessian      # :exact, or :gauss_newton for a NonlinearLeastSquares obs

An :unreliable verdict means the Gaussian inner approximation is a poor fit at the mode, so the marginals downstream of it should be treated with caution.

obs_hessian reports whether the inner approximation used the exact observation Hessian or the Gauss–Newton approximation that a NonlinearLeastSquaresModel applies — the default for a Gaussian observation with a nonlinear-in-x mean. When it is :gauss_newton, the latent marginals and log p(y) carry that approximation; pass nls = false to the model constructor to force the exact path instead.

Reference

Latte.INLAResult Type
julia
INLAResult{HM, LM, Mode, Expl, Conv, Time, Model, Opts, Acc}

Results structure for INLA inference containing all outputs from the inference process.

This structure provides organized access to all results from INLA inference, including hyperparameter marginals, latent marginals, diagnostic information, and model comparison metrics.

Type Parameters

All fields are fully typed for type stability and performance.

Fields

  • hyperparameter_marginals::HM: NamedTuple mapping parameter names to marginal distributions for each hyperparameter

  • latent_marginals::LM: Vector of marginal distributions for latent variables (WeightedMixture)

  • hyperparameter_mode::Mode: Mode of the hyperparameter posterior (WorkingHyperparameters)

  • exploration::Expl: Results from posterior exploration (HyperparameterExploration)

  • convergence::Conv: Convergence diagnostics and information (NamedTuple)

  • computation_time::Time: Timing breakdown by computation phase (NamedTuple)

  • model::Model: Original INLA model specification (LatentGaussianModel)

  • options::Opts: Options used for inference (NamedTuple)

  • accumulators::Acc: Tuple of PosteriorAccumulator objects with computed metrics (e.g., DIC, marginal likelihood)

  • linear_predictor_marginals::Union{Nothing, Vector}: Marginals for linear predictors η (if augmented model)

  • base_latent_marginals::Union{Nothing, Vector}: Marginals for base latent components (if augmented model)

  • augmentation_info::Union{Nothing, AugmentationInfo}: Metadata about latent field augmentation

Usage

julia
result = inla_inference(model, y)

# Access hyperparameter marginals (by name)
result.hyperparameter_marginals.τ  # Marginal for τ hyperparameter
mean(result.hyperparameter_marginals.τ)  # Mean of τ hyperparameter

# Access latent marginals
result.latent_marginals[1]  # First latent variable marginal (WeightedMixture)

# Access mode (WorkingHyperparameters)
result.hyperparameter_mode       # WorkingHyperparameters
convert(NamedTuple, convert(NaturalHyperparameters, result.hyperparameter_mode))  # Convert to NamedTuple in natural space

# Access diagnostics
result.convergence.mode_converged      # Did mode finding converge?
result.computation_time.total          # Total computation time
result.computation_time.mode_finding   # Time spent finding mode

# Access model comparison metrics
result.accumulators[1]                 # First accumulator (e.g., DICAccumulator)
result.accumulators[1].DIC             # DIC value
result.accumulators[1].p_D             # Effective parameters
source
Latte.latent_marginals Function
julia
latent_marginals(r::InferenceResult) -> Vector{<:Distribution}
latent_marginals(r::InferenceResult, name::Symbol) -> Vector{<:Distribution}

Marginal posterior distributions for the latent field. The vector form returns all marginals positionally. The name-keyed form returns the slice corresponding to that latent-field group (e.g. , :u); returns a 1-element vector for scalar groups.

source
Latte.base_latent_marginals Function
julia
base_latent_marginals(result::INLAResult)

Marginals of the original model's base latent components, uniform across result modes.

The augmented representation slices the base block out of the full latent vector; the compact default stores only the base latent, so its latent marginals already are these. Prefer this accessor to the result.base_latent_marginals field, which is nothing under the compact default.

See also latent_marginals, linear_predictor_marginals.

source
GaussianMarkovRandomFields.linear_predictor_marginals Function
julia
linear_predictor_marginals(ga, obs_lik) -> (μ_η, v_η, eta_likelihood)

Posterior marginals of the per-observation linear predictor η_i under a Gaussian approximation ga to the latent posterior.

For each observation i the result contains (μ_η[i], v_η[i]) — the mean and variance of the scalar predictor η_i that obs_lik consumes. The third return value is an observation likelihood whose own indexing matches μ_η's layout: feeding μ_η directly into loglik, pointwise_loglik, loggrad, or loghessian yields per-observation outputs in the same order as μ_η.

The dispatch recurses on the observation likelihood structure:

  • ExponentialFamilyLikelihood: η_i = x[indices[i]] (or η_i = x_i when indices === nothing). μ_η and v_η are the corresponding slices of mean(ga) / var(ga); eta_likelihood is the same likelihood with indices === nothing so it consumes the returned (smaller) μ_η directly.

  • LinearlyTransformedLikelihood: affine predictor η = A x + b (the offset b = lik.offset is omitted when nothing), giving μ_η = A · mean(ga) + b and v_η = diag(A · Σ · Aᵀ) from the posterior's selected-inversion output (the constant b does not affect the variance). eta_likelihood = lik.base_likelihood. Assumes the base's own indices field is nothing (the standard wrapping pattern); an indexed base is unusual and not specially handled.

  • CompositeLikelihood: per-component results concatenated. eta_likelihood is a fresh CompositeLikelihood whose components are the per-component stripped likelihoods with indices re-assigned to their slice of the concatenated η, so the result can be evaluated against μ_η directly. Composite-of-composite is not specially handled — flatten upstream.

Hard constraints

If ga carries a hard linear constraint A_c x = e (either a ConstrainedGMRF or a WorkspaceGMRF with constraint info populated), v_η subtracts the standard correction diag(A · A_tilde_T · L_c⁻ᵀ · L_c⁻¹ · A_tilde_Tᵀ · Aᵀ) from the unconstrained diag(A · Σ · Aᵀ), reusing the cached A_tilde_T = Σ A_cᵀ and L_c = chol(A_c Σ A_cᵀ). The mean is the constrained mean returned by mean(ga).

Sparse-pattern assumption

For LinearlyTransformedLikelihood, the variance computation reads Σ from the posterior's selected inversion, which only fills entries at the Cholesky factor pattern of the precision matrix Q. When Q's pattern subsumes that of Aᵀ A — automatic when the posterior comes out of gaussian_approximation with a LinearlyTransformedObservationModel obs side — every entry Σ[j, k] needed by diag(A Σ Aᵀ) is present and the result is exact. With a hand-rolled prior whose pattern is too narrow, missing Σ entries silently contribute zero and v_η underestimates; arrange Q's pattern to include Aᵀ A if you build the posterior outside the package's standard flow.

source
Latte.hyperparameter_marginals Function
julia
hyperparameter_marginals(r::InferenceResult) -> Vector{<:Distribution}
hyperparameter_marginals(r::InferenceResult, name::Symbol) -> Vector{<:Distribution}

Marginal posterior distributions for the hyperparameters, analogous to latent_marginals. Semantics depend on the method — see concrete implementations' docstrings (e.g. INLA returns natural-space spline marginals, TMB returns working-space Gaussian approximations).

source
Latte.observation_marginals Function
julia
observation_marginals(result::INLAResult; rtol::Real = 1.0e-3, atol::Real = 1.0e-6)

Compute marginal distributions for observations (fitted values) by transforming linear predictor marginals through the inverse link function.

Mathematical Background

For an observation model with link function g:

  • Linear predictor: η (in ℝ, typically Gaussian-like)

  • Expected observation: μ = g⁻¹(η) (in observation space)

This function transforms the marginal distributions for η to obtain marginal distributions for μ = g⁻¹(η), which represent the fitted values or expected observations under the model.

Requirements

This function requires:

  1. Augmented latent model: The model must have been created with automatic augmentation (default for LinearlyTransformedObservationModel).

  2. ExponentialFamily observation model: Currently only supported for ExponentialFamily models with extractable link functions.

Arguments

  • result::INLAResult: INLA inference results with linear predictor marginals

  • rtol::Real = 1.0e-3: Relative tolerance for numerical integration in moment calculations

  • atol::Real = 1.0e-6: Absolute tolerance for numerical integration in moment calculations

Returns

A vector of TransformedWeightedMixture distributions, one for each observation, representing the marginal distribution of the expected observation μᵢ = g⁻¹(ηᵢ).

Each distribution supports the full Distributions.jl interface:

  • mean(obs_marginal): Expected value of the observation

  • var(obs_marginal): Variance of the observation

  • quantile(obs_marginal, p): Quantiles for credible intervals

  • pdf(obs_marginal, y): Density evaluation

  • rand(obs_marginal): Sampling

Examples

julia
# After running INLA with augmented model
result = inla(model, y)

# Get observation marginals (fitted values)
obs_marginals = observation_marginals(result)

# Access statistics for each observation
for i in 1:length(obs_marginals)
    μ_mean = mean(obs_marginals[i])
    μ_std = std(obs_marginals[i])
    μ_ci = (quantile(obs_marginals[i], 0.025), quantile(obs_marginals[i], 0.975))
    println("Observation $i: μ = $μ_mean ± $μ_std, 95% CI: $μ_ci")
end

# For Poisson regression with log link, these represent the rate parameter λ
# For logistic regression, these represent the probability p

Common Link Functions and Their Interpretations

  • LogLink (Poisson, Gamma, etc.): μ = exp(η) represents the rate/scale parameter

  • LogitLink (Binomial, Bernoulli): μ = logistic(η) represents the success probability

  • IdentityLink (Gaussian): μ = η is the mean directly

Implementation Notes

  • Uses TransformedWeightedMixture which applies change of variables to the linear predictor marginals.

  • Moments (mean, variance) computed via numerical integration (1D quadrature).

  • The bijector stored internally is the link function g; inverse is taken when needed.

Error Handling

  • Throws an error if result.linear_predictor_marginals is nothing (augmentation not used).

  • Throws an error if the observation model is not an ExponentialFamily.

  • Throws an error if the link function is not supported by get_bijector.

source
Latte.latent_groups Function
julia
latent_groups(model::LatentGaussianModel) -> OrderedDict{Symbol, UnitRange{Int}}

Name → augmented-latent-range mapping for a DPPL-built LGM (empty for hand-built LGMs). Matches latent_groups(::INLAResult) so lookup by name works interchangeably on the model and its inference result.

source
julia
latent_groups(r::InferenceResult) -> OrderedDict{Symbol, UnitRange{Int}}

Name → index-range mapping for the latent field. Scalar group maps to i:i; vector group of length p maps to a:(a+p-1). When no naming exists (manually-constructed LGM), returns an empty OrderedDict.

Populated by DSL / formula layers; empty otherwise.

source
Latte.hyperparameter_groups Function
julia
hyperparameter_groups(r::InferenceResult) -> OrderedDict{Symbol, UnitRange{Int}}

Name → index-range mapping for the hyperparameters, analogous to latent_groups.

source
Latte.hyperparameter_mode Function
julia
hyperparameter_mode(r::InferenceResult) -> NaturalHyperparameters

Mode of the hyperparameter posterior in natural space. For INLA this centres the grid; for TMB this is the MAP (the answer); for HMC-Laplace this is the warm-start used before sampling.

source
Latte.log_marginal_likelihood Function
julia
log_marginal_likelihood(r::InferenceResult) -> Union{Float64, Nothing}

Approximation to log p(y). Each method produces a different approximation (INLA: grid integral of Laplace-approx integrand; TMB: Laplace at MAP; HMC: requires bridge sampling). Returns nothing when the method has no natural way to produce an estimate. Concrete implementations' docstrings spell out which approximation is computed.

source
Latte.converged Function
julia
converged(r::InferenceResult) -> Bool

Whether the underlying optimisation / exploration converged.

source
Latte.time_elapsed Function
julia
time_elapsed(r::InferenceResult) -> Float64

Total wall-clock time for inference, in seconds.

source
Latte.pushforward Function
julia
pushforward(marginal, g)

The distribution of g(X) where X ~ marginal. Moments are computed by integration, so mean(pushforward(m, exp)) is the true E[exp(X)] — not exp(E[X]) (the Jensen-inequality trap).

g may be exp, log, identity, or any Bijectors bijector. This is the way to recover a derived hyperparameter's posterior: e.g. when a model declares log_α ~ Normal(...) and uses α = exp(log_α), the posterior of α is pushforward(result.hyperparameter_marginals.log_α, exp). (A declared hyperparameter's marginal is already in natural space, so this isn't needed there.)

source
Latte.diagnose Function
julia
diagnose(r::InferenceResult; M = 500, rng = Random.default_rng())

Run the PSIS-k̂ diagnostic at the hyperparameter mode of a Laplace-based inference result. Works uniformly over INLAResult, TMBResult, and HMCLaplaceResult — all three have a well-defined inner Laplace at their MAP (INLA uses it for grid centering; TMB reports it as the answer; HMC-Laplace uses it per-sample).

Returns

NamedTuple{(:rel_ess, :ess, :pareto_k, :interpretation, :obs_hessian, :M)}:

  • rel_ess ∈ (0, 1] — relative effective sample size (primary metric)

  • ess — absolute ESS of the importance weights

  • pareto_k — Zhang-Stephens GPD shape parameter

  • interpretation:excellent / :acceptable / :unreliable

  • obs_hessian:gauss_newton if the observation Hessian is the Gauss–Newton approximation (NonlinearLeastSquaresModel), else :exact

  • M — number of Gaussian samples used

:unreliable suggests switching to a method that doesn't rely on the Laplace approximation being exact (e.g., a future sparsenuts(lgm, y) that samples the joint (θ, x) directly).

source