Results

Marginal Posteriors

Understand posterior marginal distributions and use utility functions to evaluate, transform, sample, and summarize them.

What are Marginal Posteriors?

Definition

A marginal posterior distribution \(\pi(\theta_i \mid \mathbf{y})\) is the posterior probability distribution for a single parameter \(\theta_i\), integrating out all other parameters. It answers: "Given the data, what do we believe about this specific parameter?"

After fitting a model with pyINLA, you obtain marginal posteriors for:

In pyINLA, each marginal is a 2-column pandas.DataFrame with column names x (parameter values) and y (density values).

Accessing the four marginal types

After fitting, you reach each of the four groups through a named attribute on the result object. Every marginal is a 2-column pandas.DataFrame with columns x (parameter value grid) and y (density).

import numpy as np
import pandas as pd
from pyinla import pyinla

# Synthetic data: y = 1 + 0.5 x + u[group] + noise.
rng = np.random.default_rng(0)
n, K = 200, 20
group = rng.integers(1, K + 1, size=n)
u = rng.normal(0, 0.5, size=K)
x = rng.normal(size=n)
y = 1.0 + 0.5 * x + u[group - 1] + rng.normal(0, 0.2, size=n)
df = pd.DataFrame({"y": y, "x": x, "group": group})

# A small Gaussian model with one fixed effect and one IID random effect.
model = {
    "response": "y",
    "fixed":   ["1", "x"],
    "random":  [{"id": "group", "model": "iid"}],
}

# Opt in to linear-predictor marginals (otherwise the attribute is None).
res = pyinla(
    model=model, family="gaussian", data=df,
    control={"compute": {"return_marginals_predictor": True}},
)

# --- 1. Fixed effects --------------------------------------------------
# dict keyed by effect name; each value is a DataFrame with columns x, y.
marg_x = res.marginals_fixed["x"]
# Use pyinla.emarginal for expectations (Simpson quadrature on the grid):
posterior_mean_x = pyinla.emarginal(lambda v: v, marg_x)           # E[x]
posterior_var_x  = pyinla.emarginal(lambda v: v**2, marg_x) - posterior_mean_x**2

# --- 2. Hyperparameters ------------------------------------------------
# dict keyed by R-INLA's descriptive labels.
marg_prec = res.marginals_hyperpar["Precision for the Gaussian observations"]
# Transform precision -> variance via pyinla.tmarginal (handles the
# Jacobian, re-grids, and returns a properly ordered marginal):
marg_var = pyinla.tmarginal(lambda tau: 1.0 / tau, marg_prec)
posterior_mean_var = pyinla.emarginal(lambda v: v, marg_var)
# Quick summary (mean, sd, quantiles, mode):
pyinla.zmarginal(marg_var)

# --- 3. Random effects -------------------------------------------------
# dict-of-dicts: term -> per-level DataFrame keyed by "index.<k>".
group_marginals = res.marginals_random["group"]
marg_group_5    = group_marginals["index.5"]                       # density for level 5
mean_group_5    = pyinla.emarginal(lambda v: v, marg_group_5)

# --- 4. Linear predictor -----------------------------------------------
# dict keyed by "Predictor.001", "Predictor.002", ...
marg_pred_1 = res.marginals_linear_predictor["Predictor.001"]      # eta_1
ci_pred_1   = pyinla.qmarginal([0.025, 0.975], marg_pred_1)        # 95% credible interval

How INLA Computes Marginal Posteriors

INLA computes marginal posteriors through a two-step numerical integration process:

$$\pi(x_i \mid \mathbf{y}) = \int \pi(x_i \mid \boldsymbol{\theta}, \mathbf{y}) \, \pi(\boldsymbol{\theta} \mid \mathbf{y}) \, d\boldsymbol{\theta}$$

This integral is approximated as a weighted sum over carefully selected hyperparameter configurations:

$$\pi(x_i \mid \mathbf{y}) \approx \sum_{k=1}^{K} \tilde{\pi}(x_i \mid \boldsymbol{\theta}^{(k)}, \mathbf{y}) \times \tilde{\pi}(\boldsymbol{\theta}^{(k)} \mid \mathbf{y}) \times \Delta_k$$

Two Key Approximations

  • Approximation Strategy: How to compute \(\tilde{\pi}(x_i \mid \boldsymbol{\theta}, \mathbf{y})\) for each latent field element
  • Integration Strategy: How to select integration points \(\boldsymbol{\theta}^{(k)}\) and weights \(\Delta_k\)

The Hyperparameter Posterior

The posterior for hyperparameters is approximated using the Laplace approximation:

$$\tilde{\pi}(\boldsymbol{\theta} \mid \mathbf{y}) \propto \frac{\pi(\mathbf{x}, \boldsymbol{\theta}, \mathbf{y})}{\tilde{\pi}_G(\mathbf{x} \mid \boldsymbol{\theta}, \mathbf{y})} \bigg|_{\mathbf{x} = \mathbf{x}^*(\boldsymbol{\theta})}$$

where \(\mathbf{x}^*(\boldsymbol{\theta})\) is the mode of \(\pi(\mathbf{x} \mid \boldsymbol{\theta}, \mathbf{y})\) and \(\tilde{\pi}_G\) is the Gaussian approximation.

Approximation Strategies

The approximation strategy controls how INLA computes the conditional marginal \(\pi(x_i \mid \boldsymbol{\theta}, \mathbf{y})\) for each latent field element. Four strategies are available:

Gaussian

$$\tilde{\pi}_G(x_i \mid \boldsymbol{\theta}, \mathbf{y}) = \mathcal{N}(x_i \mid \mu_i(\boldsymbol{\theta}), \sigma_i^2(\boldsymbol{\theta}))$$

Uses only the mean and variance from the Gaussian approximation to the full conditional. Fast because these are already computed during hyperparameter optimization.

Use when: True posterior is approximately Gaussian, speed is critical.

Simplified Laplace

$$\tilde{\pi}_{SLA}(x_i \mid \boldsymbol{\theta}, \mathbf{y}) = \mathcal{N}(x_i \mid \mu_i, \sigma_i^2) \times \exp(\text{spline}(x_i))$$

Mean-skew corrected Gaussian by fitting a Skew-Normal. Uses cubic spline correction that accounts for location bias and skewness.

Use when: General-purpose applications. Best balance of speed and accuracy.

The user-facing default for strategy is "auto", which lets INLA pick among the four based on model size and structure; for most models it resolves to simplified.laplace.

Laplace

$$\tilde{\pi}_{LA}(x_i \mid \boldsymbol{\theta}, \mathbf{y}) \propto \frac{\pi(\mathbf{x}, \boldsymbol{\theta}, \mathbf{y})}{\tilde{\pi}_{GG}(\mathbf{x}_{-i} \mid x_i, \boldsymbol{\theta}, \mathbf{y})} \bigg|_{\mathbf{x}_{-i} = \mathbf{x}^*_{-i}(x_i)}$$

Full Laplace approximation evaluated at each value of \(x_i\). Most expensive but provides the best accuracy for non-Gaussian posteriors.

Use when: Maximum accuracy needed, non-Gaussian likelihoods, small datasets.

Adaptive

Uses full Laplace for short latent effects (length ≤ adaptive_max, default 25) and Simplified Laplace for longer effects.

Balances accuracy for small effects with efficiency for large ones.

Use when: Models with both short and long latent effects where accuracy matters for short ones.

Setting the Approximation Strategy

from pyinla import pyinla

# Gaussian approximation (fastest)
res_ga = pyinla(
    model=model, family='poisson', data=df,
    control={'inla': {'strategy': 'gaussian'}}
)

# Simplified Laplace (default when strategy='auto')
res_sla = pyinla(
    model=model, family='poisson', data=df,
    control={'inla': {'strategy': 'simplified.laplace'}}
)

# Full Laplace (most accurate)
res_la = pyinla(
    model=model, family='poisson', data=df,
    control={'inla': {'strategy': 'laplace'}}
)

# Adaptive (hybrid approach)
res_adaptive = pyinla(
    model=model, family='poisson', data=df,
    control={'inla': {'strategy': 'adaptive', 'adaptive_max': 15}}
)

Comparison

Strategy Best For
gaussian Large datasets, quick exploration
simplified.laplace General-purpose applications
laplace Non-Gaussian likelihoods, publication-quality fits
adaptive Mixed short / long latent effects

VB Correction

The Variational Bayes (VB) correction is a separate mechanism that improves the Gaussian approximation by iteratively correcting the mean (or variance). It is enabled by default in compact mode and works alongside the approximation strategy.

How VB Correction Works

When the Gaussian approximation is used, the mean of the latent field may be biased. VB correction iteratively updates the mean using variational methods to reduce this bias. The correction is applied during the optimization phase before computing marginals.

VB Settings

import copy
import numpy as np
import pandas as pd
from pyinla import pyinla

# Synthetic data: y = 1 + 0.5 x + noise
rng = np.random.default_rng(0)
n = 200
x = rng.normal(size=n)
y = 1.0 + 0.5 * x + rng.normal(0, 0.3, size=n)
df = pd.DataFrame({"y": y, "x": x})

model = {"response": "y", "fixed": ["1", "x"]}

# Default: VB correction enabled with mean strategy
res = pyinla(
    model=copy.deepcopy(model), family='gaussian', data=df,
    control={
        'inla': {
            'control_vb': {
                'enable': True,           # Enable VB correction (default: auto)
                'strategy': 'mean',       # 'mean' or 'variance'
                'iter_max': 25,           # Maximum iterations
                'hessian_strategy': 'full'  # 'full', 'partial', 'diagonal'
            }
        }
    }
)

# Disable VB correction
res_no_vb = pyinla(
    model=copy.deepcopy(model), family='gaussian', data=df,
    control={'inla': {'control_vb': {'enable': False}}}
)
Option Default Description
enable 'auto' Enable VB correction (auto enables in compact mode)
strategy 'mean' Correct the mean ('mean') or variance ('variance')
iter_max 25 Maximum number of VB iterations
hessian_update 2 Frequency of Hessian updates
hessian_strategy 'full' Hessian computation: 'full', 'partial', 'diagonal'

Integration Strategies

The integration strategy controls how INLA integrates over the hyperparameter space \(\boldsymbol{\theta}\) to compute the final marginals. INLA first finds the posterior mode \(\boldsymbol{\theta}^*\) and the Hessian matrix \(\mathbf{H}\), then explores the hyperparameter space using one of the strategies below:

Grid

Regular grid exploration centered at the mode. Points are included if:

$$|\log \pi(\boldsymbol{\theta}^* \mid \mathbf{y}) - \log \pi(\boldsymbol{\theta} \mid \mathbf{y})| < \delta$$

First explores along principal axes, then adds combinations satisfying the threshold.

Use when: Few hyperparameters (1-3), want thorough coverage.

CCD

Central Composite Design places integration points strategically using response surface methodology. More efficient than grid for higher dimensions.

Uses eigenvalue decomposition of the Hessian to work in a rescaled, orthogonalized space.

Use when: Multiple hyperparameters, efficiency matters.

The user-facing default for int_strategy is "auto", which lets INLA pick a strategy from model size and dimensionality; for most models it resolves to ccd.

Empirical Bayes

Uses only the posterior mode:

$$\pi(\boldsymbol{\theta} \mid \mathbf{y}) \approx \delta(\boldsymbol{\theta} - \boldsymbol{\theta}^*)$$

Ignores hyperparameter uncertainty. Single evaluation at the mode.

Use when: Speed is critical, hyperparameters well-determined, quick diagnostics.

User-supplied

You provide the integration design directly via an int_design matrix. Each row gives a hyperparameter configuration plus a weight; INLA evaluates the conditional marginal at each row and combines.

Two modes:

  • "user" — rows are in the original \(\boldsymbol{\theta}\) parameterization.
  • "user.std" — rows are in the rescaled / Hessian-orthogonalized standardized space.

Use when: You want explicit control over the integration grid (sensitivity analysis, custom designs).

Setting the Integration Strategy

from pyinla import pyinla

# CCD integration (auto resolves to this for typical models)
res_ccd = pyinla(
    model=model, family='gaussian', data=df,
    control={'inla': {'int_strategy': 'ccd'}}
)

# Grid integration (thorough)
res_grid = pyinla(
    model=model, family='gaussian', data=df,
    control={'inla': {'int_strategy': 'grid'}}
)

# Empirical Bayes (fastest, mode only)
res_eb = pyinla(
    model=model, family='gaussian', data=df,
    control={'inla': {'int_strategy': 'eb'}}
)

# User-supplied design: each row is (theta_1, ..., theta_p, weight).
# Below is a minimal 3-point design for a single-hyperparameter model.
import numpy as np
design = np.array([
    [-1.0, 1.0],     # theta = -1 (log-precision), weight = 1
    [ 0.0, 2.0],     # theta =  0,                 weight = 2
    [ 1.0, 1.0],     # theta =  1,                 weight = 1
])
res_user = pyinla(
    model=model, family='gaussian', data=df,
    control={'inla': {'int_strategy': 'user', 'int_design': design}}
)

Integration knobs

Beyond the strategy itself, control['inla'] exposes several numeric knobs that fine-tune the grid (effective for grid, mostly ignored under eb and ccd):

KeyDefaultWhat it controls
dz0.75Step size in the rescaled (Hessian-orthogonalized) integration grid.
diff_logdens6Threshold δ: points are kept only when their log-density is within δ of the mode.
npoints9Number of CCD integration points (the central-composite design size).
cutoff1e-4Drop integration weights below this fraction of the maximum.
stencil5Finite-difference stencil size for derivatives at the mode (5, 7, or 9).
h0.005Finite-difference step size for the Hessian.
interpolator"auto"How marginals are interpolated between integration points.

Combining Strategies

You can combine approximation and integration strategies:

# Fast: Gaussian + Empirical Bayes
control_fast = {
    'inla': {
        'strategy': 'gaussian',
        'int_strategy': 'eb'
    }
}

# Accurate: Laplace + Grid
control_accurate = {
    'inla': {
        'strategy': 'laplace',
        'int_strategy': 'grid'
    }
}

# Balanced (default): Simplified Laplace + CCD
control_balanced = {
    'inla': {
        'strategy': 'simplified.laplace',
        'int_strategy': 'ccd'
    }
}
Tip: Start with strategy='gaussian' and int_strategy='eb' for rapid model development. Switch to defaults or more accurate settings for final results.

Marginals for Different Model Components

Latent Field Elements

For latent field elements \(x_i\) (fixed effects, random effects), marginals are computed using the approximation strategy over all integration points:

$$\pi(x_i \mid \mathbf{y}) \approx \sum_{k=1}^{K} \tilde{\pi}(x_i \mid \boldsymbol{\theta}^{(k)}, \mathbf{y}) \times w_k$$

Hyperparameters

For hyperparameters \(\theta_j\), the marginal is obtained by integrating out other hyperparameters:

$$\pi(\theta_j \mid \mathbf{y}) = \int \pi(\boldsymbol{\theta} \mid \mathbf{y}) \, d\boldsymbol{\theta}_{-j}$$

This is done numerically from the joint hyperparameter posterior evaluated at the integration points.

Accessing Different Marginals

from pyinla import pyinla

res = pyinla(model=model, family='gaussian', data=df)

# Fixed effects (regression coefficients)
for name, marg in res.marginals_fixed.items():
    print(f"{name}: shape {marg.shape}")

# Hyperparameters (precision, variance, etc.)
for name, marg in res.marginals_hyperpar.items():
    print(f"{name}: shape {marg.shape}")

# Random effects (if model has them)
if res.marginals_random:
    for effect_name, marginals in res.marginals_random.items():
        print(f"{effect_name}: {len(marginals)} marginals")

# Linear predictor (fitted values)
if res.marginals_linear_predictor is not None:
    print(f"Linear predictor: {len(res.marginals_linear_predictor)} marginals")

Accessing Marginals

import numpy as np
import pandas as pd
import pyinla

# --- Generate a small synthetic dataset --------------------------------
rng = np.random.default_rng(0)
n  = 200
x1 = rng.normal(size=n)
x2 = rng.normal(size=n)
y  = 1.0 + 2.0 * x1 - 0.5 * x2 + rng.normal(scale=0.3, size=n)
data = pd.DataFrame({"y": y, "x1": x1, "x2": x2})

# --- Fit a Gaussian linear model (dict-style spec) ---------------------
res = pyinla.pyinla(
    formula={'response': 'y', 'fixed': ['1', 'x1', 'x2']},
    family='gaussian',
    data=data,
)

# --- Access marginals --------------------------------------------------
marg_intercept = res.marginals_fixed["(Intercept)"]
marg_x1        = res.marginals_fixed["x1"]
# Hyperparameter keys use INLA's descriptive labels:
marg_precision = res.marginals_hyperpar["Precision for the Gaussian observations"]

# Each marginal is a pandas DataFrame with two columns: 'x' (parameter values) and 'y' (density)
print(marg_x1.shape)   # e.g., (43, 2)
print(marg_x1.head())  # First 5 rows of the DataFrame

Marginal Utility Functions

pyINLA provides functions to work with marginal distributions as if they were continuous. Import them directly from pyinla:

import pyinla

# All functions available as pyinla.
pyinla.dmarginal(0.5, marg)   # Density at x=0.5
pyinla.pmarginal(0.5, marg)   # P(X <= 0.5)
pyinla.qmarginal(0.95, marg)  # 95th percentile

Quick Reference

Function Purpose Returns
dmarginalEvaluate density at point(s)Density value(s)
pmarginalCumulative probability P(X ≤ x)Probability
qmarginalQuantile function (inverse CDF)Quantile value
rmarginalRandom samplingArray of samples
emarginalExpected value of a function E[f(X)]Scalar
tmarginalTransform marginal via functionNew marginal
smarginalSmooth/interpolate marginalRefined marginal
mmarginalMode (MAP estimate)Scalar
zmarginalSummary statisticsDict with mean, sd, quantiles
hpdmarginalHighest posterior density intervalHPD bounds

dmarginal - Density Evaluation

Evaluates the marginal posterior density at one or more points using monotonic spline interpolation.

$$f(x) = \pi(\theta = x \mid \mathbf{y})$$

Mathematical Details

Uses PCHIP (Piecewise Cubic Hermite Interpolating Polynomial) interpolation to create a smooth, monotonicity-preserving approximation. For points outside the support, returns 0.

Parameters

ParameterTypeDefaultDescription
xfloat or array-likePoint(s) at which to evaluate the density.
marginal(n, 2) array / dict / DataFrameMarginal with columns / keys x, y.
logboolFalseIf True, return the log-density (and -inf outside the support).

Example

import pyinla
import numpy as np
import matplotlib.pyplot as plt

marg = res.marginals_fixed["x1"]   # DataFrame with columns ['x', 'y']

# Evaluate at a single point
d = pyinla.dmarginal(0.5, marg)
print(f"Density at 0.5: {d:.4f}")

# Log-density variant (useful for numerical stability and likelihood ratios)
log_d = pyinla.dmarginal(0.5, marg, log=True)

# Evaluate at multiple points for plotting
x_grid = np.linspace(marg["x"].min(), marg["x"].max(), 200)
y_grid = pyinla.dmarginal(x_grid, marg)

plt.figure(figsize=(8, 5))
plt.plot(x_grid, y_grid, 'b-', linewidth=2, label='Interpolated')
plt.scatter(marg["x"], marg["y"], s=20, c='red', alpha=0.5, label='Original points')
plt.xlabel(r'$\theta$')
plt.ylabel(r'$\pi(\theta \mid y)$')
plt.legend()
plt.title('Posterior Marginal Density')
plt.show()

pmarginal - Cumulative Distribution

Computes the cumulative distribution function (CDF): the probability that the parameter is less than or equal to a given value.

$$F(x) = P(\theta \leq x \mid \mathbf{y}) = \int_{-\infty}^{x} \pi(\theta \mid \mathbf{y}) \, d\theta$$

Mathematical Details

Integration uses the trapezoidal rule on the discrete marginal, followed by PCHIP interpolation of the cumulative values. This ensures monotonicity of the CDF.

Parameters

ParameterTypeDefaultDescription
qfloat or array-likePoint(s) at which to evaluate the CDF.
marginal(n, 2) array / dict / DataFrameMarginal with columns / keys x, y.
normalizeboolTrueRescale the cumulative integral to end at exactly 1 before interpolating.
lengthint2048Number of points on the internal interpolation grid.

Example

import pyinla

marg = res.marginals_fixed["x1"]

# Probability that coefficient is negative
p_negative = pyinla.pmarginal(0, marg)
print(f"P(x1 < 0) = {p_negative:.4f}")

# Probability that coefficient is in interval [0.2, 0.8]
p_interval = pyinla.pmarginal(0.8, marg) - pyinla.pmarginal(0.2, marg)
print(f"P(0.2 < x1 < 0.8) = {p_interval:.4f}")

# One-sided credible interval
if p_negative < 0.05:
    print("Strong evidence that x1 > 0 (95% credible)")
Use case: Testing hypotheses like "Is this coefficient significantly different from zero?" Compute pyinla.pmarginal(0, marg) to get the posterior probability of being negative.

qmarginal - Quantile Function

Computes quantiles (inverse CDF). Given a probability p, returns the value x such that P(X ≤ x) = p.

$$Q(p) = F^{-1}(p) = \inf\{x : F(x) \geq p\}$$

Parameters

ParameterTypeDefaultDescription
pfloat or array-likeProbability or probabilities in [0, 1]. Output shape matches p.
marginal(n, 2) array / dict / DataFrameMarginal with columns / keys x, y.
lengthint2048Number of points on the internal interpolation grid.

Example

import pyinla

marg = res.marginals_fixed["x1"]

# Median
median = pyinla.qmarginal(0.5, marg)
print(f"Median: {median:.4f}")

# 95% equal-tailed credible interval
q025 = pyinla.qmarginal(0.025, marg)
q975 = pyinla.qmarginal(0.975, marg)
print(f"95% CI: [{q025:.4f}, {q975:.4f}]")

# Multiple quantiles at once
quantiles = pyinla.qmarginal([0.025, 0.25, 0.5, 0.75, 0.975], marg)
print(f"Quantiles: {quantiles}")

rmarginal - Random Sampling

Generates random samples from the marginal posterior using inverse transform sampling.

$$X = F^{-1}(U), \quad U \sim \text{Uniform}(0, 1)$$

Mathematical Details

The inverse transform method generates uniform random numbers and transforms them through the quantile function. This produces samples that follow the target marginal distribution.

Parameters

ParameterTypeDefaultDescription
nintNumber of samples to draw.
marginal(n, 2) array / dict / DataFrameMarginal with columns / keys x, y.
rngnp.random.GeneratorNoneOptional generator for reproducibility. Defaults to a fresh np.random.default_rng().

Example

import pyinla
import numpy as np

marg = res.marginals_fixed["x1"]

# Generate samples
samples = pyinla.rmarginal(10000, marg)

# For reproducibility
rng = np.random.default_rng(42)
samples_reproducible = pyinla.rmarginal(10000, marg, rng=rng)

# Monte Carlo estimates
print(f"Sample mean: {samples.mean():.4f}")
print(f"Sample std:  {samples.std():.4f}")
print(f"P(X > 0):    {(samples > 0).mean():.4f}")
Use case: Monte Carlo integration, propagating uncertainty through nonlinear functions, or creating posterior predictive samples.

emarginal - Expected Value of Function

Computes the expected value of a function applied to the marginal distribution.

$$\mathbb{E}[g(\theta) \mid \mathbf{y}] = \int g(\theta) \cdot \pi(\theta \mid \mathbf{y}) \, d\theta$$

Mathematical Details

Integration uses Simpson's rule on the smoothed marginal, providing \(O(h^4)\) convergence for smooth functions.

Parameters

ParameterTypeDefaultDescription
funcallable (x) -> scalar or vectorFunction applied to the grid. Vector-valued returns become independent expectations.
marginal(n, 2) array / dict / DataFrameMarginal with columns / keys x, y.
*args, **kwargsanyExtra arguments forwarded to fun at every grid point.

Works for any fundoes not require monotonicity. Use this rather than tmarginal when you only need a moment.

Example

import pyinla
import numpy as np

marg = res.marginals_fixed["x1"]

# Posterior mean (E[X])
mean = pyinla.emarginal(lambda x: x, marg)
print(f"Mean: {mean:.4f}")

# Posterior variance (E[X^2] - E[X]^2)
ex2 = pyinla.emarginal(lambda x: x**2, marg)
variance = ex2 - mean**2
print(f"Variance: {variance:.4f}")

# Expected value of exp(theta) - useful for log-scale parameters
e_exp = pyinla.emarginal(np.exp, marg)
print(f"E[exp(theta)]: {e_exp:.4f}")

tmarginal - Transform Marginal

Transforms a marginal distribution through a monotonic function, returning a new marginal for the transformed variable.

$$\text{If } Y = g(\theta), \text{ then } \pi_Y(y) = \pi_\theta(g^{-1}(y)) \cdot \left|\frac{d}{dy}g^{-1}(y)\right|$$

Change of Variables

For a monotonic transformation \(Y = g(\theta)\), the density of Y is obtained via the change of variables formula. The Jacobian accounts for how the transformation stretches or compresses the density.

Parameters

ParameterTypeDefaultDescription
funcallable (x) -> yMonotonic transformation applied element-wise.
marginal(n, 2) array / dict / DataFrameMarginal of X to transform.
nint2048Number of points on the output grid.
h_difffloat~6e-05Step size for the numerical derivative of fun (used in the Jacobian).
methodstr"quantile"Resampling scheme: "quantile" (default, uses CDF inversion) or "linear" (evenly spaced over the support).

Example

import pyinla
import numpy as np

# pyinla returns marginals on the user (precision) scale, not log-scale,
# so to operate on log(precision) first transform back:
marg_prec     = res.marginals_hyperpar["Precision for the Gaussian observations"]
marg_log_prec = pyinla.tmarginal(np.log, marg_prec)

# Or transform precision to standard deviation directly: sd = 1 / sqrt(precision)
marg_sd = pyinla.tmarginal(lambda x: 1.0 / np.sqrt(x), marg_prec)

# Increase the internal sampling resolution for a smoother result
marg_sd_hi = pyinla.tmarginal(
    fun      = lambda x: 1.0 / np.sqrt(x),
    marginal = marg_prec,
    n        = 4096,
)
Important: The transformation function must be monotonic. For non-monotonic transformations, use rmarginal to sample, transform the samples, then estimate the density.

smarginal - Smooth/Interpolate

Creates a smoother, higher-resolution version of a marginal distribution using log-density spline interpolation on a non-uniform t-map.

Parameters

ParameterTypeDefaultDescription
marginal(n, 2) array / dict / DataFrameMarginal with columns / keys x, y.
logboolFalseIf True, return the smoothed log-density (column y holds log-densities).
extrapolatefloat0.0Symmetric padding (in input units) added on each side of the support.
keep_typeboolFalseIf the input is an (n, 2) ndarray, return the same ndarray form; otherwise output is always {'x','y'} dict.
factorint15Output resolution multiplier. Roughly factor × len(marginal) output points.

Example

import pyinla

marg = res.marginals_fixed["x1"]

# Original might have ~50 points; smooth via spline interpolation
# factor controls density: ~factor x the number of input points.
marg_smooth = pyinla.smarginal(marg, factor=15)

print(f"Original points: {len(marg)}")
print(f"Smoothed points: {len(marg_smooth['x'])}")

mmarginal - Mode (MAP Estimate)

Returns the mode - the value with highest posterior density (Maximum A Posteriori estimate).

$$\hat{\theta}_{\text{MAP}} = \arg\max_\theta \pi(\theta \mid \mathbf{y})$$

Parameters

ParameterTypeDefaultDescription
marginal(n, 2) array / dict / DataFrameMarginal with columns / keys x, y.

Returns a single float. The search is a coarse 1%-99% quantile grid then a 1-D refinement on the bracketing interval.

Example

import pyinla

marg = res.marginals_fixed["x1"]

# Mode (MAP estimate)
mode = pyinla.mmarginal(marg)

# Compare with mean and median
mean = pyinla.emarginal(lambda x: x, marg)
median = pyinla.qmarginal(0.5, marg)

print(f"Mode:   {mode:.4f}")
print(f"Mean:   {mean:.4f}")
print(f"Median: {median:.4f}")

zmarginal - Summary Statistics

Computes a comprehensive summary: mean, standard deviation, and key quantiles.

Parameters

ParameterTypeDefaultDescription
marginal(n, 2) array / dict / DataFrameMarginal with columns / keys x, y.
silentboolFalseIf True, suppress the printed table and only return the dict.

Returns

KeyDescription
meanPosterior mean
sdPosterior standard deviation
modePosterior mode (MAP)
quant0.0252.5% quantile
quant0.2525% quantile (lower quartile)
quant0.5Median
quant0.7575% quantile (upper quartile)
quant0.97597.5% quantile

Example

import pyinla

marg = res.marginals_fixed["x1"]

# Get full summary
summary = pyinla.zmarginal(marg)

print("Posterior Summary for x1:")
print(f"  Mean:   {summary['mean']:.4f}")
print(f"  SD:     {summary['sd']:.4f}")
print(f"  95% CI: [{summary['quant0.025']:.4f}, {summary['quant0.975']:.4f}]")

# Summarize all fixed effects
for name, marg in res.marginals_fixed.items():
    s = pyinla.zmarginal(marg)
    print(f"{name:15s}: {s['mean']:8.4f} ({s['quant0.025']:8.4f}, {s['quant0.975']:8.4f})")

hpdmarginal - HPD Interval

Computes the Highest Posterior Density (HPD) interval - the shortest interval containing a specified probability mass.

$$\text{HPD}_\alpha = \{x : \pi(x \mid y) \geq k_\alpha\}$$

where \(k_\alpha\) is chosen such that \(P(\theta \in \text{HPD}_\alpha) = 1 - \alpha\)

HPD vs Equal-Tailed Intervals

For symmetric distributions, HPD and equal-tailed intervals are identical. For skewed distributions:

  • HPD: Shortest interval (every point inside has higher density than outside)
  • Equal-tailed: Same probability in each tail

HPD is often preferred for reporting because it's the shortest credible interval.

Parameters

ParameterTypeDefaultDescription
pfloat or array-likeCredible mass(es), each in (0, 1).
marginal(n, 2) array / dict / DataFrameMarginal with columns / keys x, y. Must be unimodal.
lengthint2048Number of points on the internal interpolation grid.

Returns np.ndarray of shape (len(p), 2) with columns [low, high]. For a scalar p, the output is shape (1, 2) — call .ravel() to flatten.

Example

import numpy as np
import pandas as pd
import pyinla

# Synthetic data: y = 1 + 0.5 x1 + noise
rng = np.random.default_rng(0)
n = 200
x1 = rng.normal(size=n)
y = 1.0 + 0.5 * x1 + rng.normal(0, 0.3, size=n)
df = pd.DataFrame({"y": y, "x1": x1})

res = pyinla.pyinla(
    model={"response": "y", "fixed": ["1", "x1"]},
    family="gaussian", data=df,
)

marg = res.marginals_fixed["x1"]

# 95% HPD interval. hpdmarginal returns shape (len(p), 2); ravel for a flat [low, high].
hpd = pyinla.hpdmarginal(0.95, marg).ravel()
print(f"95% HPD:         [{hpd[0]:.4f}, {hpd[1]:.4f}]")

# 95% equal-tailed interval via the 2.5% / 97.5% quantiles.
q = pyinla.qmarginal([0.025, 0.975], marg)
print(f"95% Equal-tailed: [{q[0]:.4f}, {q[1]:.4f}]")

# HPD is ≤ equal-tailed in width; equal for symmetric marginals, shorter for skewed ones.
print(f"HPD width:         {hpd[1] - hpd[0]:.4f}")
print(f"Equal-tailed width: {q[1] - q[0]:.4f}")