| Title: | Templated Unified Library for Posterior Approximation in Bayesian Hierarchical Models |
|---|---|
| Description: | A general-purpose engine for fitting Bayesian hierarchical models with spatial fields, temporal effects, spatially varying coefficients, and multiple inference backends. Scalable spatial structure includes Hilbert space approximate Gaussian processes (HSGP; Riutort-Mayol et al. 2023 <doi:10.1007/s11222-022-10167-2>), nearest-neighbor Gaussian processes (NNGP; Datta et al. 2016 <doi:10.1080/01621459.2015.1044091>), intrinsic conditional autoregressive models (ICAR; Besag, York, and Mollie 1991 <doi:10.1007/BF00116466>), the reparameterized Besag-York-Mollie model (BYM2; Riebler et al. 2016 <doi:10.1177/0962280216660421>), and stochastic partial differential equation fields (SPDE; Lindgren, Rue, and Lindstrom 2011 <doi:10.1111/j.1467-9868.2011.00777.x>). Temporal structure covers random walks, autoregressive processes, and Gaussian processes. Inference is tiered by correctness guarantee: exact Hamiltonian Monte Carlo with the No-U-Turn sampler, Laplace and nested Laplace approximations with hyperparameter integration (Rue, Martino, and Chopin 2009 <doi:10.1111/j.1467-9868.2008.00700.x>), and variational inference. Model-specific packages plug observation likelihoods into the engine through a templated C++ callback interface. |
| Authors: | Gilles Colling [aut, cre, cph] (ORCID: <https://orcid.org/0000-0003-3070-6066>), Frances Y. Kuo [ctb, cph] (Sobol direction numbers in src/sobol_direction_numbers.h, BSD-3-clause), Stephen Joe [ctb, cph] (Sobol direction numbers in src/sobol_direction_numbers.h, BSD-3-clause), Arm Limited [cph] (exp and log routines in src/fastmath.h and src/fastmath_data.cpp, MIT), The Walnutpie Developers [cph] (WALNUTS transition in src/hmc_walnuts.cpp, ported from walnutpie, MIT) |
| Maintainer: | Gilles Colling <[email protected]> |
| License: | MIT + file LICENSE |
| Version: | 0.7.5 |
| Built: | 2026-10-16 12:51:57 UTC |
| Source: | https://github.com/gcol33/tulpa |
Build the symmetric adjacency matrix that spatial() and spatial_car()
consume, from a common spatial layout, instead of hand-coding it. The model
still receives an explicit graph (spatial(graph = g$adjacency, ...)), so the
graph stays inspectable before fitting – the engine never guesses
connectivity from coordinates silently.
adjacency() is a single front-door verb that dispatches on the kind of
input:
data.frame
cell centroids on a regular grid – queen (edge or corner) or rook (edge only) contiguity over the lattice. This covers both plain coordinate grids and rasterised cells passed as a table of centres.
sf objectpolygon contiguity from shared boundaries (queen = share a point or edge; rook = share an edge). Requires sf.
SpatRaster (terra) or stars objectraster cells – the
lattice of (non-NA) cell centres, queen or rook. Requires terra
or stars respectively.
The result is a tulpa_adjacency object holding the matrix plus the cell
identifier for each node, so a model can pass g$adjacency while the
observation data is remapped to 1-based node indices with node_index().
adjacency(x, ...) ## Default S3 method: adjacency(x, ...) ## S3 method for class 'data.frame' adjacency( x, type = c("queen", "rook"), x_coord = "x", y_coord = "y", id = NULL, order = 1L, tolerance = 1.5, offsets = NULL, ... ) ## S3 method for class 'sf' adjacency(x, type = c("queen", "rook", "touches"), id = NULL, ...) ## S3 method for class 'SpatRaster' adjacency( x, type = c("queen", "rook"), order = 1L, tolerance = 1.5, offsets = NULL, na_rm = TRUE, ... ) ## S3 method for class 'stars' adjacency( x, type = c("queen", "rook"), order = 1L, tolerance = 1.5, offsets = NULL, na_rm = TRUE, ... ) ## S3 method for class 'tulpa_adjacency' print(x, ...)adjacency(x, ...) ## Default S3 method: adjacency(x, ...) ## S3 method for class 'data.frame' adjacency( x, type = c("queen", "rook"), x_coord = "x", y_coord = "y", id = NULL, order = 1L, tolerance = 1.5, offsets = NULL, ... ) ## S3 method for class 'sf' adjacency(x, type = c("queen", "rook", "touches"), id = NULL, ...) ## S3 method for class 'SpatRaster' adjacency( x, type = c("queen", "rook"), order = 1L, tolerance = 1.5, offsets = NULL, na_rm = TRUE, ... ) ## S3 method for class 'stars' adjacency( x, type = c("queen", "rook"), order = 1L, tolerance = 1.5, offsets = NULL, na_rm = TRUE, ... ) ## S3 method for class 'tulpa_adjacency' print(x, ...)
x |
The spatial layout: a |
... |
Passed to methods. |
type |
Contiguity rule: |
x_coord, y_coord
|
For the |
id |
For the |
order |
For grid / raster layouts, the neighbourhood order (ring count):
|
tolerance |
For grid / raster layouts, the per-offset neighbour distance
cut-off as a multiple of the inferred cell size: a candidate at a lattice
offset is kept when its true centre distance is at most |
offsets |
Advanced, grid / raster layouts only: a custom neighbour
stencil as a two-column integer matrix or a list of length-2 |
na_rm |
For raster layouts, drop cells whose value is |
A tulpa_adjacency object: a list with
adjacencythe [n x n] symmetric sparse adjacency matrix
(dgCMatrix, 0/1, zero diagonal) to pass as graph / adjacency.
idsthe cell identifier for each node, in node order (length
n).
nthe number of nodes.
cellsizethe inferred cell size c(x, y) for grid / raster
layouts, or NA for polygons.
typethe contiguity rule used.
node_index() to map cell identifiers to node indices,
check_adjacency() to validate a hand-built matrix, spatial() and
spatial_car() which consume the graph.
# A 3 x 3 regular grid of cell centres grid <- expand.grid(x = 1:3, y = 1:3) grid$cell <- paste0("c", seq_len(nrow(grid))) g <- adjacency(grid, x_coord = "x", y_coord = "y", id = "cell", type = "queen") g g$adjacency # Second-order (24-neighbour) queen contiguity: any order is settable g2 <- adjacency(grid, id = "cell", order = 2) # Advanced: a custom stencil (symmetrized for the undirected field) g3 <- adjacency(grid, id = "cell", offsets = list(c(1, 0), c(0, 1))) # Use it in a model: graph stays explicit and inspectable spatial(graph = g$adjacency, formula = ~ 1 || cell_idx) # Remap observation data (original cell ids -> 1:n node indices) by key obs <- data.frame(cell = c("c5", "c1", "c5", "c9")) obs$cell_idx <- node_index(g, obs$cell) obs# A 3 x 3 regular grid of cell centres grid <- expand.grid(x = 1:3, y = 1:3) grid$cell <- paste0("c", seq_len(nrow(grid))) g <- adjacency(grid, x_coord = "x", y_coord = "y", id = "cell", type = "queen") g g$adjacency # Second-order (24-neighbour) queen contiguity: any order is settable g2 <- adjacency(grid, id = "cell", order = 2) # Advanced: a custom stencil (symmetrized for the undirected field) g3 <- adjacency(grid, id = "cell", offsets = list(c(1, 0), c(0, 1))) # Use it in a model: graph stays explicit and inspectable spatial(graph = g$adjacency, formula = ~ 1 || cell_idx) # Remap observation data (original cell ids -> 1:n node indices) by key obs <- data.frame(cell = c("c5", "c1", "c5", "c9")) obs$cell_idx <- node_index(g, obs$cell) obs
Marginal-likelihood approximation for a generalised linear mixed
model with one cluster-level intercept random effect. Adaptive
Gauss-Hermite quadrature (AGQ) generalises Laplace by replacing the
single-point Gaussian integral around the cluster's posterior mode
with n_quad-point quadrature. AGQ at n_quad = 1 recovers the
Laplace approximation; higher n_quad reduces approximation error,
especially for clusters with few observations or non-Gaussian
likelihoods.
This is an engine block, not a front door: its tuning knobs
(max_iter, tol, n_quad) sit in the signature rather than in a
control list.
Scoped to the lme4::glmer(..., nAGQ = N) use case: one intercept-
only RE term, families binomial, poisson, or gaussian. For
multi-RE or random-slope models, use tulpa_laplace() (Laplace at
the joint mode) or HMC.
agq_fit( y, X, group, n_groups = max(group), family = c("binomial", "poisson", "gaussian"), n_trials = NULL, sigma_eps = 1, n_quad = 7L, offset = NULL, beta_init = NULL, sigma_init = 1, max_iter = 200L, tol = 1e-06, verbose = FALSE )agq_fit( y, X, group, n_groups = max(group), family = c("binomial", "poisson", "gaussian"), n_trials = NULL, sigma_eps = 1, n_quad = 7L, offset = NULL, beta_init = NULL, sigma_init = 1, max_iter = 200L, tol = 1e-06, verbose = FALSE )
y |
Response vector. |
X |
Fixed-effects design matrix ( |
group |
Integer cluster labels (1-based, length |
n_groups |
Number of clusters (default |
family |
One of |
n_trials |
Trial sizes (binomial only; default |
sigma_eps |
Residual SD (gaussian only; default |
n_quad |
Number of Gauss-Hermite quadrature nodes per cluster.
|
offset |
Optional fixed per-observation offset added to eta (default all zero, i.e. none). |
beta_init |
Initial fixed-effects (default zeros). |
sigma_init |
Initial RE SD (default |
max_iter |
Optimiser iteration cap (default |
tol |
Convergence tolerance (default |
verbose |
Print optimiser summary (default |
A list with class tulpa_fit carrying:
means: named vector c(beta, log_sigma_re).
mode: same as means (Gaussian fit at the optimum).
cov: covariance matrix of (beta, log_sigma_re) from inverse
observed information.
sigma_re: estimated RE SD.
log_marginal: AGQ-approximated log marginal likelihood at the
fitted values.
n_quad: quadrature order used.
n_iter, converged: optimiser diagnostics.
inference_mode: "structured".
inference_tier: 2L.
backend: "agq".
An optimum at which the AGHQ objective is undefined (some group's solve fails there, so its value is the failure sentinel rather than a marginal likelihood) is an error naming the groups, not a returned fit.
Tier 2 (Structured). AGQ is exact in the limit n_quad -> infinity
but at finite n_quad it is a controlled approximation – same
epistemic class as Laplace.
Pinheiro, J. C., & Bates, D. M. (1995). Approximations to the log-likelihood function in the nonlinear mixed-effects model. Journal of Computational and Graphical Statistics, 4(1), 12-35.
tulpa_laplace() for the joint-mode Laplace path
(handles multi-RE and spatial structure).
set.seed(1) n_g <- 20; n_per <- 5; n <- n_g * n_per group <- rep(seq_len(n_g), each = n_per) x <- rnorm(n) X <- cbind(1, x) u <- rnorm(n_g, 0, 0.8) eta <- 0.3 + 0.7 * x + u[group] y <- rbinom(n, 1, plogis(eta)) # Compare Laplace (n_quad = 1) and AGQ-7. fit_lap <- agq_fit(y, X, group, family = "binomial", n_quad = 1) fit_agq <- agq_fit(y, X, group, family = "binomial", n_quad = 7) c(fit_lap$log_marginal, fit_agq$log_marginal)set.seed(1) n_g <- 20; n_per <- 5; n <- n_g * n_per group <- rep(seq_len(n_g), each = n_per) x <- rnorm(n) X <- cbind(1, x) u <- rnorm(n_g, 0, 0.8) eta <- 0.3 + 0.7 * x + u[group] y <- rbinom(n, 1, plogis(eta)) # Compare Laplace (n_quad = 1) and AGQ-7. fit_lap <- agq_fit(y, X, group, family = "binomial", n_quad = 1) fit_agq <- agq_fit(y, X, group, family = "binomial", n_quad = 7) c(fit_lap$log_marginal, fit_agq$log_marginal)
AIC and BIC penalise a maximised log-likelihood. logLik.tulpa_fit() reports
one only for a fit that maximised over every parameter it reports
(quantity = "log_likelihood", as agq_fit() does); a mean log posterior, a
log evidence and a conditional log marginal likelihood are not, and both
criteria refuse them rather than return a number. Compare those fits by
their evidence, or by compare_models(criterion = "waic") / "loo".
## S3 method for class 'tulpa_fit' AIC(object, ..., k = 2) ## S3 method for class 'tulpa_fit' BIC(object, ...)## S3 method for class 'tulpa_fit' AIC(object, ..., k = 2) ## S3 method for class 'tulpa_fit' BIC(object, ...)
object |
A |
... |
Further fits. |
k |
Penalty per parameter. |
As stats::AIC() / stats::BIC() for maximised log-likelihoods;
errors otherwise.
Convert a fit's posterior draws to a posterior draws object. as_draws()
returns the draws_array shape; as_draws_array(), as_draws_matrix(),
as_draws_df() and as_draws_rvars() return theirs. When the posterior
package is installed these are also registered against its generics, so
posterior::as_draws(fit) works.
as_draws(x, ...) ## S3 method for class 'tulpa_fit' as_draws(x, n_draws = NULL, seed = NULL, ...) as_draws_array(x, ...) ## S3 method for class 'tulpa_fit' as_draws_array(x, n_draws = NULL, seed = NULL, ...) as_draws_matrix(x, ...) ## S3 method for class 'tulpa_fit' as_draws_matrix(x, n_draws = NULL, seed = NULL, ...) as_draws_df(x, ...) ## S3 method for class 'tulpa_fit' as_draws_df(x, n_draws = NULL, seed = NULL, ...) as_draws_rvars(x, ...) ## S3 method for class 'tulpa_fit' as_draws_rvars(x, n_draws = NULL, seed = NULL, ...)as_draws(x, ...) ## S3 method for class 'tulpa_fit' as_draws(x, n_draws = NULL, seed = NULL, ...) as_draws_array(x, ...) ## S3 method for class 'tulpa_fit' as_draws_array(x, n_draws = NULL, seed = NULL, ...) as_draws_matrix(x, ...) ## S3 method for class 'tulpa_fit' as_draws_matrix(x, n_draws = NULL, seed = NULL, ...) as_draws_df(x, ...) ## S3 method for class 'tulpa_fit' as_draws_df(x, n_draws = NULL, seed = NULL, ...) as_draws_rvars(x, ...) ## S3 method for class 'tulpa_fit' as_draws_rvars(x, n_draws = NULL, seed = NULL, ...)
x |
A |
... |
Passed to the corresponding |
n_draws |
Number of draws to synthesize from the Gaussian approximation
for a fit that carries none. |
seed |
Optional integer seed for the synthesis. The RNG state is restored afterwards. |
Fits differ in whether they carry draws at all. Sampler and nested-Laplace
fits do, and convert directly. A Gaussian-approximation fit (mode = "laplace", mode = "eb") carries a mode and a precision instead, and
converting it means drawing from the approximation – which is a modelling
decision, not a format change, because every downstream posterior summary
would then treat a normal approximation as a posterior sample. So it is
opt-in: pass n_draws to synthesize that many draws from
N(coef(object), vcov(object)), or get an error naming the alternative.
Synthesized draws form a single chain and cover the fixed effects only.
A posterior draws object of the requested shape.
tulpa_draws_array() for the base R array without the dependency,
posterior_sample() for the raw matrix.
set.seed(1) df <- data.frame(x = rnorm(80)) df$y <- rpois(80, exp(0.5 + 0.3 * df$x)) fit <- tulpa(y ~ x, data = df, family = "poisson") if (requireNamespace("posterior", quietly = TRUE)) { posterior::summarise_draws(as_draws(fit)) }set.seed(1) df <- data.frame(x = rnorm(80)) df$y <- rpois(80, exp(0.5 + 0.3 * df$x)) fit <- tulpa(y ~ x, data = df, family = "poisson") if (requireNamespace("posterior", quietly = TRUE)) { posterior::summarise_draws(as_draws(fit)) }
Declares that a setting shaping the outer hyperparameter grid carries a
default the caller computed, not a choice the user made. The auto-recenter
pass (outer_grid_placement) leaves a user-pinned
setting exactly as given, and re-centres (or, for a prior, engages its own
regularizer over) a marked one when the fit rails against its ceiling.
Four kinds of setting take the mark:
a grid axis on a nested-Laplace prior block (sigma_grid,
tau_grid, ...) – a numeric vector of nodes, or the [n_cells x k]
matrix of pre-paired coordinates the families whose axis is a matrix
take (mcar / miid's logchol_grid, tgmrf's theta_grid_built);
an entry of tulpa_nested_laplace_joint()'s phi_grid – one arm's
dispersion axis;
a scalar grid-construction knob in control, for a driver that
builds its axes rather than taking them (fit_st_nested()'s
n_grid_spatial, tau_upper, ...);
a prior_sigma hyperprior specification – a list, e.g.
list("pc.prec", c(U = 3, alpha = 0.01)).
Wrapper packages are the intended caller: one that builds a default of its own – because it derives a second axis from it, hands the same vector to several blocks, or exposes its own argument with a default – would otherwise be indistinguishable from a user who pinned that setting deliberately. Mark it and the rescue stays live. A setting whose value is exactly the engine's own default is recognised without a mark; anything else needs one – and a dispersion axis ALWAYS needs one, since the engine has no default of its own to recognise it against.
The mark is an attribute, so it is dropped by sort(), [, c() and
as.numeric(): build the value first, mark it last.
auto_grid(x, place = TRUE)auto_grid(x, place = TRUE)
x |
Numeric vector or matrix of grid nodes, a numeric scalar knob, or a prior-specification list. |
place |
May the auto-placement pass move this axis onto its own
posterior? |
x carrying the marker attribute. Numeric input is coerced to
double IN PLACE, so everything else it carries – dim() and dimnames()
above all – survives the mark; a list is returned unchanged apart from
the attribute.
place = FALSE separates the two questions the mark otherwise answers at
once. Provenance – whose choice these nodes are – and placement policy –
whether the pass may move them – are different questions, and a package that
measured its own default as the one to integrate has an answer to the second
that is not "the user pinned it". Such an axis is treated exactly as a pin
everywhere the engine ACTS on it (the placement pass leaves it, refinement
densifies within its span rather than following the posterior past the end
nodes, and no curvature is computed for it), and differs only in what the fit
REPORTS: "default_axis_pinned" rather than "axis_pinned", so a reader is
not told they pinned an axis they never wrote down. Leaving the mark off
instead buys the same integration and says the wrong thing about it.
place is read wherever the engine would otherwise RE-PLACE the setting: a
grid axis, and a scalar grid-construction knob. A prior_sigma
specification is not re-placed but replaced, by the engine's own
regularizer, and carries no place.
is_auto_grid(), auto_grid_place(),
tulpa_nested_laplace_joint(), fit_st_nested()
prior <- list(type = "icar", sigma_grid = auto_grid(c(0.1, 0.5, 1, 2, 3))) is_auto_grid(prior$sigma_grid) is_auto_grid(auto_grid(list("pc.prec", c(U = 3, alpha = 0.01)))) auto_grid_place(auto_grid(c(0.5, 1, 2), place = FALSE))prior <- list(type = "icar", sigma_grid = auto_grid(c(0.1, 0.5, 1, 2, 3))) is_auto_grid(prior$sigma_grid) is_auto_grid(auto_grid(list("pc.prec", c(U = 3, alpha = 0.01)))) auto_grid_place(auto_grid(c(0.5, 1, 2), place = FALSE))
Reads back what auto_grid()'s place argument recorded, so a wrapper that
rebuilds a value (as.numeric() drops every attribute) can re-apply both
halves of the declaration rather than only the provenance half.
auto_grid_place(x)auto_grid_place(x)
x |
Any object. |
FALSE when x was marked auto_grid(place = FALSE), TRUE
otherwise – including for a value carrying no mark at all, which the
engine holds because it reads as a pin rather than because it asked to be
held.
auto_grid_place(auto_grid(c(0.5, 1, 2))) auto_grid_place(auto_grid(c(0.5, 1, 2), place = FALSE))auto_grid_place(auto_grid(c(0.5, 1, 2))) auto_grid_place(auto_grid(c(0.5, 1, 2), place = FALSE))
Per posterior draw s, R2_s = Var_i(mu_si) / (Var_i(mu_si) + Var_res_s),
where mu_si are the response-scale fitted means and Var_res_s is the
family's residual variance averaged over observations (Gelman et al. 2019).
The linear predictor is rebuilt per draw exactly as in
posterior_predict() (fixed effects + random effects + offset at the
training data).
bayes_R2(object, ...) ## S3 method for class 'tulpa_fit' bayes_R2( object, ndraws = NULL, summary = TRUE, probs = c(0.025, 0.975), seed = NULL, ... )bayes_R2(object, ...) ## S3 method for class 'tulpa_fit' bayes_R2( object, ndraws = NULL, summary = TRUE, probs = c(0.025, 0.975), seed = NULL, ... )
object |
A |
... |
Passed to methods. |
ndraws |
Number of posterior draws to use. Defaults to all stored draws, or 400 on the draw-free Laplace tier. |
summary |
Summarize the per-draw values (default |
probs |
Quantiles reported by the summary (default 2.5% / 97.5%). |
seed |
Optional integer seed (RNG state is restored on exit), used by the Gaussian fixed-effect sampling on draw-free fits. |
With summary = TRUE (default) a one-row data frame with
estimate (posterior median), std.error, and the probs quantiles;
with summary = FALSE the vector of per-draw R^2 values.
Gelman, Goodrich, Gabry & Vehtari (2019). R-squared for Bayesian regression models. The American Statistician 73(3):307-309.
set.seed(1) d <- data.frame(x = rnorm(200)) d$y <- rnorm(200, 2 * d$x, 1) fit <- tulpa(y ~ x, data = d, family = "gaussian", mode = "laplace", phi = 1) bayes_R2(fit)set.seed(1) d <- data.frame(x = rnorm(200)) d$y <- rnorm(200, 2 * d$x, 1) fit <- tulpa(y ~ x, data = d, family = "gaussian", mode = "laplace", phi = 1) bayes_R2(fit)
Estimate the log marginal likelihood from
posterior draws via the Meng-Wong / Gronau bridge-sampling identity.
Useful for Bayes factors, model comparison, and as a sanity check on
Laplace-approximated marginal likelihoods.
The classical iterative scheme (Meng & Wong 1996; Gronau et al. 2017)
is used with a multivariate-normal proposal fit to half of the
posterior draws – the other half is used for the bridge ratio so the
proposal is independent of the samples that score it. All
computations are done in log-space via logsumexp for numerical
stability.
bridge_sampling( draws, log_posterior, n_proposal = nrow(draws), max_iter = 1000L, tol = 1e-10, split = TRUE, verbose = FALSE )bridge_sampling( draws, log_posterior, n_proposal = nrow(draws), max_iter = 1000L, tol = 1e-10, split = TRUE, verbose = FALSE )
draws |
Numeric matrix of posterior draws, one parameter per
column. Typically |
log_posterior |
Function |
n_proposal |
Number of proposal draws. Default |
max_iter |
Maximum bridge iterations (default 1000). |
tol |
Convergence tolerance on |
split |
Logical: split the posterior draws in half so the
proposal is fit on one half and scored on the other (default
|
verbose |
Print iteration history (default |
A list with:
log_marginal: the bridge-sampling estimate of .
n_iter: bridge iterations to convergence.
converged: logical.
re_sd: relative MSE estimate (Fruehwirth-Schnatter 2004); a
rough quality check, smaller is better.
proposal: the fitted proposal list(mean, cov).
Bridge sampling is a post-hoc marginal-likelihood estimator, not a sampling backend, so it does not appear in the INFERENCE_TIERS registry. It operates on draws from any Tier-1 (Exact) backend.
Meng, X.-L., & Wong, W. H. (1996). Simulating ratios of normalizing constants via a simple identity: a theoretical exploration. Statistica Sinica, 6, 831-860.
Gronau, Q. F., Sarafoglou, A., Matzke, D., Ly, A., Boehm, U., Marsman, M., ... & Steingroever, H. (2017). A tutorial on bridge sampling. Journal of Mathematical Psychology, 81, 80-97.
# Toy: marginal likelihood of N(theta | 0, 1) under Y = theta + eps, # eps ~ N(0, 1), prior theta ~ N(0, 10). Closed-form available. y <- 1.5 log_post <- function(theta) { dnorm(y, theta, 1, log = TRUE) + dnorm(theta, 0, 10, log = TRUE) } draws <- matrix(rnorm(2000, mean = y * 100 / 101, sd = sqrt(100 / 101)), ncol = 1) bs <- bridge_sampling(draws, log_post) bs$log_marginal # should match dnorm(y, 0, sqrt(101), log = TRUE)# Toy: marginal likelihood of N(theta | 0, 1) under Y = theta + eps, # eps ~ N(0, 1), prior theta ~ N(0, 10). Closed-form available. y <- 1.5 log_post <- function(theta) { dnorm(y, theta, 1, log = TRUE) + dnorm(theta, 0, 10, log = TRUE) } draws <- matrix(rnorm(2000, mean = y * 100 / 101, sd = sqrt(100 / 101)), ncol = 1) bs <- bridge_sampling(draws, log_post) bs$log_marginal # should match dnorm(y, 0, sqrt(101), log = TRUE)
stats::fitted(), stats::residuals(), stats::predict() and posterior_predict() for a
categorical response: a tulpa_multinomial() fit (family = "multinomial")
or a tulpa_ordinal() fit (family = "ordinal"). The response is one of K
classes, so the response-scale quantities are N x K matrices with one column
per class.
fitted() returns the class probabilities at the posterior mode.
residuals() returns, per class, the indicator of the observed class minus
its fitted probability ("response"), or that difference divided by the
indicator's standard deviation sqrt(p (1 - p)) ("pearson"). A
categorical response has no single scalar residual; each class column is
the Bernoulli residual of that class's indicator.
predict() returns the link scale – the K - 1 baseline-category logits of
the multinomial model (N x (K - 1)), or the ordinal location predictor
x' beta (length N) – or the class probabilities (type = "response").
posterior_predict() draws one class per observation for each posterior
draw and returns the class indices, with the response levels attached;
simulate.tulpa_fit() turns them into factor columns.
The pointwise log-likelihood behind cpo(), dic(), loo::waic() and
loo::loo() is the log probability of each observed class at each stored
posterior draw; dic() evaluates it at the posterior-mean parameters.
## S3 method for class 'tulpa_categorical' fitted(object, ...) ## S3 method for class 'tulpa_categorical' residuals(object, type = c("pearson", "response"), ...) ## S3 method for class 'tulpa_categorical' predict( object, newdata = NULL, type = c("link", "response"), se.fit = FALSE, level = 0.95, ... ) ## S3 method for class 'tulpa_categorical' posterior_predict(object, newdata = NULL, ndraws = NULL, seed = NULL, ...) ## S3 method for class 'tulpa_categorical' pp_check(object, ndraws = 50, ...)## S3 method for class 'tulpa_categorical' fitted(object, ...) ## S3 method for class 'tulpa_categorical' residuals(object, type = c("pearson", "response"), ...) ## S3 method for class 'tulpa_categorical' predict( object, newdata = NULL, type = c("link", "response"), se.fit = FALSE, level = 0.95, ... ) ## S3 method for class 'tulpa_categorical' posterior_predict(object, newdata = NULL, ndraws = NULL, seed = NULL, ...) ## S3 method for class 'tulpa_categorical' pp_check(object, ndraws = 50, ...)
object |
A |
... |
Ignored. |
type |
For |
newdata |
Optional data frame of covariates; |
se.fit |
For |
level |
Credible-interval level (default 0.95). |
ndraws |
Number of posterior draws for |
seed |
Optional integer seed (RNG state is restored on exit). |
fitted(): an N x K probability matrix. residuals(): an N x K
matrix. predict(): a matrix (or, for the ordinal link scale, a vector);
with se.fit = TRUE a list of fit, se.fit, lower, upper of that
shape. posterior_predict(): an ndraws x N integer matrix of class
indices with attributes levels and ordered.
tulpa_multinomial(), tulpa_ordinal()
Check that a hand-built adjacency matrix is a well-formed spatial graph
before passing it to spatial() / spatial_car(): square, finite,
symmetric, zero on the diagonal, 0/1 valued, free of negative weights and of
isolated nodes, and connected. adjacency() runs the same checks on the
graphs it constructs. The spatial constructors refuse a graph with a
missing, non-finite or negative entry.
check_adjacency(adjacency, ids = NULL) ## S3 method for class 'tulpa_adjacency_check' print(x, ...)check_adjacency(adjacency, ids = NULL) ## S3 method for class 'tulpa_adjacency_check' print(x, ...)
adjacency |
A matrix (dense or sparse |
ids |
Optional cell identifiers; if supplied, their length must match
|
x |
A |
... |
Ignored. |
Invisibly, a tulpa_adjacency_check list with the per-check results
(square, finite, symmetric, zero_diag, binary, nonneg,
isolated-node indices, n_components and component_sizes, edge count)
and an overall ok flag. Issues are reported via warning() and printed;
the function does not stop, so every problem surfaces in one pass.
adjacency() to construct a graph, node_index().
adj <- matrix(0, 4, 4) for (i in 1:3) adj[i, i + 1] <- adj[i + 1, i] <- 1 check_adjacency(adj)adj <- matrix(0, 4, 4) for (i in 1:3) adj[i, i + 1] <- adj[i + 1, i] <- 1 check_adjacency(adj)
Runs the core convergence diagnostics on a fit and reports whether Rhat,
bulk-ESS, and divergence thresholds are all met. A terse companion to
diagnostic_summary() for use in scripts and tests.
check_diagnostics( fit, rhat_threshold = 1.01, ess_threshold = 400, quiet = FALSE )check_diagnostics( fit, rhat_threshold = 1.01, ess_threshold = 400, quiet = FALSE )
fit |
A |
rhat_threshold |
Maximum acceptable Rhat (default 1.01). |
ess_threshold |
Minimum acceptable bulk-ESS (default 400). |
quiet |
Logical; if TRUE, suppress messages (default FALSE). |
Invisibly, TRUE if all checks pass, FALSE if any fail, or NA
for a non-chain (approximation) fit where Rhat/ESS do not apply.
diagnostic_summary(), diagnostics(), n_divergent()
set.seed(123) df <- data.frame(x = rnorm(60)) df$y <- rpois(60, exp(0.5 + 0.3 * df$x)) fit <- tulpa(y ~ x, data = df, family = "poisson", mode = "hmc", control = list(n_iter = 500L, warmup = 250L, n_chains = 2L, seed = 1L)) check_diagnostics(fit)set.seed(123) df <- data.frame(x = rnorm(60)) df$y <- rpois(60, exp(0.5 + 0.3 * df$x)) fit <- tulpa(y ~ x, data = df, family = "poisson", mode = "hmc", control = list(n_iter = 500L, warmup = 250L, n_chains = 2L, seed = 1L)) check_diagnostics(fit)
Produces a 2x2 or 1x3 panel of diagnostic plots:
QQ plot of PIT residuals vs Uniform (with KS p-value)
Residuals vs fitted (with lowess smoother)
Dispersion histogram (observed variance vs simulated)
Spatial correlogram via Moran's I (if coords provided)
check_model(object, ...) ## Default S3 method: check_model(object, coords = NULL, nsim = 250L, seed = 123L, ...)check_model(object, ...) ## Default S3 method: check_model(object, coords = NULL, nsim = 250L, seed = 123L, ...)
object |
A fitted model with |
... |
Passed to methods. |
coords |
Optional N x 2 coordinate matrix for spatial panel |
nsim |
Number of simulations (default 250) |
seed |
Random seed (default 123) |
Invisible list with ks_p, disp_ratio, moran (if spatial)
set.seed(1) d <- data.frame(x = rnorm(80)) d$y <- rpois(80, exp(0.3 + 0.5 * d$x)) fit <- tulpa(y ~ x, data = d, family = "poisson", mode = "laplace") check_model(fit, nsim = 50)set.seed(1) d <- data.frame(x = rnorm(80)) d$y <- rpois(80, exp(0.3 + 0.5 * d$x)) fit <- tulpa(y ~ x, data = d, family = "poisson", mode = "laplace") check_model(fit, nsim = 50)
Fixed-effect coefficients
## S3 method for class 'tulpa_fit' coef(object, ...)## S3 method for class 'tulpa_fit' coef(object, ...)
object |
A |
... |
Ignored. |
Named numeric vector of fixed-effect posterior means (the Laplace
mode for the Laplace tier). Random effects come from ranef().
Rank fitted models best-first by an information criterion. "waic" and
"loo" use the native pointwise log-likelihood layer (tulpa_criteria() /
tulpa_psis()) – WAIC and PSIS-LOO respectively, computed from each fit's
[n_draws x n_obs] pointwise log-likelihood (fit$draws$log_lik), with no
loo package dependency. "loglik" returns the (integrated) joint
log-likelihood with the parameter count. A fit carrying no pointwise
log-likelihood (a deterministic / point approximation) yields NA criterion
columns rather than an error, so the table always has one row per model.
"wbic" ranks by wbic(), an estimate of each fit's negative log evidence,
and needs every fit to carry its tempered draws (mode = "smc",
control = list(wbic = TRUE)); a fit without them gets an NA row.
compare_models(..., criterion = c("waic", "loo", "wbic", "loglik"))compare_models(..., criterion = c("waic", "loo", "wbic", "loglik"))
... |
Named |
criterion |
|
A data frame. For "wbic" (ranked best-first): model, wbic,
delta (WBIC gap to the best model, >= 0), and weight
(exp(-delta) normalized: the posterior model probability under equal
prior model weights, with each log evidence read off its WBIC).
For "loglik": model, n_params, logLik,
quantity and conditioned_on (which log-scale quantity logLik()
reports, and the hyperparameters it holds fixed; fits that disagree on
either are refused rather than ranked). For
"waic" / "loo" (ranked best-first): model, elpd, se_elpd,
p_eff, ic (-2 * elpd), delta (elpd gap to the best model),
se_diff (SE of that pointwise elpd difference), and weight (the
Akaike-style weight on the criterion).
model_average() for model-averaged predictions, tulpa_criteria()
and tulpa_psis() for the native criteria layer.
set.seed(1) df <- data.frame(x = rnorm(120)) df$y <- rpois(120, exp(0.4 + 0.5 * df$x)) f1 <- tulpa(y ~ x, data = df, family = "poisson") f2 <- tulpa(y ~ 1, data = df, family = "poisson") compare_models(full = f1, null = f2, criterion = "waic")set.seed(1) df <- data.frame(x = rnorm(120)) df$y <- rpois(120, exp(0.4 + 0.5 * df$x)) f1 <- tulpa(y ~ x, data = df, family = "poisson") f2 <- tulpa(y ~ 1, data = df, family = "poisson") compare_models(full = f1, null = f2, criterion = "waic")
Credible intervals for the fixed effects
## S3 method for class 'tulpa_fit' confint(object, parm = NULL, level = 0.95, ...)## S3 method for class 'tulpa_fit' confint(object, parm = NULL, level = 0.95, ...)
object |
A |
parm |
Parameter names or indices (default: all fixed effects).
Indices address the fixed effects. A name that is not a fixed effect is
read off the fit's posterior draws when they carry a column of that name
(for example a sampler's |
level |
Interval level, a single number in |
... |
Ignored. |
Matrix with lower and upper columns, labelled as
stats::confint.default() labels them ("2.5 %" and "97.5 %" at the
default level). A nested-Laplace fit carries
interval_source / interval_declined (which read produced the bounds –
by default the grid's Gaussian-mixture CDF), retained_mass (the share of
the grid weight the bounds are conditional on), and skew_applied, one
logical per reported coefficient saying whether its bounds are the
inner-Laplace skew-corrected quantiles or not. See summary.tulpa_fit().
Generic front doors onto the two criteria tulpa_criteria() computes that
the loo package owns no generic for. WAIC and PSIS-LOO have theirs
(loo::waic(), loo::loo()), so a model package registers methods on
those rather than on new names that would mask them.
pointwise_loglik(object, ...) ## Default S3 method: pointwise_loglik(object, ...) ## S3 method for class 'tulpa_fit' pointwise_loglik(object, ndraws = NULL, ...) dic(object, ...) ## Default S3 method: dic(object, loglik_at_mean = NULL, ...) ## S3 method for class 'tulpa_fit' dic(object, ...) cpo(object, ...) ## Default S3 method: cpo(object, ...) ## S3 method for class 'tulpa_fit' cpo(object, ...) ## S3 method for class 'tulpa_fit' waic(x, ...) ## S3 method for class 'tulpa_fit' loo(x, ...)pointwise_loglik(object, ...) ## Default S3 method: pointwise_loglik(object, ...) ## S3 method for class 'tulpa_fit' pointwise_loglik(object, ndraws = NULL, ...) dic(object, ...) ## Default S3 method: dic(object, loglik_at_mean = NULL, ...) ## S3 method for class 'tulpa_fit' dic(object, ...) cpo(object, ...) ## Default S3 method: cpo(object, ...) ## S3 method for class 'tulpa_fit' cpo(object, ...) ## S3 method for class 'tulpa_fit' waic(x, ...) ## S3 method for class 'tulpa_fit' loo(x, ...)
object, x
|
A pointwise log-likelihood matrix (draws x observations), or
a fitted model object a method is registered for, such as a |
... |
For |
ndraws |
Number of posterior draws the matrix is evaluated at. Defaults
to all stored draws, or 400 on the draw-free Laplace tier; a smaller
number subsamples the stored ones. Read by the |
loglik_at_mean |
Length- |
pointwise_loglik() is the one door onto the [n_draws x n_obs] matrix
itself, the input every criterion above is computed from. compare_models()
and model_average() call it (through an internal wrapper) rather than
assuming the engine's own fit layout, so a model package that registers
pointwise_loglik.<its fit class>() – alongside its own waic() / loo()
/ dic() / cpo() methods – reaches model comparison and averaging too.
The default methods take a draws x observations pointwise log-likelihood
matrix, the same input tulpa_criteria() takes. A model package registers a
method taking its own fit object, builds the matrix from the posterior, and
delegates here.
The tulpa_fit methods build that matrix from the fit itself, from the same
source compare_models(criterion = "waic") reads: a log_lik the backend
stored with its draws, else the family density evaluated at posterior draws
of the in-sample linear predictor (sampler draws, the outer-grid mixture of a
nested-Laplace fit, or Gaussian draws at the fixed-effect mode and
covariance). The draws are pinned to a fixed internal seed, so repeated
calls return the same numbers and leave the session RNG untouched.
dic() plugs in the posterior mean of the linear predictor, so its
p_dic counts effective parameters in the linear predictor's
parameterization; where the fit stored its log_lik and carries no linear
predictor draws, the DIC fields are NA and dbar is still reported.
A fit with no pointwise log-likelihood (no stored response, a family that is
not one built-in family name, or a linear predictor the fit cannot
reproduce) is refused with an error naming the fit's class and the reason.
loo::waic() and loo::loo() dispatch to the tulpa_fit methods once
loo is loaded, and return loo's own waic / psis_loo objects,
so loo::loo_compare() reads them. On an MCMC chain fit loo() passes
relative effective sample sizes computed over the fit's chains; on an i.i.d.
or approximation fit the draws are independent and r_eff is 1.
dic() and cpo() return a tulpa_criteria object. waic()
returns a loo waic object and loo() a loo psis_loo
object.
tulpa_criteria() for every criterion at once and for what the LOO
unit means; compare_models() to rank several fits.
set.seed(1) y <- rnorm(40) mu <- matrix(rnorm(200 * 40, sd = 0.2), 200, 40) ll <- dnorm(matrix(y, 200, 40, byrow = TRUE), mean = mu, log = TRUE) cpo(ll) d <- data.frame(x = rnorm(120)) d$y <- rpois(120, exp(0.4 + 0.6 * d$x)) fit <- tulpa(y ~ x, data = d, family = "poisson", mode = "laplace") dic(fit) cpo(fit)$lpml if (requireNamespace("loo", quietly = TRUE)) loo::waic(fit)set.seed(1) y <- rnorm(40) mu <- matrix(rnorm(200 * 40, sd = 0.2), 200, 40) ll <- dnorm(matrix(y, 200, 40, byrow = TRUE), mean = mu, log = TRUE) cpo(ll) d <- data.frame(x = rnorm(120)) d$y <- rpois(120, exp(0.4 + 0.6 * d$x)) fit <- tulpa(y ~ x, data = d, family = "poisson", mode = "laplace") dic(fit) cpo(fit)$lpml if (requireNamespace("loo", quietly = TRUE)) loo::waic(fit)
Provides a comprehensive diagnostic report for a tulpa model, combining convergence metrics, divergence information, and actionable recommendations.
diagnostic_summary(fit, quiet = FALSE) ## S3 method for class 'tulpa_diagnostic_summary' print(x, ...)diagnostic_summary(fit, quiet = FALSE) ## S3 method for class 'tulpa_diagnostic_summary' print(x, ...)
fit |
A |
quiet |
Logical; if TRUE, suppress printed output (default: FALSE). |
x |
A |
... |
Ignored. |
A list with class tulpa_diagnostic_summary containing:
Overall status: "PASS", "WARN", or "FAIL"
Number of divergent transitions
Percentage of divergent transitions
Data frame of parameters with worst Rhat
Data frame of parameters with worst ESS
E-BFMI value (HMC only)
approximation fits only: the outer PSIS k-hat, or the grid quadrature ESS when no k-hat was produced
approximation fits only: the range
over coefficients of the share of the fixed-effect marginal variance the
hyperparameter integration contributed, between / (within + between)
under the law of total variance over the outer grid. Near zero means the
reported interval is what conditioning on a single hyperparameter point
would have given. Reported without a threshold and never scored: a small
share is not by itself a miscalibrated interval. NA when the fit
retains no per-cell pieces to decompose
approximation fits only, and only when there is no
k-hat: WHY – "not_requested" and
"unguessable_axis: <axis>" are benign or permanent,
"degenerate_proposal" and "grid_too_small" are signals about the fit,
and "internal_inconsistency" is an engine bug and raises the status to
"WARN"
approximation fits only: the
largest scored inner-Laplace |gamma_3|, or why nothing was scored
("coupled_arm" marks arms the inner layer could
score neither per observation nor through the cell tensor)
data frame of grid axes the fit's own resolved path could not read and dropped as engine defaults: one row per field, with the block, family, path and the axis that path integrated instead. Absent whenever every supplied axis was used
Character vector of recommendations
check_diagnostics(), diagnostics(), plot_diagnostics()
set.seed(123) df <- data.frame(x = rnorm(60)) df$y <- rpois(60, exp(0.5 + 0.3 * df$x)) fit <- tulpa(y ~ x, data = df, family = "poisson", mode = "hmc", control = list(n_iter = 500L, warmup = 250L, n_chains = 2L, seed = 1L)) ds <- diagnostic_summary(fit) print(ds)set.seed(123) df <- data.frame(x = rnorm(60)) df$y <- rpois(60, exp(0.5 + 0.3 * df$x)) fit <- tulpa(y ~ x, data = df, family = "poisson", mode = "hmc", control = list(n_iter = 500L, warmup = 250L, n_chains = 2L, seed = 1L)) ds <- diagnostic_summary(fit) print(ds)
The diagnostic front door for any tulpa fit. What a fit's posterior draws can be asked depends on how they were produced, so this reads the draws provenance and returns the diagnostic that applies:
improved Rhat (the maximum of rank-normalized split-Rhat and folded split-Rhat), bulk / tail / mean / sd / quantile effective sample size, and Monte Carlo standard errors, following Vehtari et al. (2021). Split-Rhat is defined for a single chain, so a result is produced for any number of chains.
the approximation-reliability table –
the PSIS tail-shape pareto_k scored against the exact inner-Laplace
marginal and the outer-grid quadrature effective sample size (the
OUTER hyperparameter-grid integration layer), the inner-Laplace
skewness diagnostic gamma_3 when computed (the INNER Gaussian
approximation to the latent field, a separate layer pareto_k does not
cover), a combined whole-fit verdict naming which layer degrades when
one does, and a per-parameter posterior summary. See the
"Approximation reliability" sections below for the full description of
this table and its attributes.
no sample to diagnose; returns NULL with a
message naming the backend.
Provenance is read from $draws_kind (stamped by tulpa_dispatch), falling
back to the backend registry's emits property and then to an inner
$joint_fit. A fit that predates the tag is treated as a chain, so an older
fit is never silently refused.
diagnostics(fit, ...) ## Default S3 method: diagnostics( fit, pars = NULL, measures = c("rhat", "ess_bulk", "ess_tail"), probs = c(0.05, 0.95), sbc = NULL, ... ) ## S3 method for class 'laplace_diagnostics' print(x, ...) ## S3 method for class 'sbc' diagnostics(fit, ...)diagnostics(fit, ...) ## Default S3 method: diagnostics( fit, pars = NULL, measures = c("rhat", "ess_bulk", "ess_tail"), probs = c(0.05, 0.95), sbc = NULL, ... ) ## S3 method for class 'laplace_diagnostics' print(x, ...) ## S3 method for class 'sbc' diagnostics(fit, ...)
fit |
A |
... |
Passed to the method. |
pars |
Optional character vector of parameter names to restrict to. |
measures |
Character vector selecting which diagnostics to compute, in
output-column order. Available: |
probs |
Numeric probabilities for the quantile-based measures
( |
sbc |
Optional |
x |
A |
For a chain fit, a data frame with a parameter column followed by
one column per requested measure; entries are NA for parameters that are
constant or have too few draws. For an i.i.d. approximation fit, the
laplace_diagnostics table (see the "Approximation-reliability table
columns" section below for its attributes). For a point fit, NULL.
diagnostics() is an S3 generic so a model package can answer for its own
fit class (diagnostics.ratiod_fit(), say) rather than shadowing this
export with a same-named function. The default method does the provenance
routing described above and is what a method should delegate to once it has
assembled a draws array.
The tables above score ONE fit's own internal reliability. Whether the
backend's posterior is CALIBRATED is a different question, answered over many
simulated data sets by sbc(), and the two disagree in both directions – a
fit whose reliability band is clean on both layers can still fail
calibration, and one whose outer k-hat is well past the escalation threshold
can pass. So the band is a screen, not a verdict. Pass an sbc result as
sbc = and the calibration verdict is attached to the table and printed
underneath it; diagnostics() called on the sbc result itself returns its
report table.
Per-parameter reliability diagnostics for a fit whose posterior draws are
i.i.d. samples from a deterministic approximation (the nested-Laplace
grid-mixture posterior sum_k w_k N(mode_k, V_k)), where the between-chain
Gelman-Rubin Rhat reported for a chain fit does not apply. This is the
table that plays Rhat's role for the deterministic engine: it answers "did
the approximation work", not "did the chains mix".
The headline is a Pareto-smoothed importance-sampling (PSIS) reliability
diagnostic for the OUTER hyperparameter-grid integration. The nested
integrator scores its hyperparameter grid against the exact inner-Laplace
marginal posterior with a generalized-Pareto fit to the upper tail of the
importance ratios log p_target(theta) - log q_proposal(theta)
(see tulpa_psis()); the resulting tail-shape pareto_k is the "did the
outer integration work" number – k-hat < 0.5 good, 0.5-0.7 usable,
>= 0.7 unreliable (Vehtari et al. 2024; Yao et al. 2018). It is computed at
fit time and read back here; a fit that did not run the diagnostic, or whose
grid proposal degenerated, reports it as NA and is assessed on the grid
quadrature reliability instead.
pareto_k scores the outer integration only. A high pareto_k on a fit
whose grid quadrature is healthy (ess_grid well above 1, largest cell
weight modest) flags outer-integration (CI-width) calibration in the
right-skewed hyperparameter tail and does not by itself invalidate the
point estimates, which the grid quadrature governs.
outer_regime qualifies what a high pareto_k means, and is the reason a
bare threshold on pareto_k is not a reliability verdict. A sharp
hyperparameter posterior collapses the grid onto ~1 cell (ess_grid near
1); the outer integration has then degenerated to a point evaluation at the
modal hyperparameter, so pareto_k is scoring how well a Gaussian at that
mode stands in for the hyperparameter marginal, not how well a grid
integrated it. Where the dominant cell is INTERIOR to the grid the collapse
is benign – the grid bracketed the mode, the estimate is empirical Bayes
there, and only integrated hyperparameter uncertainty is missing. Where it
sits at a grid BOUNDARY the grid may simply be too narrow: grid_edge_axes
/ grid_edge_sides name the axes to widen. On a fit whose pareto_k
cleared the good band, the outer diagnostic also fits a skew-normal proposal
and reports the marginal's estimated skewness as outer_skew_max, so an
inflated k-hat that was purely the symmetric proposal's mismatch with a
skewed variance-component marginal is both corrected and explained. A
skew-normal has Gaussian tails, so this can never mask a genuinely
heavy-tailed target.
The grid quadrature reliability – the effective sample size
ess_grid = 1 / sum(w_k^2) of the outer integration weights and the largest
single cell weight – is always computed from the stored grid: a grid that
collapses onto one cell (ess_grid near 1) integrates no hyperparameter
uncertainty, while a spread grid does.
A SEPARATE layer – the inner Gaussian Laplace approximation to the
latent-field conditional posterior pi(x | theta, y), which pareto_k
does not cover – is scored by inner_skew: the leading-order Edgeworth
skewness estimate gamma_3 (Rue, Martino & Chopin 2009 Sec 3.2.3) at the
fitted MAP grid cell, computed when control$diagnose_skew = TRUE (the
default) on the fitting call. Reading a high pareto_k alone as "the fit
is broken" conflates the two layers: an occu_cover batch flagged 42/78
species "unreliable" on outer k-hat alone when their point estimates,
governed by the healthy inner layer, were fine – the
reliability attribute is the combined verdict that names which layer
degrades, if either does.
Each parameter row also carries the rank-normalized split-Rhat and bulk /
tail effective sample size of the draws (Vehtari et al. 2021). On i.i.d.
draws these sit at ~1.00 and ~n_draws by construction; they are reported,
clearly as i.i.d.-draw Monte-Carlo diagnostics and not chain mixing, to
document that the reported posterior summaries are not Monte-Carlo-limited.
A posterior sample is what those per-parameter rows are computed from, and
nothing else here needs one: every reliability quantity above is read off the
fit. So a fit that carries no draws – a default single-block nested-Laplace
fit, whose posterior is the retained outer-grid mixture rather than a sample
– reports the full band with an empty per-parameter body and n_draws = NA,
and records why in the param_table_declined attribute.
tulpa_posterior_draws() samples that mixture where the rows are wanted.
The one case that still returns NULL is a fit with neither draws nor any
reliability quantity, such as a plain Laplace fit with no outer grid to
score.
The PSIS pareto_k diagnoses the OUTER (hyperparameter) integration: whether
the Gaussian-proposal-over-grid approximation of the marginal hyperparameter
posterior p(theta | data) can be importance-corrected to the exact inner
marginal. This is the dominant approximation in nested Laplace and the one
with an exactly evaluable target. A full latent-space PSIS against the exact
joint posterior pi(x) is not computed: the latent prior marginal
p(x) = integral p(x | theta) p(theta) dtheta has no closed form, and for
the marginalized-occupancy / cover-hurdle likelihoods the exact joint density
is evaluable only inside the C++ kernel, so a stored fit cannot reconstruct
it. The grid quadrature reliability is the complementary stored-fit number.
The inner layer carries a SECOND score, inner_pareto_k, which needs no
likelihood derivative at all and therefore answers where inner_skew
declines. The inner Gaussian at the fitted hyperparameter is an importance
proposal for the exact conditional posterior, and the joint density is the
target, so PSIS on that ratio scores the inner approximation directly. It is
computed on the same probed indices along the same conditional-mean curve,
one dimension per index, since importance sampling degrades with dimension
and a k-hat over the whole latent field would report n_x rather than the
approximation. A Pareto shape index is scale-free, so it is banded only on
indices whose realized importance efficiency shows a correction worth
describing; inner_pareto_k_uniform records that none did, which is what a
well-approximated inner layer looks like.
inner_skew diagnoses the INNER (latent-field) Laplace: whether the
Gaussian approximation to pi(x_i | theta, y) is itself a good fit, at
each scored latent index i. gamma_3 is exact for a gaussian-family
coefficient (the log-likelihood is exactly quadratic in eta) and
declines to NA – never a silently-wrong 0 ("perfectly Gaussian") –
whenever no per-observation third-derivative oracle is available: a
coupled multi-process likelihood (e.g. tulpaObs's occu_cover, whose
arms combine non-separably through a CellCouplingSpec) or a family with
no registered third derivative. Only the requested latent indices are
scored (every arm's fixed-effects coefficients by default – see
control$skew_idx), since each index costs one extra linear solve; the
full latent field is not scored by default on a large spatial field.
The table has one row per parameter – parameter, mean, sd, n_draws,
mcse_mean, moment_source – carrying attributes. There is deliberately
no rhat / ess_* column: those are what the draws-provenance gate
withholds on a non-chain fit, and this table is where it dispatches instead.
Where the draws were sampled from an outer-grid mixture the fit carries
(attr(draws, "grid_mixture"), stamped by tulpa_posterior_draws() on a
single-block fit, by tulpa_re_cov_nested() and by model packages),
mean / sd are that mixture's exact moments, mcse_mean is 0 and
moment_source is "mixture_moments"; otherwise they are the draw
moments, mcse_mean is sd / sqrt(n_draws) and moment_source is
"draws".
pareto_kthe outer PSIS reliability k-hat (NA if not computed).
pareto_k_band"good" / "ok" / "unreliable" / NA.
pareto_k_declined, pareto_k_declined_note
when pareto_k is
NA, WHY: "not_requested", "not_applicable",
"unguessable_axis" (naming the axis – a permanent limitation of that
family, so read ess_grid instead), "draws_too_few",
"grid_too_small", "no_varying_axis", "degenerate_proposal", or
"internal_inconsistency" (an engine bug worth reporting), plus a
one-line reading of it.
pareto_k_is_essimportance-sampling ESS on the smoothed weights.
pareto_k_ci_low, pareto_k_ci_high, pareto_k_se_boot,
pareto_k_band_confident, diagnose_draws
the bootstrap interval of the outer k-hat, its standard error, whether the interval sits inside one band, and the draw count it used; present when the fit ran the bootstrap.
k_quality_requested, k_quality_reached, k_quality_best,
k_quality_reason, k_quality_rounds
the outcome of a
control$k_quality request; present when one was made.
inner_skew, inner_skew_idx
the per-index inner-Laplace
skewness gamma_3 and the latent indices it was probed at; present
when the probe ran.
ess_grid, n_grid, rel_ess_grid, max_weight
grid quadrature
reliability. n_grid counts the cells SOLVED, so a posterior sharp
enough to underflow all but one of them reads ess_grid = 1.00 of 124 cells rather than of one, and rel_ess_grid is the share of the
solved grid the quadrature actually uses.
outer_regime"spread" / "collapsed_interior" /
"collapsed_edge" – whether the outer grid integrated hyperparameter
uncertainty at all, and if not whether its dominant cell is interior
(empirical Bayes at the mode: point estimates sound, hyperparameter
uncertainty not integrated) or against a grid boundary (widen it).
grid_edge_axes, grid_edge_sides
for an edge collapse, the axes the dominant cell sits against and on which side.
grid_edge_mass_axesaxes holding material weight on one of
their own boundary nodes, as axis:side –
that axis truncates its own marginal there whether or not its mode sits
on the node, which is the weaker and more common statement
grid_railed_axes cannot make. Read in every regime, a spread grid
included.
outer_skew_maxlargest estimated |skewness| of the hyperparameter
marginal, computed only when the k-hat triggered the skew-normal
proposal rescue (NA means the Gaussian proposal already fit, not
"symmetric and unchecked").
outer_regime_notea one-line reading of a collapsed regime or of boundary mass on an axis, absent on a spread grid carrying neither.
grid_railed_axesouter axes whose OWN marginal is maximal at one
of their own endpoints, as axis:side – the span does not contain that
axis's posterior mode, so its marginal is a truncated tail at any
spacing. Reported whether or not the engine was allowed to, able to, or
built to move the axis.
grid_placement, grid_recentred_axes,
grid_placement_declined, grid_placement_note
whether the outer grid was re-centred, on which axes, and – when it was not – why.
scopethe outer diagnostic's scope string.
scope_backend, scope_title, scope_layers, scope_text
what
the table describes: the backend it was read for, the header title,
the reliability layers that backend has (c("outer", "inner") for a
nested-Laplace fit, empty for a backend with neither), and the scope
sentence – for a nested fit what its outer layer integrates over, for
any other backend what its draws are and which reliability quantity
it computes. A backend without the two layers has no whole-fit
verdict, and its reliability is NA.
inner_skew_maxthe largest |gamma_3| among the scored latent
indices (NA if control$diagnose_skew = FALSE or nothing scored).
inner_skew_band"good" / "ok" / "unreliable" / NA, banded
on inner_skew_max by the general skewness-magnitude convention
(Bulmer 1979) – not a Rue-Martino-Chopin-specific cutoff.
inner_skew_scored, inner_skew_probed
how many of the probed
latent indices returned a finite gamma_3 vs how many were probed.
inner_skew_declined, inner_skew_arms_declined,
inner_skew_declined_note
when nothing was scored, WHY:
"coupled_arm" (STRUCTURAL – the coupled arms have neither a
per-observation sum nor a cell third-derivative tensor to read, so the
outer k-hat is the only reliability number this fit has),
"curvature3_unavailable", "no_finite_contribution",
"no_probe_indices", "not_requested", "backend_unsupported",
"solve_failed", or "not_converged" (the probe re-solve stopped
short of a mode, so neither inner score has a point to read); the arms
(1-based) a joint fit had no oracle for, which is also set on a
PARTIALLY scored fit; and a one-line reading.
inner_pareto_k,
inner_pareto_k_band
the inner-Laplace importance k-hat over the
probed subspace, and its band on the same convention as the outer
k-hat. Available wherever a mode was found, including a fit gamma_3
cannot score.
inner_pareto_k_rel_ess,
inner_pareto_k_is_ess
the smallest realized importance efficiency and effective sample size across the probed indices – how much correcting the inner Gaussian actually needs, which is what makes the scale-free shape above readable.
inner_pareto_k_uniformTRUE when no probed index carried a
material correction, i.e. the inner Gaussian reproduces the
conditional posterior over the sampled region.
inner_pareto_k_scored, inner_pareto_k_probed
how many probed indices returned a finite k-hat vs how many were probed.
inner_pareto_k_declined, inner_pareto_k_declined_note
when
it is NA, WHY, from the same closed vocabulary the outer k-hat
uses.
reliabilitythe combined whole-fit verdict:
"reliable" only when both layers are good; otherwise names which
layer is scoped or flags both as unreliable. The inner layer enters
through the worse of its two scores, so a fit whose cubic term
declined is still assessed.
and a trailing summary attribute (a
one-row data frame of the headline numbers) for printing.
Vehtari, Gelman, Simpson, Carpenter & Burkner (2021). Rank-normalization, folding, and localization: an improved Rhat for assessing convergence of MCMC. Bayesian Analysis 16(2):667-718.
Vehtari, Simpson, Gelman, Yao & Gabry (2024). Pareto smoothed importance sampling. JMLR 25(72):1-58.
Yao, Vehtari, Simpson & Gelman (2018). Yes, but did it work?: Evaluating variational inference. ICML, PMLR 80:5581-5590.
Rue, Martino & Chopin (2009). Approximate Bayesian inference for latent Gaussian models by using integrated nested Laplace approximations. JRSS-B 71(2):319-392.
sbc() for calibration, the approximation-reliability sections
above for the i.i.d.-fit table in full, tulpa_draws_array(),
tulpa_posterior_draws(), tulpa_psis(), plot_rhat(), plot_ess(),
diagnostic_summary(), check_diagnostics()
set.seed(1) df <- data.frame(x = rnorm(60)) df$y <- rpois(60, exp(0.5 + 0.3 * df$x)) # chain fit -> Rhat / ESS hmc <- tulpa(y ~ x, data = df, family = "poisson", mode = "hmc", control = list(n_iter = 500L, warmup = 250L, n_chains = 2L, seed = 1L)) diagnostics(hmc) # deterministic fit -> PSIS approximation reliability smc <- tulpa(y ~ x, data = df, family = "poisson", mode = "smc") diagnostics(smc)set.seed(1) df <- data.frame(x = rnorm(60)) df$y <- rpois(60, exp(0.5 + 0.3 * df$x)) # chain fit -> Rhat / ESS hmc <- tulpa(y ~ x, data = df, family = "poisson", mode = "hmc", control = list(n_iter = 500L, warmup = 250L, n_chains = 2L, seed = 1L)) diagnostics(hmc) # deterministic fit -> PSIS approximation reliability smc <- tulpa(y ~ x, data = df, family = "poisson", mode = "smc") diagnostics(smc)
Tests first-order autocorrelation in temporally-ordered residuals.
durbin_watson( object, alternative = c("two.sided", "greater", "less"), time = NULL, resid_type = "pearson" )durbin_watson( object, alternative = c("two.sided", "greater", "less"), time = NULL, resid_type = "pearson" )
object |
A fitted model, or a numeric vector of temporally-ordered
residuals. A fit's residuals are in its data's row order, so pass |
alternative |
|
time |
Optional time index, one per residual; the residuals are put in
increasing |
resid_type |
Residual type if extracting from a model (default
|
An htest object with DW statistic, lag-1 r, and p-value
set.seed(1) e <- as.numeric(arima.sim(list(ar = 0.6), n = 100)) durbin_watson(e) # Residuals recorded out of time order: tt <- sample(100) durbin_watson(e[tt], time = tt)set.seed(1) e <- as.numeric(arima.sim(list(ar = 0.6), n = 100)) durbin_watson(e) # Residuals recorded out of time order: tt <- sample(100) durbin_watson(e[tt], time = tt)
Computes E-BFMI (Betancourt 2016) from the per-iteration Hamiltonian energies an HMC or NUTS sampler records: the variance of the energy transition between successive iterations over the variance of the marginal energy. Each chain is diffed separately, since a transition across a chain boundary compares unrelated trajectories, and the lowest chain value is reported: one badly exploring chain is enough to distrust a fit. Values below 0.3 indicate that momentum resampling moves the sampler too little between energy level sets.
energy_bfmi(energy, chain = NULL)energy_bfmi(energy, chain = NULL)
energy |
Numeric vector of per-iteration energies. |
chain |
Chain index of each entry of |
A list with value (the lowest per-chain E-BFMI, NA when no
chain has two finite, non-constant energies), per_chain,
energy_centred (energies centred within each chain) and energy_diff
(within-chain transitions), the last two pooled across chains for
plotting.
set.seed(1) energy <- c(cumsum(rnorm(200, sd = 0.3)), rnorm(200, sd = 1)) energy_bfmi(energy, chain = rep(1:2, each = 200))$per_chainset.seed(1) energy <- c(cumsum(rnorm(200, sd = 0.3)), rnorm(200, sd = 1)) energy_bfmi(energy, chain = rep(1:2, each = 200))$per_chain
The response families family = accepts on the engine's front doors, in
their canonical (default-link) spelling. A family's *_<link> variants
(e.g. "gamma_inverse") are listed separately by linked_family_names().
family_names()family_names()
A character vector of family names.
family_names()family_names()
The posterior of a fit's spatial field, one row per spatial unit: an areal region (a row of the adjacency matrix) or an SPDE mesh node. The value reported is the field's contribution to the linear predictor at that unit, the quantity a covariate is read against.
field_effects(object)field_effects(object)
object |
A |
Where the posterior comes from follows what the fit computes:
A ModelData sampler fit (mode = "hmc" and its siblings) with an
ICAR, BYM2 or proper-CAR field: every draw's field contribution is
evaluated through the engine's own linear-predictor assembly, so an
intrinsic field is centred as its density centres it and a BYM2 field is
the combined sigma * (sqrt(rho) * s * phi + sqrt(1 - rho) * theta) at
that draw's sigma and rho (s the scaling factor). A unit that no
observation falls in contributes nothing to the linear predictor and is
reported as NA.
A Polya-Gamma Gibbs fit (mode = "gibbs") stores the field it
samples (phi_spatial[k], plus theta_spatial[k] for BYM2, summed);
the rows summarize those draws.
A nested-Laplace fit with an ICAR or proper-CAR field: each outer
cell contributes its conditional mode, and its marginal variance where
the fit retained one, weighted by the cell's posterior weight. With the
variances the summary is the exact mean, SD and quantiles of that
Gaussian mixture (source = "mixture"); without them the weighted mean
of the cell modes, with no spread (source = "mode"). A BYM2 field is
reported where the fit stores its grid-weighted combined field
(fit_st_nested()); a tulpa() nested BYM2 fit carries its two
components with cell-dependent mixing and errors.
A conditional-Laplace areal fit (mode = "laplace" / "eb")
reports the field at its mode, with no spread. For BYM2 this is the
combined field at the sigma = 1, rho = 0.5 the conditional kernel
fixes.
An SPDE fit (fit_spde()) reports the mesh-node field: its draws on
the sampler path, its mode (the posterior-mean hyperparameters' fit on
the nested path) otherwise.
A fit whose field cannot be located this way (a continuous GP / NNGP /
HSGP field, a restricted field, an inline spatial() varying-coefficient
field) errors naming the field and the backend, as does a fit with no
spatial field. A temporal field is read by temporal().
A data frame with one row per spatial unit: unit (the node
index), label (the adjacency row name where it has one, else the index),
estimate, sd, the 2.5% / 97.5% bounds conf.low / conf.high, and
source: "sampled" for a posterior draw summary, "mixture" for a
nested-Laplace Gaussian mixture, "mode" for a point at the mode (sd
and the bounds NA). The field type is attached as attribute "field".
smooth_effects() for an s(x) term, ranef() for random
effects, temporal() for a temporal field.
set.seed(1) K <- 12 W <- matrix(0, K, K) for (i in 1:K) { j <- if (i < K) i + 1 else 1; W[i, j] <- W[j, i] <- 1 } region <- sample(1:K, 200, replace = TRUE) d <- data.frame(region = region, x = rnorm(200)) d$y <- rpois(200, exp(0.3 + 0.5 * d$x + sin(2 * pi * region / K))) fit <- tulpa(y ~ x + spatial(region), data = d, family = "poisson", spatial = list(type = "icar", adjacency = W), mode = "laplace") fe <- field_effects(fit) plot(fe$unit, fe$estimate, type = "b")set.seed(1) K <- 12 W <- matrix(0, K, K) for (i in 1:K) { j <- if (i < K) i + 1 else 1; W[i, j] <- W[j, i] <- 1 } region <- sample(1:K, 200, replace = TRUE) d <- data.frame(region = region, x = rnorm(200)) d$y <- rpois(200, exp(0.3 + 0.5 * d$x + sin(2 * pi * region / K))) fit <- tulpa(y ~ x + spatial(region), data = d, family = "poisson", spatial = list(type = "icar", adjacency = W), mode = "laplace") fe <- field_effects(fit) plot(fe$unit, fe$estimate, type = "b")
Fits a GLM with a Matern spatial field via the SPDE approach. Uses CHOLMOD sparse solver with optional nested Laplace for hyperparameter integration.
fit_spde( y, X, spatial, family = "binomial", n_trials = NULL, range = NULL, sigma = NULL, nested_laplace = is.null(range) || is.null(sigma), phi = 1, offset = NULL, re_idx = NULL, n_re_groups = 0L, sigma_re = 1, mode = c("laplace", "nuts"), hyperprior = c("proper", "flat"), control = list() )fit_spde( y, X, spatial, family = "binomial", n_trials = NULL, range = NULL, sigma = NULL, nested_laplace = is.null(range) || is.null(sigma), phi = 1, offset = NULL, re_idx = NULL, n_re_groups = 0L, sigma_re = 1, mode = c("laplace", "nuts"), hyperprior = c("proper", "flat"), control = list() )
y |
Integer response vector. |
X |
Design matrix. |
spatial |
A |
family |
Distribution family: |
n_trials |
Integer vector of trial sizes (binomial only). |
range |
Spatial range parameter. If NULL, uses nested Laplace to integrate over range and sigma. |
sigma |
Marginal standard deviation. If NULL, uses nested Laplace. |
nested_laplace |
Logical. If TRUE (default when range/sigma are NULL), use nested Laplace approximation over hyperparameters. |
phi |
Dispersion passed to the family, held fixed. One convention at
every door: for Defaulted, it conditions at 1 and says so with a warning, since for a
family that reads a dispersion that is a modelling choice rather than a
neutral value. |
offset |
Optional fixed additive term on the linear predictor
( |
re_idx, n_re_groups, sigma_re
|
Optional single iid random-intercept
|
mode |
Inference method (the method is an argument, not a parallel
verb): |
hyperprior |
|
control |
A named list of numerical / tuning knobs (statistical arguments stay in the signature above). Recognized entries:
|
A list with:
mode: mode of the latent field (beta + mesh node effects)
log_marginal: log marginal likelihood
converged: convergence flag
spatial: the spatial specification (for prediction)
pareto_k, pareto_k_is_ess: outer Pareto- and its
importance-sampling ESS (NA when diagnose_k = FALSE)
pareto_k_proposal_source, pareto_k_first_pass: which proposal
family the reported k-hat came from (several candidates are scored and
the best kept), and the k-hat of the first pass – the proposal exactly
as placed, before refinement. A large gap says the placement is poor
even where the verdict is fine.
nested: nested Laplace results (if used)
Lindgren, Rue & Lindstrom (2011). An explicit link between Gaussian fields and Gaussian Markov random fields: the stochastic partial differential equation approach. JRSS-B 73(4):423-498. Rue, Martino & Chopin (2009). Approximate Bayesian inference for latent Gaussian models by using integrated nested Laplace approximations. JRSS-B 71(2):319-392.
if (requireNamespace("fmesher", quietly = TRUE)) { set.seed(1) n <- 200L coords <- cbind(runif(n), runif(n)) mesh <- fmesher::fm_mesh_2d(loc = coords, max.edge = c(0.15, 0.4), cutoff = 0.05) fem <- fmesher::fm_fem(mesh) A <- as(fmesher::fm_basis(mesh, loc = coords), "CsparseMatrix") spec <- spatial_spde_custom(C = fem$c0, G = fem$g1, A = A, nu = 1, prior_range = c(0.3, 0.5), prior_sigma = c(0.6, 0.05)) w <- as.numeric(rnorm(spec$n_mesh, 0, 0.6)); w <- w - mean(w) x <- rnorm(n) y <- rpois(n, exp(2.0 + 0.5 * x + as.numeric(spec$A %*% w))) fit <- fit_spde(y = y, X = cbind(1, x), spatial = spec, family = "poisson") fit$nested$range_mean }if (requireNamespace("fmesher", quietly = TRUE)) { set.seed(1) n <- 200L coords <- cbind(runif(n), runif(n)) mesh <- fmesher::fm_mesh_2d(loc = coords, max.edge = c(0.15, 0.4), cutoff = 0.05) fem <- fmesher::fm_fem(mesh) A <- as(fmesher::fm_basis(mesh, loc = coords), "CsparseMatrix") spec <- spatial_spde_custom(C = fem$c0, G = fem$g1, A = A, nu = 1, prior_range = c(0.3, 0.5), prior_sigma = c(0.6, 0.05)) w <- as.numeric(rnorm(spec$n_mesh, 0, 0.6)); w <- w - mean(w) x <- rnorm(n) y <- rpois(n, exp(2.0 + 0.5 * x + as.numeric(spec$A %*% w))) fit <- fit_spde(y = y, X = cbind(1, x), spatial = spec, family = "poisson") fit$nested$range_mean }
Fits y ~ X beta + u_spatial[s] + v_temporal[t] with a spatial field –
areal (icar / bym2 / car_proper) or continuous (hsgp / nngp) –
and a temporal field (rw1 / rw2 / ar1), integrating the spatial
hyperparameter(s), temporal precision, and (for ar1) the temporal
autocorrelation over a hyperparameter grid via the
cpp_nested_laplace_st_* kernels. The fixed-effect posterior is the
grid-marginalised mixture; the spatial and temporal field posterior means are
the grid-weighted latent modes.
fit_st_nested( y, X, spatial_idx, adjacency, temporal_idx, n_times, spatial_type = c("icar", "bym2", "car_proper", "hsgp", "nngp"), temporal_type = c("ar1", "rw1", "rw2"), family = "binomial", n_trials = NULL, phi = 1, cyclic = FALSE, re_idx = NULL, n_re_groups = 0L, sigma_re = 1, hyperprior = c("proper", "flat"), control = list(), coords = NULL, nn = 10L, cov_type = 2L, hsgp_m = 6L, hsgp_c = 1.5 ) ## S3 method for class 'tulpa_st_nested' print(x, ...)fit_st_nested( y, X, spatial_idx, adjacency, temporal_idx, n_times, spatial_type = c("icar", "bym2", "car_proper", "hsgp", "nngp"), temporal_type = c("ar1", "rw1", "rw2"), family = "binomial", n_trials = NULL, phi = 1, cyclic = FALSE, re_idx = NULL, n_re_groups = 0L, sigma_re = 1, hyperprior = c("proper", "flat"), control = list(), coords = NULL, nn = 10L, cov_type = 2L, hsgp_m = 6L, hsgp_c = 1.5 ) ## S3 method for class 'tulpa_st_nested' print(x, ...)
y |
Response vector. |
X |
Fixed-effects design matrix ( |
spatial_idx |
Integer per-observation spatial-unit index (1-based).
For |
adjacency |
Spatial adjacency (a symmetric 0/1 matrix or |
temporal_idx |
Integer per-observation time index (1-based). |
n_times |
Number of distinct time points. |
spatial_type |
|
temporal_type |
|
family |
Response family (see |
n_trials |
Binomial denominators, or |
phi |
Dispersion passed to the family, held fixed. One convention at
every door: for Defaulted, it conditions at 1 and says so with a warning, since for a
family that reads a dispersion that is a modelling choice rather than a
neutral value. |
cyclic |
Logical; wrap the temporal field (seasonal). Default |
re_idx, n_re_groups, sigma_re
|
Optional single iid random-intercept term
alongside the fields (conditioned on |
hyperprior |
The prior an outer hyperparameter axis carries when the
call states no density for it, |
control |
A list of numerical / grid knobs: For The A grid knob PINS the axes it shapes, and a pin always wins – but
pinning is decided by value, not by presence: a knob
set to the engine's own default, or marked with |
coords |
Coordinate matrix for a continuous spatial field, required
when |
nn |
Number of nearest neighbours per location, |
cov_type |
Integer NNGP covariance code ( |
hsgp_m, hsgp_c
|
Hilbert-space GP basis size (per dimension, default 6)
and boundary factor (default 1.5), |
x |
A |
... |
Ignored. |
A tulpa_fit (subclass tulpa_nested_laplace) carrying the
fixed-effect posterior (draws via the grid mixture), spatial_effects,
temporal_effects, log_marginal, weights, theta_grid over
(tau_spatial, tau_temporal, rho) for an areal spatial field or
(sigma2, lengthscale, tau_temporal, rho) / (sigma2, phi_gp, tau_temporal, rho) for "hsgp" / "nngp", and family / n_trials /
phi (the
R-level convention), which is what fitted(), residuals(),
posterior_predict() and simulate() read the response family from.
Also carries pareto_k_regime
("spread" / "collapsed_interior" / "collapsed_edge", see
tulpa_nested_laplace_joint()'s return docs for the definition) and
outer_grid_placement ("fixed" or "auto_recentered") plus, on a
"fixed" placement, outer_grid_recenter_declined
("grid_knobs_overridden" / "grid_not_collapsed" /
"no_usable_curvature" / "refit_failed" / "sd_ceiling_unresolved" /
"sd_floor_unresolved"). A recentred fit also carries
outer_grid_pinned_axes, the axes whose knobs were pinned and whose
nodes were therefore kept, and outer_grid_recenter_sd_clamp /
_sd_raw / _sd_used – per moved axis, which mode-SD bound the
placement hit, the SD the stencil measured, and the SD the axis was laid
from. A bound-decline is PER AXIS here: the axes the
mode-find did resolve are still re-placed, and
outer_grid_recenter_sd_declined names the ones that kept their incoming
nodes and on which bound, so a partially re-placed grid is not read as a
fully re-placed one. With every free axis declined the pass reports the
grid as the fixed one it still is.
tulpa() (front door), tulpa_nested_laplace() (single field).
set.seed(1) n_s <- 16L; n_t <- 8L; N <- 400L adj <- matrix(0, n_s, n_s) for (i in 1:(n_s - 1)) adj[i, i + 1] <- adj[i + 1, i] <- 1 s <- sample(n_s, N, TRUE); tt <- sample(n_t, N, TRUE) us <- as.numeric(scale(cumsum(rnorm(n_s)))); vt <- as.numeric(scale(cumsum(rnorm(n_t)))) x <- rnorm(N) y <- rbinom(N, 1, plogis(0.2 + 0.5 * x + 0.7 * us[s] + 0.6 * vt[tt])) fit <- fit_st_nested(y, cbind(1, x), s, adj, tt, n_t, family = "binomial")set.seed(1) n_s <- 16L; n_t <- 8L; N <- 400L adj <- matrix(0, n_s, n_s) for (i in 1:(n_s - 1)) adj[i, i + 1] <- adj[i + 1, i] <- 1 s <- sample(n_s, N, TRUE); tt <- sample(n_t, N, TRUE) us <- as.numeric(scale(cumsum(rnorm(n_s)))); vt <- as.numeric(scale(cumsum(rnorm(n_t)))) x <- rnorm(N) y <- rbinom(N, 1, plogis(0.2 + 0.5 * x + 0.7 * us[s] + 0.6 * vt[tt])) fit <- fit_st_nested(y, cbind(1, x), s, adj, tt, n_t, family = "binomial")
In-sample mean response from the fixed effects and the observation offset
(E[y] = g^{-1}(X beta + offset), trial-scaled for binomial). Random
effects are held at their prior mean of zero; group-level effects are in
ranef(). y - fitted(object) equals
residuals(object, type = "response"). On a zero-inflated fit
(ziformula) the mean is the mixture's, (1 - pi) E[y | eta] with
pi = plogis(X_zi beta_zi); over a zero-truncated family that is the hurdle
mean.
## S3 method for class 'tulpa_fit' fitted(object, ...)## S3 method for class 'tulpa_fit' fitted(object, ...)
object |
A |
... |
Ignored. |
Numeric vector of fitted mean responses, length nobs.
The fixed-effect point estimates, equivalent to coef() on a tulpa_fit.
Provided under the fixef name for code written against the lme4 / nlme
interface: lme4::fixef(fit) and nlme::fixef(fit) dispatch here too when
either package is installed, so a tulpa_fit can be dropped into an
lme4-shaped workflow.
fixef(object, ...) ## S3 method for class 'tulpa_fit' fixef(object, ...)fixef(object, ...) ## S3 method for class 'tulpa_fit' fixef(object, ...)
object |
A |
... |
Ignored. |
Note the deliberate difference from lme4::coef.merMod, which returns
per-group sums of the fixed and random effects. On a tulpa_fit, coef()
returns the fixed effects alone and fixef() is its synonym; the random
effects are ranef().
Named numeric vector of fixed-effect estimates.
set.seed(1) df <- data.frame(x = rnorm(80)) df$y <- rpois(80, exp(0.5 + 0.3 * df$x)) fit <- tulpa(y ~ x, data = df, family = "poisson") fixef(fit)set.seed(1) df <- data.frame(x = rnorm(80)) df$y <- rpois(80, exp(0.5 + 0.3 * df$x)) fit <- tulpa(y ~ x, data = df, family = "poisson") fixef(fit)
Performs Geweke's convergence diagnostic, comparing the mean of the first portion of a chain to the last portion. It is a within-chain diagnostic, so on a multi-chain fit it is computed for each chain separately.
geweke_test(fit, frac1 = 0.1, frac2 = 0.5, pars = NULL) ## S3 method for class 'tulpa_geweke' print(x, ...)geweke_test(fit, frac1 = 0.1, frac2 = 0.5, pars = NULL) ## S3 method for class 'tulpa_geweke' print(x, ...)
fit |
A |
frac1 |
Fraction of each chain for the first window (default: 0.1). |
frac2 |
Fraction of each chain for the second window (default: 0.5).
Both lie in |
pars |
Character vector of parameter names (default: all main params). |
x |
A |
... |
Ignored. |
The Geweke test computes a z-score comparing the means of early and late
portions of a chain. Large z-scores (|z| > 2) indicate the chain has not
converged. The chains are never pooled: concatenated, the "first" window
would come from the first chain and the "last" from another, which compares
two chains rather than a chain's start with its end (use diagnostics()
for the between-chain R-hat). A parameter that is constant within a chain
has an undefined z-score, reported as NA.
A data frame of class tulpa_geweke with columns chain,
parameter, z_score and p_value, one row per chain and parameter.
diagnostics(), check_diagnostics()
set.seed(123) df <- data.frame(x = rnorm(60)) df$y <- rpois(60, exp(0.5 + 0.3 * df$x)) fit <- tulpa(y ~ x, data = df, family = "poisson", mode = "hmc", control = list(n_iter = 500L, warmup = 250L, n_chains = 2L, seed = 1L)) geweke_test(fit)set.seed(123) df <- data.frame(x = rnorm(60)) df$y <- rpois(60, exp(0.5 + 0.3 * df$x)) fit <- tulpa(y ~ x, data = df, family = "poisson", mode = "hmc", control = list(n_iter = 500L, warmup = 250L, n_chains = 2L, seed = 1L)) geweke_test(fit)
Model-level summary statistics (broom-compatible)
## S3 method for class 'tulpa_fit' glance(x, ...)## S3 method for class 'tulpa_fit' glance(x, ...)
x |
A |
... |
Ignored. |
Single-row data frame.
set.seed(1) df <- data.frame(x = rnorm(100)) df$y <- rpois(100, exp(0.3 * df$x)) fit <- tulpa(y ~ x, data = df, family = "poisson") glance(fit)set.seed(1) df <- data.frame(x = rnorm(100)) df$y <- rpois(100, exp(0.3 * df$x)) fit <- tulpa(y ~ x, data = df, family = "poisson") glance(fit)
Sample from a posterior using independence Metropolis-Hastings with a multivariate-normal proposal centred at the Laplace mode and scaled by the inverse Hessian. For posteriors that are well- approximated by a Gaussian near the mode this is dramatically cheaper than HMC: each iteration is one log-posterior evaluation plus one accept/reject step. Embarrassingly parallel across chains.
Use cases:
Quick draws when Laplace is "almost right" but you want asymptotically-correct samples.
Cross-validation, simulation studies, or other repeated-fit workflows where HMC startup cost dominates.
Sanity check on the Laplace approximation: low IMH acceptance means the posterior is far from Gaussian and Laplace is biased.
imh_laplace( log_posterior, mode, hessian, n_iter = 2000L, warmup = n_iter%/%2L, scale = 1, init = NULL, thin = 1L, seed = NULL, verbose = FALSE )imh_laplace( log_posterior, mode, hessian, n_iter = 2000L, warmup = n_iter%/%2L, scale = 1, init = NULL, thin = 1L, seed = NULL, verbose = FALSE )
log_posterior |
Function |
mode |
Numeric vector of length |
hessian |
Symmetric positive-definite |
n_iter |
Total iterations including warmup (default 2000). |
warmup |
Warmup iterations to discard (default |
scale |
Optional inflation factor on the proposal covariance (default 1.0). Values slightly > 1 (e.g., 1.5) give heavier-tailed proposals that improve mixing when Laplace underestimates posterior spread. |
init |
Optional starting parameter vector (default = |
thin |
Keep every |
seed |
Optional integer RNG seed. Scoped to this call: the caller's
|
verbose |
Print acceptance rate at end (default |
A list with class tulpa_fit carrying:
draws: (n_iter - warmup) / thin x d matrix of draws.
means: posterior means.
accept_prob: per-iteration acceptance indicators.
mean_accept: overall post-warmup acceptance rate.
log_prob: per-draw log posterior values.
inference_mode: "exact".
inference_tier: 1L.
backend: "imh_laplace".
Tier 1 (Exact). The MH accept/reject step makes the chain asymptotically correct under the standard MH conditions. Tier status does not depend on Laplace's quality – only its quality affects efficiency.
tulpa_laplace() for the mode + Hessian,
bridge_sampling() for marginal-likelihood estimation on the
resulting draws.
# Toy: Bernoulli logistic with one covariate. set.seed(1) n <- 100 x <- rnorm(n) eta <- 0.3 + 1.2 * x y <- rbinom(n, 1, plogis(eta)) X <- cbind(1, x) lap <- tulpa_laplace(y, n_trials = rep(1L, n), X = X, family = "binomial") log_post <- function(beta) { eta <- as.numeric(X %*% beta) sum(y * eta - log1p(exp(eta))) + sum(dnorm(beta, 0, 10, log = TRUE)) } fit <- imh_laplace(log_post, mode = lap$mode[1:2], hessian = lap$H_beta, n_iter = 1000) fit$mean_accept colMeans(fit$draws)# Toy: Bernoulli logistic with one covariate. set.seed(1) n <- 100 x <- rnorm(n) eta <- 0.3 + 1.2 * x y <- rbinom(n, 1, plogis(eta)) X <- cbind(1, x) lap <- tulpa_laplace(y, n_trials = rep(1L, n), X = X, family = "binomial") log_post <- function(beta) { eta <- as.numeric(X %*% beta) sum(y * eta - log1p(exp(eta))) + sum(dnorm(beta, 0, 10, log = TRUE)) } fit <- imh_laplace(log_post, mode = lap$mode[1:2], hessian = lap$H_beta, n_iter = 1000) fit$mean_accept colMeans(fit$draws)
Displays information about the inference modes available in tulpa, including their tiers, guarantees, and appropriate use cases.
inference_mode_info()inference_mode_info()
NULL, invisibly. Called for the side effect of printing the
available inference modes, their tiers, guarantees, and use cases to the
console.
inference_mode_info()inference_mode_info()
Is an outer-grid setting marked as a default?
is_auto_grid(x)is_auto_grid(x)
x |
Any object. |
TRUE when x carries the auto_grid() marker. This is the
PROVENANCE question – whose choice the nodes are – and is TRUE whether
or not the mark also asked for them to be integrated as written; that is
auto_grid_place().
auto_grid(), auto_grid_place()
is_auto_grid(auto_grid(c(0.5, 1, 2))) is_auto_grid(c(0.5, 1, 2))is_auto_grid(auto_grid(c(0.5, 1, 2))) is_auto_grid(c(0.5, 1, 2))
Wraps a user-defined latent block (currently a tgmrf() object) so the
formula parser can recognise and route it to the inference layer. The
call is structural – it is never executed at fit time. The parser
evaluates the inner expression in the formula's environment, removes
the latent(...) term from the fixed-effects formula, and attaches the
resulting object to parsed$latent_blocks.
latent(block)latent(block)
block |
A |
Returns block invisibly. Outside a formula context the call is
a no-op pass-through.
n <- 8 blk <- tgmrf( Q = function(theta) Matrix::Diagonal(n, exp(-2 * theta[1])), prior = function(theta) stats::dnorm(theta[1], 0, 1, log = TRUE), init = c(log_sigma = 0)) parsed <- tulpa_parse_formula(y ~ x + latent(blk)) parsed$n_latent_blocksn <- 8 blk <- tgmrf( Q = function(theta) Matrix::Diagonal(n, exp(-2 * theta[1])), prior = function(theta) stats::dnorm(theta[1], 0, 1, log = TRUE), init = c(log_sigma = 0)) parsed <- tulpa_parse_formula(y ~ x + latent(blk)) parsed$n_latent_blocks
Define latent factors for capturing unmeasured shared structure between
all processes. Factors are observation-level random effects
that enter both linear predictors when shared = TRUE (default).
latent_factor( n_factors = 1L, prior = NULL, shared = TRUE, constraint = c("sum_to_zero", "first_zero"), scale = TRUE ) ## S3 method for class 'tulpa_latent' print(x, ...)latent_factor( n_factors = 1L, prior = NULL, shared = TRUE, constraint = c("sum_to_zero", "first_zero"), scale = TRUE ) ## S3 method for class 'tulpa_latent' print(x, ...)
n_factors |
Integer; number of latent factors. Default is 1. More factors capture more complex unmeasured structure but increase computational cost and risk overfitting. |
prior |
Prior for factor standard deviations. Default is a PC prior with P(sigma > 1) = 0.01, which shrinks toward simpler models. |
shared |
Logical; if TRUE (default), latent factors enter both all process linear predictors identically. If FALSE, factors only affect the first process. |
constraint |
Identifiability constraint for factors:
|
scale |
Logical; if TRUE (default), factor loadings are standardized to have unit variance before applying sigma. |
x |
A |
... |
Ignored. |
When modeling multiple processes, they often share unmeasured confounders. For example, in relative abundance data, both the focal species count and total count might be affected by:
Observer skill (unmeasured)
Local microhabitat conditions (unmeasured)
Weather on sampling day (unmeasured)
Without accounting for these shared drivers, estimates can be biased. Latent factors capture this shared structure without requiring the confounders to be measured.
For observation i with K latent factors, on each of two model arms (processes 1 and 2):
where:
are standardized factor scores
are factor standard deviations with PC prior
Identifiability: for each k
Because factors enter both linear predictors identically (when shared), they cancel in derived quantities (e.g., ratios, differences):
This means factors capture shared effects that would otherwise bias derived quantities.
Start with n_factors = 1 for simple unmeasured confounding
Use n_factors = 2-3 if you suspect multiple independent confounders
More than 3 factors is rarely needed and risks overfitting
The PC prior provides regularization, shrinking unneeded factors toward zero
Latent factors differ from random effects in several ways:
Random effects are grouped (e.g., site-level), factors are observation-level
Random effects require grouping structure, factors don't
Factors capture residual correlation not explained by observed predictors
You can use both together: random effects for known grouping, factors for residual unmeasured confounding.
A tulpa_latent object consumed by ratio / multi-arm model
packages built on tulpa (e.g. tulpaRatio); not read by the single-response
tulpa() front door (use latent(tgmrf(...)) for a single-response
latent Gaussian block).
prior_pc() for prior specification; latent() / tgmrf() for a
single-response in-formula latent Gaussian block
# Basic latent factor (single shared factor) latent_factor() # Two latent factors latent_factor(n_factors = 2) # Custom prior (more regularization) latent_factor(n_factors = 1, prior = prior_pc(U = 0.5, alpha = 0.01)) # Numerator-only factor (not shared) latent_factor(n_factors = 1, shared = FALSE) # The spec is consumed by a ratio / multi-arm model package (tulpaRatio owns # the two-arm `species | total ~ ...` formula and the negbin/negbin ratio # family); it is that package's fitter, not the single-response tulpa() front # door, that reads the `latent_factor()` object.# Basic latent factor (single shared factor) latent_factor() # Two latent factors latent_factor(n_factors = 2) # Custom prior (more regularization) latent_factor(n_factors = 1, prior = prior_pc(U = 0.5, alpha = 0.01)) # Numerator-only factor (not shared) latent_factor(n_factors = 1, shared = FALSE) # The spec is consumed by a ratio / multi-arm model package (tulpaRatio owns # the two-arm `species | total ~ ...` formula and the negbin/negbin ratio # family); it is that package's fitter, not the single-response tulpa() front # door, that reads the `latent_factor()` object.
Extract latent factor posteriors from fit
latent_factors(fit, ...) ## Default S3 method: latent_factors(fit, summary = TRUE, probs = c(0.025, 0.5, 0.975), ...)latent_factors(fit, ...) ## Default S3 method: latent_factors(fit, summary = TRUE, probs = c(0.025, 0.5, 0.975), ...)
fit |
A fit sampled with latent factors: its draw matrix ( |
... |
Passed to methods. |
summary |
Logical; if TRUE, return summary statistics. If FALSE, return full posterior draws. |
probs |
Quantiles for summary. Default is c(0.025, 0.5, 0.975). |
If summary = TRUE, a data frame with columns for observation,
factor, and summary statistics. If summary = FALSE, a matrix of
posterior draws.
# A fit sampled with latent factors comes from a model package (tulpaRatio # reads the latent_factor() spec). The accessor reads two things off it: # draw columns named factor[observation,factor] and the latent layout. Here # both are simulated for three observations on two factors. set.seed(1) cols <- sprintf("factor[%d,%d]", rep(1:3, 2), rep(1:2, each = 3)) draws <- matrix(rnorm(200 * 6, mean = rep(c(-1, 0, 1), 2), sd = 0.3), 200, 6, byrow = TRUE, dimnames = list(NULL, cols)) fit <- structure( list(draws = draws, .internal = list(latent_info = list(type = "latent", n_factors = 2L, n_obs = 3L))), class = "tulpa_fit") latent_factors(fit) dim(latent_factors(fit, summary = FALSE))# A fit sampled with latent factors comes from a model package (tulpaRatio # reads the latent_factor() spec). The accessor reads two things off it: # draw columns named factor[observation,factor] and the latent layout. Here # both are simulated for three observations on two factors. set.seed(1) cols <- sprintf("factor[%d,%d]", rep(1:3, 2), rep(1:2, each = 3)) draws <- matrix(rnorm(200 * 6, mean = rep(c(-1, 0, 1), 2), sd = 0.3), 200, 6, byrow = TRUE, dimnames = list(NULL, cols)) fit <- structure( list(draws = draws, .internal = list(latent_info = list(type = "latent", n_factors = 2L, n_obs = 3L))), class = "tulpa_fit") latent_factors(fit) dim(latent_factors(fit, summary = FALSE))
What this returns depends on what the fit computed, and the returned object
names it in a quantity attribute:
## S3 method for class 'tulpa_fit' logLik(object, ...)## S3 method for class 'tulpa_fit' logLik(object, ...)
object |
A |
... |
Ignored. |
"log_posterior_mean"a sampler fit: the mean over the draws of
the log joint posterior density, prior included, in that backend's own
parameterization: the unconstrained coordinates with their Jacobians
for the ModelData samplers (hmc, ess, sghmc, sgld, mclmc,
smc, vi), and the natural scale each quantity is sampled on for the
RE-covariance and Polya-Gamma Gibbs samplers. Values are therefore
comparable across fits of the same backend only.
"log_evidence"a deterministic fit that estimated no
hyperparameter from the data: the log marginal probability of the data
under the model as specified. A hyperparameter the fit integrated counts,
and so does one the caller supplied (a phi, a sigma_re, an outer grid
laid at one value), which is part of the model rather than an estimate. A
Laplace fit of a model with nothing to integrate reports its log marginal
likelihood here, which already is that quantity.
"log_marginal_likelihood"a deterministic fit that estimated some
hyperparameters by maximising over the same data (empirical Bayes, or
estimate_phi = TRUE): the log marginal likelihood at those estimates.
The conditioned_on attribute names them.
"log_likelihood"a fit that maximised over every parameter it
reports, with the random effects integrated out (agq_fit()): the
maximised log-likelihood, with df the number of maximised parameters.
On a nested-Laplace fit the value is the log evidence of its outer grid,
log sum_k exp(log_marginal_k + log_cell_k), where log_marginal already
carries each axis's hyperprior density on its integration coordinate and
log_cell_k is the cell's absolute volume there. Every default outer axis
carries a proper prior (a PC prior on each scale and range, a uniform on each
bounded axis), so the value is the evidence under that prior and does not move
with where the nodes were laid or how many there are, provided the grid
covers the posterior. A fit carrying an axis with no proper prior (a flat
random-effect covariance prior, a dispersion whose family has no PC prior yet,
an axis on a block that does not carry its coordinates) reports NA with
declined = "improper_hyperprior" and names the axes in a declined_axes
attribute. A central-composite (CCD) design reproduces moments and carries no
cell volume, so a fit integrated on one reports NA with a declined
attribute.
A sampler fit whose producer kept no per-draw log posterior reports NA
with quantity = "log_posterior_mean" and
declined = "no_log_posterior_recorded". The Polya-Gamma Gibbs routes that
recentre a proper field every sweep (the NNGP and multiscale-GP fields, and
the negative-binomial kernel's iid random-effect block) leave no written
density invariant and record none; a fit carrying neither draws
nor a log marginal declines with "no_goodness_quantity_recorded". A value
of NA always carries a declined attribute.
Values with a different quantity or conditioned_on are not on one scale,
and compare_models(criterion = "loglik") refuses such a set. Only a
"log_likelihood" is what AIC and BIC penalise, so AIC.tulpa_fit() and
BIC.tulpa_fit() refuse every other quantity; compare those fits by their
evidence, or by compare_models(criterion = "waic") / "loo", which score
the pointwise predictive density.
A logLik object with quantity and conditioned_on attributes,
and a declined attribute naming the reason when no value could be read.
Sample from a posterior using MALA: a Langevin proposal driven by the gradient of the log posterior, corrected by a Metropolis- Hastings accept/reject step. Each iteration is one log-posterior + one gradient evaluation. The natural stepping stone between random-walk MH and HMC: cheaper per iteration than HMC (no leapfrog integration), better-mixing than RWMH because the drift term moves toward higher density.
Step size epsilon is adapted via dual averaging during warmup to
target an acceptance rate of 0.574 (Roberts & Rosenthal 1998 optimal
for high-dimensional Gaussians).
mala( log_posterior, grad_log_posterior, init, n_iter = 2000L, warmup = n_iter%/%2L, epsilon = 0.1, target_accept = 0.574, mass_diag = NULL, mass_matrix = NULL, thin = 1L, seed = NULL, verbose = FALSE )mala( log_posterior, grad_log_posterior, init, n_iter = 2000L, warmup = n_iter%/%2L, epsilon = 0.1, target_accept = 0.574, mass_diag = NULL, mass_matrix = NULL, thin = 1L, seed = NULL, verbose = FALSE )
log_posterior |
Function |
grad_log_posterior |
Function |
init |
Numeric vector: initial state (must give finite log_posterior). |
n_iter |
Total iterations including warmup (default 2000). |
warmup |
Warmup iterations (step-size adaptation + discarded;
default |
epsilon |
Initial step size (default 0.1). Adapted during warmup. |
target_accept |
Target acceptance during warmup adaptation (default 0.574, the Roberts & Rosenthal 1998 optimum). |
mass_diag |
Optional preconditioner: a length- |
mass_matrix |
Optional dense preconditioner: a |
thin |
Keep every |
seed |
Optional integer RNG seed. Scoped to this call: the caller's
|
verbose |
Print acceptance + step-size summary at end (default FALSE). |
A list with class tulpa_fit carrying:
draws: post-warmup draws.
means, n_params, n_samples, log_prob.
accept_prob: per-iteration accepted indicators (post-warmup).
mean_accept: post-warmup acceptance rate.
epsilon: final adapted step size.
inference_mode: "exact".
inference_tier: 1L.
backend: "mala".
Tier 1 (Exact). The MH step makes the chain asymptotically correct. Mixing depends on whether the gradient gives useful local geometry – poor for posteriors with very different scales across dimensions (use a preconditioner or HMC instead).
Roberts, G. O., & Tweedie, R. L. (1996). Exponential convergence of Langevin distributions and their discrete approximations. Bernoulli, 2(4), 341-363.
Roberts, G. O., & Rosenthal, J. S. (1998). Optimal scaling of discrete approximations to Langevin diffusions. JRSS B, 60(1), 255-268.
log_post <- function(t) -0.5 * sum((t - c(1, 2))^2) grad <- function(t) -(t - c(1, 2)) fit <- mala(log_post, grad, init = c(0, 0), n_iter = 1000) colMeans(fit$draws) # near c(1, 2) fit$mean_accept # should adapt toward 0.574log_post <- function(t) -0.5 * sum((t - c(1, 2))^2) grad <- function(t) -(t - c(1, 2)) fit <- mala(log_post, grad, init = c(0, 0), n_iter = 1000) colMeans(fit$draws) # near c(1, 2) fit$mean_accept # should adapt toward 0.574
Returns a fit's posterior draws only when they form a genuine MCMC chain
($draws_kind == "chain", or an untagged legacy fit); for an i.i.d. /
approximation fit (nested Laplace, VI, SMC, ...) it returns NULL, because
chain diagnostics do not apply. This is the accessor diagnostics()
gates on. For the provenance-agnostic posterior sample used by summaries,
see posterior_sample().
mcmc_draws(fit)mcmc_draws(fit)
fit |
A |
The chain draws matrix/array, or NULL for a non-chain fit.
posterior_sample(), diagnostics()
set.seed(1) d <- data.frame(x = rnorm(60)) d$y <- rpois(60, exp(0.5 + 0.3 * d$x)) fit <- tulpa(y ~ x, data = d, family = "poisson", mode = "hmc", control = list(n_iter = 400L, warmup = 200L, n_chains = 2L, seed = 1L)) dim(mcmc_draws(fit))set.seed(1) d <- data.frame(x = rnorm(60)) d$y <- rpois(60, exp(0.5 + 0.3 * d$x)) fit <- tulpa(y ~ x, data = d, family = "poisson", mode = "hmc", control = list(n_iter = 400L, warmup = 200L, n_chains = 2L, seed = 1L)) dim(mcmc_draws(fit))
Combine fitted values from several models using native model weights computed
from the pointwise PSIS-LOO (or WAIC) elpd via tulpa_psis() – no loo
package dependency. "loo" / "waic" give stacking weights (the
simplex-optimal predictive combination); "pbma" / "pbma+" give
pseudo-BMA(+) weights. Every model must carry an [n_draws x n_obs] pointwise
log-likelihood (fit$draws$log_lik) over the same observations.
model_average( ..., weights = c("loo", "waic", "pbma", "pbma+"), fitted_fn = fitted )model_average( ..., weights = c("loo", "waic", "pbma", "pbma+"), fitted_fn = fitted )
... |
Named |
weights |
|
fitted_fn |
Function extracting a length- |
A list with averaged (the weighted fitted vector), weights (the
named model weights), and comparison (the compare_models() table).
Yao, Vehtari, Simpson & Gelman (2018). Using stacking to average Bayesian predictive distributions. Bayesian Analysis 13(3):917-1007.
set.seed(1) df <- data.frame(x = rnorm(120)) df$y <- rpois(120, exp(0.4 + 0.5 * df$x)) f1 <- tulpa(y ~ x, data = df, family = "poisson", mode = "hmc", control = list(n_iter = 500L, warmup = 250L, seed = 1L)) f2 <- tulpa(y ~ 1, data = df, family = "poisson", mode = "hmc", control = list(n_iter = 500L, warmup = 250L, seed = 1L)) ma <- model_average(full = f1, null = f2, weights = "waic") ma$weightsset.seed(1) df <- data.frame(x = rnorm(120)) df$y <- rpois(120, exp(0.4 + 0.5 * df$x)) f1 <- tulpa(y ~ x, data = df, family = "poisson", mode = "hmc", control = list(n_iter = 500L, warmup = 250L, seed = 1L)) f2 <- tulpa(y ~ 1, data = df, family = "poisson", mode = "hmc", control = list(n_iter = 500L, warmup = 250L, seed = 1L)) ma <- model_average(full = f1, null = f2, weights = "waic") ma$weights
Tests whether residuals exhibit spatial structure after model fitting. Supports inverse-distance and k-nearest-neighbour weight matrices. No external dependencies (uses normal approximation for inference).
moran_i( object, coords, weights = c("inverse", "knn"), k = 10L, resid_type = "pearson", alternative = c("two.sided", "greater", "less") )moran_i( object, coords, weights = c("inverse", "knn"), k = 10L, resid_type = "pearson", alternative = c("two.sided", "greater", "less") )
object |
A fitted model, or a numeric vector of residuals |
coords |
N x 2 coordinate matrix (required) |
weights |
Weight scheme: |
k |
Number of neighbours for knn (default 10) |
resid_type |
Residual type if extracting from model (default |
alternative |
|
An htest object with Moran's I, expected I, and p-value
set.seed(1) coords <- cbind(runif(50), runif(50)) resid <- rnorm(50) moran_i(resid, coords)set.seed(1) coords <- cbind(runif(50), runif(50)) resid <- rnorm(50) moran_i(resid, coords)
Counts divergent transitions recorded by an HMC/NUTS fit, reading whichever
field the backend populated (the top-level $divergent flag vector every
sampler in this package writes, or $diagnostics$divergent,
$diagnostics$divergent_idx, $diagnostics$n_divergent, $n_divergent).
It reads the same record plot_divergences() locates the divergent rows
from, so the two always agree on one fit.
n_divergent(fit)n_divergent(fit)
fit |
A |
Integer count of divergent transitions (0 if none are recorded).
plot_divergences(), check_diagnostics()
set.seed(1) d <- data.frame(x = rnorm(60)) d$y <- rpois(60, exp(0.5 + 0.3 * d$x)) fit <- tulpa(y ~ x, data = d, family = "poisson", mode = "hmc", control = list(n_iter = 400L, warmup = 200L, n_chains = 2L, seed = 1L)) n_divergent(fit)set.seed(1) d <- data.frame(x = rnorm(60)) d$y <- rpois(60, exp(0.5 + 0.3 * d$x)) fit <- tulpa(y ~ x, data = d, family = "poisson", mode = "hmc", control = list(n_iter = 400L, warmup = 200L, n_chains = 2L, seed = 1L)) n_divergent(fit)
Number of observations in a tulpa fit
## S3 method for class 'tulpa_fit' nobs(object, ...)## S3 method for class 'tulpa_fit' nobs(object, ...)
object |
A |
... |
Ignored. |
Integer observation count.
Translate a vector of original cell identifiers into the 1-based node indices
of a tulpa_adjacency graph, by key (not by row order). Use it to add the
node-index column the model's spatial grouping bar needs to the observation
data, which typically has many rows per cell and a different row order than
the graph.
node_index(graph, ids)node_index(graph, ids)
graph |
A |
ids |
A vector of cell identifiers to look up (matched against
|
An integer vector the same length as ids, giving each one's node
index in graph (NA for identifiers absent from the graph).
grid <- expand.grid(x = 1:3, y = 1:3) grid$cell <- paste0("c", seq_len(nrow(grid))) g <- adjacency(grid, id = "cell") obs <- data.frame(cell = c("c5", "c1", "c9")) obs$cell_idx <- node_index(g, obs$cell) obsgrid <- expand.grid(x = 1:3, y = 1:3) grid$cell <- paste0("c", seq_len(nrow(grid))) g <- adjacency(grid, id = "cell") obs <- data.frame(cell = c("c5", "c1", "c9")) obs$cell_idx <- node_index(g, obs$cell) obs
Single-path Pathfinder (Zhang, Carpenter, Gelman, Vehtari 2022):
run L-BFGS toward the posterior mode, fit a Gaussian at the optimum
using the inverse-Hessian estimate, and report draws plus the ELBO.
Cheap, derivative-only, embarrassingly parallel – meant as an HMC
warm-start, an initialiser for imh_laplace(), or a quick
sanity check on the Laplace approximation.
This implementation is single-path only. The full multi-path Pathfinder (K parallel L-BFGS runs + mixture proposal + Pareto- smoothed importance reweighting) is a follow-on. The single-path version is what most users want as a Laplace-equivalent diagnostic.
pathfinder( log_posterior, init, grad_log_posterior = NULL, n_draws = 1000L, max_iter = 100L, tol = 1e-06, seed = NULL, verbose = FALSE )pathfinder( log_posterior, init, grad_log_posterior = NULL, n_draws = 1000L, max_iter = 100L, tol = 1e-06, seed = NULL, verbose = FALSE )
log_posterior |
Function |
init |
Numeric vector: initial point for L-BFGS. Should be in the support of the posterior (finite log_posterior). |
grad_log_posterior |
Optional function returning the gradient
of |
n_draws |
Number of draws from the fitted Gaussian (default 1000). |
max_iter |
L-BFGS iteration cap (default 100). |
tol |
Gradient-norm tolerance for L-BFGS convergence (default 1e-6). |
seed |
Optional integer RNG seed. Scoped to this call: the caller's
|
verbose |
Print L-BFGS / ELBO summary at end (default FALSE). |
A list with class tulpa_fit carrying:
draws: n_draws x d matrix of Gaussian draws at the mode.
means: posterior means (= mode for a Gaussian fit).
mode: the L-BFGS mode.
cov: the proposal covariance (solve(-hessian)).
elbo: Monte-Carlo estimate of E_q[log p - log q].
n_iter: L-BFGS iterations.
converged: logical.
inference_mode: "structured".
inference_tier: 2L.
backend: "pathfinder".
Tier 2 (Structured). The output is a Gaussian approximation, not
samples from the exact posterior – same epistemic class as
tulpa_laplace(). Pair with imh_laplace() for an exact-tier
upgrade.
Zhang, L., Carpenter, B., Gelman, A., & Vehtari, A. (2022). Pathfinder: parallel quasi-Newton variational inference. Journal of Machine Learning Research, 23(306), 1-49.
imh_laplace() for an exact-tier MH using the Pathfinder
Gaussian as proposal; bridge_sampling() for marginal-likelihood
estimation on the resulting draws.
# Toy: 2-D conjugate normal. y <- c(0.5, -0.7) log_post <- function(t) { sum(dnorm(y, t, 1, log = TRUE)) + sum(dnorm(t, 0, sqrt(10), log = TRUE)) } pf <- pathfinder(log_post, init = c(0, 0), n_draws = 2000) pf$mode # near c(0.45, -0.64) pf$elbo# Toy: 2-D conjugate normal. y <- c(0.5, -0.7) log_post <- function(t) { sum(dnorm(y, t, 1, log = TRUE)) + sum(dnorm(t, 0, sqrt(10), log = TRUE)) } pf <- pathfinder(log_post, init = c(0, 0), n_draws = 2000) pf$mode # near c(0.45, -0.64) pf$elbo
For each observation, computes the quantile of the observed value within the posterior predictive distribution. If the model is correct, PIT residuals follow Uniform(0, 1).
pit_residuals(object, ...) ## Default S3 method: pit_residuals(object, observed = NULL, nsim = 250L, seed = 123L, ...)pit_residuals(object, ...) ## Default S3 method: pit_residuals(object, observed = NULL, nsim = 250L, seed = 123L, ...)
object |
A fitted model with a |
... |
Passed to methods. |
observed |
Observed response vector (required if |
nsim |
Number of simulations (default 250) |
seed |
Random seed (default 123) |
For integer-valued responses, a randomisation step avoids discrete artefacts: the residual is drawn uniformly between P(sim < obs) and P(sim <= obs).
Numeric vector of length n_obs with values in [0, 1]
# From a matrix of replicates (n_obs x nsim) and the observed response: set.seed(1) y <- rpois(30, 3) sims <- matrix(rpois(30 * 100, 3), nrow = 30) r <- pit_residuals(sims, observed = y) summary(r) # From a fitted model with a simulate() method: set.seed(1) d <- data.frame(x = rnorm(80)) d$y <- rpois(80, exp(0.3 + 0.5 * d$x)) fit <- tulpa(y ~ x, data = d, family = "poisson", mode = "laplace") head(pit_residuals(fit, nsim = 50))# From a matrix of replicates (n_obs x nsim) and the observed response: set.seed(1) y <- rpois(30, 3) sims <- matrix(rpois(30 * 100, 3), nrow = 30) r <- pit_residuals(sims, observed = y) summary(r) # From a fitted model with a simulate() method: set.seed(1) d <- data.frame(x = rnorm(80)) d$y <- rpois(80, exp(0.3 + 0.5 * d$x)) fit <- tulpa(y ~ x, data = d, family = "poisson", mode = "laplace") head(pit_residuals(fit, nsim = 50))
Creates autocorrelation function (ACF) plots for selected parameters. High autocorrelation indicates slow mixing and low effective sample size.
plot_acf(fit, pars = NULL, lags = 25, n_pars = 6)plot_acf(fit, pars = NULL, lags = 25, n_pars = 6)
fit |
A |
pars |
Character vector of parameter names. If NULL, selects worst-mixing parameters based on ESS. |
lags |
Maximum number of lags to compute (default: 25). |
n_pars |
Maximum number of parameters to plot (default: 6). |
Ideal ACF plots show rapid decay to zero. Slow decay indicates high autocorrelation, which reduces effective sample size and may indicate poor mixing.
A ggplot object (if ggplot2 available) or base R plot (invisible).
set.seed(123) df <- data.frame(x = rnorm(60)) df$y <- rpois(60, exp(0.5 + 0.3 * df$x)) fit <- tulpa(y ~ x, data = df, family = "poisson", mode = "hmc", control = list(n_iter = 500L, warmup = 250L, n_chains = 2L, seed = 1L)) plot_acf(fit) plot_acf(fit, lags = 10)set.seed(123) df <- data.frame(x = rnorm(60)) df$y <- rpois(60, exp(0.5 + 0.3 * df$x)) fit <- tulpa(y ~ x, data = df, family = "poisson", mode = "hmc", control = list(n_iter = 500L, warmup = 250L, n_chains = 2L, seed = 1L)) plot_acf(fit) plot_acf(fit, lags = 10)
Creates visualizations to investigate divergent transitions. Parallel coordinates and scatter plots highlight where in parameter space divergences occur.
plot_divergences(fit, pars = NULL, type = c("parcoord", "scatter"))plot_divergences(fit, pars = NULL, type = c("parcoord", "scatter"))
fit |
A |
pars |
Character vector of parameter names. If NULL, uses variance parameters. |
type |
Plot type: "parcoord" (parallel coordinates) or "scatter". |
Divergent transitions indicate regions of high posterior curvature that the sampler cannot efficiently explore. Common causes:
Very narrow funnels (hierarchical models)
Strong correlations
Multi-modality
A ggplot object or base R plot (invisible).
set.seed(123) df <- data.frame(x = rnorm(60)) df$y <- rpois(60, exp(0.5 + 0.3 * df$x)) fit <- tulpa(y ~ x, data = df, family = "poisson", mode = "hmc", control = list(n_iter = 500L, warmup = 250L, n_chains = 2L, seed = 1L)) plot_divergences(fit) plot_divergences(fit, type = "scatter")set.seed(123) df <- data.frame(x = rnorm(60)) df$y <- rpois(60, exp(0.5 + 0.3 * df$x)) fit <- tulpa(y ~ x, data = df, family = "poisson", mode = "hmc", control = list(n_iter = 500L, warmup = 250L, n_chains = 2L, seed = 1L)) plot_divergences(fit) plot_divergences(fit, type = "scatter")
Creates overlaid histograms of marginal energy and energy transition, along with the E-BFMI statistic. Low E-BFMI indicates poor exploration of the posterior.
plot_energy(fit)plot_energy(fit)
fit |
A |
E-BFMI (Energy Bayesian Fraction of Missing Information) compares the distribution of energy levels to energy transitions. Values below 0.3 indicate the sampler may not be exploring the full posterior.
A ggplot object (if ggplot2 available) or base R plot.
diagnostic_summary(), check_diagnostics()
set.seed(123) df <- data.frame(x = rnorm(60)) df$y <- rpois(60, exp(0.5 + 0.3 * df$x)) fit <- tulpa(y ~ x, data = df, family = "poisson", mode = "hmc", control = list(n_iter = 500L, warmup = 250L, n_chains = 2L, seed = 1L)) plot_energy(fit)set.seed(123) df <- data.frame(x = rnorm(60)) df$y <- rpois(60, exp(0.5 + 0.3 * df$x)) fit <- tulpa(y ~ x, data = df, family = "poisson", mode = "hmc", control = list(n_iter = 500L, warmup = 250L, n_chains = 2L, seed = 1L)) plot_energy(fit)
Creates a visual display of effective sample size (ESS) for all parameters, expressed as a ratio of ESS to total samples. Low ESS indicates high autocorrelation.
plot_ess(fit, type = c("bulk", "tail"), threshold = 400, pars = NULL)plot_ess(fit, type = c("bulk", "tail"), threshold = 400, pars = NULL)
fit |
A |
type |
Type of ESS: "bulk" (default) or "tail". |
threshold |
Minimum acceptable ESS (default: 400). |
pars |
Character vector of parameter names to include. |
ESS/iter ratio interpretation:
Green: ESS >= threshold
Yellow: threshold/2 <= ESS < threshold
Red: ESS < threshold/2
A ggplot object (if ggplot2 available) or base R plot (invisible).
plot_rhat(), diagnostic_summary(), diagnostics()
set.seed(123) df <- data.frame(x = rnorm(60)) df$y <- rpois(60, exp(0.5 + 0.3 * df$x)) fit <- tulpa(y ~ x, data = df, family = "poisson", mode = "hmc", control = list(n_iter = 500L, warmup = 250L, n_chains = 2L, seed = 1L)) plot_ess(fit) plot_ess(fit, type = "tail")set.seed(123) df <- data.frame(x = rnorm(60)) df$y <- rpois(60, exp(0.5 + 0.3 * df$x)) fit <- tulpa(y ~ x, data = df, family = "poisson", mode = "hmc", control = list(n_iter = 500L, warmup = 250L, n_chains = 2L, seed = 1L)) plot_ess(fit) plot_ess(fit, type = "tail")
Create publication-ready maps from a tulpa spatial fit: the fitted response surface or the prediction uncertainty (credible-interval width), on the response scale.
plot_map( x, coords = NULL, what = c("fitted", "uncertainty"), summary = c("median", "mean", "q2.5", "q97.5", "sd"), newdata = NULL, title = NULL, palette = "viridis", points = FALSE, point_color = "grey30", point_size = 0.5, na_color = "transparent", crs = NULL, legend_title = NULL, ... )plot_map( x, coords = NULL, what = c("fitted", "uncertainty"), summary = c("median", "mean", "q2.5", "q97.5", "sd"), newdata = NULL, title = NULL, palette = "viridis", points = FALSE, point_color = "grey30", point_size = 0.5, na_color = "transparent", crs = NULL, legend_title = NULL, ... )
x |
A |
coords |
A data frame or matrix with spatial coordinates (columns
named 'x'/'y', 'X'/'Y', 'lon'/'lat', 'longitude'/'latitude', or
'Easting'/'Northing'). Required if |
what |
What to plot: "fitted" (default, the response-scale point prediction) or "uncertainty" (the 95% credible-interval width). |
summary |
Which summary statistic for |
newdata |
Optional data frame with prediction locations and covariates.
If NULL and |
title |
Plot title. If NULL, auto-generated based on |
palette |
Color palette: "viridis" (default), "magma", "plasma", "inferno", "cividis", "mako", "rocket", or a custom vector of colors. |
points |
Logical; if TRUE, overlay observation points. Default FALSE. |
point_color |
Color for observation points. Default "grey30". |
point_size |
Size for observation points. Default 0.5. |
na_color |
Color for NA values. Default "transparent". |
crs |
Coordinate reference system (proj4 string or EPSG code). If NULL, uses planar coordinates. |
legend_title |
Title for the color legend. If NULL, auto-generated. |
... |
Additional arguments passed to ggplot2 theme functions. |
This function provides a streamlined workflow for visualizing spatial predictions from tulpa models. It handles:
Extracting predictions from tulpa_fit objects
Converting to appropriate spatial format (stars/sf)
Creating publication-quality maps with sensible defaults
Uncertainty visualization via credible interval width
For custom maps or more control, extract predictions using predict() and
use ggplot2 directly with geom_stars() or geom_sf().
A ggplot2 object that can be further customized.
This function requires ggplot2. For raster-style maps, stars and sf
are also needed. Install with:
install.packages(c("ggplot2", "stars", "sf"))
predict.tulpa_fit() for predictions at new locations
if (requireNamespace("ggplot2", quietly = TRUE)) { set.seed(123) n_sites <- 20 df <- data.frame( y = rbinom(n_sites, 20, 0.4), elevation = rnorm(n_sites), site = factor(seq_len(n_sites)), lon = runif(n_sites), lat = runif(n_sites) ) adj <- matrix(0, n_sites, n_sites) for (i in 1:(n_sites - 1)) adj[i, i + 1] <- adj[i + 1, i] <- 1 fit <- tulpa( y ~ elevation + spatial(site), data = df, family = "binomial", n_trials = rep(20L, n_sites), spatial = spatial_car(adj, group_var = "site"), mode = "laplace" ) # Areal (CAR/ICAR) fits carry no point coordinates, so pass `coords`: cc <- df[, c("lon", "lat")] plot_map(fit, coords = cc) # fitted response surface plot_map(fit, what = "uncertainty", coords = cc) plot_map_panel(fit, coords = cc) # both side by side }if (requireNamespace("ggplot2", quietly = TRUE)) { set.seed(123) n_sites <- 20 df <- data.frame( y = rbinom(n_sites, 20, 0.4), elevation = rnorm(n_sites), site = factor(seq_len(n_sites)), lon = runif(n_sites), lat = runif(n_sites) ) adj <- matrix(0, n_sites, n_sites) for (i in 1:(n_sites - 1)) adj[i, i + 1] <- adj[i + 1, i] <- 1 fit <- tulpa( y ~ elevation + spatial(site), data = df, family = "binomial", n_trials = rep(20L, n_sites), spatial = spatial_car(adj, group_var = "site"), mode = "laplace" ) # Areal (CAR/ICAR) fits carry no point coordinates, so pass `coords`: cc <- df[, c("lon", "lat")] plot_map(fit, coords = cc) # fitted response surface plot_map(fit, what = "uncertainty", coords = cc) plot_map_panel(fit, coords = cc) # both side by side }
Create a multi-panel figure with the fitted-value map and the prediction-uncertainty map side by side.
plot_map_panel(x, newdata = NULL, ncol = 2, ...)plot_map_panel(x, newdata = NULL, ncol = 2, ...)
x |
A |
newdata |
Optional data frame with prediction locations. |
ncol |
Number of columns in the grid. Default 2. |
... |
Additional arguments passed to |
A patchwork object (if patchwork is installed) or a list of ggplots.
if (requireNamespace("ggplot2", quietly = TRUE)) { set.seed(123) n_sites <- 20 df <- data.frame( y = rbinom(n_sites, 20, 0.4), elevation = rnorm(n_sites), site = factor(seq_len(n_sites)), lon = runif(n_sites), lat = runif(n_sites) ) adj <- matrix(0, n_sites, n_sites) for (i in 1:(n_sites - 1)) adj[i, i + 1] <- adj[i + 1, i] <- 1 fit <- tulpa( y ~ elevation + spatial(site), data = df, family = "binomial", n_trials = rep(20L, n_sites), spatial = spatial_car(adj, group_var = "site"), mode = "laplace" ) cc <- df[, c("lon", "lat")] plot_map_panel(fit, coords = cc) plot_map_panel(fit, coords = cc, ncol = 1) }if (requireNamespace("ggplot2", quietly = TRUE)) { set.seed(123) n_sites <- 20 df <- data.frame( y = rbinom(n_sites, 20, 0.4), elevation = rnorm(n_sites), site = factor(seq_len(n_sites)), lon = runif(n_sites), lat = runif(n_sites) ) adj <- matrix(0, n_sites, n_sites) for (i in 1:(n_sites - 1)) adj[i, i + 1] <- adj[i + 1, i] <- 1 fit <- tulpa( y ~ elevation + spatial(site), data = df, family = "binomial", n_trials = rep(20L, n_sites), spatial = spatial_car(adj, group_var = "site"), mode = "laplace" ) cc <- df[, c("lon", "lat")] plot_map_panel(fit, coords = cc) plot_map_panel(fit, coords = cc, ncol = 1) }
Creates a pairs plot showing bivariate relationships between parameters. Divergent transitions (if present) are highlighted to help identify problematic posterior regions.
plot_pairs( fit, pars = NULL, highlight_divergent = TRUE, n_pars = 5, alpha = 0.3 )plot_pairs( fit, pars = NULL, highlight_divergent = TRUE, n_pars = 5, alpha = 0.3 )
fit |
A |
pars |
Character vector of parameter names. If NULL, selects main variance parameters. |
highlight_divergent |
Logical; highlight divergent transitions in red (default: TRUE). |
n_pars |
Maximum number of parameters (default: 5). |
alpha |
Point transparency (default: 0.3). |
Pairs plots help identify:
Strong correlations between parameters (potential non-identifiability)
Multimodality
Regions where divergences cluster (indicating problematic geometry)
A ggplot object (if ggplot2/GGally available) or base R plot.
plot_divergences(), diagnostics()
set.seed(123) df <- data.frame(x = rnorm(60)) df$y <- rpois(60, exp(0.5 + 0.3 * df$x)) fit <- tulpa(y ~ x, data = df, family = "poisson", mode = "hmc", control = list(n_iter = 500L, warmup = 250L, n_chains = 2L, seed = 1L)) plot_pairs(fit) plot_pairs(fit, n_pars = 2)set.seed(123) df <- data.frame(x = rnorm(60)) df$y <- rpois(60, exp(0.5 + 0.3 * df$x)) fit <- tulpa(y ~ x, data = df, family = "poisson", mode = "hmc", control = list(n_iter = 500L, warmup = 250L, n_chains = 2L, seed = 1L)) plot_pairs(fit) plot_pairs(fit, n_pars = 2)
Creates a visual display of Rhat values for all parameters, with color-coding to highlight convergence issues. Rhat values > 1.01 indicate potential convergence problems.
plot_rhat(fit, threshold = 1.01, pars = NULL)plot_rhat(fit, threshold = 1.01, pars = NULL)
fit |
A |
threshold |
Rhat threshold for warnings (default: 1.01). |
pars |
Character vector of parameter names to include. If NULL (default), includes all main parameters (excludes high-dimensional RE/spatial). |
Color coding:
Green: Rhat < 1.01 (converged)
Yellow: 1.01 <= Rhat < 1.05 (borderline)
Red: Rhat >= 1.05 (not converged)
A ggplot object (if ggplot2 available) or base R plot (invisible).
plot_ess(), diagnostic_summary(), diagnostics()
# Diagnostic plots require a fitted tulpa model # See tulpa() examples for fitting models set.seed(123) df <- data.frame(x = rnorm(60)) df$y <- rpois(60, exp(0.5 + 0.3 * df$x)) fit <- tulpa(y ~ x, data = df, family = "poisson", mode = "hmc", control = list(n_iter = 500L, warmup = 250L, n_chains = 2L, seed = 1L)) plot_rhat(fit)# Diagnostic plots require a fitted tulpa model # See tulpa() examples for fitting models set.seed(123) df <- data.frame(x = rnorm(60)) df$y <- rpois(60, exp(0.5 + 0.3 * df$x)) fit <- tulpa(y ~ x, data = df, family = "poisson", mode = "hmc", control = list(n_iter = 500L, warmup = 250L, n_chains = 2L, seed = 1L)) plot_rhat(fit)
Plot fixed-effect posteriors
## S3 method for class 'tulpa_fit' plot(x, type = c("density", "trace", "pairs", "smooth"), term = NULL, ...)## S3 method for class 'tulpa_fit' plot(x, type = c("density", "trace", "pairs", "smooth"), term = NULL, ...)
x |
A |
type |
One of |
term |
For |
... |
Passed to plotting functions. |
The input x, returned invisibly. Called for the side effect of
producing base-graphics plots of the fixed-effect posteriors.
Density overlay of prior predictive draws. Requires bayesplot; falls back to base graphics matplot of a subset of draws otherwise.
## S3 method for class 'tulpa_prior_predict' plot(x, process = 1L, max_draws = 50L, ...)## S3 method for class 'tulpa_prior_predict' plot(x, process = 1L, max_draws = 50L, ...)
x |
A tulpa_prior_predict object |
process |
Process index or name (multi-process families) |
max_draws |
Maximum draws to overlay. Default 50. |
... |
Passed through. |
A ggplot object (via bayesplot) when bayesplot is installed;
otherwise NULL invisibly, after drawing a base-graphics overlay. Called
for the density overlay of the prior predictive draws.
Plot method for spatiotemporal effects
## S3 method for class 'tulpa_st_summary' plot(x, type = "heatmap", ...)## S3 method for class 'tulpa_st_summary' plot(x, type = "heatmap", ...)
x |
Spatiotemporal effects object |
type |
Plot type: |
... |
Additional arguments passed to plotting functions |
A ggplot object when ggplot2 is installed; otherwise NULL
invisibly, after drawing a base-graphics plot. Called for the side effect
of visualizing the spatiotemporal interaction effects.
Plot method for tulpa_svc_posterior
## S3 method for class 'tulpa_svc_posterior' plot(x, term = 1, type = "mean", ...)## S3 method for class 'tulpa_svc_posterior' plot(x, term = 1, type = "mean", ...)
x |
A tulpa_svc_posterior object |
term |
Which term to plot (name or index). Default: first term. |
type |
Plot type: "mean" (default), "sd", or quantile (e.g., "q50") |
... |
Additional arguments passed to plotting functions |
A ggplot object when ggplot2 is installed; otherwise NULL
invisibly, after drawing a base-graphics map. Called for the side effect of
mapping the selected spatially-varying coefficient.
Plot method for tulpa_temporal_posterior
## S3 method for class 'tulpa_temporal_posterior' plot(x, component = NULL, type = "ribbon", ...)## S3 method for class 'tulpa_temporal_posterior' plot(x, component = NULL, type = "ribbon", ...)
x |
A tulpa_temporal_posterior object |
component |
Which component to plot (for multi-scale). Default: first. |
type |
Plot type: "ribbon" (default) or "line" |
... |
Additional arguments passed to plotting functions |
A ggplot object when ggplot2 is installed; otherwise NULL
invisibly, after drawing a base-graphics plot. Called for the side effect
of plotting the temporal-effect posterior.
Plot method for tulpa_tvc_posterior
## S3 method for class 'tulpa_tvc_posterior' plot(x, term = 1, type = "ribbon", ...)## S3 method for class 'tulpa_tvc_posterior' plot(x, term = 1, type = "ribbon", ...)
x |
A tulpa_tvc_posterior object |
term |
Which term to plot (name or index). Default: first term. |
type |
Plot type: "ribbon" (default) or "line" |
... |
Additional arguments passed to plotting functions |
A ggplot object when ggplot2 is installed; otherwise NULL
invisibly, after drawing a base-graphics plot. Called for the side effect
of plotting the selected temporally-varying coefficient.
Useful for exploring drivers of occupancy/detection/abundance variation after model fitting. Fits a weighted linear model and optionally generates bootstrap confidence intervals.
post_hoc_lm( formula, data, weights = NULL, n_boot = 1000L, probs = c(0.025, 0.975) ) ## S3 method for class 'post_hoc_lm' print(x, ...)post_hoc_lm( formula, data, weights = NULL, n_boot = 1000L, probs = c(0.025, 0.975) ) ## S3 method for class 'post_hoc_lm' print(x, ...)
formula |
Model formula (e.g., |
data |
A data.frame with response and predictors. |
weights |
Optional weights (e.g., inverse of standard errors). |
n_boot |
Number of bootstrap replicates for CI (default 1000, 0 to skip). |
probs |
Quantile probabilities for bootstrap CI (default 0.025, 0.975). |
x |
A |
... |
Ignored. |
A list of class "post_hoc_lm" with:
data.frame of coefficient estimates and CIs
the underlying lm object
matrix of bootstrap coefficient samples (if n_boot > 0)
R-squared from the fitted model
# Explore drivers of per-site estimates after fitting a model. site <- data.frame( psi_hat = c(0.2, 0.5, 0.8, 0.4, 0.6, 0.3), se = c(0.05, 0.04, 0.06, 0.05, 0.03, 0.05), trait = c(1.0, 2.5, 3.8, 1.9, 3.1, 1.2) ) fit <- post_hoc_lm(psi_hat ~ trait, data = site, weights = 1 / site$se^2, n_boot = 200L) fit# Explore drivers of per-site estimates after fitting a model. site <- data.frame( psi_hat = c(0.2, 0.5, 0.8, 0.4, 0.6, 0.3), se = c(0.05, 0.04, 0.06, 0.05, 0.03, 0.05), trait = c(1.0, 2.5, 3.8, 1.9, 3.1, 1.2) ) fit <- post_hoc_lm(psi_hat ~ trait, data = site, weights = 1 / site$se^2, n_boot = 200L) fit
Draw replicated responses from the posterior predictive distribution: the
in-sample linear predictor is drawn with every component the fit estimated
– fixed effects, formula random effects, the offset, and any spatial or
temporal field – and pushed through the family's sampling distribution. The
same draws give the pointwise log-likelihood behind
compare_models(criterion = "waic") / "loo".
Where the draws come from follows what the fit carries:
A ModelData sampler fit (mode = "hmc" and its siblings) evaluates
the engine's own linear predictor at each draw, so a field is drawn
jointly with everything else.
A nested-Laplace fit draws from its outer grid: a replicate picks a
cell by its weight and draws each observation from that cell's Gaussian
for the linear predictor (fitted_eta, fitted_eta_var). The cell's
joint covariance across observations is not retained, so observations
within a replicate are independent given the cell. A fit run with
control$fitted_var = FALSE carries no fitted_eta_var, and its
replicates hold the across-cell spread only.
Any other fit carries its coefficients rather than its linear
predictor. Fits with posterior draws use them (fixed and random effects
jointly per draw). A Laplace or EB fit (mode = "laplace" / "eb")
samples the fixed effects from N(coef(fit), vcov(fit)) and each
draw's random effects from their Gaussian conditional given those fixed
effects under the joint Laplace precision of [beta | b] at the mode,
so the two carry their posterior correlation (on a conditional Laplace
fit this is the joint Gaussian draw; an EB fit is conditional on the
estimated variance components). A conditional-Laplace spatial fit holds
its random effects and areal field at their posterior mode, which
over-disperses the linear predictor wherever the intercept is aliased
with them; an SPDE field enters at its posterior mean.
At newdata the prediction is population level (random effects at zero),
matching predict.tulpa_fit().
A zero-inflated fit (ziformula) draws the structural-zero logit from the
same posterior draw as the count predictor; each replicate observation is a
structural zero with probability plogis(X_zi beta_zi) and otherwise a draw
from the family (a zero-truncated family gives the hurdle model).
posterior_predict(object, ...) ## S3 method for class 'tulpa_fit' posterior_predict( object, newdata = NULL, ndraws = NULL, n_trials = NULL, seed = NULL, ... )posterior_predict(object, ...) ## S3 method for class 'tulpa_fit' posterior_predict( object, newdata = NULL, ndraws = NULL, n_trials = NULL, seed = NULL, ... )
object |
A |
... |
Passed to methods. |
newdata |
Optional data frame of covariates to predict at. Population
level (fixed effects only); |
ndraws |
Number of posterior draws to use. Defaults to all stored draws, or 400 on the draw-free Laplace tier. |
n_trials |
Binomial / beta-binomial trial counts for the replicates.
Defaults to the training trials when |
seed |
Optional integer seed (RNG state is restored on exit). |
A ndraws x n_obs numeric matrix of replicated responses.
pp_check(), which uses these replicates; simulate.tulpa_fit().
set.seed(1) d <- data.frame(y = rpois(100, 4), x = rnorm(100)) fit <- tulpa(y ~ x, data = d, family = "poisson", mode = "laplace") yrep <- posterior_predict(fit, ndraws = 100) dim(yrep) # 100 x 100set.seed(1) d <- data.frame(y = rpois(100, 4), x = rnorm(100)) fit <- tulpa(y ~ x, data = d, family = "poisson", mode = "laplace") yrep <- posterior_predict(fit, ndraws = 100) dim(yrep) # 100 x 100
Returns a fit's posterior draws for summary purposes (quantiles, derived
quantities, density plots), regardless of how they were produced – an MCMC
chain, a nested-Laplace node mixture, or a variational sample all answer
here. For the chain-only view used by convergence diagnostics, see
mcmc_draws().
posterior_sample(fit)posterior_sample(fit)
fit |
A |
A fit that carries no draws returns NULL with a message naming its backend,
the posterior representation it carries instead, and the accessor that turns
that representation into draws where one exists – an
absent draws matrix is a property of the backend, not a failure of the
accessor, and saying which is the difference between a diagnosable answer and
a bare NULL.
The posterior draws matrix/array, or NULL if the fit carries none.
mcmc_draws(), tulpa_posterior_draws(), diagnostics()
set.seed(1) df <- data.frame(x = rnorm(80)) df$y <- rpois(80, exp(0.5 + 0.3 * df$x)) fit <- tulpa(y ~ x, data = df, family = "poisson") dim(posterior_sample(fit))set.seed(1) df <- data.frame(x = rnorm(80)) df$y <- rpois(80, exp(0.5 + 0.3 * df$x)) fit <- tulpa(y ~ x, data = df, family = "poisson") dim(posterior_sample(fit))
Generate posterior predictive checks for a fitted tulpa model. Compares observed data to replicated data from the posterior predictive distribution.
pp_check(object, ...) ## S3 method for class 'tulpa_fit' pp_check( object, type = c("dens_overlay", "scatter", "intervals", "stat"), component = NULL, stat = mean, ndraws = 50, ... )pp_check(object, ...) ## S3 method for class 'tulpa_fit' pp_check( object, type = c("dens_overlay", "scatter", "intervals", "stat"), component = NULL, stat = mean, ndraws = 50, ... )
object |
A |
... |
Additional arguments passed to bayesplot functions |
type |
Type of check: "dens_overlay", "scatter", "intervals", "stat" |
component |
Which component: "numerator", "denominator", or "both" |
stat |
Function for "stat" type (default: mean) |
ndraws |
Number of posterior draws to use (default: 50 for plots) |
A ggplot object
# pp_check is a generic; model packages (e.g. tulpaObs, tulpaRatio) provide # the posterior-predictive method for their fits. set.seed(123) n <- 200L df <- data.frame(y = rpois(n, 5), x = rnorm(n), site = factor(rep(1:10, each = 20))) fit <- tulpa(y ~ x + (1 | site), data = df, family = "poisson", mode = "hmc", control = list(n_iter = 500, warmup = 250)) # Density overlay (dispatches to the model package's pp_check method). if (requireNamespace("bayesplot", quietly = TRUE)) { pp_check(fit, type = "dens_overlay") }# pp_check is a generic; model packages (e.g. tulpaObs, tulpaRatio) provide # the posterior-predictive method for their fits. set.seed(123) n <- 200L df <- data.frame(y = rpois(n, 5), x = rnorm(n), site = factor(rep(1:10, each = 20))) fit <- tulpa(y ~ x + (1 | site), data = df, family = "poisson", mode = "hmc", control = list(n_iter = 500, warmup = 250)) # Density overlay (dispatches to the model package's pp_check method). if (requireNamespace("bayesplot", quietly = TRUE)) { pp_check(fit, type = "dens_overlay") }
Prediction of the linear predictor at newdata, on the link or response
scale. The fixed-effect part is X beta with credible bounds from the
fixed-effect covariance (vcov()). For a fit carrying a continuous spatial
field, the posterior-mean field is interpolated (kriged) to the newdata
coordinates and added to the linear predictor by default, so predict()
gives the conditional (location-specific) prediction. Three continuous field
families are supported: an SPDE Matern field (spatial_spde()), projected
through the mesh; a Hilbert-space GP field (spatial_gp(approx = "hsgp")),
where the Laplacian basis is re-evaluated at the new coordinates (with the
training centring / boundary); and a GP / NNGP field (spatial_gp()),
interpolated by the NNGP conditional mean at each new location's nearest
training locations. The HSGP and GP/NNGP fields are marginalised over the
hyperparameter grid (not plugged in at the posterior mean). Ordinary random
effects are held at zero (population level); add group effects from ranef()
when needed. An areal (ICAR / BYM2 / CAR) or temporal (RW1 / RW2 / AR1) field
is held at zero in the same way, at the training design too; the in-sample
linear predictor with every fitted component is what posterior_predict()
draws and what compare_models(criterion = "waic") / "loo" score.
## S3 method for class 'tulpa_fit' predict( object, newdata = NULL, type = c("link", "response"), se.fit = FALSE, level = 0.95, include_field = TRUE, ... )## S3 method for class 'tulpa_fit' predict( object, newdata = NULL, type = c("link", "response"), se.fit = FALSE, level = 0.95, include_field = TRUE, ... )
object |
A |
newdata |
Data frame of covariates (and, for an SPDE fit, the coordinate
columns named in the spec's coordinate formula). If |
type |
|
se.fit |
If |
level |
Credible-interval level (default 0.95). |
include_field |
For a continuous-spatial fit (SPDE, HSGP, or GP/NNGP),
add the kriged field to the prediction (default |
... |
Ignored. |
If se.fit = FALSE, a numeric vector. If se.fit = TRUE, a data
frame with fit, se.fit (link scale), lower, upper on the requested
scale.
print() shows a fit in a few lines: how it was produced (backend and
inference tier), the call, the observation count and the fixed-effect point
estimates. summary() returns the full report as a summary.tulpa_fit
object, which prints as a sectioned display and whose parts are read by
name; coef() on it returns the fixed-effect table.
## S3 method for class 'tulpa_fit' print(x, ...) ## S3 method for class 'tulpa_fit' summary(object, level = 0.95, ...) ## S3 method for class 'summary.tulpa_fit' coef(object, ...) ## S3 method for class 'summary.tulpa_fit' print(x, digits = max(3L, getOption("digits") - 3L), ...)## S3 method for class 'tulpa_fit' print(x, ...) ## S3 method for class 'tulpa_fit' summary(object, level = 0.95, ...) ## S3 method for class 'summary.tulpa_fit' coef(object, ...) ## S3 method for class 'summary.tulpa_fit' print(x, digits = max(3L, getOption("digits") - 3L), ...)
x |
A |
... |
Ignored. |
object |
A |
level |
Credible-interval level of the fixed-effect table, a single
number in |
digits |
Significant digits in the printed tables. |
The fixed-effect table reads the same posterior coef(), confint() and
vcov() read. Sampler tiers report empirical quantiles; the Laplace tier
reports the Gaussian approximation, and so does any fit whose reported
posterior is a closed-form Gaussian (expectation propagation, the
multinomial Laplace fit), whose draws are then samples from that Gaussian
and are not what the summary reads.
On a nested-Laplace fit the estimate and standard error are the
hyperparameter-grid-marginalized moments, and the bounds invert the Gaussian
mixture sum_k w_k N(mu_kj, V_kjj) that grid defines, rather than reading
mu +/- z sigma off the single Gaussian matching those moments. The table's
interval_source attribute records which read produced them
("mixture_cdf", "gaussian_moment", "skew_map_cell", or
"skew_map_cell/mixture_cdf" when the two are in play on different
coefficients) and interval_declined says why, whenever the mixture read
did not run. Its retained_mass attribute gives the share of the grid
weight whose cells retained a fixed-effect block: 1 on a complete grid, and
below 1 on one that dropped a positive-weight cell, whose report is then the
posterior conditional on the cells that remain.
Where no per-cell block was retained the estimate falls back to the
grid-weighted average of the per-cell modes, restricted to the cells whose
inner solve reached a mode. A fit where none did reports NA with
interval_declined = "not_converged", rather than the vector its Newton
started from as an estimate.
With control$skew_correct = TRUE a coefficient whose inner-Laplace
gamma_3 is in the band it is valid on reports Cornish-Fisher quantiles
instead, and the table's skew_applied attribute names which coefficients
took the correction. That correction is measured at the MAP cell, so it is
reported on its own and is not composed with the mixture read; a coefficient
it declines keeps the mixture read rather than falling back further.
The reliability headline is read from diagnostics(), so it answers the
question that applies to how the draws were produced: Rhat and effective
sample size for a chain, the approximation-reliability band (outer
Pareto-k, grid quadrature, inner Laplace, combined verdict) for i.i.d.
draws from a deterministic approximation, and nothing for a point summary.
print.tulpa_fit() and print.summary.tulpa_fit() return their
input invisibly.
summary.tulpa_fit() returns a list of class summary.tulpa_fit:
call, formula, family
as the fit records them, NULL where
it records none.
inferencehow the fit was produced: a list of backend,
inference_tier, inference_mode and selection_reason (why
mode = "auto" chose that backend).
n_obs, n_draws
the observation count and the number of
posterior draws the fixed-effect summaries read (NULL without
draws).
levelthe interval level of coefficients.
coefficientsdata frame, one row per fixed effect named by the
term: estimate, std.error and the lower / upper credible bounds,
labelled as confint.tulpa_fit() labels them. Carries the
interval_source, interval_declined, retained_mass and
skew_applied attributes described in Details. Also returned by
coef() on the summary.
randomthe random-effect standard deviations as VarCorr()
reports them, with their estimated / sampled / conditioned label;
NULL on a fit with no random-effect terms.
hyperparametersdata frame of the integrated hyperparameters,
one row each: parameter, mean, sd, median and the 2.5% /
97.5% bounds ci_lo / ci_hi. On a nested-Laplace fit these are the
outer-grid axes; on the random-effect covariance integrators the
derived standard deviations and correlations. NULL where the fit
integrated none.
sigmathe residual / dispersion scale a fit reports as one
number, else NULL.
smooth_termsthe fit's s(...) term metadata, NULL without
smoothers; smooth_effects() returns the fitted curves.
reliabilitya list: kind ("chain", "iid" or "point",
the draws provenance diagnostics() routes on); on a chain
rhat_max, ess_bulk_min and ess_tail_min over the parameters;
on i.i.d. draws band, the one-row headline diagnostics() attaches
as its summary attribute (outer pareto_k and its band, grid
quadrature ESS, inner-Laplace scores, reliability), and
moment_source, which read the per-parameter moments came from;
verdict, the whole-fit reading ("converged" / "not converged"
on a chain that records its convergence check, the combined
reliability verdict on i.i.d. draws, NA otherwise);
convergence, the fit's own convergence record; and the
backend-specific quality numbers mean_accept (Metropolis acceptance
rate), n_divergent (divergent transitions) and elbo (the
variational objective), each NULL where the backend has none.
axis_fields_droppedthe grid axes the fit's own resolved path
could not read, one row per dropped field (block, type, field, path,
integrates, reason); NULL whenever every supplied axis was used.
diagnostic_summary() reads the same record in sentences.
priora list whose beta is the Gaussian fixed-effect prior the
fit ran under, as list(mean, sd): the engine default
prior_normal(0, 2.5) whenever the caller supplied none, and NULL
on the paths that express no Gaussian prior on the fixed effects.
coef.summary.tulpa_fit() returns the coefficients data frame.
coef.tulpa_fit(), confint.tulpa_fit(), VarCorr(),
diagnostics(), tulpa_outer_grid()
set.seed(1) df <- data.frame(x = rnorm(60)) df$y <- rpois(60, exp(0.5 + 0.3 * df$x)) fit <- tulpa(y ~ x, data = df, family = "poisson", mode = "laplace") fit s <- summary(fit) s coef(s) s$inference$backendset.seed(1) df <- data.frame(x = rnorm(60)) df$y <- rpois(60, exp(0.5 + 0.3 * df$x)) fit <- tulpa(y ~ x, data = df, family = "poisson", mode = "laplace") fit s <- summary(fit) s coef(s) s$inference$backend
Generic outer-grid nested Laplace driver. Builds a grid over the hyperparameters of a single latent prior block (spatial or temporal), runs an inner Laplace at each grid point with warm-starting, and integrates over the grid to give proper hyperparameter marginals.
Supported priors:
Spatial (areal): "icar" (1D grid on tau), "bym2"
(2D on (sigma, rho)), "car_proper" (2D on (tau, rho); rho lives
in the eigenvalue interval (1/lambda_min, 1/lambda_max) of
D^{-1} W).
Spatial (continuous): "nngp" (2D on (sigma2, phi_gp)),
"hsgp" (2D on (sigma2, lengthscale)).
Temporal: "rw1", "rw2" (1D grid on tau), "ar1" (2D on (tau, rho))
SPDE continuous spatial: see cpp_nested_laplace_spde() (separate
entry, rebuilds Q via SPDE Q-builder).
## S3 method for class 'tulpa_nested_laplace' print(x, ...) tulpa_nested_laplace( y, n_trials, X, prior = NULL, spec = NULL, data = NULL, re_idx = NULL, n_re_groups = 0L, sigma_re = 1, family = "binomial", phi = 1, offset = NULL, likelihood = NULL, hyperprior = c("proper", "flat"), control = list() )## S3 method for class 'tulpa_nested_laplace' print(x, ...) tulpa_nested_laplace( y, n_trials, X, prior = NULL, spec = NULL, data = NULL, re_idx = NULL, n_re_groups = 0L, sigma_re = 1, family = "binomial", phi = 1, offset = NULL, likelihood = NULL, hyperprior = c("proper", "flat"), control = list() )
x |
A nested-Laplace fit. |
... |
Ignored. |
y |
Response vector. |
n_trials |
Trial sizes (binomial). Pass |
X |
Fixed-effects design matrix. |
prior |
A list describing the latent prior block. Required field
Any block accepts an optional
An Grids of a two-axis type ( A default grid axis is a starting axis, not a hard ceiling: for |
spec |
Optional |
data |
Data frame used to validate |
re_idx |
Optional 1-based RE group index per obs (defaults to no RE). |
n_re_groups |
RE group count (default 0). |
sigma_re |
RE standard deviation (default 1). |
family |
|
phi |
Dispersion passed to the family, held fixed. One convention at
every door: for Defaulted, it conditions at 1 and says so with a warning, since for a
family that reads a dispersion that is a modelling choice rather than a
neutral value. |
offset |
Optional per-observation offset added to the linear predictor
(length |
likelihood |
Optional model-supplied likelihood, replacing the built-in
|
hyperprior |
The prior an outer hyperparameter axis carries when the
call states no density for it, |
control |
Optional list of perf/numerical tuning knobs (statistical
arguments stay top-level), following the
|
print() shows the integrated hyperparameters, the outer-grid
size, the outer Pareto- diagnostic when present and the
wall-clock timing line ("fit in 5h 25m (grid 2h 09m)") when
fit$timing is attached, then the fixed effects.
A list with:
theta_grid: matrix or vector of grid hyperparameter values.
log_marginal: log p(y, mode | theta_k) at each grid point.
weights: integration weights normalising to sum 1.
theta_mean, theta_sd: posterior moments per hyperparameter.
n_iter: inner Newton iterations per grid point.
modes: matrix [n_grid x n_x] of inner modes, when stored.
pareto_k, pareto_k_is_ess: outer Pareto- and its
importance-sampling ESS (NA when not computed for the grid; see
control$diagnose_k).
pareto_k_proposal_source, pareto_k_first_pass: which proposal family
the reported k-hat came from (the outer k-hat is scored against several
candidates and the best is kept), and the k-hat of the FIRST pass – the
proposal exactly as this backend placed it, before any candidate refined
or replaced it. A large gap between the two says the nodes are badly
scaled around the hyperparameter posterior even though the verdict is
fine; no gap says the placement was already right.
inner_skew, inner_skew_idx, inner_skew_dropped: the inner-Laplace
skewness diagnostic (gamma_3) at each scored latent index and its
1-based index, plus a count of (index, observation) contributions
dropped for a non-finite third derivative (see control$diagnose_skew).
timing: named numeric of wall-clock seconds (total, setup,
grid, postproc, diagnostics); the grid phase is the inner
Laplace pass that scales with grid size. Surfaced one-line in print.
prune_cheap_log_marginal, prune_mask, prune_n_pruned, prune_tol:
present when the grid was screened and the safety gate did not
trip – the cheap-screen log-marginal per cell (the truncated solve's
value plus half its Newton decrement, the second-order estimate of the
converged value the cells are ranked by; the decrement itself is
prune_screen_decrement), the logical mask of
pruned cells, the pruned-cell count, and the threshold actually applied.
prune_log_gap_cut, prune_cheap_lm_spread, prune_min_keep,
prune_n_floor_restored: the same screen read on the scale it operates
on – how many nats below the best screened cell the tolerance cut at,
how many nats the screened surface spans, the kept-cell floor applied,
and how many cells that floor put back. A cut that is a sliver of the
spread is a tolerance with no resolution on this grid.
prune_dropped_mass_bound, prune_n_repaired, prune_gate_mass: the
posterior mass the dropped cells may carry, with each lifted by the worst
screening error measured on a kept cell; how many dropped cells the
driver solved in full to bring that under the gate; and the gate.
prune_fallback_triggered, prune_fallback_reason: present only when
the gate did trip; the rest of the fit is then the full-grid result and
the reason string records which check fired.
prior: echoed input.
Rue, Martino & Chopin (2009). Approximate Bayesian inference for latent Gaussian models by using integrated nested Laplace approximations. JRSS-B 71(2):319-392.
set.seed(1) S <- 30L # spatial units arranged in a chain nb <- lapply(seq_len(S), function(s) setdiff(c(s - 1L, s + 1L), c(0L, S + 1L))) nn <- lengths(nb) field <- as.numeric(scale(cumsum(rnorm(S, 0, 0.4)))) # smooth spatial field idx <- rep(seq_len(S), each = 6L); n <- length(idx); x <- rnorm(n) y <- rbinom(n, 1L, plogis(-0.2 + 0.6 * x + field[idx])) prior <- list(type = "icar", n_spatial_units = S, spatial_idx = idx, adj_row_ptr = c(0L, cumsum(nn)), adj_col_idx = unlist(nb) - 1L, n_neighbors = nn, tau_grid = c(0.5, 1, 2, 4, 8)) fit <- tulpa_nested_laplace(y, rep(1L, n), cbind(1, x), prior = prior, family = "binomial") fit$theta_mean # marginalized ICAR precisionset.seed(1) S <- 30L # spatial units arranged in a chain nb <- lapply(seq_len(S), function(s) setdiff(c(s - 1L, s + 1L), c(0L, S + 1L))) nn <- lengths(nb) field <- as.numeric(scale(cumsum(rnorm(S, 0, 0.4)))) # smooth spatial field idx <- rep(seq_len(S), each = 6L); n <- length(idx); x <- rnorm(n) y <- rbinom(n, 1L, plogis(-0.2 + 0.6 * x + field[idx])) prior <- list(type = "icar", n_spatial_units = S, spatial_idx = idx, adj_row_ptr = c(0L, cumsum(nn)), adj_col_idx = unlist(nb) - 1L, n_neighbors = nn, tau_grid = c(0.5, 1, 2, 4, 8)) fit <- tulpa_nested_laplace(y, rep(1L, n), cbind(1, x), prior = prior, family = "binomial") fit$theta_mean # marginalized ICAR precision
Outer-grid nested Laplace driver for joint models – multiple response arms sharing one latent prior block, parameterized as a per-arm field amplitude (sigma) on a unit-precision latent.
Supported priors:
"bym2" – outer grid over (sigma, rho [, alpha]).
Latent: phi (n_s) | theta (n_s) with unit-precision
ICAR + iid components.
"icar" – outer grid over (sigma [, alpha]).
Latent: phi (n_s) with unit-precision ICAR.
"car_proper" – outer grid over (sigma, rho_car [, alpha]).
Latent: phi (n_s) with Q = D - rho_car * W.
Other backends (NNGP, HSGP, RW1/2, AR1) follow the same interface and land under Phase 3.
## S3 method for class 'tulpa_nested_laplace_joint' print(x, ...) ## S3 method for class 'tulpa_nested_laplace_joint_multi' print(x, ...) tulpa_nested_laplace_joint( responses, prior, copy = NULL, phi_grid = NULL, prior_sigma = NULL, prior_alpha = NULL, prior_phi = NULL, hyperprior = c("proper", "flat"), cell_coupling = "separable", control = list() )## S3 method for class 'tulpa_nested_laplace_joint' print(x, ...) ## S3 method for class 'tulpa_nested_laplace_joint_multi' print(x, ...) tulpa_nested_laplace_joint( responses, prior, copy = NULL, phi_grid = NULL, prior_sigma = NULL, prior_alpha = NULL, prior_phi = NULL, hyperprior = c("proper", "flat"), cell_coupling = "separable", control = list() )
x |
A joint nested-Laplace fit. |
... |
Ignored. |
responses |
A named list of arm specs (length >= 1). Each arm:
|
prior |
A list describing the shared latent prior block. Required
field
On the list-of-blocks path an An empty |
copy |
Multi-block copy specification (multi-block The |
phi_grid |
Optional list specifying per-arm dispersion axes on the
outer grid. Accepts either a named list (keys = arm names) or a
positional list of length
Family-specific interpretation of
A dispersion axis is PLACED like a prior block's scale axis when the caller
declares it a default with Each |
prior_sigma, prior_alpha
|
Optional regularizing hyperpriors on the
donor field amplitude
WHICH AXES ONE SPEC REACHES. A list-of-blocks prior_alpha = list(
list(block = 1, prior = list("pc.prec", c(4.0, 0.01))),
list(block = 2, prior = list("half_normal", 2.0))
)
A block the list does not name carries no prior on that axis. An entry
naming a block that carries no such axis is an error, and the message
names the blocks that do. The per-block shape is a multi-block one: on a
single-block
|
prior_phi |
Optional regularizing hyperprior on the per-arm
dispersion axes declared through |
hyperprior |
The prior an outer hyperparameter axis carries when the
call states no density for it, |
cell_coupling |
Character scalar naming a per-cell coupled likelihood
registered against tulpa's process-global registry (see
|
control |
Optional list of perf/numerical tuning knobs (statistical
arguments stay top-level), following the
|
A list of class c("tulpa_nested_laplace_joint", "tulpa_nested_laplace", "list") with:
theta_grid, theta_names – outer-grid hyperparameter values
(includes the alpha axis when copy is set).
log_marginal, weights – per-grid-point log-marginal and integration
weights (sum to 1).
theta_mean, theta_sd – posterior moments per hyperparameter,
including alpha when copy is set.
theta_median, theta_ci_lo, theta_ci_hi – weighted-quantile
median and 2.5/97.5 empirical CI per hyperparameter axis (same
names as theta_mean). Recommended summary for right-skewed
scale-like axes (alpha at small n_pos, sigma/range/phi near
a boundary), where the posterior mean is pulled by the right
tail away from the bulk and mean +/- 1.96 sd mis-states the
uncertainty.
theta_sd_source – per axis, what the reported SD was read off:
"weighted" (the spread of the weights, on a resolved axis),
"stencil" (a parabola at the modal node) or "within_cell" (the
modal node's mass spread over its cell, the read the interval of an
unresolved axis comes from). theta_sd_stencil_declined says why a
parabola was not formed.
modes – [n_grid x n_x] matrix of inner modes.
fitted_eta – [n_grid x N] matrix of the linear predictor at each
cell's own mode, offset and latent field included. Present on a
single-arm fit, which is the one that has a single linear predictor to
report; a cell that was pruned or whose block preparation failed reads
NA. fitted_eta_var is its per-cell within-cell variance, present
under control$fitted_var (the default). Together these are the
per-cell Gaussian posterior_predict() draws a replicate from.
n_iter – inner Newton iterations per grid point.
arm_layout – list with per-arm beta_start, re_start,
spatial offset(s) and n_x for decoding modes.
prior, responses, copy – echoed inputs.
timing – named numeric of wall-clock seconds: total plus the
setup (validation / encoding / grid construction), grid (inner
Laplace solves, including adaptive-refinement and consistency passes),
postproc (weight / moment / marginal assembly) and diagnostics
(outer Pareto-) phases. The grid phase is the one that
scales with grid size and core count. Surfaced one-line in print.
pareto_k, pareto_k_is_ess, pareto_k_scope – outer
Pareto- accuracy diagnostic and its importance-sampling
ESS (both NA when control$diagnose_k = FALSE or the fit declines;
see the diagnose_k control knob). pareto_k < 0.7 indicates the
nested integration is reliable; >= 0.7 that the (skewed / heavy-
tailed) hyperparameter posterior is misfit by the Gaussian grid and
the fit should escalate to an exact debias.
pareto_k_declined – when pareto_k is NA, WHY:
"not_requested" (control$diagnose_k = FALSE; nothing is wrong),
"unguessable_axis: <axis>" (a support the engine will not guess, e.g.
car_proper's rho_car – a PERMANENT limitation of that family, so read
the quadrature ESS instead), "draws_too_few", "grid_too_small",
"no_varying_axis", "degenerate_proposal" (the outer mode curvature
came back non-finite – a signal about the fit), "not_applicable", or
"internal_inconsistency" (an engine bug worth reporting). NA on a
fit whose WAS computed.
pareto_k_proposal_source – how the outer importance proposal the
scores was built: "mode_hessian" from the Laplace
curvature at the hyperparameter mode (the CCD design's, or a
finite-difference Hessian when a sharp posterior collapses the grid),
"grid_moment" from the grid-weighted covariance, "moment_matched"
when the moment-matching refinement improved on either, "grid_mixture"
when the faithful mixture-over-grid-cells proposal won, or
"skew_normal" when the skew-normal rescue did. NA when the
diagnostic is off or declines. The mode-Hessian source keeps the
meaningful when the grid concentrates on ~1 cell.
pareto_k_regime – whether the outer grid actually integrated
hyperparameter uncertainty: "spread", or "collapsed_interior" /
"collapsed_edge" when the quadrature effective sample size falls below
two cells so the integration has degenerated to a point evaluation at
the modal hyperparameter. An interior collapse is benign (the grid
bracketed the mode; the fit is empirical Bayes at that mode and only the
integrated hyperparameter uncertainty is missing); an edge collapse means
the mode sits at an extreme node, so the grid may be too narrow.
Attached from the stored weights, so it is present even with
control$diagnose_k = FALSE.
pareto_k_grid_edge_axes, pareto_k_grid_edge_sides – for an edge
collapse, which axes the dominant cell sits against and on which side
("lower" / "upper"); widen those axes and refit to confirm the mode
is bracketed. Axes the grid pins to one value are excluded (pinned, not
at a boundary).
axis_span – per outer axis, the span the fit worked over and the two
terms that separate it from the nodes it was given: nodes (the range
of the axis's initial continuum nodes), declared (their support, i.e.
those nodes plus the half node step the outermost cells own – for k
equally spaced nodes that is k / (k - 1) times the node range on the
axis's integration coordinate, so 2x at two nodes and 1.125x at nine),
integrated (the region the final grid's cell measure integrates, the
same interval axis_support reports), refine (the mode refinement
ran under: "none" / "densify" / "extend") and n_nodes (initial
and final continuum node counts).
outer_grid_placement – "fixed" (the default sigma_grid axis was
used as-is) or "auto_recentered" when a railed sigma axis
triggered the mode-Hessian recenter-and-refit (see the prior
argument above). outer_grid_recenter_attempts (integer)
and outer_grid_prior_added (logical: whether the light default
PC(U=3, alpha=0.01) sigma prior was applied) are set alongside it, plus
outer_grid_prior_declined = "prior_pinned" when a second attempt ran
but the caller's pinned prior_sigma held that prior back.
outer_grid_recenter_declined – on a "fixed" placement, why the
recenter did not run: "grid_not_collapsed" (the grid already brackets
the mode – the common case, no refit needed), "axis_pinned" (the
caller pinned sigma_grid; mark it with auto_grid() if it is a
default rather than a choice), "default_axis_pinned" (a wrapper
package declared the nodes with auto_grid(place = FALSE) and asked
for them as written – nothing you pinned), or "no_usable_curvature"
(the outer mode-find the recenter lays the axis from did not reach a
mode with a usable curvature, e.g. a car_proper grid whose rho_car
axis has unguessable support).
Absent when the fit WAS recentred. A joint fit's field SD and its
per-arm dispersion are placed by different passes over the same grid,
and this slot reduces over them rather than holding the last to speak:
an axis held because it was declared does not stand as the fit's answer
while a pass with a movable axis has one.
outer_grid_axis_declined – the same question PER AXIS, as a named
character vector. The slot above holds one reason for the whole fit and
is written only while the fit is unplaced, so on a fit where one axis
moved and another did not it says auto_recentered and nothing about
the axis that stayed – which is the axis grid_coarsest_axis then
names. Written by the field-SD passes for sigma (b<k>.sigma on a
copy block) and by the per-arm dispersion pass for phi_<arm>, and
carried across a later pass placing a different axis; absent on a fit
no pass declined an axis of. An axis a pass DID move that still rails
reads "attempts_exhausted" (or the reason its last attempt could not be
laid), and is the axis the railed-mode warning names.
outer_grid_pilot – present only when control$recenter_pilot ran: the
pilot's resolution (n_pilot), its cell count (cells), the axes it
thinned (axes) and those it could not (axes_kept), and what the
placement was DECIDED from – the pilot's own regime, ess_grid and
edge_axes. The reported grid is never the grid the trigger was read
on, so a two-pass fit is legible from the object rather than only from
the progress log. outer_grid_pilot_declined = "pilot_placement_declined"
records a pilot whose detection placed nothing and which was therefore
followed by the full fit and the full fit's own detection.
outer_grid_recenter_sd_clamp, outer_grid_recenter_sd_raw,
outer_grid_recenter_sd_used – on a recentred fit, one entry per moved
axis: which mode-SD bound the placement hit ("none" / "floor" /
"ceiling"), the SD the finite-difference stencil MEASURED, and the SD
the axis was actually laid from. A clamped axis was
laid from a substituted spread rather than a measured one, and the
reported interval is read off that span; the two used to be
indistinguishable on the fit.
pareto_k_outer_skew – per-axis skewness of the hyperparameter marginal
in the proposal's whitened coordinate, present only when a
above the good band triggered the skew-normal rescue pass.
It is the EXPLANATION for an inflated : a symmetric Gaussian
proposal against a right-skewed variance-component marginal has a heavy
importance-ratio tail whatever the integration's quality. NULL when the
Gaussian proposal already fit.
pareto_k_se_boot, pareto_k_ci_low, pareto_k_ci_high,
pareto_k_se_formula, pareto_k_tail_points,
pareto_k_tail_points_requested, pareto_k_band_confident – the outer
Pareto- uncertainty, present whenever the
diagnostic ran. pareto_k_se_boot is the bootstrap SE of the k-hat and
pareto_k_ci_low / pareto_k_ci_high its 2.5\
quantiles – the estimator's sampling spread GIVEN the proposal, not a
posterior credible interval; pareto_k_se_formula is the closed-form
GPD-shape MLE asymptotic SE cross-check. pareto_k_tail_points is the GPD
tail size used and pareto_k_tail_points_requested the k_tail_points
request (NA when automatic). pareto_k_band_confident is TRUE iff the
bootstrap CI lies within one reliability band, NA when the bootstrap was
off or could not fit. diagnose_draws and diagnose_cost_ratio (the
diagnostic's draw budget and its wall-clock cost relative to the fit) are
attached at the top level.
pareto_k_by_arm, pareto_k_by_arm_is_ess, pareto_k_by_arm_scope –
present only with control$diagnose_k = "by_arm".
Named (by arm) outer Pareto- restricted to each arm's
hyperparameter axes, the other arms held at their posterior mean, so a
tail-heavy joint k can be localised to one arm. A per-arm entry is NA
when that arm carries no varying axis. Each arm carries its own bootstrap
uncertainty: pareto_k_by_arm_se_boot, pareto_k_by_arm_ci_low /
pareto_k_by_arm_ci_high, pareto_k_by_arm_se_formula,
pareto_k_by_arm_tail_points and pareto_k_by_arm_band_confident.
k_quality_requested, k_quality_reached, k_quality_best,
k_quality_miss, k_quality_reason, k_quality_rounds,
k_quality_k_trace – the reliability verdict for the
control$k_quality intent. k_quality_requested
echoes the intent; k_quality_best is the band THIS fit reached
("good" / "ok" / "unreliable", or "uncertain" when the bootstrap CI
crosses a boundary) – the band of the returned fit, not the best band
seen while escalating, which k_quality_k_trace carries;
k_quality_reached is TRUE/FALSE for an "ok" /
"good" target (NA for "report" / "none"), never silently
downgraded; k_quality_miss classifies a miss as "resolution" (the
confidently outside the band – a grid deficiency) or
"precision" (the bootstrap CI straddling a boundary – the estimator's
variance), NA when there is no miss to classify, and is what the
escalation reads once refinement is exhausted; k_quality_reason
records why it stopped;
k_quality_rounds is the number of escalation re-fits performed (0 when
the first fit sufficed or escalation was off); and k_quality_k_trace is
the outer round by round, starting at the first fit's, so a
chase that did not descend monotonically is visible. The refinement rungs
rebuild the proposal from the grid they just changed, so a rising trace is
a re-founded proposal on a strictly larger grid rather than a worse fit,
which is why the LAST round is returned rather than the lowest-
one.
adaptive_grid_info – when adaptive_grid = TRUE, a list with
triggered_axes (character) and n_points_added (integer)
describing the refinement passes. NULL otherwise.
local_ccd_info – when local_ccd engaged (or k_refine = "ccd"), a
list with n_cells_refined, n_nodes_added, the refined cells, the
n_cells_before / n_cells_after grid sizes, n_design_nodes (the cells
that carry an in-cell design weight rather than their own mass),
design_mass (the share of the integration weight sitting on them) and
cell_share (the share each refined cell held on the BASE grid, before
any node was placed). The two shares differ: the replacement nodes sit
nearer the peak than the cell's own coordinate did, so refining raises
the share the refined region holds, and reading both separates how
concentrated the base grid already was from how much the refinement
concentrated it. Three per-cell readings come with it, on three
orthogonal axes, none of them gating anything. Shape: misfit (the
standardized cubic magnitude of the part of the cell the design cannot
represent, the one reading skew_max gates on, with misfit_declined /
cells_declined for the cells put back as their own mass atoms).
Centring: offset (the norm of the whitened gradient, i.e. how far the
cell's own peak sat from the cell's coordinate in units of its marginal
spread) and mode_gain (the same displacement in the cell's own
curvature units, 0.5 * g' (-H)^-1 g in nats, read off the same fit's
quadratic coefficients and comparable across cells whose curvature
differs; NA where -H is not positive definite, a cell with no
interior peak to be displaced from). Mass: log_mass_ratio (the cell's
refined mass over the coarse atom it replaced, log_mass_refined - log_mass_coarse, both carried on the log scale with the cell's outer
design weight in them) and max_node_weight (the share the single
largest node takes of its own cell's refined mass). Each carries a
_declined twin for the cells the gate put back, whose nodes were
evaluated before the score was read. misfit certifies that the design
can represent the cell's shape and nothing more: a cell can be exactly
quadratic and still be read at a point that is not representative of it
(offset / mode_gain), and refining it can still move how much mass it
competes for against its unrefined neighbours (log_mass_ratio, the
embedded-rule local error indicator of adaptive cubature). NULL when
local CCD was off or declined (single-block, < 4 axes, an active
phi_grid, or no peaked interior cell).
integration_requested, integration_declined – what integration
asked for, and why the requested CCD or adaptive lattice did not run. $integration names the
integrator that RAN, and .nl_node_support() keys the interval
construction off it, so a caller who asked for a moment rule and
received a density grid reads the reason here rather than inferring it.
integration_declined is NA_character_ when nothing was declined, and
otherwise one of "axis_count" (fewer transformable latent axes than the
requested mode's threshold), "unguessable_axis" (a CAR_proper
rho_car or a non-BYM2 rho), "degenerate_axis" (a single-valued
axis), "modefind_ridge" / "modefind_boundary" /
"modefind_degenerate" / "modefind_failed", "hessian_singular",
"hessian_not_pd", "copy_atom_components" (more copy atoms than the
design splits into, see copy_atom_mass), "copy_atom_mass" (a
declared atom mass outside [0, 1)) or "placement_budget" (placing
the design would cost more inner solves than integrating the tensor grid
it replaces; see the ccd_budget control knob). Under
integration = "grid_adaptive" it is one of
"adaptive_small_grid" (fewer outer cells than
adaptive_grid_min_cells), "adaptive_degenerate_lattice",
"adaptive_seed_failed" (no finite marginal in the coarse seed),
"adaptive_diffuse_seed" / "adaptive_flood_cap" /
"adaptive_kept_cap" (the posterior mass spreads over more than
adaptive_grid_max_frac of the dense grid, read at the seed, during the
flood, or on the closed kept region), or "adaptive_error" (the flood
raised an error, reported as a warning carrying its message); the fit
then integrates the dense tensor. Multi-block fits only.
ccd_modefind_evals, ccd_modefind_budget, ccd_modefind_rounds,
ccd_modefind_seconds – what PLACING the CCD cost: inner Newton solves
spent by the mode-find (seed, step calibration and every round of every
mixture component), the ceiling they were spent against, the rounds
taken and the wall-clock seconds. Present on any multi-block fit that
attempted a placement, including one that declined – a decline reports
what it had already paid – and absent when no placement was attempted,
which is not the same as a placement that cost nothing. Read alongside
$integration to judge whether the CCD earned its design on this model
rather than arguing it from the round caps.
weight_kind – one entry per outer-grid cell, "mass" or "design".
A tensor cell holds the mass of its own cell and a CCD node holds a
design weight, so a fit integrated by one rule reports one value
throughout; a locally refined grid carries both, and this says which per
cell instead of leaving it to be read off integration.
theta_interval_read, theta_interval_design_mass – what the reported
per-axis theta_median / theta_ci_lo / theta_ci_hi were read off, and
how much of the integration weight sits on nodes whose cumulative sum is
not a CDF. "density" is a weighted quantile over cells that discretize
the posterior (design_mass 0), "moment_rule" an interval from the
moments a central-composite design delivers (design_mass 1), and
"mixed" the locally CCD-refined grid, which carries both kinds at once.
A mixed support still reports the weighted quantile, which measured best
against a converged reference in both design_mass regimes; the share is
the regime variable, since on that part of the support the quantile is
bounded by the refined cells' own grid neighbourhoods rather than by the
posterior. Every nested path stamps the pair, not only
the multi-block driver.
within_cell_requested, theta_within_cell,
theta_within_cell_declined – the WITHIN-CELL construction the same
intervals were read with. The kind above says what the
integrator left; this says how each cell's mass was spread inside its own
box when the grid was read back. "log_quadratic" is the default and
interpolates each row's node log densities; "box_uniform"
(control$within_cell) puts the cumulative full mass at each cell edge;
"chord" puts the cumulative mid-mass at each cell coordinate, the same
masses over the same boxes with the knots moved half a cell. The
construction is recorded per axis, and an axis whose cell partition could
not be built falls back to "chord" on its own with the reason in
theta_within_cell_declined ("support_<kind>", "single_node",
"boxes_do_not_tile", "no_usable_node"). A "log_quadratic" request
falls back to "box_uniform" first, with "unresolved" (no row
resolves its own conditional), "not_a_grid_axis" or
"no_node_density" as the reason.
theta_cell_edge_coord, theta_cell_edge_declined – per axis, the
COORDINATE that axis's outer cell edges were mirrored in, and why the
support the axis declares did not produce them. A
declared support is authoritative: its mirrored edge is used when finite
and inside it, and otherwise the axis reports its extreme grid
coordinate – which the containment test has already placed inside the
support – rather than falling through to a coordinate guessed from the
node values, which is what put a lower bound of 0 on a positive axis
and a bound above 1 on a unit one. An axis that declares no support at
all keeps the guess by design, but its mirrored edge is checked for
being a representable double that brackets the coordinates, and falls
back to the extreme grid coordinate with
"mirrored_edge_not_representable" when it is not.
theta_cell_edge_declined is NA where nothing was declined.
outer_grid_cell_width, outer_grid_axis_sd, outer_grid_h_over_sd –
per axis, the cell width and the posterior SD in that axis's own
coordinate, and their ratio. A within-cell reconstruction resolves an
interval endpoint to within one cell, so how much of the reported width
and of its realized coverage is a property of where the grid fell rather
than of the posterior is governed by this ratio; below
.nl_diag("grid_resolved") the cells are narrower than the posterior
they discretize and the two constructions converge to each other. NA on
an axis whose own marginal is maximal at an endpoint, which is the
placement question outer_grid_railed_axes reports.
outer_grid_resolution_declined – per axis, why that axis carries no
ratio, from the closed vocabulary too_few_nodes, mode_at_edge,
coord_not_finite, spacing_not_finite, stencil_degenerate,
curvature_not_negative; NA on an axis that scored. The reasons are
not interchangeable: mode_at_edge says the grid does not contain that
axis's own posterior mode, which is a stronger statement about the fit
than any ratio, and the whole-grid resolved verdict is withheld while
one is present rather than being read off the axes that did score.
prune_cheap_log_marginal, prune_mask, prune_n_pruned,
prune_tol, prune_screen_iters – present only when prune = TRUE
and the safety gate did not fall back. Cheap-pass log-marginals at every
cell – the truncated solve's value plus half its Newton decrement, the
second-order estimate of the converged value the screen ranks by, so a
cell whose inner mode had far to move from its warm start is not ranked
on where the truncation left it (gcol33/tulpa#919); that decrement is
prune_screen_decrement –
a logical mask of pruned cells, the pruned-cell count, the
threshold actually applied, and the screening depth the driver actually
ran at. Pruned cells have log_marginal = -Inf so they get zero
weight under .nl_normalise_weights_safe.
prune_log_gap_cut, prune_cheap_lm_spread, prune_min_keep,
prune_n_floor_restored – the screen read on the scale it operates on:
how many nats below the best screened cell the tolerance cut at, how
many nats the screened surface spans, the kept-cell floor applied, and
how many cells that floor put back. A cut that is a sliver of the spread
is a tolerance with no resolution on this grid.
prune_dropped_mass_bound, prune_n_repaired, prune_gate_mass – the
posterior mass the dropped cells may carry, with each lifted by the
worst screening error measured on a kept cell; how many dropped cells
the driver solved in full to bring that under the gate; and the gate.
outer_mode_carried – TRUE on a placement refit that read the outer
mode it was laid from rather than finding it again, which it does unless
the placed grid rails.
prune_fallback_triggered, prune_fallback_reason – present only when
a fallback to the full grid fired. The returned fit is the full-grid
(unpruned) result; the reason string records whether the cheap-pass
safety gate tripped or the screened grid left the placement pass no
curvature.
n_threads_outer_requested, n_threads_outer_realised – the outer-grid
width asked for and the width the grid ran at. The driver clamps the
request to the team the OpenMP environment hands out, so a fit launched
under OMP_NUM_THREADS=1 with n_threads_outer = 10 runs serial; the
pair is what a timing has to be recorded against.
converged, n_iter, score_max – per cell, whether the inner Newton
reached a mode within control$max_iter, the iterations it took, and
the largest absolute component of the joint penalized score at the
mode it reported (the residual a cell that stopped short stopped at). A cell that
stopped without a mode is solved again from the mode of its nearest
converged cell (nonconverged_resolve counts the cells that first
stalled and those the re-solve recovered); one that still has no mode
stays in the integration at the value it stopped at, is listed in
nonconverged_cells with the posterior mass such cells carry in
nonconverged_mass, and is named by diagnostics(). Past the screen's
mass gate the fit warns.
Each arm's linear predictor reads
where is a unit-precision latent (ICAR(tau=1) for ICAR/BYM2,
or the BYM2 mix; CAR_proper uses the structure of
). All arms share the donor amplitude
from the outer-grid sigma_grid axis. The copy arm's
amplitude is , where
is a direct outer-grid axis taken from copy$alpha_grid. The Cartesian
product is over (sigma, [rho/rho_car,] alpha[, phi]). Direct
as an outer-grid axis (rather than a post-hoc ratio
) avoids plug-in bias on the
weakly-identified ratio at small n_pos and lets a regularizing
hyperprior land on directly.
Rue, Martino & Chopin (2009). Approximate Bayesian inference for latent Gaussian models by using integrated nested Laplace approximations. JRSS-B 71(2):319-392.
tulpa_nested_laplace() for the single-arm engine.
set.seed(1) S <- 25L # shared spatial units (chain graph) nb <- lapply(seq_len(S), function(s) setdiff(c(s - 1L, s + 1L), c(0L, S + 1L))) nn <- lengths(nb) field <- as.numeric(scale(cumsum(rnorm(S, 0, 0.4)))) mk_arm <- function(m, fam) { si <- sample(S, m, replace = TRUE); x <- rnorm(m) lin <- 0.2 + 0.5 * x + 0.8 * field[si] y <- if (fam == "binomial") rbinom(m, 1L, plogis(lin)) else lin + rnorm(m, 0, 0.5) list(y = as.numeric(y), n_trials = rep(1L, m), X = cbind(1, x), spatial_idx = si, family = fam, phi = if (fam == "gaussian") 0.5 else 1) } prior <- list(type = "icar", n_spatial_units = S, adj_row_ptr = c(0L, cumsum(nn)), adj_col_idx = unlist(nb) - 1L, n_neighbors = nn, sigma_grid = c(0.3, 0.7, 1.4)) fit <- tulpa_nested_laplace_joint( responses = list(occ = mk_arm(200L, "binomial"), pos = mk_arm(200L, "gaussian")), prior = prior) fit$theta_mean # shared field amplitude, integrated across both armsset.seed(1) S <- 25L # shared spatial units (chain graph) nb <- lapply(seq_len(S), function(s) setdiff(c(s - 1L, s + 1L), c(0L, S + 1L))) nn <- lengths(nb) field <- as.numeric(scale(cumsum(rnorm(S, 0, 0.4)))) mk_arm <- function(m, fam) { si <- sample(S, m, replace = TRUE); x <- rnorm(m) lin <- 0.2 + 0.5 * x + 0.8 * field[si] y <- if (fam == "binomial") rbinom(m, 1L, plogis(lin)) else lin + rnorm(m, 0, 0.5) list(y = as.numeric(y), n_trials = rep(1L, m), X = cbind(1, x), spatial_idx = si, family = fam, phi = if (fam == "gaussian") 0.5 else 1) } prior <- list(type = "icar", n_spatial_units = S, adj_row_ptr = c(0L, cumsum(nn)), adj_col_idx = unlist(nb) - 1L, n_neighbors = nn, sigma_grid = c(0.3, 0.7, 1.4)) fit <- tulpa_nested_laplace_joint( responses = list(occ = mk_arm(200L, "binomial"), pos = mk_arm(200L, "gaussian")), prior = prior) fit$theta_mean # shared field amplitude, integrated across both arms
Extract posterior distributions of temporal effects from a fitted tulpa model with temporal specification.
## S3 method for class 'tulpa_temporal_field_fit' print(x, ...) ## S3 method for class 'tulpa_temporal_field' print(x, ...) temporal( object, component = "all", summary = FALSE, probs = c(0.025, 0.5, 0.975), ... ) ## S3 method for class 'tulpa_fit' temporal( object, component = "all", summary = FALSE, probs = c(0.025, 0.5, 0.975), ... ) ## S3 method for class 'tulpa_temporal_posterior' print(x, ...) ## S3 method for class 'tulpa_temporal_posterior' summary(object, probs = c(0.025, 0.5, 0.975), ...) ## S3 method for class 'tulpa_temporal_summary' print(x, ...)## S3 method for class 'tulpa_temporal_field_fit' print(x, ...) ## S3 method for class 'tulpa_temporal_field' print(x, ...) temporal( object, component = "all", summary = FALSE, probs = c(0.025, 0.5, 0.975), ... ) ## S3 method for class 'tulpa_fit' temporal( object, component = "all", summary = FALSE, probs = c(0.025, 0.5, 0.975), ... ) ## S3 method for class 'tulpa_temporal_posterior' print(x, ...) ## S3 method for class 'tulpa_temporal_posterior' summary(object, probs = c(0.025, 0.5, 0.975), ...) ## S3 method for class 'tulpa_temporal_summary' print(x, ...)
x |
A |
... |
Ignored |
object |
A |
component |
Which component to extract for multi-scale models:
|
summary |
Logical; if TRUE, return summary statistics instead of full posterior draws. |
probs |
Quantiles to compute if |
temporal() is overloaded. Given a fitted model it is the accessor described
here. Given a one-sided formula (or a named formula = / structure =
argument) it is instead the inline varying-coefficient field constructor used
in a tulpa() model formula, the temporal mirror of spatial():
temporal(formula = ~ 1 + x || time, structure = "rw1") declares a smooth
temporal level (the intercept column) plus a temporally varying slope on each
covariate column. structure is one of "rw1" (default), "rw2", or
"ar1"; only the double bar || (independent fields) is supported.
On a sampler fit the posterior is the field's own draws. On a
nested-Laplace fit it is the Gaussian mixture the outer grid defines: each
hyperparameter cell contributes its conditional mode and marginal variance,
weighted by the cell's posterior weight, and summary() reports that
mixture's exact mean, SD and quantiles. The draws of such a fit are
sampled from the same mixture, each from its cell's joint Gaussian, so they
carry the correlation between time points as well as each point's marginal,
and a quantity derived from them (a trend, a difference between two times)
has the right spread. A field so large that its per-cell covariance was not
retained (more than 4 million values over the grid) is drawn with
independent time points within each cell instead. The time column of the
summary holds the time values the field was fitted on.
A tulpa_temporal_posterior object
summary() on a tulpa_temporal_posterior returns a
tulpa_temporal_summary data frame with one row per time point (per
component for multi-scale fits), holding the posterior mean, SD and
requested quantiles of the temporal effect.
temporal_multiscale(), temporal_rw1()
set.seed(131) df <- data.frame(year = 1:40, x = rnorm(40)) df$count <- rpois(40, exp(1 + 0.2 * df$x)) fit <- tulpa( count ~ x, data = df, family = "poisson", temporal = temporal_multiscale("year", trend = "rw2", seasonal = 12), mode = "exact", control = list(n_iter = 200L, n_warmup = 100L, seed = 1L) ) # Extract all temporal effects temp_post <- temporal(fit) summary(temp_post)set.seed(131) df <- data.frame(year = 1:40, x = rnorm(40)) df$count <- rpois(40, exp(1 + 0.2 * df$x)) fit <- tulpa( count ~ x, data = df, family = "poisson", temporal = temporal_multiscale("year", trend = "rw2", seasonal = 12), mode = "exact", control = list(n_iter = 200L, n_warmup = 100L, seed = 1L) ) # Extract all temporal effects temp_post <- temporal(fit) summary(temp_post)
Specify a beta prior for a parameter bounded in (0, 1).
prior_beta(alpha = 1, beta = 1)prior_beta(alpha = 1, beta = 1)
alpha |
First shape parameter. Must be positive. |
beta |
Second shape parameter. Must be positive. |
alpha = beta = 1: Uniform(0, 1)
alpha = beta = 2: Symmetric, peaked at 0.5
alpha > beta: Skewed toward 1
alpha < beta: Skewed toward 0
A tulpa_prior object
prior_beta(1, 1) # Uniform prior_beta(2, 2) # Symmetric, centered at 0.5 prior_beta(5, 2) # Skewed toward 1 (for high autocorrelation)prior_beta(1, 1) # Uniform prior_beta(2, 2) # Symmetric, centered at 0.5 prior_beta(5, 2) # Skewed toward 1 (for high autocorrelation)
Specify an exponential prior for a positive parameter.
prior_exponential(rate = 1)prior_exponential(rate = 1)
rate |
Rate parameter (lambda). Must be positive. |
A tulpa_prior object
prior_exponential(1)prior_exponential(1)
prior list for tulpa_nested_laplace() from a tulpa spec objectValidates a tulpa_temporal or tulpa_spatial specification against
data, then converts it to the prior list shape consumed by
tulpa_nested_laplace(). Mainly an internal helper for callers that already
have a fitted spec; users typically pass spec + data directly to
tulpa_nested_laplace() instead.
Supported spec types:
tulpa_temporal with type in {"rw1", "rw2", "ar1"}
tulpa_spatial, areal type in {"car", "icar", "car_proper", "bym2"}.
An areal spec returns the graph fields only – type, spatial_idx,
n_spatial_units, adj_row_ptr, adj_col_idx, n_neighbors – plus
scale_factor for bym2 and the eigenvalue-derived rho_bounds for
proper CAR. No grid field is returned for any areal type: the outer axes
are built later by the family's registry defaults() closure, and proper
CAR's (tau, rho) grid is built there from those bounds.
tulpa_gp / tulpa_hsgp, continuous type in {"gp", "nngp", "hsgp"}:
validated against data and routed to the nngp / hsgp nested kernel
through the shared converter.
SPDE is the one continuous field not built here – it carries its own
(range, sigma) FEM integrator; call fit_spde() with the spatial_spde()
spec.
prior_from_spec(spec, data)prior_from_spec(spec, data)
spec |
A |
data |
Data frame the spec resolves time/group/site indices against. |
A prior list ready for tulpa_nested_laplace().
d <- data.frame(year = rep(1:10, each = 3)) pr <- prior_from_spec(temporal_ar1("year"), d) str(pr)d <- data.frame(year = rep(1:10, each = 3)) pr <- prior_from_spec(temporal_ar1("year"), d) str(pr)
Specify a gamma prior for a positive parameter.
prior_gamma(shape = 2, rate = 0.1)prior_gamma(shape = 2, rate = 0.1)
shape |
Shape parameter (alpha). Must be positive. |
rate |
Rate parameter (beta). Must be positive. |
Mean = shape/rate, Variance = shape/rate^2.
A tulpa_prior object
prior_gamma(2, 0.1) # Mean = 20, weakly informative prior_gamma(1, 1) # Exponential(1)prior_gamma(2, 0.1) # Mean = 20, weakly informative prior_gamma(1, 1) # Exponential(1)
Specify a half-Cauchy prior for a positive parameter. Has heavier tails than half-normal, often used for variance parameters.
prior_half_cauchy(scale = 2.5)prior_half_cauchy(scale = 2.5)
scale |
Scale parameter. Must be positive. |
A tulpa_prior object
prior_half_cauchy(2.5)prior_half_cauchy(2.5)
Specify a half-normal (truncated at 0) prior for a positive parameter.
prior_half_normal(sd = 1)prior_half_normal(sd = 1)
sd |
Scale parameter. Must be positive. |
A tulpa_prior object
prior_half_normal(1)prior_half_normal(1)
Specify a normal prior for a parameter.
prior_normal(mean = 0, sd = 2.5)prior_normal(mean = 0, sd = 2.5)
mean |
Prior mean. Default 0. |
sd |
Prior standard deviation. Must be positive. |
A tulpa_prior object
prior_normal(0, 2.5) prior_normal(0, 1)prior_normal(0, 2.5) prior_normal(0, 1)
Specify a PC prior for a positive parameter (typically a standard deviation). PC priors shrink toward simpler models by penalizing deviation from a base model.
prior_pc(U = 1, alpha = 0.01)prior_pc(U = 1, alpha = 0.01)
U |
Upper bound. P(x > U) = alpha. |
alpha |
Tail probability. Default 0.01. |
The PC prior is specified via: P(sigma > U) = alpha
U is the upper bound you consider "large"
alpha is the probability of exceeding it
This implies an exponential prior with rate = -log(alpha) / U.
A tulpa_prior object
Simpson, D., Rue, H., Riebler, A., Martins, T. G., & Sorbye, S. H. (2017). Penalising model component complexity: A principled, practical approach to constructing priors. Statistical Science, 32(1), 1-28.
prior_pc(U = 1, alpha = 0.01) # P(sigma > 1) = 0.01 prior_pc(U = 0.5, alpha = 0.05) # Tighter, P(sigma > 0.5) = 0.05prior_pc(U = 1, alpha = 0.01) # P(sigma > 1) = 0.01 prior_pc(U = 0.5, alpha = 0.05) # Tighter, P(sigma > 0.5) = 0.05
Draw datasets from the prior predictive distribution: parameters are sampled from their priors (no data conditioning) and pushed through the model's linear predictor and the family's simulator.
Useful for checking whether priors imply plausible data ranges before fitting.
prior_predict( formula, family, data, priors = NULL, n_draws = 100, seed = NULL, ... ) ## S3 method for class 'tulpa_prior_predict' print(x, ...)prior_predict( formula, family, data, priors = NULL, n_draws = 100, seed = NULL, ... ) ## S3 method for class 'tulpa_prior_predict' print(x, ...)
formula |
A model formula (e.g., |
family |
A |
data |
Data frame containing covariates and grouping factors. Used for dimensions and design matrices; the response column may be absent or NA. |
priors |
Prior specification ( |
n_draws |
Number of prior parameter draws. Default 100. |
seed |
Optional integer seed for reproducibility. |
... |
Passed to |
x |
A |
A tulpa_prior_predict object: a list with
y: list of length n_draws, each element the simulated response for
that draw (matching whatever shape family$simulate_fn returns).
theta: list of length n_draws of parameter draws (beta, sigma,
RE coefficients u, family-specific extras).
linpred: list of length n_draws, each a list of linear predictor
vectors per process.
family: the family used.
n_draws, n_obs.
# Toy Gaussian family for illustration fam <- tulpa_family( name = "gaussian", simulate_fn = function(eta, params, n_obs, ...) { rnorm(n_obs, eta[[1]], params$sigma_y) }, extra_params = list(sigma_y = prior_half_normal(1)) ) df <- data.frame(y = rep(0, 20), x = rnorm(20)) pp <- prior_predict(y ~ x, fam, df, n_draws = 50, seed = 1) length(pp$y) # 50# Toy Gaussian family for illustration fam <- tulpa_family( name = "gaussian", simulate_fn = function(eta, params, n_obs, ...) { rnorm(n_obs, eta[[1]], params$sigma_y) }, extra_params = list(sigma_y = prior_half_normal(1)) ) df <- data.frame(y = rep(0, 20), x = rnorm(20)) pp <- prior_predict(y ~ x, fam, df, n_draws = 50, seed = 1) length(pp$y) # 50
Display the default tulpa_priors() specification: the priors the
simulation tools (prior_predict(), tulpa_simulate()) draw from, and a
starting point for customizing them. tulpa() does not read a
tulpa_priors object; the no-family printout closes with the defaults a
tulpa() fit does use, which differ for the random-effect SDs (see
tulpa_priors()).
priors_default(family = NULL, spatial = FALSE, temporal = FALSE)priors_default(family = NULL, spatial = FALSE, temporal = FALSE)
family |
|
spatial |
Logical; if TRUE, include spatial priors. Default FALSE. |
temporal |
Logical; if TRUE, include temporal priors. Default FALSE. |
The family-specific overdispersion lines name the ratio families a model
package registers (negbin_negbin, negbin_gamma, poisson_gamma,
gamma_gamma, beta_binomial); they are not among tulpa's own
family_names().
Default priors in tulpa_priors() follow these principles:
Fixed effects (beta): Normal(0, 2.5) - weakly informative, allows coefficients roughly in [-5, 5] on the link scale.
Random effect SD (sigma): PC prior with P(sigma > 1) = 0.01 - favors simpler models with smaller variance components.
Overdispersion (phi): PC prior with P(phi > 10) = 0.01 on the NB2 size
phi (larger phi is less overdispersion; phi -> Inf is the Poisson
limit). The default keeps phi finite, allowing overdispersion while
penalising extreme values.
Temporal correlation (rho): Beta(2, 2) - symmetric prior centered at 0.5, appropriate for AR(1) correlation.
Spatial mixing (rho_spatial): Beta(1, 1) = Uniform(0, 1) - no prior preference for structured vs. unstructured spatial variation.
Invisibly returns a tulpa_priors object with the defaults.
Primarily called for its side effect of printing.
tulpa_priors() for creating custom priors
# Defaults for all families (no family argument) priors_default() # Family-specific defaults take a tulpa_family object. Model packages # (e.g. tulpaRatio) register rich families; a minimal one is enough here. fam <- tulpa_family( name = "poisson_gamma", simulate_fn = function(eta, params, n_obs, ...) rpois(n_obs, exp(eta[[1]])) ) priors_default(fam) # Including spatial parameters priors_default(fam, spatial = TRUE) # Use as a starting point for customization my_priors <- priors_default(fam) my_priors$beta <- prior_normal(0, 1) # Tighter prior on fixed effects# Defaults for all families (no family argument) priors_default() # Family-specific defaults take a tulpa_family object. Model packages # (e.g. tulpaRatio) register rich families; a minimal one is enough here. fam <- tulpa_family( name = "poisson_gamma", simulate_fn = function(eta, params, n_obs, ...) rpois(n_obs, exp(eta[[1]])) ) priors_default(fam) # Including spatial parameters priors_default(fam, spatial = TRUE) # Use as a starting point for customization my_priors <- priors_default(fam) my_priors$beta <- prior_normal(0, 1) # Tighter prior on fixed effects
Random-effect summaries
ranef(object, ...) ## S3 method for class 'tulpa_fit' ranef(object, ...)ranef(object, ...) ## S3 method for class 'tulpa_fit' ranef(object, ...)
object |
A |
... |
Ignored. |
What each backend reports for a group effect follows what it computes:
Sampler tier and the RE-covariance Gibbs debias
(tulpa_re_cov_gibbs(), reached by tulpa(..., control = list(re_cov = "gibbs"))) draw the random effects jointly with everything
else, so estimate / sd / bounds are the empirical posterior summaries.
The RE-covariance integrator (tulpa_re_cov_nested()) carries a
Gaussian per-group posterior at each Sigma node; the reported summaries
are the exact moments and quantiles of the weighted mixture of those, so
they carry both the within-node curvature and the Sigma uncertainty.
A group effect the subspace debias selected
(control$subspace_debias) is moved by the Metropolis sampler at every
node instead, and is reported from those draws.
The Laplace tier reports the conditional mode with no spread (sd and
the bounds are NA), which is the only per-group quantity it forms.
The source column says per row which of these produced it: "sampled" for
a posterior draw summary, "mixture" for the node mixture, "mode" for a
conditional mode.
A fit whose backend never forms a per-group posterior at all (the adaptive
Gauss-Hermite inner marginal integrates each group out by quadrature) errors
with that reason rather than returning an empty table, which would be
indistinguishable from a model with no random effects. A model that genuinely
has none returns a zero-row data frame.
Data frame with one row per random-effect coefficient: term (the
group level, and the coefficient for a random slope), estimate, sd,
the 2.5% / 97.5% bounds conf.low / conf.high, and source (which
construction the row came from). sd and the bounds are NA on a backend
that reports a point per group (see Details).
field_effects() for a spatial field, smooth_effects() for an
s(x) term.
set.seed(1) df <- data.frame(x = rnorm(100), g = factor(rep(1:10, 10))) df$y <- rpois(100, exp(0.3 * df$x)) fit <- tulpa(y ~ x + (1 | g), data = df, family = "poisson") ranef(fit)set.seed(1) df <- data.frame(x = rnorm(100), g = factor(rep(1:10, 10))) df$y <- rpois(100, exp(0.3 * df$x)) fit <- tulpa(y ~ x + (1 | g), data = df, family = "poisson") ranef(fit)
Construct the default weakly-informative hyperprior used by
tulpa_re_cov_nested() for one covariance block: independent
Penalized-Complexity (PC) priors on the marginal standard deviations
sigma_i together with an LKJ prior on the correlation matrix R
(correlated block) or no correlation (diagonal block), returned as a
log_prior_theta function in the block's integration coordinates.
re_cov_pc_lkj_prior(n_coefs, prior_sigma = NULL, eta = NULL, correlated = TRUE)re_cov_pc_lkj_prior(n_coefs, prior_sigma = NULL, eta = NULL, correlated = TRUE)
n_coefs |
Number of coefficients |
prior_sigma |
|
eta |
LKJ shape. |
correlated |
|
For a correlated block the prior is specified on the natural scale,
p(sigma, R) = LKJ(R | eta) * prod_i PC(sigma_i), then pushed to the
log-Cholesky coordinates theta of Sigma = L L' by the exact
change-of-variables Jacobian. For a diagonal (uncorrelated) block the LKJ
factor drops and theta_i = log sigma_i with Jacobian sum_i theta_i.
PC prior (Simpson et al. 2017) on each marginal SD: exponential with rate
lambda = -log(alpha) / U, so P(sigma_i > U) = alpha – the
prior_sigma = c(U, alpha) convention also used by the SPDE prior in tulpa.
LKJ prior (Lewandowski et al. 2009) on the correlation matrix:
p(R) = det(R)^(eta - 1) / c_d(eta). eta = 1 is uniform over correlation
matrices; eta > 1 concentrates toward the identity. The normalizing
constant is kept, so the prior is a density and a grid's evidence can be read
under it.
Jacobian (correlated block): with theta packing log L_ii on the diagonal
and the raw strict-lower entries of L, the change of variables from
(sigma, R) to theta adds sum_i (c + 2 - i) * log L_ii - c * sum_i log sigma_i to log p(sigma, R). (Composition of the log-diagonal map, the
standard Cholesky-to-covariance Jacobian 2^c prod_i L_ii^(c+1-i), and the
covariance-to-(sigma, R) Jacobian; verified against numerical
differentiation in test-re-cov-prior.R.)
A function(theta) returning the scalar log prior density in the
block's integration coordinates, suitable for one block of the
log_prior_theta argument of tulpa_re_cov_nested().
# Log prior of a correlated 2 x 2 block at the origin of its log-Cholesky # coordinates (log sd_1, log sd_2, L_21): lp <- re_cov_pc_lkj_prior(2) lp(c(0, 0, 0)) # A diagonal block takes one log-SD coordinate per coefficient: re_cov_pc_lkj_prior(2, correlated = FALSE)(c(0, 0))# Log prior of a correlated 2 x 2 block at the origin of its log-Cholesky # coordinates (log sd_1, log sd_2, L_21): lp <- re_cov_pc_lkj_prior(2) lp(c(0, 0, 0)) # A diagonal block takes one log-SD coordinate per coefficient: re_cov_pc_lkj_prior(2, correlated = FALSE)(c(0, 0))
Population-level residuals from the fixed-effect fitted mean: "response"
is y - E[y | eta] on the response scale (trial-scaled for binomial,
offset included); "pearson" additionally scales by the family standard
deviation sqrt(Var(y | eta)) at the fitted linear predictor. Random
effects are held at zero, matching fitted(). On a zero-inflated fit both
the mean and the variance are the mixture's.
## S3 method for class 'tulpa_fit' residuals(object, type = c("pearson", "response"), ...)## S3 method for class 'tulpa_fit' residuals(object, type = c("pearson", "response"), ...)
object |
A |
type |
|
... |
Ignored. |
Numeric vector of length nobs(object).
Pools per-imputation block fits via Rubin's rules. When every draw also
carries a per-coefficient skewness vector $gamma, the third cumulant
is pooled by the law of total cumulants
giving a pooled skewness $gamma alongside the usual $mean / $se.
If any draw is missing $gamma, the third-cumulant path is skipped.
kappa3 is the third cumulant of the equal-weight mixture of the draws, so
gamma is standardized by that mixture's variance,
mean(se^2) + mean((beta - mean(beta))^2), not by V_total: Rubin's
(1 + 1/K) inflation of the between-draw variance is a correction to the
standard error for finite K, not a moment of the mixture.
rubins_pool(draws)rubins_pool(draws)
draws |
List of K draws. Each draw is a named list of submodel
results, each with |
A named list of pooled submodel summaries (every submodel any draw
carries), each with mean, se, V_within, V_between, V_total, K
(number of draws that contributed), and when applicable gamma (pooled
skewness) and kappa3 (pooled third cumulant).
# Three imputation draws of a two-coefficient submodel. draws <- list( list(m = list(beta = c(1.0, 0.50), se = c(0.2, 0.10))), list(m = list(beta = c(1.2, 0.45), se = c(0.2, 0.10))), list(m = list(beta = c(0.9, 0.55), se = c(0.2, 0.10))) ) pooled <- rubins_pool(draws) pooled$m$mean pooled$m$se# Three imputation draws of a two-coefficient submodel. draws <- list( list(m = list(beta = c(1.0, 0.50), se = c(0.2, 0.10))), list(m = list(beta = c(1.2, 0.45), se = c(0.2, 0.10))), list(m = list(beta = c(0.9, 0.55), se = c(0.2, 0.10))) ) pooled <- rubins_pool(draws) pooled$m$mean pooled$m$se
Scores whether an inference algorithm's posterior is CALIBRATED, by reading the whole marginal CDF rather than one or two nominal levels. Draw a truth from the distribution the fit updates, simulate a data set at that truth, fit, and take the probability integral transform (PIT) of the truth under the reported posterior. Under exact inference those PIT values are exactly Uniform(0, 1), so the entire ECDF is the measurement (Talts et al. 2018).
sbc(object, ...) ## Default S3 method: sbc(object, ...) ## S3 method for class 'character' sbc( object = c("prior_predictive", "posterior"), simulator = NULL, fitter = NULL, model = NULL, n_sim = 100L, quantities = NULL, flat_prior = character(), level = 0.95, seed = 0L, n_ref = NULL, control = list(), ... ) ## S3 method for class 'sbc' print(x, ...) ## S3 method for class 'sbc' summary(object, baseline = NULL, ...) ## S3 method for class 'sbc_summary' print(x, ...) ## S3 method for class 'sbc' plot(x, arm = NULL, quantity = NULL, folded = FALSE, ...)sbc(object, ...) ## Default S3 method: sbc(object, ...) ## S3 method for class 'character' sbc( object = c("prior_predictive", "posterior"), simulator = NULL, fitter = NULL, model = NULL, n_sim = 100L, quantities = NULL, flat_prior = character(), level = 0.95, seed = 0L, n_ref = NULL, control = list(), ... ) ## S3 method for class 'sbc' print(x, ...) ## S3 method for class 'sbc' summary(object, baseline = NULL, ...) ## S3 method for class 'sbc_summary' print(x, ...) ## S3 method for class 'sbc' plot(x, arm = NULL, quantity = NULL, folded = FALSE, ...)
object |
An |
... |
Ignored. |
simulator, fitter
|
The prior-predictive callbacks; see the contract
above. Required for |
model |
The list of posterior-SBC callbacks. Required for
|
n_sim |
Number of simulations. |
quantities |
Optional character vector restricting which quantities are scored. Default scores every quantity every arm reports. |
flat_prior |
Character vector of scored quantities held FIXED under a flat prior, admitted by a structural argument the caller is asserting. Prior-predictive only. |
level |
Simultaneous band level. |
seed |
Offset added to the simulation index, so simulation |
n_ref |
Optional; when supplied it is passed to |
control |
List of knobs: |
x |
An |
baseline |
Optional arm name. When given, the summary also carries the
PAIRED CRPS differences of every other arm against it, seed by seed. A
negative |
arm, quantity
|
Optional character vectors selecting which panels to draw. Default draws every (arm, quantity). |
folded |
Plot the folded PIT instead of the raw one. |
An object of class sbc:
pitone row per (simulation, arm, quantity): the truth, the raw and folded PIT, the CRPS and the predictive kind.
reportone row per (arm, quantity): the KS distance, whether the ECDF stayed inside the simultaneous band, the exact uniformity p-value, the same three folded, and the mean CRPS with its standard error.
bandsthe calibrated simultaneous band, by sample size.
premiseswhat each guard concluded.
crps_rolethe role the CRPS column has under this experiment.
"prior_predictive"ordinary SBC. theta ~ p(theta),
y ~ p(y | theta), fit, PIT. It reports self-consistency AVERAGED over
the whole generative distribution.
"posterior"calibration CONDITIONAL on an observed data set
(Sailynoja et al. 2026, Algorithm 2). theta' ~ pi(theta | y_obs),
y ~ pi(y | theta'), and the PIT is taken under the AUGMENTED posterior
pi(theta | y, y_obs). That is ordinary SBC with pi(theta | y_obs) in
the role of the prior, so the same band, the same folded read and the
same proper score all carry over – and it needs no proper prior.
A fixed-truth sweep is not an SBC experiment and is not offered here: its PIT is not uniform under correct inference and its CRPS is a descriptive loss, not a proper posterior score.
Three reads of the same PIT sample, per (arm, quantity):
the PIT ECDF against an exact SIMULTANEOUS band. A pointwise binomial band is not simultaneous – at n = 100, holding each order statistic at 95 percent holds all of them together at 0.4471 – so the band here is calibrated by bisection against the exact crossing probability of the uniform order statistics.
2 |u - 1/2|, also uniform, and where a symmetric
over- or under-dispersion shows after cancelling in the raw ECDF.
the strictly proper score, closed form for the nested tier's
own Gaussian mixture, paired seed by seed through
summary(x, baseline = ).
Every discrete PIT (a rank, a grid axis, a draw set) is randomized within its
atom, u = F(theta^-) + V P(theta), so one uniform reference and one band
serve every quantity. Reading rank / n_ref against a continuous uniform is
the classic silent SBC bug.
For experiment = "prior_predictive":
simulator(seed)returns a list carrying theta, a named numeric
vector of the true values of the scored quantities, plus whatever the
fitter needs. It must be a pure function of its seed.
fitter(d)returns a named list of ARMS, each a named list over
quantities, each entry a predictive built by sbc_mixture() and its
siblings. Several arms read off one solve per seed, so an arm-to-arm
difference carries no fit-to-fit noise.
For experiment = "posterior", model is a list of six callbacks –
data_obs, fit(data), draw_theta(fit, seed), simulate(theta, seed),
pool(data_obs, replicate), arms(fit, data) – plus an optional
group_ids(data) used to verify the fresh-groups premise below. The driver
hands draw_theta and simulate DIFFERENT seeds, so set.seed(seed) at the
top of each is the correct fixture; sharing one makes the replicate's noise a
function of the truth, which is not p(y | theta').
The prior-predictive experiment draws the truth from the prior, so an
IMPROPER prior cannot be used – and the nested-Laplace door puts no prior on
the fixed effects. A scored quantity whose truth does not move across
simulations was not drawn from a proper prior, and sbc() errors on it
before spending the fits, pointing at experiment = "posterior". A location
parameter whose flat prior leaves the PIT uniform by a structural argument is
admitted through flat_prior, which is itself checked and travels on the
result.
The posterior experiment rests on two premises, each of which silently
invalidates the result when broken. The augmented posterior must condition on
BOTH data sets, so a pool() returning no more than the replicate (or no
more than the observed data) is refused. And the replicate must be
conditionally independent of the observed data given theta, which for a
hierarchical model whose group effects are integrated out means FRESH groups.
Supply group_ids and the observable half of that is verified – the group
LABELS are disjoint – and omit it and the result records the premise as
unverified rather than assumed. The other half, that the simulator drew those
groups' effects from the prior rather than conditionally on the observed
data, is not visible from outside the callback and is not claimed.
Talts, Betancourt, Simpson, Vehtari & Gelman (2018). Validating Bayesian inference algorithms with simulation-based calibration. arXiv:1804.06788.
Sailynoja, Schmitt, Buerkner & Vehtari (2026). Posterior SBC: simulation-based calibration checking conditional on data. Statistics and Computing 36:78. doi:10.1007/s11222-026-10825-9
Grimit, Gneiting, Berrocal & Johnson (2006). The continuous ranked probability score for circular variables and its application to mesoscale forecast ensemble verification. QJRMS 132(621C):2925-2942.
sbc_mixture() for the predictive shapes a fitter reports,
diagnostics() for the single-fit reliability band that screens what this
measures, pit_residuals() for single-fit posterior-predictive PIT
residuals (a different quantity).
# A conjugate normal-normal model, whose posterior is exact, so its PIT must # be uniform: the harness scoring itself. sim <- function(seed) { set.seed(seed) mu <- rnorm(1) list(y = rnorm(10L, mu, 1), theta = c(mu = mu)) } fitter <- function(d) { v <- 1 / (1 + length(d$y)) list(exact = list(mu = sbc_normal(v * sum(d$y), sqrt(v))), narrow = list(mu = sbc_normal(v * sum(d$y), sqrt(v) / 2))) } res <- sbc("prior_predictive", simulator = sim, fitter = fitter, n_sim = 60L) res summary(res, baseline = "exact")# A conjugate normal-normal model, whose posterior is exact, so its PIT must # be uniform: the harness scoring itself. sim <- function(seed) { set.seed(seed) mu <- rnorm(1) list(y = rnorm(10L, mu, 1), theta = c(mu = mu)) } fitter <- function(d) { v <- 1 / (1 + length(d$y)) list(exact = list(mu = sbc_normal(v * sum(d$y), sqrt(v))), narrow = list(mu = sbc_normal(v * sum(d$y), sqrt(v) / 2))) } res <- sbc("prior_predictive", simulator = sim, fitter = fitter, n_sim = 60L) res summary(res, baseline = "exact")
The tagged representations sbc() reads. A fitter (or a posterior-SBC
arms) callback returns a named list of ARMS, each a named list over
quantities, and each entry is one of these – the shape the backend actually
reports for that quantity. Everything downstream (the PIT, the CRPS, drawing
from a predictive) dispatches on the kind tag, so a new backend shape is
one entry in three switches rather than a parallel scorer.
sbc_mixture(mu, var, w = NULL) sbc_normal(mean, sd) sbc_discrete(support, probs) sbc_rank(rank, n_ref) sbc_draws(x)sbc_mixture(mu, var, w = NULL) sbc_normal(mean, sd) sbc_discrete(support, probs) sbc_rank(rank, n_ref) sbc_draws(x)
mu, var, w
|
Component means, variances ( |
mean, sd
|
Mean and standard deviation ( |
support, probs
|
Finite support and its non-negative probabilities, one per support point; normalized. |
rank, n_ref
|
The rank in |
x |
Posterior draws. |
These are the extension point, not alternative front doors: sbc() is the
verb, and these are the argument type it consumes.
sbc_mixture() is what an outer hyperparameter grid defines for a fixed
effect – component k is N(mu_k, var_k) with weight w_k, which is
exactly the mixture a nested-Laplace fit reports. sbc_normal() is the
one-component case, with its own constructor so a collapsed-moment read says
what it is. sbc_discrete() is a distribution on a finite support, which is
what a discrete hyperparameter grid defines for its own axis. sbc_rank() is
a rank of the truth among n_ref reference values, which is what a joint
log-likelihood comparison against posterior draws produces – it needs no
entry in the simulator's theta, since the comparison against the truth
already happened when the rank was formed. sbc_draws() is for a backend
reporting no analytic marginal.
The last three have ATOMS, so their PIT is randomized within the atom by
sbc(); reading a rank against a continuous uniform is the classic silent
SBC bug.
A list carrying a kind tag and that shape's parameters.
sbc_normal(0.3, 0.1) sbc_mixture(mu = c(0, 1), var = c(1, 4), w = c(0.7, 0.3)) sbc_discrete(support = c(0.5, 1, 2), probs = c(0.2, 0.5, 0.3))sbc_normal(0.3, 0.1) sbc_mixture(mu = c(0, 1), var = c(1, 4), w = c(0.7, 0.3)) sbc_discrete(support = c(0.5, 1, 2), probs = c(0.2, 0.5, 0.3))
Drops per-element latent-field entries (names ending in a bracketed index,
e.g. u[12], w[3]) so plots and summaries focus on scalar coefficients
and hyperparameters rather than thousands of latent values.
select_main_params(param_names)select_main_params(param_names)
param_names |
Character vector of parameter names. |
The subset of param_names that are not bracketed-index entries
(or the full vector if that would be empty).
select_main_params(c("beta[1]", "beta[2]", "log_sigma_re", "re[1]", "re[2]"))select_main_params(c("beta[1]", "beta[2]", "log_sigma_re", "re[1]", "re[2]"))
Base-R alias for posterior_predict(): each simulation is one posterior
predictive replicate at the training data. A categorical fit
(tulpa_multinomial(), tulpa_ordinal()) simulates factor columns carrying
the response levels.
## S3 method for class 'tulpa_fit' simulate(object, nsim = 1, seed = NULL, ...)## S3 method for class 'tulpa_fit' simulate(object, nsim = 1, seed = NULL, ...)
object |
A |
nsim |
Number of simulated datasets (default 1). |
seed |
Optional integer seed (RNG state is restored on exit). |
... |
Ignored. |
A data frame with nsim columns (sim_1, ...), one row per
observation, following the stats::simulate() convention: its "seed"
attribute is the value of seed (with the RNG kind as attribute "kind")
when one was given, else the state of .Random.seed before simulation.
The grid-marginalized posterior mean of each s(x) term's latent values,
one estimate per node (bin midpoint or unique covariate value). The latent
block values are read from the fit's per-grid modes, weighted by the
hyperparameter grid weights.
smooth_effects(object, term = 1L)smooth_effects(object, term = 1L)
object |
A |
term |
Which smoother: index or covariate name. Default 1. |
A data frame with columns x (node location) and estimate (the
posterior-mean smooth at that node), with the covariate name as an
attribute "var".
field_effects() for a spatial field, ranef() for random
effects.
set.seed(1) d <- data.frame(x = runif(300, -2, 2)) d$y <- rpois(300, exp(0.3 + sin(2 * d$x))) fit <- tulpa(y ~ s(x), data = d, family = "poisson") sm <- smooth_effects(fit) plot(sm$x, sm$estimate, type = "l")set.seed(1) d <- data.frame(x = runif(300, -2, 2)) d$y <- rpois(300, exp(0.3 + sin(2 * d$x))) fit <- tulpa(y ~ s(x), data = d, family = "poisson") sm <- smooth_effects(fit) plot(sm$x, sm$estimate, type = "l")
Declare one or more areal (CAR / Besag) fields over a graph from an lme4-style bar formula. The bar's left-hand side lists the coefficients that vary smoothly over the graph; the right-hand side names the graph-node index. Each coefficient becomes an independent CAR field, entering the linear predictor scaled by that coefficient's per-observation design value:
where is the graph node of observation , is
the CAR field for design column , and is that column's
value at observation . The intercept column is all ones, so
~ 1 || cell is the ordinary spatial intercept field; a covariate column
(e.g. time) gives a spatially varying slope on that covariate (a
per-region trend).
Use it inline in a tulpa() model formula, the same way a random-effect
bar is written:
y ~ time + spatial(graph = adj, formula = ~ 1 + time || cell) + (1 | site)
spatial(graph, formula, proper = FALSE, shared = NULL, by = NULL) ## S3 method for class 'tulpa_spatial_field_fit' print(x, ...) ## S3 method for class 'tulpa_spatial_field' print(x, ...)spatial(graph, formula, proper = FALSE, shared = NULL, by = NULL) ## S3 method for class 'tulpa_spatial_field_fit' print(x, ...) ## S3 method for class 'tulpa_spatial_field' print(x, ...)
graph |
Symmetric adjacency matrix of the spatial graph
( |
formula |
One-sided formula carrying a grouping bar, e.g.
|
proper |
Logical; |
shared |
Optional shared-effect handle, passed through to the field
blocks (see the model docs). Default |
by |
Optional replicated-CAR factor: a bare column name (or a string)
naming a factor in the model data. With |
x |
A |
... |
Ignored. |
Each field is independent (its own precision), matching INLA's two
separate f(cell, model = "besag") and f(cell.slope, time, model = "besag") fields. Nesting (a / b), interaction, or expression
grouping is rejected: the grouping must be a single graph node. Add
ordinary nested random effects, e.g. (1 | site), as separate terms.
A tulpa_spatial_field object describing the field(s). It is
expanded into one CAR block per design column at fit time, when the data
is available.
spatial_car() for a single areal field passed via the
spatial = argument, spatial_svc() for the coordinate-based
(Gaussian-process) spatially varying coefficient.
# Chain graph over 10 cells adj <- matrix(0, 10, 10) for (i in 1:9) adj[i, i + 1] <- adj[i + 1, i] <- 1 # Spatial intercept plus a spatially varying time slope f <- spatial(graph = adj, formula = ~ 1 + time || cell) print(f)# Chain graph over 10 cells adj <- matrix(0, 10, 10) for (i in 1:9) adj[i, i + 1] <- adj[i + 1, i] <- 1 # Spatial intercept plus a spatially varying time slope f <- spatial(graph = adj, formula = ~ 1 + time || cell) print(f)
Specify a Besag-York-Mollie 2 (BYM2) spatial random effect. BYM2 decomposes the spatial effect into a structured (ICAR) component and an unstructured (IID) component, with a mixing parameter controlling the proportion of variance attributable to spatial structure.
BYM2 is preferred over plain CAR when you want to:
Distinguish structured vs unstructured spatial variation
Have an interpretable spatial fraction parameter
Use the scaling from Riebler et al. (2016)
spatial_bym2( adjacency, level = c("group", "obs"), group_var = NULL, shared = NULL, scale_factor = NULL, parameterization = c("standard", "collapsed") )spatial_bym2( adjacency, level = c("group", "obs"), group_var = NULL, shared = NULL, scale_factor = NULL, parameterization = c("standard", "collapsed") )
adjacency |
Symmetric adjacency matrix ( |
level |
Either |
group_var |
Name of the grouping variable in the data; required
when |
shared |
Optional shared-effect handle (see model docs). |
scale_factor |
Scaling factor for the ICAR component. If NULL (default), computed from the adjacency matrix following Riebler et al. On a graph with several connected components each component is scaled to unit generalized variance separately and an isolated node (island) gets unit variance on its structured part, following Freni-Sterrantino et al. (2018). A supplied value is one scale for the whole graph. |
parameterization |
|
A tulpa_spatial object
Riebler, A., Sorbye, S. H., Simpson, D., & Rue, H. (2016). An intuitive Bayesian spatial model for disease mapping that accounts for scaling. Statistical Methods in Medical Research, 25(4), 1145-1165.
Freni-Sterrantino, A., Ventrucci, M., & Rue, H. (2018). A note on intrinsic conditional autoregressive models for disconnected graphs. Spatial and Spatio-temporal Epidemiology, 26, 25-34.
# Create adjacency matrix for 10 regions (chain structure) adj <- matrix(0, 10, 10) for (i in 1:9) { adj[i, i+1] <- adj[i+1, i] <- 1 } # Create BYM2 spatial structure bym2 <- spatial_bym2(adj, level = "group", group_var = "region") print(bym2) # Disease mapping with BYM2 spatial smoothing set.seed(456) n_regions <- 10 epi_data <- data.frame( region = factor(rep(1:n_regions, each = 4)), age = rnorm(n_regions * 4, 50, 10) ) epi_data$cases <- rbinom(nrow(epi_data), size = 100, prob = 0.15) fit <- tulpa( cases ~ age + spatial(region), spatial = spatial_bym2(adj, level = "group", group_var = "region"), data = epi_data, family = "binomial", n_trials = rep(100L, nrow(epi_data)), mode = "laplace" ) summary(fit)# Create adjacency matrix for 10 regions (chain structure) adj <- matrix(0, 10, 10) for (i in 1:9) { adj[i, i+1] <- adj[i+1, i] <- 1 } # Create BYM2 spatial structure bym2 <- spatial_bym2(adj, level = "group", group_var = "region") print(bym2) # Disease mapping with BYM2 spatial smoothing set.seed(456) n_regions <- 10 epi_data <- data.frame( region = factor(rep(1:n_regions, each = 4)), age = rnorm(n_regions * 4, 50, 10) ) epi_data$cases <- rbinom(nrow(epi_data), size = 100, prob = 0.15) fit <- tulpa( cases ~ age + spatial(region), spatial = spatial_bym2(adj, level = "group", group_var = "region"), data = epi_data, family = "binomial", n_trials = rep(100L, nrow(epi_data)), mode = "laplace" ) summary(fit)
Constructs a conditional autoregressive spatial random effect from an
adjacency matrix. With proper = FALSE (the default) this is the
improper CAR / ICAR (type = "car"); with proper = TRUE it returns
the same object as spatial_car_proper().
spatial_car( adjacency, level = c("group", "obs"), group_var = NULL, proper = FALSE, shared = NULL, parameterization = c("standard", "collapsed") )spatial_car( adjacency, level = c("group", "obs"), group_var = NULL, proper = FALSE, shared = NULL, parameterization = c("standard", "collapsed") )
adjacency |
Symmetric adjacency matrix ( |
level |
Either |
group_var |
Name of the grouping variable in the data; required
when |
proper |
If |
shared |
Optional shared-effect handle (see model docs). |
parameterization |
|
A tulpa_spatial object with type = "car" (or
"car_proper" when proper = TRUE).
spatial_car_proper(), spatial_bym2().
adj <- matrix(0, 10, 10) for (i in 1:9) adj[i, i + 1] <- adj[i + 1, i] <- 1 spatial_car(adj, level = "group", group_var = "region")adj <- matrix(0, 10, 10) for (i in 1:9) adj[i, i + 1] <- adj[i + 1, i] <- 1 spatial_car(adj, level = "group", group_var = "region")
Convenience wrapper for spatial_car(..., proper = TRUE). Creates a
proper conditional autoregressive (CAR) spatial random effect with the
autocorrelation parameter rho estimated from the data.
Use this when you want spatial autocorrelation to be a parameter of the model rather than fixed at 1 (as in ICAR). rho ~= 0 collapses to IID, rho ~= 1 approaches ICAR.
spatial_car_proper( adjacency, level = c("group", "obs"), group_var = NULL, shared = NULL )spatial_car_proper( adjacency, level = c("group", "obs"), group_var = NULL, shared = NULL )
adjacency |
Symmetric adjacency matrix ( |
level |
Either |
group_var |
Name of the grouping variable in the data; required
when |
shared |
Optional shared-effect handle (see model docs). |
A tulpa_spatial object with type = "car_proper".
spatial_car() for ICAR (rho fixed at 1),
spatial_bym2() for the BYM2 decomposition.
adj <- matrix(0, 10, 10) for (i in 1:9) adj[i, i+1] <- adj[i+1, i] <- 1 spec <- spatial_car_proper(adj, level = "group", group_var = "site") print(spec)adj <- matrix(0, 10, 10) for (i in 1:9) adj[i, i+1] <- adj[i+1, i] <- 1 spec <- spatial_car_proper(adj, level = "group", group_var = "site") print(spec)
Specify a Gaussian-process spatial random effect, approximated with a nearest-neighbour GP (NNGP) for scalability. Captures smooth spatial variation from point-referenced coordinates.
spatial_gp( coords, approx = c("nngp", "hsgp"), cov = c("exponential", "matern"), nu = 1.5, nn = 15, m = 6, c = 1.5, sigma_prior_U = 1, sigma_prior_alpha = 0.01, shared = NULL, scale_coords = TRUE, parameterization = c("noncentered", "centered", "collapsed") ) ## S3 method for class 'tulpa_gp' print(x, ...) ## S3 method for class 'tulpa_hsgp' print(x, ...)spatial_gp( coords, approx = c("nngp", "hsgp"), cov = c("exponential", "matern"), nu = 1.5, nn = 15, m = 6, c = 1.5, sigma_prior_U = 1, sigma_prior_alpha = 0.01, shared = NULL, scale_coords = TRUE, parameterization = c("noncentered", "centered", "collapsed") ) ## S3 method for class 'tulpa_gp' print(x, ...) ## S3 method for class 'tulpa_hsgp' print(x, ...)
coords |
A formula ( |
approx |
GP approximation: |
cov |
Covariance function (NNGP only). One of |
nu |
Matern smoothness parameter, one of |
nn |
Number of nearest neighbours used in the NNGP approximation. |
m |
Number of HSGP basis functions per dimension ( |
c |
HSGP boundary factor, |
sigma_prior_U, sigma_prior_alpha
|
Penalized-complexity prior on the
field's marginal standard deviation ( |
shared |
Whether the spatial effect is shared across processes in a
multi-process model. |
scale_coords |
Logical. Standardize coordinates before fitting
(default |
parameterization |
Latent parameterization for the exact-NUTS field.
One of |
x |
A |
... |
Ignored. |
A tulpa_gp object (also of class tulpa_spatial).
spatial_car(), spatial_bym2() for areal spatial effects.
# GP spatial specification from coordinate columns spatial_gp(~ lon + lat) spatial_gp(~ lon + lat, cov = "matern", nu = 1.5)# GP spatial specification from coordinate columns spatial_gp(~ lon + lat) spatial_gp(~ lon + lat, cov = "matern", nu = 1.5)
Specify a multi-scale spatial random effect that decomposes spatial variation into local (fine-scale) and regional (broad-scale) components. Each scale has its own range and variance parameters.
This is particularly useful for large datasets (>100k observations) where spatial patterns exist at multiple scales.
spatial_multiscale( coords, scales = c("local", "regional"), approx = c("nngp", "hsgp"), m = 6L, c_boundary = 1.5, range_local = c(0.01, 1), range_regional = c(1, 10), cov = c("exponential", "matern"), nu = 1.5, nn_local = 10, nn_regional = 30, shared = NULL, scale_coords = TRUE, sampler = c("auto", "noncentered", "centered", "interweaved") ) ## S3 method for class 'tulpa_multiscale' print(x, ...)spatial_multiscale( coords, scales = c("local", "regional"), approx = c("nngp", "hsgp"), m = 6L, c_boundary = 1.5, range_local = c(0.01, 1), range_regional = c(1, 10), cov = c("exponential", "matern"), nu = 1.5, nn_local = 10, nn_regional = 30, shared = NULL, scale_coords = TRUE, sampler = c("auto", "noncentered", "centered", "interweaved") ) ## S3 method for class 'tulpa_multiscale' print(x, ...)
coords |
A one-sided formula specifying coordinate columns (e.g.,
|
scales |
Character vector specifying scale names. Default: |
approx |
Approximation method: |
m |
Number of HSGP basis functions per dimension (default 6). Only
used when |
c_boundary |
Boundary factor for HSGP domain extension (default 1.5).
Only used when |
range_local |
Plausible range interval for the local scale as
|
range_regional |
Plausible range interval for the regional scale, read
the same way. Default: |
cov |
Covariance function: |
nu |
Smoothness parameter for Matern covariance, one of |
nn_local |
Number of nearest neighbors for local scale. Default 10. |
nn_regional |
Number of nearest neighbors for regional scale. Default 30. |
shared |
Logical; if TRUE (default), spatial effects enter both all processes. |
scale_coords |
Logical; if TRUE (default), coordinates are centred
and divided by one common factor before computing distances (see
|
sampler |
Latent parameterization for the exact-NUTS field. |
x |
A |
... |
Ignored. |
The multi-scale model decomposes spatial variation additively:
where each component follows an independent Gaussian process:
Identifiability: With sufficient data (>500 locations), the two scales are typically well-identified when prior ranges are non-overlapping. PC priors on variance components help prevent overfitting.
Computational cost: Approximately 1.5-2x the cost of single-scale GP, as two NNGP likelihoods must be evaluated.
A tulpa_multiscale object
spatial_gp() for single-scale GP, temporal_multiscale() for
multi-scale temporal effects
# Create multi-scale spatial structure ms <- spatial_multiscale( ~ lon + lat, range_local = c(0.1, 0.5), range_regional = c(1, 5) ) print(ms) set.seed(101) n <- 25 df <- data.frame( lon = runif(n, 0, 10), lat = runif(n, 0, 10), depth = rnorm(n), temp = rnorm(n) ) df$count <- rpois(n, exp(1 + 0.2 * df$depth)) # Both scales are sampled by exact NUTS (mode = "exact"); the field is # returned as gp_local[i] / gp_regional[i] draws. fit <- tulpa( count ~ depth + temp, data = df, family = "poisson", spatial = spatial_multiscale( ~ lon + lat, range_local = c(0.1, 0.5), range_regional = c(1, 5), nn_local = 5L, nn_regional = 8L ), mode = "exact", control = list(n_iter = 60L, n_warmup = 30L, seed = 1L) ) summary(fit)# Create multi-scale spatial structure ms <- spatial_multiscale( ~ lon + lat, range_local = c(0.1, 0.5), range_regional = c(1, 5) ) print(ms) set.seed(101) n <- 25 df <- data.frame( lon = runif(n, 0, 10), lat = runif(n, 0, 10), depth = rnorm(n), temp = rnorm(n) ) df$count <- rpois(n, exp(1 + 0.2 * df$depth)) # Both scales are sampled by exact NUTS (mode = "exact"); the field is # returned as gp_local[i] / gp_regional[i] draws. fit <- tulpa( count ~ depth + temp, data = df, family = "poisson", spatial = spatial_multiscale( ~ lon + lat, range_local = c(0.1, 0.5), range_regional = c(1, 5), nn_local = 5L, nn_regional = 8L ), mode = "exact", control = list(n_iter = 60L, n_warmup = 30L, seed = 1L) ) summary(fit)
Summarises the posterior of spatial hyperparameters. For sampler-tier fits this reads the raw hyperparameter draws; for a nested-Laplace spatial fit it summarises the outer hyperparameter grid. Works with ICAR, BYM2, GP (NNGP), CAR, SPDE, and SVC spatial types.
spatial_range(object, probs = c(0.025, 0.975))spatial_range(object, probs = c(0.025, 0.975))
object |
A |
probs |
Quantile probabilities for the summary (default 0.025, 0.975). |
A range is reported in the units of the coordinates as supplied, also when
the field was fitted on standardized coordinates (scale_coords = TRUE,
the default of spatial_gp() and spatial_svc()). A mode = "laplace" fit
conditions on the field's hyperparameters rather than inferring them, so it
carries no posterior for this function to summarise.
A data.frame with rows for each spatial hyperparameter and columns
mean, sd, and one quantile column per entry of probs (named from the
probability, e.g. q2.5, q97.5 for the defaults).
set.seed(1) S <- 12L adj <- matrix(0, S, S) for (i in 1:(S - 1)) adj[i, i + 1] <- adj[i + 1, i] <- 1 d <- data.frame(region = factor(rep(1:S, each = 5)), x = rnorm(S * 5)) field <- as.numeric(scale(cumsum(rnorm(S, 0, 0.5)))) d$y <- rpois(nrow(d), exp(0.3 + 0.4 * d$x + field[d$region])) fit <- tulpa(y ~ x + spatial(region), data = d, family = "poisson", spatial = spatial_car(adj, level = "group", group_var = "region")) spatial_range(fit)set.seed(1) S <- 12L adj <- matrix(0, S, S) for (i in 1:(S - 1)) adj[i, i + 1] <- adj[i + 1, i] <- 1 d <- data.frame(region = factor(rep(1:S, each = 5)), x = rnorm(S * 5)) field <- as.numeric(scale(cumsum(rnorm(S, 0, 0.5)))) d$y <- rpois(nrow(d), exp(0.3 + 0.4 * d$x + field[d$region])) fit <- tulpa(y ~ x + spatial(region), data = d, family = "poisson", spatial = spatial_car(adj, level = "group", group_var = "region")) spatial_range(fit)
Apply Restricted Spatial Regression to mitigate spatial confounding. RSR orthogonalizes the spatial effect to the covariate space, preventing the spatial random effect from absorbing covariate information.
This is important when covariates are spatially smooth (e.g., climate variables, elevation) because the spatial random effect can "steal" variance from these covariates, leading to biased coefficient estimates.
spatial_rsr(spatial, restrict_to) ## S3 method for class 'tulpa_rsr' print(x, ...)spatial_rsr(spatial, restrict_to) ## S3 method for class 'tulpa_rsr' print(x, ...)
spatial |
An areal specification – |
restrict_to |
Formula specifying which covariates to orthogonalize
against (e.g., |
x |
A |
... |
Passed to the print method of the restricted field. |
The RSR approach (Reich et al., 2006; Hodges & Reich, 2010) modifies the spatial random effect to be orthogonal to the fixed effect design matrix:
where is the projection matrix onto the column
space of X. The projector is built at the FIELD's own resolution: one row
per areal unit, or one per unique location for an NNGP field, with the
restricted design averaged over the observations at each.
What RSR estimates. The restriction puts no part of the shared, smooth signal in the field, so the fixed effect takes all of it: RSR targets the MARGINAL association between the covariate and the response, where the unrestricted spatial model targets the association conditional on the field. The two differ by exactly the covariate's projection onto the field, so they are different estimands rather than a biased and an unbiased version of one (Bradley, 2024). Measured on a confounded continuous fixture (5 seeds, conditional slope 1.0, marginal 1.71): the restricted fit averaged 0.06 from the marginal value and 0.74 from the conditional one, the unrestricted fit 0.10 and 0.63.
The restriction is not free. In the geostatistical setting Hanks et al. (2015) measured POORER coverage under RSR than under the spatial model that does not restrict, and credible intervals that can be inappropriately narrow under model misspecification; Khan and Calder (2022) report the same on areal structure, with a non-spatial model competitive on coverage and higher Type-S error rates under RSR. Read an RSR interval as an interval for the marginal association under a correctly specified model, and prefer a posterior-predictive check (Hanks et al., 2015) where that is in doubt.
When to use RSR:
Covariates are spatially smooth (environmental gradients)
The marginal association is the quantity of interest
Coefficients appear attenuated toward zero
When NOT to use RSR:
Covariates are spatially uncorrelated
Spatial effect is the primary quantity of interest
Prediction is the main goal
Interval coverage matters more than the point estimate
A restricted field is fitted by nested Laplace (mode = "structured" or
"nested_laplace", under any family that path takes, and alongside a
random-effect term, a temporal field, smoothers or latent() blocks), where
the field reaches the linear predictor through the projector S P_perp
(S the observation-to-unit or observation-to-location incidence), or, for
a binomial response, by the Polya-Gamma Gibbs sampler (mode = "gibbs").
mode = "auto" selects Gibbs for a binomial response and nested Laplace
otherwise. Areal and NNGP fields take both routes.
A modified spatial specification with RSR enabled
Reich, B. J., Hodges, J. S., & Zadnik, V. (2006). Effects of residual smoothing on the posterior of the fixed effects in disease-mapping models. Biometrics, 62(4), 1197-1206.
Hodges, J. S., & Reich, B. J. (2010). Adding spatially-correlated errors can mess up the fixed effect you love. The American Statistician, 64(4), 325-334.
Hanks, E. M., Schliep, E. M., Hooten, M. B., & Hoeting, J. A. (2015). Restricted spatial regression in practice: geostatistical models, confounding, and robustness under model misspecification. Environmetrics, 26(4), 243-254.
Khan, K., & Calder, C. A. (2022). Restricted spatial regression methods: implications for inference. Journal of the American Statistical Association, 117(537), 482-494.
Bradley, J. R. (2024). Restricted spatial regression is reasonable statistical practice: clarifications, interpretations, and new developments. arXiv:2408.05106.
# Create RSR spatial structure on an areal field W <- matrix(0, 4, 4) for (i in 1:3) W[i, i + 1] <- W[i + 1, i] <- 1 rsr <- spatial_rsr( spatial_car(W, level = "obs"), restrict_to = ~ depth + temp ) print(rsr) # Areal binomial data on a chain of regions, covariate spatially confounded set.seed(404) n_regions <- 12 W <- matrix(0, n_regions, n_regions) for (i in 1:(n_regions - 1)) W[i, i + 1] <- W[i + 1, i] <- 1 df <- data.frame(region = factor(rep(1:n_regions, each = 5))) df$x <- as.integer(df$region) / 4 + rnorm(nrow(df), 0, 0.5) df$y <- rbinom(nrow(df), 20, plogis(-0.5 + 0.6 * df$x)) # RSR orthogonalises the spatial field to x, protecting its coefficient fit <- tulpa( y ~ x + spatial(region), data = df, family = "binomial", n_trials = rep(20L, nrow(df)), spatial = spatial_rsr(spatial_car(W, level = "obs"), restrict_to = ~ x), mode = "auto", control = list(n_iter = 500L, warmup = 250L) ) summary(fit) # The same modifier on a continuous field: one observation per location, # the projector built at the unique coordinates. set.seed(7) n <- 80 pts <- data.frame(lon = runif(n), lat = runif(n)) pts$x <- as.numeric(scale(pts$lon + pts$lat + rnorm(n, 0, 0.3))) pts$y <- rbinom(n, 25, plogis(-0.2 + pts$x)) fit_gp <- tulpa( y ~ x, data = pts, family = "binomial", n_trials = rep(25L, n), spatial = spatial_rsr(spatial_gp(~ lon + lat), restrict_to = ~ x), mode = "gibbs", control = list(n_iter = 500L, warmup = 250L) ) summary(fit_gp)# Create RSR spatial structure on an areal field W <- matrix(0, 4, 4) for (i in 1:3) W[i, i + 1] <- W[i + 1, i] <- 1 rsr <- spatial_rsr( spatial_car(W, level = "obs"), restrict_to = ~ depth + temp ) print(rsr) # Areal binomial data on a chain of regions, covariate spatially confounded set.seed(404) n_regions <- 12 W <- matrix(0, n_regions, n_regions) for (i in 1:(n_regions - 1)) W[i, i + 1] <- W[i + 1, i] <- 1 df <- data.frame(region = factor(rep(1:n_regions, each = 5))) df$x <- as.integer(df$region) / 4 + rnorm(nrow(df), 0, 0.5) df$y <- rbinom(nrow(df), 20, plogis(-0.5 + 0.6 * df$x)) # RSR orthogonalises the spatial field to x, protecting its coefficient fit <- tulpa( y ~ x + spatial(region), data = df, family = "binomial", n_trials = rep(20L, nrow(df)), spatial = spatial_rsr(spatial_car(W, level = "obs"), restrict_to = ~ x), mode = "auto", control = list(n_iter = 500L, warmup = 250L) ) summary(fit) # The same modifier on a continuous field: one observation per location, # the projector built at the unique coordinates. set.seed(7) n <- 80 pts <- data.frame(lon = runif(n), lat = runif(n)) pts$x <- as.numeric(scale(pts$lon + pts$lat + rnorm(n, 0, 0.3))) pts$y <- rbinom(n, 25, plogis(-0.2 + pts$x)) fit_gp <- tulpa( y ~ x, data = pts, family = "binomial", n_trials = rep(25L, n), spatial = spatial_rsr(spatial_gp(~ lon + lat), restrict_to = ~ x), mode = "gibbs", control = list(n_iter = 500L, warmup = 250L) ) summary(fit_gp)
Specify a continuous Matern spatial field using the SPDE approach (Lindgren, Rue & Lindstrom 2011). Builds a triangular mesh from observation coordinates, computes FEM matrices, and passes them to tulpa's SPDE Laplace engine with CHOLMOD sparse solver.
spatial_spde( coords, data = NULL, mesh = NULL, boundary = NULL, max_edge = NULL, cutoff = 0, nu = 1, prior_range = NULL, prior_sigma = NULL )spatial_spde( coords, data = NULL, mesh = NULL, boundary = NULL, max_edge = NULL, cutoff = 0, nu = 1, prior_range = NULL, prior_sigma = NULL )
coords |
A formula |
data |
Optional data.frame for formula evaluation. |
mesh |
A pre-built |
boundary |
Optional boundary: a two-column matrix of polygon vertices, an sf polygon, or NULL for convex hull with extension. |
max_edge |
Maximum edge length for mesh refinement. A single value
or |
cutoff |
Minimum distance between mesh vertices. Default 0. |
nu |
Matern smoothness parameter. A positive number. Integer |
prior_range |
PC prior on the spatial range, |
prior_sigma |
PC prior on the marginal standard deviation, |
A tulpa_spatial object with type "spde".
set.seed(42) coords <- cbind(runif(50), runif(50)) spec <- spatial_spde(coords) print(spec)set.seed(42) coords <- cbind(runif(50), runif(50)) spec <- spatial_spde(coords) print(spec)
Specify a continuous Matern spatial field using externally-provided FEM matrices. Use this with meshes from fmesher, rSPDE, or any other source.
spatial_spde_custom( C, G, A, nu = 1, prior_range = NULL, prior_sigma = NULL, coords = NULL )spatial_spde_custom( C, G, A, nu = 1, prior_range = NULL, prior_sigma = NULL, coords = NULL )
C |
Mass matrix (n_mesh x n_mesh sparse matrix, e.g. from |
G |
Stiffness matrix (n_mesh x n_mesh sparse matrix, e.g. from |
A |
Projection matrix (n_obs x n_mesh sparse matrix, e.g. from |
nu |
Matern smoothness parameter. A positive number; integer values
give the exact FEM construction, fractional values the BRASIL rational SPDE
approximation (supported by |
prior_range |
PC prior on the spatial range, |
prior_sigma |
PC prior on the marginal standard deviation, |
coords |
Observation coordinates (an |
A tulpa_spatial object with type "spde".
# FEM matrices of a 6-node 1-D chain mesh, observed at the nodes: n <- 6L C <- Matrix::Diagonal(n) G <- Matrix::bandSparse(n, k = c(-1, 0, 1), diagonals = list(rep(-1, n - 1), c(1, rep(2, n - 2), 1), rep(-1, n - 1))) A <- Matrix::Diagonal(n) sp <- spatial_spde_custom(C, G, A, prior_range = c(1, 0.5), prior_sigma = c(1, 0.01)) sp$type# FEM matrices of a 6-node 1-D chain mesh, observed at the nodes: n <- 6L C <- Matrix::Diagonal(n) G <- Matrix::bandSparse(n, k = c(-1, 0, 1), diagonals = list(rep(-1, n - 1), c(1, rep(2, n - 2), 1), rep(-1, n - 1))) A <- Matrix::Diagonal(n) sp <- spatial_spde_custom(C, G, A, prior_range = c(1, 0.5), prior_sigma = c(1, 0.01)) sp$type
Specify a spatially varying coefficient (SVC): one or more fixed-effect coefficients are allowed to vary smoothly over space, with the variation governed by a Gaussian process (NNGP or HSGP approximation).
spatial_svc( coords, terms = 1, cov = c("exponential", "matern"), nn = 15, shared = NULL, scale_coords = TRUE, approx = c("nngp", "hsgp"), m = 6, c_boundary = 1.5, parameterization = c("noncentered", "centered") ) ## S3 method for class 'tulpa_svc' print(x, ...)spatial_svc( coords, terms = 1, cov = c("exponential", "matern"), nn = 15, shared = NULL, scale_coords = TRUE, approx = c("nngp", "hsgp"), m = 6, c_boundary = 1.5, parameterization = c("noncentered", "centered") ) ## S3 method for class 'tulpa_svc' print(x, ...)
coords |
A formula ( |
terms |
Which coefficients vary over space. A formula, an integer vector
of design-matrix column indices, or a character vector of term names.
Default |
cov |
Covariance function. One of |
nn |
Number of nearest neighbours used in the NNGP approximation
( |
shared |
Whether the effect is shared across processes in a
multi-process model. |
scale_coords |
Logical. Standardize coordinates before fitting
(default |
approx |
Spatial approximation. |
m |
Number of basis functions per dimension for the HSGP approximation
( |
c_boundary |
Boundary-extension factor for the HSGP domain
( |
parameterization |
Latent parameterization for the exact-NUTS field
( |
x |
A |
... |
Ignored. |
A tulpa_svc object (also of class tulpa_spatial).
spatial_gp() for a spatial random effect (rather than a varying
coefficient).
# Intercept that varies smoothly over space spatial_svc(~ lon + lat)# Intercept that varies smoothly over space spatial_svc(~ lon + lat)
Functions to specify spatiotemporal interaction effects for tulpa models. These capture dependencies that arise when spatial patterns vary over time, or when temporal trends differ across space.
Specify a spatiotemporal interaction effect for tulpa models. The interaction captures structured or unstructured deviation from the additive spatial + temporal model.
No tulpa backend fits an interaction term, so this constructor errors. The
additive space-time model is fitted by tulpa(spatial = , temporal = ) and
by fit_st_nested().
spatiotemporal( spatial, temporal, type = c("I", "II", "III", "IV", "iid", "separable"), shared = NULL )spatiotemporal( spatial, temporal, type = c("I", "II", "III", "IV", "iid", "separable"), shared = NULL )
spatial |
A spatial specification from |
temporal |
A temporal specification from |
type |
Interaction type:
|
shared |
Logical; if TRUE (default), spatiotemporal effect enters both all processes. Set to FALSE for process-specific effects (triggers warning about potential confounding). |
Spatiotemporal interactions extend the basic additive model:
to include interactions:
where captures space-time interactions.
Interaction Types (following Knorr-Held, 2000):
Type I: Unstructured interaction - IID
Type II: Structured time, unstructured space - temporal structure at each location
Type III: Structured space, unstructured time - spatial structure at each time
Type IV: Structured space AND time - full Kronecker interaction
Separable Models:
Separable: Covariance is Kronecker product
Non-separable: GP with joint space-time metric
Type I (IID)
Independent random effect for each space-time combination:
This is the simplest form, requiring S*T parameters but capturing no structured interaction.
Type II (Temporal structure per location)
Each location has its own temporal random effect:
This captures location-specific temporal trends but assumes independence across locations.
Type III (Spatial structure per time point)
Each time point has its own spatial random effect:
This captures time-specific spatial patterns but assumes independence across time points.
Type IV (Full structure)
Kronecker product of spatial and temporal precision matrices:
This is the most constrained model, assuming the interaction has the same structure as the marginal effects.
Separable
For GP-based effects, assumes separable covariance:
Nothing: the call always signals an error.
Knorr-Held, L. (2000). Bayesian modelling of inseparable space-time variation in disease risk. Statistics in Medicine, 19(17-18), 2555-2567.
spatial_car(), spatial_gp(), temporal_rw1(), temporal_ar1()
adj <- matrix(0, 4, 4) for (i in 1:3) adj[i, i + 1] <- adj[i + 1, i] <- 1 # No backend fits an interaction term, so the constructor refuses; the # additive model is tulpa(spatial = , temporal = ). try(spatiotemporal(spatial_car(adj, group_var = "region"), temporal_rw1("year"), type = "IV"))adj <- matrix(0, 4, 4) for (i in 1:3) adj[i, i + 1] <- adj[i + 1, i] <- 1 # No backend fits an interaction term, so the constructor refuses; the # additive model is tulpa(spatial = , temporal = ). try(spatiotemporal(spatial_car(adj, group_var = "region"), temporal_rw1("year"), type = "IV"))
Extract posterior distributions of spatiotemporal interaction effects from a fitted tulpa model.
spatiotemporal_effects( object, format = c("array", "long", "summary"), probs = c(0.025, 0.5, 0.975), ... ) ## Default S3 method: spatiotemporal_effects( object, format = c("array", "long", "summary"), probs = c(0.025, 0.5, 0.975), ... ) ## S3 method for class 'tulpa_st_summary' print(x, ...)spatiotemporal_effects( object, format = c("array", "long", "summary"), probs = c(0.025, 0.5, 0.975), ... ) ## Default S3 method: spatiotemporal_effects( object, format = c("array", "long", "summary"), probs = c(0.025, 0.5, 0.975), ... ) ## S3 method for class 'tulpa_st_summary' print(x, ...)
object |
A fit carrying a |
format |
Output format: |
probs |
Quantiles to compute if |
... |
Ignored |
x |
A |
For format = "array", an S x T x draws numeric array carrying
n_spatial, n_times and n_draws attributes; for "long", a data
frame with columns s, t, draw and value; for "summary", a
tulpa_st_summary data frame with columns s, t, mean, one column
per entry of probs and sd, which has print() and plot() methods.
# A Knorr-Held interaction block is attached by a model package's own # fitter (see spatiotemporal() for what the engine fits). The accessor reads # its layout and an n_draws x (S * T) draw matrix stored s-fastest: column # (t - 1) * S + s holds region s at time t. Here both are simulated for # S = 3 regions over T = 4 times. set.seed(1) S <- 3L; n_t <- 4L st_mean <- outer(c(-0.5, 0, 0.5), seq(-0.3, 0.3, length.out = n_t)) draws <- matrix(rnorm(100 * S * n_t, mean = rep(c(st_mean), each = 100), sd = 0.1), nrow = 100) fit <- structure( list(spatiotemporal = list(n_spatial = S, n_times = n_t), .internal = list(spatiotemporal_draws = draws)), class = "tulpa_fit") st_effects <- spatiotemporal_effects(fit, format = "summary") head(st_effects) dim(spatiotemporal_effects(fit, format = "array"))# A Knorr-Held interaction block is attached by a model package's own # fitter (see spatiotemporal() for what the engine fits). The accessor reads # its layout and an n_draws x (S * T) draw matrix stored s-fastest: column # (t - 1) * S + s holds region s at time t. Here both are simulated for # S = 3 regions over T = 4 times. set.seed(1) S <- 3L; n_t <- 4L st_mean <- outer(c(-0.5, 0, 0.5), seq(-0.3, 0.3, length.out = n_t)) draws <- matrix(rnorm(100 * S * n_t, mean = rep(c(st_mean), each = 100), sd = 0.1), nrow = 100) fit <- structure( list(spatiotemporal = list(n_spatial = S, n_times = n_t), .internal = list(spatiotemporal_draws = draws)), class = "tulpa_fit") st_effects <- spatiotemporal_effects(fit, format = "summary") head(st_effects) dim(spatiotemporal_effects(fit, format = "array"))
Specify a non-separable Gaussian Process for spatiotemporal effects.
Unlike separable models where the covariance factors as ,
non-separable models allow for direct space-time interaction in the covariance.
No tulpa backend fits a joint space-time covariance, so this constructor
errors. A spatial GP alongside a temporal field is fitted directly by
fit_st_nested() (spatial_type = "hsgp" or "nngp"); tulpa()'s
formula front door does not yet route a continuous spatial field together
with a temporal field (it does for an areal one – icar/bym2/car_proper –
via tulpa(spatial = , temporal = ); gcol33/tulpa#812).
spatiotemporal_gp( coords, time_var, cov_space = c("exponential", "matern", "gaussian", "spherical"), cov_time = c("exponential", "matern", "gaussian"), nonsep_type = c("product", "sum", "gneiting", "cressie_huang"), nn = 15, shared = NULL )spatiotemporal_gp( coords, time_var, cov_space = c("exponential", "matern", "gaussian", "spherical"), cov_time = c("exponential", "matern", "gaussian"), nonsep_type = c("product", "sum", "gneiting", "cressie_huang"), nn = 15, shared = NULL )
coords |
A one-sided formula specifying coordinate columns (e.g.,
|
time_var |
Name of the time variable in data. |
cov_space |
Spatial covariance: |
cov_time |
Temporal covariance: |
nonsep_type |
Non-separability type:
|
nn |
Number of nearest neighbors for NNGP approximation. Default 15. |
shared |
Logical; if TRUE (default), effect enters both processes. |
The non-separable covariance functions allow for more flexible space-time dependence:
Gneiting class:
where h is spatial lag, u is temporal lag, and parameters control the space-time interaction.
Cressie-Huang class: Constructed via Fourier transform methods to ensure positive definiteness.
Nothing: the call always signals an error.
Gneiting, T. (2002). Nonseparable, stationary covariance functions for space-time data. Journal of the American Statistical Association, 97(458), 590-600.
Cressie, N., & Huang, H. C. (1999). Classes of nonseparable, spatio-temporal stationary covariance functions. Journal of the American Statistical Association, 94(448), 1330-1340.
# No backend fits a joint space-time covariance, so the constructor refuses: try(spatiotemporal_gp(~ lon + lat, "year", nonsep_type = "gneiting"))# No backend fits a joint space-time covariance, so the constructor refuses: try(spatiotemporal_gp(~ lon + lat, "year", nonsep_type = "gneiting"))
Extract posterior distributions of spatially-varying coefficients (SVCs) from a fitted tulpa model with SVC specification.
svc(object, terms = NULL, summary = FALSE, probs = c(0.025, 0.5, 0.975), ...) ## S3 method for class 'tulpa_fit' svc(object, terms = NULL, summary = FALSE, probs = c(0.025, 0.5, 0.975), ...) ## S3 method for class 'tulpa_svc_posterior' summary(object, probs = c(0.025, 0.5, 0.975), ...) ## S3 method for class 'tulpa_svc_posterior' print(x, ...) ## S3 method for class 'tulpa_svc_summary' print(x, ...)svc(object, terms = NULL, summary = FALSE, probs = c(0.025, 0.5, 0.975), ...) ## S3 method for class 'tulpa_fit' svc(object, terms = NULL, summary = FALSE, probs = c(0.025, 0.5, 0.975), ...) ## S3 method for class 'tulpa_svc_posterior' summary(object, probs = c(0.025, 0.5, 0.975), ...) ## S3 method for class 'tulpa_svc_posterior' print(x, ...) ## S3 method for class 'tulpa_svc_summary' print(x, ...)
object |
A |
terms |
Which SVC terms to extract. If NULL (default), extracts all. |
summary |
Logical; if TRUE, return summary statistics instead of full posterior draws. |
probs |
Quantiles to compute if |
... |
Ignored |
x |
A |
A tulpa_svc_posterior object containing:
draws: Array of posterior draws (draws x locations x terms)
coords: Coordinate matrix
term_names: Names of SVC terms
summary() on a tulpa_svc_posterior returns a tulpa_svc_summary
data frame with one row per location and term, holding the observation
index, term name, coordinates, and the posterior mean, SD and requested
quantiles of each spatially varying coefficient.
spatial_svc(), plot.tulpa_svc_posterior()
set.seed(303) n <- 25L df <- data.frame(lon = runif(n), lat = runif(n), x = rnorm(n)) bsurf <- 0.9 * sin(2.8 * df$lon) + 0.7 * cos(2.2 * df$lat) df$count <- rpois(n, exp(0.2 + (0.8 + bsurf) * df$x)) # The varying slope on `x` is a spatial field; SVC is exact-mode only. fit <- tulpa( count ~ x, data = df, family = "poisson", spatial = spatial_svc(~ lon + lat, terms = ~ x - 1, nn = 5L), mode = "exact", control = list(n_iter = 80L, n_warmup = 40L, seed = 1L) ) svc_post <- svc(fit) summary(svc_post)set.seed(303) n <- 25L df <- data.frame(lon = runif(n), lat = runif(n), x = rnorm(n)) bsurf <- 0.9 * sin(2.8 * df$lon) + 0.7 * cos(2.2 * df$lat) df$count <- rpois(n, exp(0.2 + (0.8 + bsurf) * df$x)) # The varying slope on `x` is a spatial field; SVC is exact-mode only. fit <- tulpa( count ~ x, data = df, family = "poisson", spatial = spatial_svc(~ lon + lat, terms = ~ x - 1, nn = 5L), mode = "exact", control = list(n_iter = 80L, n_warmup = 40L, seed = 1L) ) svc_post <- svc(fit) summary(svc_post)
A stationary autoregressive process of order p,
, as a
user-defined GMRF latent block for a model formula via
latent(temporal_ar(...)) – the general-order extension of
temporal_ar2(), sharing the same construction: the precision is the exact
stationary AR(p) GMRF (the banded inverse of the Yule-Walker Toeplitz
autocovariance), and stationarity is enforced by the partial-autocorrelation
parameterization (Levinson-Durbin map of p unconstrained
atanh_psi hyperparameters), so the hyperparameter integration never
leaves the stationary region.
The block's hyperparameters are log_tau (log innovation precision) and
atanh_psi1..atanh_psi<p>; the AR coefficients are recovered through
psi_j = tanh(atanh_psi_j) and the Levinson-Durbin recursion. Note the
outer integration cost grows with the hyperparameter count p + 1; orders
beyond 3-4 are better served by a sampler tier.
temporal_ar( time_idx, p = 2L, n_times = NULL, prior_tau_sd = 2, prior_psi_sd = 1.5, name = NULL ) ## S3 method for class 'tulpa_temporal_latent_block' print(x, ...)temporal_ar( time_idx, p = 2L, n_times = NULL, prior_tau_sd = 2, prior_psi_sd = 1.5, name = NULL ) ## S3 method for class 'tulpa_temporal_latent_block' print(x, ...)
time_idx |
Integer vector (1-based) of the time point for each observation. |
p |
Autoregressive order (>= 1). |
n_times |
Number of distinct time points; defaults to |
prior_tau_sd, prior_psi_sd
|
Prior SDs for |
name |
Optional block label (default |
x |
A temporal AR(p) block. |
... |
Passed to the |
A tgmrf / tulpa_latent_block object.
temporal_ar2() (the p = 2 shorthand), temporal_ar1() for the
first-order formula-integrated process, tgmrf() for the general
user-defined GMRF interface.
set.seed(1) Tt <- 150L w <- numeric(Tt); w[1:3] <- rnorm(3) for (t in 4:Tt) w[t] <- 0.4 * w[t-1] + 0.2 * w[t-2] - 0.2 * w[t-3] + rnorm(1, 0, 0.4) d <- data.frame(t = seq_len(Tt), y = w + rnorm(Tt, 0, 0.3)) fit <- tulpa(y ~ latent(temporal_ar(d$t, p = 3)), data = d, family = "gaussian", mode = "nested_laplace")set.seed(1) Tt <- 150L w <- numeric(Tt); w[1:3] <- rnorm(3) for (t in 4:Tt) w[t] <- 0.4 * w[t-1] + 0.2 * w[t-2] - 0.2 * w[t-3] + rnorm(1, 0, 0.4) d <- data.frame(t = seq_len(Tt), y = w + rnorm(Tt, 0, 0.3)) fit <- tulpa(y ~ latent(temporal_ar(d$t, p = 3)), data = d, family = "gaussian", mode = "nested_laplace")
Specify a first-order autoregressive temporal random effect.
AR1 models temporal correlation where each time point depends on
the previous one: phi[t] = rho * phi[t-1] + epsilon[t].
Unlike RW1/RW2, AR1 is a proper (stationary) model with an estimated autocorrelation parameter rho.
temporal_ar1(time_var, group_var = NULL, shared = NULL, rho_prior = NULL)temporal_ar1(time_var, group_var = NULL, shared = NULL, rho_prior = NULL)
time_var |
A formula ( |
group_var |
Optional formula ( |
shared |
Whether the temporal effect is shared across processes in a
multi-process model. |
rho_prior |
Prior for the AR(1) autocorrelation. Default |
The AR1 process has marginal variance sigma^2 / (1 - rho^2) and correlation between time points t and s of rho^|t-s|.
The precision matrix is tridiagonal and full rank, so no constraints are needed.
A tulpa_temporal object
# Create temporal AR1 specification temporal_ar1("year") temporal_ar1("year", group = "site") # AR1 temporal correlation set.seed(127) df <- data.frame( year = rep(1:20, each = 3), x = rnorm(60) ) f <- as.numeric(arima.sim(list(ar = 0.7), 20, sd = 0.4)) df$count <- rpois(60, exp(1 + 0.3 * df$x + f[df$year])) fit <- tulpa( count ~ x, data = df, family = "poisson", temporal = temporal_ar1("year"), mode = "auto" ) summary(fit)# Create temporal AR1 specification temporal_ar1("year") temporal_ar1("year", group = "site") # AR1 temporal correlation set.seed(127) df <- data.frame( year = rep(1:20, each = 3), x = rnorm(60) ) f <- as.numeric(arima.sim(list(ar = 0.7), 20, sd = 0.4)) df$count <- rpois(60, exp(1 + 0.3 * df$x + f[df$year])) fit <- tulpa( count ~ x, data = df, family = "poisson", temporal = temporal_ar1("year"), mode = "auto" ) summary(fit)
A stationary second-order autoregressive process
as a user-defined
GMRF latent block, for use in a model formula via
latent(temporal_ar2(...)). It reuses tulpa's nested-Laplace / NUTS
machinery (no dedicated C++ kernel). The precision is the exact stationary
AR(2) GMRF; stationarity is enforced by the PACF parameterization, so the
hyperparameter grid never leaves the stationarity region.
The block's hyperparameters are log_tau (log innovation precision) and
atanh_psi1, atanh_psi2 (unconstrained partial autocorrelations); the AR
coefficients are recovered as phi2 = tanh(atanh_psi2),
phi1 = tanh(atanh_psi1) (1 - phi2).
temporal_ar2( time_idx, n_times = NULL, prior_tau_sd = 2, prior_psi_sd = 1.5, name = "ar2" )temporal_ar2( time_idx, n_times = NULL, prior_tau_sd = 2, prior_psi_sd = 1.5, name = "ar2" )
time_idx |
Integer vector (1-based) of the time point for each observation. |
n_times |
Number of distinct time points; defaults to |
prior_tau_sd, prior_psi_sd
|
Prior SDs for |
name |
Optional block label. |
A tgmrf / tulpa_latent_block object.
temporal_ar1() for the first-order (formula-integrated) process;
tgmrf() for the general user-defined GMRF interface.
set.seed(1) Tt <- 120L w <- numeric(Tt); w[1:2] <- rnorm(2) for (t in 3:Tt) w[t] <- 0.5 * w[t-1] + 0.3 * w[t-2] + rnorm(1, 0, 0.4) d <- data.frame(t = seq_len(Tt), y = w + rnorm(Tt, 0, 0.3)) # `phi` is the gaussian residual VARIANCE, here the 0.3 the series was # simulated with. Left unsupplied the fit conditions on 1 and says so. fit <- tulpa(y ~ latent(temporal_ar2(d$t)), data = d, family = "gaussian", phi = 0.3^2, mode = "nested_laplace")set.seed(1) Tt <- 120L w <- numeric(Tt); w[1:2] <- rnorm(2) for (t in 3:Tt) w[t] <- 0.5 * w[t-1] + 0.3 * w[t-2] + rnorm(1, 0, 0.4) d <- data.frame(t = seq_len(Tt), y = w + rnorm(Tt, 0, 0.3)) # `phi` is the gaussian residual VARIANCE, here the 0.3 the series was # simulated with. Left unsupplied the fit conditions on 1 and says so. fit <- tulpa(y ~ latent(temporal_ar2(d$t)), data = d, family = "gaussian", phi = 0.3^2, mode = "nested_laplace")
Returns posterior summary for temporal hyperparameters (tau, rho for AR1, sigma/lengthscale for temporal GP).
temporal_corr(object, probs = c(0.025, 0.975))temporal_corr(object, probs = c(0.025, 0.975))
object |
A |
probs |
Quantile probabilities (default 0.025, 0.975). |
A data.frame with rows for each temporal hyperparameter.
set.seed(127) df <- data.frame(year = rep(1:20, each = 3), x = rnorm(60)) f <- as.numeric(arima.sim(list(ar = 0.7), 20, sd = 0.4)) df$count <- rpois(60, exp(1 + 0.3 * df$x + f[df$year])) fit <- tulpa(count ~ x, data = df, family = "poisson", temporal = temporal_ar1("year")) temporal_corr(fit)set.seed(127) df <- data.frame(year = rep(1:20, each = 3), x = rnorm(60)) f <- as.numeric(arima.sim(list(ar = 0.7), 20, sd = 0.4)) df$count <- rpois(60, exp(1 + 0.3 * df$x + f[df$year])) fit <- tulpa(count ~ x, data = df, family = "poisson", temporal = temporal_ar1("year")) temporal_corr(fit)
Specify a Gaussian Process (GP) temporal random effect for irregularly-spaced or continuous time points. Unlike RW1/RW2/AR1 which assume equally-spaced observations, GP temporal effects model correlation as a function of time distance.
This is particularly useful for:
Irregularly-spaced time series
Continuous time (e.g., exact timestamps)
Smooth temporal trends with uncertainty
temporal_gp( time_var, cov = c("exponential", "matern", "gaussian", "periodic"), nu = 1.5, period = NULL, group_var = NULL, shared = NULL, scale_coords = TRUE, parameterization = c("noncentered", "centered") ) ## S3 method for class 'tulpa_temporal_gp' print(x, ...)temporal_gp( time_var, cov = c("exponential", "matern", "gaussian", "periodic"), nu = 1.5, period = NULL, group_var = NULL, shared = NULL, scale_coords = TRUE, parameterization = c("noncentered", "centered") ) ## S3 method for class 'tulpa_temporal_gp' print(x, ...)
time_var |
Name of the time variable in data. Can be a formula
(e.g., |
cov |
Covariance function: |
nu |
Smoothness parameter for Matern covariance, one of:
|
period |
Period for periodic covariance (e.g., 12 for monthly, 365 for
daily data with annual cycle). Only used when |
group_var |
Optional name of grouping variable for panel data. If provided, separate GPs are estimated for each group. |
shared |
Logical; if TRUE (default), temporal effect enters both all processes. |
scale_coords |
Logical; if TRUE (default), time values are scaled to
unit variance before computing distances. Either way the lengthscale's
support is placed relative to the spread of the times, and
|
parameterization |
Parameterization for GP effects:
|
x |
A |
... |
Ignored. |
The GP temporal model adds a time-correlated random effect:
where follows a Gaussian process:
The correlation function depends on time distance :
Exponential: - continuous but not differentiable
Matern: Smooth with tunable roughness via
Gaussian: - infinitely differentiable
Periodic: - for seasonal data
Implementation: the exponential kernel (equivalently Matern with
nu = 0.5) is an Ornstein-Uhlenbeck process, so its joint density
factorizes into a first-order Markov chain and evaluates in O(T) with no
matrix. The other kernels have no finite-dimensional state-space form and
are evaluated from a dense T x T Cholesky, where T is the number of distinct
time points.
The field is fit on the sampler path: its two hyperparameters
(log_sigma2_temporal_gp, logit_phi_temporal_gp) are sampled jointly with
the field and the fixed effects, so pass a sampler mode such as
mode = "hmc". There is no nested-Laplace kernel for it.
A tulpa_temporal_gp object
temporal_rw1(), temporal_ar1() for equally-spaced temporal effects,
spatial_gp() for spatial GP effects
# Create GP temporal specification temporal_gp("timestamp") temporal_gp("day", cov = "matern", nu = 1.5) temporal_gp("month", cov = "periodic", period = 12) # Irregularly-spaced time series set.seed(140) times <- sort(runif(40, 0, 10)) df <- data.frame( time = times, x = rnorm(40), y = rbinom(40, 1, 0.5) ) fit <- tulpa( y ~ x, data = df, family = "binomial", temporal = temporal_gp("time"), mode = "hmc", control = list(n_iter = 200, warmup = 100, n_chains = 1) )# Create GP temporal specification temporal_gp("timestamp") temporal_gp("day", cov = "matern", nu = 1.5) temporal_gp("month", cov = "periodic", period = 12) # Irregularly-spaced time series set.seed(140) times <- sort(runif(40, 0, 10)) df <- data.frame( time = times, x = rnorm(40), y = rbinom(40, 1, 0.5) ) fit <- tulpa( y ~ x, data = df, family = "binomial", temporal = temporal_gp("time"), mode = "hmc", control = list(n_iter = 200, warmup = 100, n_chains = 1) )
Specify a temporal random effect that decomposes variation into separate scales: a smooth trend, an optional seasonal cycle, and a short-term component. Each scale uses its own prior, letting slow and fast dynamics be modelled jointly.
temporal_multiscale( time_var, trend = c("rw2", "rw1", "none"), seasonal = NULL, short_term = c("ar1", "iid", "none"), group_var = NULL, shared = NULL ) ## S3 method for class 'tulpa_temporal_multiscale' print(x, ...)temporal_multiscale( time_var, trend = c("rw2", "rw1", "none"), seasonal = NULL, short_term = c("ar1", "iid", "none"), group_var = NULL, shared = NULL ) ## S3 method for class 'tulpa_temporal_multiscale' print(x, ...)
time_var |
Single character string naming the time variable in the data. |
trend |
Prior for the smooth long-term trend. One of |
seasonal |
Optional integer period ( |
short_term |
Prior for the short-term component. One of |
group_var |
Optional character string naming a grouping variable for group-specific temporal effects. |
shared |
Whether the effect is shared across processes in a
multi-process model. |
x |
A |
... |
Ignored. |
At least one of trend, seasonal, or short_term must be active.
A tulpa_temporal_multiscale object.
temporal_rw1(), temporal_rw2(), temporal_ar1() for
single-scale temporal priors.
# Trend + annual seasonal cycle + AR1 short-term component on monthly data temporal_multiscale("month", trend = "rw2", seasonal = 12, short_term = "ar1")# Trend + annual seasonal cycle + AR1 short-term component on monthly data temporal_multiscale("month", trend = "rw2", seasonal = 12, short_term = "ar1")
The temporal analogue of spatial_rsr(): constrain a temporal random effect
to be orthogonal to a set of covariates, so a temporally smooth covariate does
not have its fixed-effect coefficient attenuated by a confounded temporal
field, .
No tulpa backend applies that projection to a temporal field, so this
constructor errors rather than returning a specification that would fit as the
unrestricted temporal model. spatial_rsr() is the wired spatial analogue.
temporal_rtr(temporal, restrict_to)temporal_rtr(temporal, restrict_to)
temporal |
A |
restrict_to |
A one-sided formula giving the covariate space the temporal
effect is made orthogonal to, e.g. |
Nothing: the call always signals an error.
spatial_rsr(), temporal_rw1(), temporal_ar1()
# No backend fits a restricted temporal field, so the constructor refuses: try(temporal_rtr(temporal_rw1("year"), ~ x))# No backend fits a restricted temporal field, so the constructor refuses: try(temporal_rtr(temporal_rw1("year"), ~ x))
Specify a first-order random walk temporal random effect. RW1 penalizes
first differences, so adjacent time points are smoothed toward each other:
phi[t] - phi[t-1] ~ N(0, sigma^2).
temporal_rw1(time_var, group_var = NULL, cyclic = FALSE, shared = NULL)temporal_rw1(time_var, group_var = NULL, cyclic = FALSE, shared = NULL)
time_var |
A formula ( |
group_var |
Optional formula ( |
cyclic |
Logical. If |
shared |
Whether the temporal effect is shared across processes in a
multi-process model. |
The precision matrix is rank T - 1 (one constraint needed). RW1 is the
least smooth of the random-walk priors; for smoother trends see
temporal_rw2(), and for a stationary alternative see temporal_ar1().
A tulpa_temporal object.
temporal_rw2(), temporal_ar1() for other temporal priors.
# Create temporal RW1 specification temporal_rw1("year") temporal_rw1("month", cyclic = TRUE)# Create temporal RW1 specification temporal_rw1("year") temporal_rw1("month", cyclic = TRUE)
Specify a second-order random walk temporal random effect.
RW2 penalizes deviations from linearity, resulting in smoother trends
than RW1: phi[t] - 2*phi[t-1] + phi[t-2] ~ N(0, sigma^2).
temporal_rw2(time_var, group_var = NULL, cyclic = FALSE, shared = NULL)temporal_rw2(time_var, group_var = NULL, cyclic = FALSE, shared = NULL)
time_var |
A formula ( |
group_var |
Optional formula ( |
cyclic |
Logical. If |
shared |
Whether the temporal effect is shared across processes in a
multi-process model. |
RW2 produces smoother curves than RW1 because it penalizes the second derivative (curvature) rather than the first derivative (slope). It requires at least 3 time points.
The precision matrix is rank T-2 (two constraints needed).
A tulpa_temporal object
# Create temporal RW2 specification temporal_rw2("year") # Smooth temporal trend set.seed(126) df <- data.frame( year = rep(1:20, each = 4), x = rnorm(80) ) trend <- sin(seq(0, 2, length.out = 20)) df$count <- rpois(80, exp(1 + 0.3 * df$x + trend[df$year])) fit <- tulpa( count ~ x, data = df, family = "poisson", temporal = temporal_rw2("year"), mode = "auto" ) summary(fit)# Create temporal RW2 specification temporal_rw2("year") # Smooth temporal trend set.seed(126) df <- data.frame( year = rep(1:20, each = 4), x = rnorm(80) ) trend <- sin(seq(0, 2, length.out = 20)) df$count <- rpois(80, exp(1 + 0.3 * df$x + trend[df$year])) fit <- tulpa( count ~ x, data = df, family = "poisson", temporal = temporal_rw2("year"), mode = "auto" ) summary(fit)
Specify a time-varying coefficient (TVC): one or more fixed-effect
coefficients are allowed to evolve over time, with the evolution governed by
a temporal prior (rw1, rw2, ar1 or gp).
rw1, rw2 and ar1 read the time index as a position on a grid, so
consecutive instants are one step apart whatever the data says. gp is the
continuous-time structure: the coefficient is a Gaussian process over the
distinct time VALUES, which is what irregular spacing needs. It is distinct
from temporal_gp(), a GP over time entering the linear predictor additively
(eta_i += f(t_i)); here the GP IS a coefficient (eta_i += x_i w(t_i)).
temporal_tvc( time_var, terms = 1, structure = c("rw1", "rw2", "ar1", "gp"), cov = c("exponential", "matern", "gaussian", "periodic"), nu = 1.5, period = NULL, group_var = NULL, shared = NULL, sigma_prior_U = 1, sigma_prior_alpha = 0.01, scale_coords = TRUE ) ## S3 method for class 'tulpa_tvc' print(x, ...)temporal_tvc( time_var, terms = 1, structure = c("rw1", "rw2", "ar1", "gp"), cov = c("exponential", "matern", "gaussian", "periodic"), nu = 1.5, period = NULL, group_var = NULL, shared = NULL, sigma_prior_U = 1, sigma_prior_alpha = 0.01, scale_coords = TRUE ) ## S3 method for class 'tulpa_tvc' print(x, ...)
time_var |
Single character string naming the time variable in the data.
|
terms |
Which coefficients vary over time. A formula, an integer vector
of design-matrix column indices, or a character vector of term names.
Default |
structure |
Temporal prior governing how the coefficients evolve. One of
|
cov, nu, period
|
Covariance kernel for |
group_var |
Optional character string naming a grouping variable for group-specific time-varying coefficients. |
shared |
Whether the effect is shared across processes in a
multi-process model. |
sigma_prior_U, sigma_prior_alpha
|
Penalized-complexity prior on each
varying coefficient's marginal standard deviation, calibrated so that
|
scale_coords |
Logical, |
x |
A |
... |
Ignored. |
A tulpa_tvc object.
temporal_rw1(), temporal_rw2(), temporal_ar1() for the
underlying temporal priors; temporal_gp() for a GP over time that is not
a varying coefficient.
# Intercept that drifts as a first-order random walk over year temporal_tvc("year", structure = "rw1") # A slope evolving as a continuous-time GP over irregularly-spaced visits temporal_tvc("day", terms = ~ x - 1, structure = "gp", cov = "matern")# Intercept that drifts as a first-order random walk over year temporal_tvc("year", structure = "rw1") # A slope evolving as a continuous-time GP over irregularly-spaced visits temporal_tvc("day", terms = ~ x - 1, structure = "gp", cov = "matern")
Compares the variance of observed data to the variance expected under
the fitted model (via simulation). Ratio > 1 = overdispersion;
< 1 = underdispersion. Equivalent to DHARMa::testDispersion().
test_dispersion(object, ...) ## Default S3 method: test_dispersion( object, observed = NULL, nsim = 250L, seed = 123L, alternative = c("two.sided", "greater", "less"), ... )test_dispersion(object, ...) ## Default S3 method: test_dispersion( object, observed = NULL, nsim = 250L, seed = 123L, alternative = c("two.sided", "greater", "less"), ... )
object |
A fitted model with |
... |
Passed to methods. |
observed |
Observed response vector (optional – extracted from fit) |
nsim |
Number of simulations (default 250) |
seed |
Random seed (default 123) |
alternative |
|
An htest object with dispersion ratio and p-value
set.seed(1) d <- data.frame(x = rnorm(80)) d$y <- rpois(80, exp(0.3 + 0.5 * d$x)) fit <- tulpa(y ~ x, data = d, family = "poisson", mode = "laplace") test_dispersion(fit, nsim = 50)set.seed(1) d <- data.frame(x = rnorm(80)) d$y <- rpois(80, exp(0.3 + 0.5 * d$x)) fit <- tulpa(y ~ x, data = d, family = "poisson", mode = "laplace") test_dispersion(fit, nsim = 50)
Counts how many observations fall outside the min-to-max range of
all simulations. Under a correct model, the expected number is
approximately 2 * N / (nsim + 1). Equivalent to DHARMa::testOutliers().
test_outliers(object, ...) ## Default S3 method: test_outliers(object, observed = NULL, nsim = 250L, seed = 123L, ...)test_outliers(object, ...) ## Default S3 method: test_outliers(object, observed = NULL, nsim = 250L, seed = 123L, ...)
object |
A fitted model with |
... |
Passed to methods. |
observed |
Observed response vector (optional) |
nsim |
Number of simulations (default 250) |
seed |
Random seed (default 123) |
An htest object (binomial test)
set.seed(1) d <- data.frame(x = rnorm(80)) d$y <- rpois(80, exp(0.3 + 0.5 * d$x)) fit <- tulpa(y ~ x, data = d, family = "poisson", mode = "laplace") test_outliers(fit, nsim = 50)set.seed(1) d <- data.frame(x = rnorm(80)) d$y <- rpois(80, exp(0.3 + 0.5 * d$x)) fit <- tulpa(y ~ x, data = d, family = "poisson", mode = "laplace") test_outliers(fit, nsim = 50)
If the model is correct, PIT residuals should follow Uniform(0, 1).
Applies a Kolmogorov-Smirnov test against that null.
Equivalent to DHARMa::testUniformity().
test_uniformity( object, observed = NULL, nsim = 250L, seed = 123L, plot = FALSE )test_uniformity( object, observed = NULL, nsim = 250L, seed = 123L, plot = FALSE )
object |
A fitted model, a numeric vector of PIT residuals, or a matrix of simulations |
observed |
Observed data (if object is simulations matrix) |
nsim |
Number of simulations (default 250) |
seed |
Random seed (default 123) |
plot |
If TRUE, draws a QQ plot |
An htest object (KS test result)
set.seed(1) test_uniformity(runif(50)) # PIT residuals supplied directly set.seed(1) d <- data.frame(x = rnorm(80)) d$y <- rpois(80, exp(0.3 + 0.5 * d$x)) fit <- tulpa(y ~ x, data = d, family = "poisson", mode = "laplace") test_uniformity(fit, nsim = 50)set.seed(1) test_uniformity(runif(50)) # PIT residuals supplied directly set.seed(1) d <- data.frame(x = rnorm(80)) d$y <- rpois(80, exp(0.3 + 0.5 * d$x)) fit <- tulpa(y ~ x, data = d, family = "poisson", mode = "laplace") test_uniformity(fit, nsim = 50)
Compares the number of zeros in observed data to the distribution
expected under the fitted model. Equivalent to DHARMa::testZeroInflation().
test_zero_inflation(object, ...) ## Default S3 method: test_zero_inflation(object, observed = NULL, nsim = 250L, seed = 123L, ...)test_zero_inflation(object, ...) ## Default S3 method: test_zero_inflation(object, observed = NULL, nsim = 250L, seed = 123L, ...)
object |
A fitted model with |
... |
Passed to methods. |
observed |
Observed response vector (optional) |
nsim |
Number of simulations (default 250) |
seed |
Random seed (default 123) |
An htest object with zero-inflation ratio and p-value
set.seed(1) d <- data.frame(x = rnorm(80)) d$y <- rpois(80, exp(0.3 + 0.5 * d$x)) fit <- tulpa(y ~ x, data = d, family = "poisson", mode = "laplace") test_zero_inflation(fit, nsim = 50)set.seed(1) d <- data.frame(x = rnorm(80)) d$y <- rpois(80, exp(0.3 + 0.5 * d$x)) fit <- tulpa(y ~ x, data = d, family = "poisson", mode = "laplace") test_zero_inflation(fit, nsim = 50)
tgmrf() lets a user define a Gaussian Markov random field as a latent
block in a tulpa model by supplying two R closures: a precision-matrix
factory Q(theta) and a log-prior on theta prior(theta). The resulting
object plugs into a formula via latent(tgmrf(...)) and is consumed by
every tulpa inference tier (Laplace, EM+Laplace, VI, nested_laplace+CCD,
NUTS, IMH-Laplace).
This is the latent-side dual of LikelihoodSpec. LikelihoodSpec lets a
model package own the observation model; tgmrf() lets a script own a
single latent block.
tgmrf( Q, prior, init, mu = NULL, graph = NULL, bounds = NULL, obs_idx = NULL, name = NULL ) ## S3 method for class 'tgmrf' print(x, ...) ## S3 method for class 'tgmrf' summary(object, ...)tgmrf( Q, prior, init, mu = NULL, graph = NULL, bounds = NULL, obs_idx = NULL, name = NULL ) ## S3 method for class 'tgmrf' print(x, ...) ## S3 method for class 'tgmrf' summary(object, ...)
Q |
A function |
prior |
A function |
init |
Numeric vector of starting values for |
mu |
Optional function |
graph |
Optional sparse matrix specifying the upper bound on |
bounds |
Optional list with components |
obs_idx |
Optional integer vector mapping each observation to a
latent slot in |
name |
Optional character; cosmetic label used by |
x |
A |
... |
Passed from |
object |
A |
For a Gaussian latent block z ~ N(mu(theta), Q(theta)^{-1}):
the gradient -Q(z - mu) and Hessian -Q wrt z are closed form, so the
user never writes gradient code. The Laplace inner step needs only
Q(theta) and mu(theta) at numeric theta. NUTS and VI additionally
use a forward finite-difference gradient on theta, costing dim(theta)
extra Q calls per outer step – cheap for the typical dim(theta) <= 5.
An object of class c("tgmrf", "tulpa_latent_block") with
components:
Q, prior, mu – the user closures (or NULL for mu).
init, theta_names, theta_dim – hyperparameter metadata.
n_latent – inferred from Q(init).
pattern – captured sparsity pattern as a dgCMatrix of 1s.
graph – user-supplied pattern, validated against pattern if given.
bounds, name – passed through.
summary() prints the block, its log prior at init and its bounds,
and returns object invisibly.
latent() for the formula slot that consumes a tgmrf block.
# Periodic AR(1) block, wrap-around tridiagonal precision. periodic_ar1 <- function(n) { tgmrf( Q = function(theta) { sigma <- exp(theta[1]); rho <- tanh(theta[2]) d <- rep((1 + rho^2) / sigma^2, n) o <- rep(-rho / sigma^2, n) M <- Matrix::bandSparse( n, k = c(-1, 0, 1), diagonals = list(o, d, o) ) M[1, n] <- M[n, 1] <- -rho / sigma^2 methods::as(M, "CsparseMatrix") }, prior = function(theta) { stats::dnorm(theta[1], 0, 1, log = TRUE) + stats::dnorm(theta[2], 0, 1, log = TRUE) }, init = c(log_sigma = 0, atanh_rho = 0), name = "periodic_ar1" ) } blk <- periodic_ar1(20) print(blk)# Periodic AR(1) block, wrap-around tridiagonal precision. periodic_ar1 <- function(n) { tgmrf( Q = function(theta) { sigma <- exp(theta[1]); rho <- tanh(theta[2]) d <- rep((1 + rho^2) / sigma^2, n) o <- rep(-rho / sigma^2, n) M <- Matrix::bandSparse( n, k = c(-1, 0, 1), diagonals = list(o, d, o) ) M[1, n] <- M[n, 1] <- -rho / sigma^2 methods::as(M, "CsparseMatrix") }, prior = function(theta) { stats::dnorm(theta[1], 0, 1, log = TRUE) + stats::dnorm(theta[2], 0, 1, log = TRUE) }, init = c(log_sigma = 0, atanh_rho = 0), name = "periodic_ar1" ) } blk <- periodic_ar1(20) print(blk)
tgmrf_cpp() is the compiled-C++ analogue of tgmrf(). Where tgmrf()
takes R closures Q(theta) / prior(theta), tgmrf_cpp() takes a
user-written .cpp file that defines the same kernels as templated C++
(so the same source compiles against every AD type tulpa uses). The
returned object has the same S3 class as tgmrf() – c("tgmrf", "tulpa_latent_block") – so every downstream consumer (formula parser,
inference layers, S3 methods) treats the two paths identically. Only the
dispatch on Q(theta) differs: registry-stored function pointer
(tgmrf_cpp()) vs Rcpp::Function callback (tgmrf()).
The user .cpp file must include <tulpa/tgmrf.h> and call the
TULPA_REGISTER_TGMRF(id, Q_fn, mu_fn, log_prior_fn) macro once. See
inst/examples/tgmrf_periodic_ar1.cpp for a worked example.
tgmrf_cpp( cpp_file, id, init, mu = NULL, graph = NULL, bounds = NULL, obs_idx = NULL, name = NULL, cache_dir = tulpa_cache_dir(), rebuild = FALSE )tgmrf_cpp( cpp_file, id, init, mu = NULL, graph = NULL, bounds = NULL, obs_idx = NULL, name = NULL, cache_dir = tulpa_cache_dir(), rebuild = FALSE )
cpp_file |
Absolute path to the user's |
id |
Character: the stable id used as the registry key. Must match
the first argument of |
init, mu, graph, bounds, obs_idx, name
|
Same arguments as |
cache_dir |
Directory for Rcpp's |
rebuild |
Force a recompile even if the cached DLL is up to date.
Default |
An object of class c("tgmrf", "tulpa_latent_block") with the
same fields as tgmrf() plus backend = "cpp" and cpp_id = id. The
Q / prior fields are R wrapper closures around the registered C++
kernels, so callers (e.g. NUTS-over-theta) that hold the object can
still call block$Q(theta) directly.
tgmrf() for the R-closure path; latent() for the formula
slot that consumes a tgmrf block.
# Compile the periodic AR(1) block shipped with the package (80 nodes, # theta = (log sigma, atanh rho)) into a temporary cache, then evaluate # its registered precision kernel at the initial theta. cpp_file <- system.file("examples", "tgmrf_periodic_ar1.cpp", package = "tulpa") blk <- tgmrf_cpp(cpp_file, id = "tgmrf_periodic_ar1", init = c(log_sigma = 0, atanh_rho = atanh(0.5)), name = "periodic_ar1", cache_dir = file.path(tempdir(), "tulpa-cache")) blk$n_latent Q <- blk$Q(c(0, atanh(0.5))) dim(Q) Q[1:3, 1:3]# Compile the periodic AR(1) block shipped with the package (80 nodes, # theta = (log sigma, atanh rho)) into a temporary cache, then evaluate # its registered precision kernel at the initial theta. cpp_file <- system.file("examples", "tgmrf_periodic_ar1.cpp", package = "tulpa") blk <- tgmrf_cpp(cpp_file, id = "tgmrf_periodic_ar1", init = c(log_sigma = 0, atanh_rho = atanh(0.5)), name = "periodic_ar1", cache_dir = file.path(tempdir(), "tulpa-cache")) blk$n_latent Q <- blk$Q(c(0, atanh(0.5))) dim(Q) Q[1:3, 1:3]
Tidy fixed-effect table (broom-compatible)
## S3 method for class 'tulpa_fit' tidy(x, conf.level = 0.95, ...)## S3 method for class 'tulpa_fit' tidy(x, conf.level = 0.95, ...)
x |
A |
conf.level |
Interval level (default 0.95). |
... |
Ignored. |
Data frame: term, estimate, std.error, conf.low, conf.high.
set.seed(1) df <- data.frame(x = rnorm(100), g = factor(rep(1:10, 10))) df$y <- rpois(100, exp(0.3 * df$x)) fit <- tulpa(y ~ x + (1 | g), data = df, family = "poisson") tidy(fit)set.seed(1) df <- data.frame(x = rnorm(100), g = factor(rep(1:10, 10))) df$y <- rpois(100, exp(0.3 * df$x)) fit <- tulpa(y ~ x + (1 | g), data = df, family = "poisson") tidy(fit)
Single entry point for fitting a Bayesian hierarchical model. tulpa()
parses the formula, builds the model matrices, selects an inference backend
through the tier/mode system (see inference_mode_info()), assembles the
arguments that backend needs, and dispatches.
The fit conditions on the random-effect standard deviations sigma_re (and,
for non-Gaussian dispersion, phi): both the Laplace (Tier 2) and the
sampler (Tier 1) paths target the posterior given these. Integrating over the
hyperparameters is the role of the nested-Laplace / EM layer.
tulpa( formula, data, family = "gaussian", mode = "auto", sigma_re = NULL, n_trials = NULL, weights = NULL, phi = 1, estimate_phi = FALSE, phi2 = NULL, beta_prior = NULL, re_prior = NULL, hyperprior = c("proper", "flat"), ziformula = NULL, zi_prior = NULL, warm_start = NULL, spatial = NULL, temporal = NULL, control = list(), ... )tulpa( formula, data, family = "gaussian", mode = "auto", sigma_re = NULL, n_trials = NULL, weights = NULL, phi = 1, estimate_phi = FALSE, phi2 = NULL, beta_prior = NULL, re_prior = NULL, hyperprior = c("proper", "flat"), ziformula = NULL, zi_prior = NULL, warm_start = NULL, spatial = NULL, temporal = NULL, control = list(), ... )
formula |
A model formula. Fixed effects, |
data |
A data frame. Incomplete rows (an |
family |
Character family name: one of |
mode |
Inference mode or backend. |
sigma_re |
Random-effect SDs to condition on: length 1 (recycled) or one
per RE term. Defaults to 1 per term with a warning. Ignored by every
backend that DETERMINES the RE scale itself – by integrating it
( |
n_trials |
Binomial denominators (length |
weights |
Optional observation weights (non-negative numeric vector,
length |
phi |
Dispersion passed to the family, held fixed. One convention at
every door: for Defaulted, it conditions at 1 and says so with a warning, since for a
family that reads a dispersion that is a modelling choice rather than a
neutral value. |
estimate_phi |
Estimate the dispersion from the data instead of
conditioning on Available under |
phi2 |
Optional second dispersion: the Student-t degrees of freedom
( |
beta_prior |
Optional |
re_prior |
Optional |
hyperprior |
The outer hyperparameter prior, |
ziformula |
Optional one-sided formula for the zero-inflation
probability, e.g. |
zi_prior |
Optional |
warm_start |
Optional starting point for the NUTS sampler, from a
cheaper fit of the same model: The variance-component slots take an adapting mass by default, because a
plug-in fit estimates no curvature for them. Passing a fit from
|
spatial |
Optional spatial-field spec. How it is addressed depends on the field family:
The mode selects how the spatial hyperparameter is handled:
|
temporal |
Optional temporal field spec, integrated by nested Laplace
for the discrete walks and sampled for the continuous ones:
|
control |
Optional list of backend tuning arguments. Each backend
accepts its own set, checked at the door:
The default One statistical knob lives here rather than in the signature: |
... |
Reserved for future statistical arguments. Nothing is read from
it today, so any entry errors: a stray name here is a misspelled argument
or a tuning knob that belongs in |
A tulpa_fit object carrying the backend's output plus
inference_mode, inference_tier, backend, selection_reason,
formula, and family. Two field-name conventions to know when
reaching into the object directly (the generic accessors handle both):
on nested-Laplace fits $weights is the hyperparameter GRID weights;
user observation weights are stored as $obs_weights. $draws is a
draws matrix on engine fits, while model-package fits may carry a list
($y_rep, $log_lik) under the same name.
No random effects and random intercepts ((1 | g)) are supported on
the design path (mode = "laplace") and the sampler path (mode = "mala",
"pathfinder", "imh_laplace", and the ModelData kernels "hmc" / "sghmc"
/ "sgld" / "mclmc" / "smc" / "vi" / "ess").
Random slopes are supported on the Laplace (Tier 2) path: there is no
scalar sigma_re to condition on, so the RE covariance Sigma is integrated
rather than fixed. This covers correlated terms ((1 + x | g), a full
Sigma), uncorrelated terms ((1 + x || g), a diagonal Sigma), and
several terms together ((1 + x | g) + (1 | h)) – each term becomes a
covariance block, and any accompanying (1 | g) term is integrated as a 1x1
block (nothing is silently conditioned at sigma_re = 1). mode = "laplace"
routes to the nested-Laplace Sigma integrator (tulpa_re_cov_nested(),
CCD design, PC + LKJ hyperprior by default – see hyperprior);
control$re_cov = "gibbs" switches to the exact
Metropolis-within-Gibbs debias (tulpa_re_cov_gibbs()), and
control$re_cov = "aghq" keeps the nested integrator but replaces the
inner joint-Laplace marginal with adaptive Gauss-Hermite quadrature
(control$n_quad, default 9 there; see n_quad in
tulpa_re_cov_nested()). Both also run on the
sampler path (mode = "mala" / "pathfinder").
mode = "gibbs" (Polya-Gamma) fits a single random-intercept model for
family = "binomial" or "neg_binomial_2", and samples the RE sd
rather than conditioning on sigma_re; tune it via
re_prior$prior_sigma_scale and a mean-zero beta_prior.
Latent prior blocks (latent(tgmrf(...))) route to the nested-Laplace
path (Tier 2), which integrates over the block hyperparameters. mode = "auto" and "structured" select it automatically when latent blocks are
present; mode = "nested_laplace" forces it. Several random-intercept
(1 | g) terms may accompany the blocks; the one-term restriction belongs
to the Polya-Gamma spatial Gibbs sampler (mode = "gibbs"), which updates
one RE block alongside the field. Joint multi-arm nested models cannot be
expressed by a single-response formula – call
tulpa_nested_laplace_joint() directly.
The ModelData sampler kernels ("hmc", "ess", "sghmc", "sgld",
"mclmc", "smc", "vi") thread the full latent vector – fixed effects,
random effects (all forms), areal spatial (icar / bym2), and temporal
(rw1 / rw2 / ar1) – through one ModelData builder and sample the
variance components jointly with the field. ess carries random effects but
declines a structured spatial / temporal block (its isotropic Gaussian-prior
block cannot represent the graph precision); continuous-coordinate fields
(gp / nngp / hsgp / spde), car_proper, and exotic latent blocks stay
on the dedicated nested-Laplace / SPDE / Polya-Gamma paths.
Rue, Martino & Chopin (2009). Approximate Bayesian inference for latent Gaussian models by using integrated nested Laplace approximations. JRSS-B 71(2):319-392. Hoffman & Gelman (2014). The No-U-Turn Sampler: adaptively setting path lengths in Hamiltonian Monte Carlo. JMLR 15(47):1593-1623.
inference_mode_info(), tulpa_laplace(), mala(), pathfinder()
set.seed(1) n <- 200L g <- sample(letters[1:12], n, replace = TRUE) d <- data.frame( y = rbinom(n, 1, plogis(-0.3 + 0.6 * rnorm(n))), x = rnorm(n), g = g ) # Random-intercept logistic GLMM, Laplace tier. fit <- tulpa(y ~ x + (1 | g), data = d, family = "binomial", mode = "laplace") coef(fit) summary(fit)set.seed(1) n <- 200L g <- sample(letters[1:12], n, replace = TRUE) d <- data.frame( y = rbinom(n, 1, plogis(-0.3 + 0.6 * rnorm(n))), x = rnorm(n), g = g ) # Random-intercept logistic GLMM, Laplace tier. fit <- tulpa(y ~ x + (1 | g), data = d, family = "binomial", mode = "laplace") coef(fit) summary(fit)
Build the block-diagonal Kronecker graph I_L (x) Q and the level-offset
node index for a replicated areal field: one independent copy of the graph
per level of a by factor, all sharing one precision. This is the
graph-side counterpart to tulpa_bar_field_specs() – that helper expands
the coefficient columns and is graph-agnostic, while replication needs the
graph, so it is a sibling rather than a new argument. A downstream package
composes the two (column expansion x replication) from the one
implementation rather than re-deriving the Kronecker remap.
tulpa_bar_field_replicate(adjacency, node, by)tulpa_bar_field_replicate(adjacency, node, by)
adjacency |
Symmetric adjacency matrix of the base graph
( |
node |
Integer vector of 1-based graph-node indices, one per observation (the resolved bar right-hand side). |
by |
A vector of the same length as |
A list:
adjacencythe [L*n_node x L*n_node] block-diagonal Kronecker
adjacency I_L (x) Q (the base graph for L == 1).
indexinteger vector, one per observation: the node index
offset into its level's copy (node + (level - 1) * n_node).
n_levelsthe number of replication levels L.
n_nodesthe base graph node count n_node.
levelsthe factor levels of by, in replicate order.
tulpa_bar_field_specs() for the coefficient-column expansion,
spatial() for the inline areal field constructor whose by = argument
this powers.
adj <- matrix(0, 4, 4) for (i in 1:3) adj[i, i + 1] <- adj[i + 1, i] <- 1 node <- rep(1:4, times = 2) lev <- rep(c("a", "b"), each = 4) rep_info <- tulpa_bar_field_replicate(adj, node, lev) dim(rep_info$adjacency) # 8 x 8 (I_2 (x) Q) rep_info$index # level b nodes offset by 4adj <- matrix(0, 4, 4) for (i in 1:3) adj[i, i + 1] <- adj[i + 1, i] <- 1 node <- rep(1:4, times = 2) lev <- rep(c("a", "b"), each = 4) rep_info <- tulpa_bar_field_replicate(adj, node, lev) dim(rep_info$adjacency) # 8 x 8 (I_2 (x) Q) rep_info$index # level b nodes offset by 4
Expand the left-hand side of an lme4-style varying-coefficient bar
(~ 1 + w || node) against a data frame into one descriptor per design
matrix column. This is the same expansion spatial() and the inline
temporal() field constructor use internally to turn a bar into one CAR /
temporal field per design column, exposed so a downstream package can reuse
the one implementation rather than re-parsing the bar grammar.
The bar's right-hand side names the node index (the graph node for a spatial
field, the time index for a temporal field); it is not expanded but is
returned as the node attribute. The left-hand side is expanded with
stats::model.matrix(): the intercept column (1) is the unweighted
(all-ones) field, and each covariate column is a varying coefficient whose
per-observation weight is that column's design value (0 + drops the
intercept).
tulpa_bar_field_specs(formula, data)tulpa_bar_field_specs(formula, data)
formula |
A one-sided formula carrying a single grouping bar, e.g.
|
data |
A data frame whose columns the bar left-hand side and the node
index refer to. The per-column weight vectors are evaluated against it, so
they have |
A list with one element per design-matrix column, each a list:
column_namecharacter; "Intercept" for the intercept column,
otherwise the model.matrix() column name (e.g. "w").
weighta numeric vector of length nrow(data) for a covariate
column (the per-observation design value scaling that field), or NULL
for the intercept column (which is the all-ones, unweighted field).
is_interceptlogical; TRUE for the intercept column.
The list carries two attributes: node (character, the node-index column
named by the bar right-hand side) and correlated (logical, TRUE for a
single |, FALSE for a double ||).
tulpa_is_spatial_bar() for the recognizer, spatial() for the
inline areal field constructor that consumes this expansion.
d <- data.frame(cell = rep(1:5, each = 4), w = rnorm(20)) # Intercept plus a varying slope on w specs <- tulpa_bar_field_specs(~ 1 + w || cell, d) length(specs) # 2 specs[[1]]$column_name # "Intercept" is.null(specs[[1]]$weight) # TRUE (unweighted field) specs[[2]]$column_name # "w" identical(specs[[2]]$weight, d$w) # TRUE attr(specs, "node") # "cell"d <- data.frame(cell = rep(1:5, each = 4), w = rnorm(20)) # Intercept plus a varying slope on w specs <- tulpa_bar_field_specs(~ 1 + w || cell, d) length(specs) # 2 specs[[1]]$column_name # "Intercept" is.null(specs[[1]]$weight) # TRUE (unweighted field) specs[[2]]$column_name # "w" identical(specs[[2]]$weight, d$w) # TRUE attr(specs, "node") # "cell"
Outer Pareto-k-hat reliability diagnostic for a grid-integrated nested fit
whose inner marginal comes from a BATCHED re-fit rather than a per-sample
closure: fits a Gaussian proposal at (theta_hat, L_scale), draws from it,
evaluates the caller's batched log-target, applies a radius cap with
heavy-tail escalation, then reports pareto_k / is_ess via the shared
PSIS/GPD core. This is more than plain PSIS smoothing over an
already-computed log-ratio vector – see tulpa_psis() for that – because
it also builds the proposal, draws, and evaluates the target.
tulpa_batched_pareto_k( theta_hat, L_scale, log_target_batched, n_samples = .nl_diag("k_samples"), radius_cap = Inf, return_draws = FALSE, tail_points = NULL, Z = NULL )tulpa_batched_pareto_k( theta_hat, L_scale, log_target_batched, n_samples = .nl_diag("k_samples"), radius_cap = Inf, return_draws = FALSE, tail_points = NULL, Z = NULL )
theta_hat, L_scale
|
Mean and Cholesky scale of the Gaussian proposal
|
log_target_batched |
Function taking an |
n_samples |
Number of draws from the proposal. |
radius_cap |
Whitened-radius cap on evaluated draws ( |
return_draws |
If |
tail_points |
Optional override for the PSIS tail size; |
Z |
Optional pre-drawn |
A list with pareto_k, is_ess and n_eval (the number of draws
whose log importance ratio was finite). When the diagnostic ran to
completion it also carries lr, those n_eval finite log importance
ratios in draw order (what pareto_k was fitted on, so
tulpa_psis(lr, tail_points)$pareto_k reproduces it), and with
return_draws = TRUE the evaluated draws U (n_eval x d) and their
smoothed log_weights. When it declined, pareto_k and is_ess are
NA and declined names the reason (e.g. "draws_too_few").
set.seed(1) # A Gaussian target a little wider than the standard-normal proposal: lt <- function(U) -0.5 * rowSums(sweep(U, 2L, c(1.2, 0.8), "/")^2) k <- tulpa_batched_pareto_k(theta_hat = c(0, 0), L_scale = diag(2), log_target_batched = lt, n_samples = 500) k$pareto_kset.seed(1) # A Gaussian target a little wider than the standard-normal proposal: lt <- function(U) -0.5 * rowSums(sweep(U, 2L, c(1.2, 0.8), "/")^2) k <- tulpa_batched_pareto_k(theta_hat = c(0, 0), L_scale = diag(2), log_target_batched = lt, n_samples = 500) k$pareto_k
Deletes cached compilation output under tulpa_cache_dir(). The cache holds
build artefacts only – a removed entry is rebuilt by the next tgmrf_cpp()
call on the same source – so it can be emptied at any time.
tulpa_cache_clear(older_than = 0)tulpa_cache_clear(older_than = 0)
older_than |
Remove only entries whose last modification is more than
this many days ago. |
The number of entries removed, invisibly.
tulpa_cache_dir() for the location.
# Deletes the caller's own cached builds, so it runs only in an # interactive session. if (interactive()) { tulpa_cache_clear(older_than = 30) # keep the last 30 days tulpa_cache_clear() # empty the cache }# Deletes the caller's own cached builds, so it runs only in an # interactive session. if (interactive()) { tulpa_cache_clear(older_than = 30) # keep the last 30 days tulpa_cache_clear() # empty the cache }
tgmrf_cpp()-compiled DLLsReports the per-user directory under tools::R_user_dir("tulpa", "cache")
where tgmrf_cpp() keeps the objects it compiles. Reporting the path does
not create it: the directory appears the first time a compile needs it, and
tulpa_cache_clear() removes what it holds.
tulpa_cache_dir(create = FALSE)tulpa_cache_dir(create = FALSE)
create |
Create the directory if it is missing. |
Absolute path (character) to the cache directory.
tulpa_cache_clear() to remove cached objects.
# Where compiled tgmrf_cpp() blocks are cached. Reporting creates nothing. tulpa_cache_dir()# Where compiled tgmrf_cpp() blocks are cached. Reporting creates nothing. tulpa_cache_dir()
control = list() surface against its canonical key setFront-door fitters across the tulpa* ecosystem carry their perf /
numerical / tuning knobs in a single control list (design principle 6).
Without a name check a misspelled knob is a silent no-op –
control$adaptve_grid fits the default and reports nothing. This validates
the names a caller supplied against the whitelist of what the target fitter
actually reads, and errors listing the allowed set.
tulpa_check_control(control, allowed, where)tulpa_check_control(control, allowed, where)
control |
The |
allowed |
Character vector of accepted knob names. |
where |
Name of the calling fitter, used in the error message. |
Consumer packages (tulpaRatio, tulpaObs) call this with their own key
registry rather than reimplementing the check.
invisible(NULL), called for the side effect of erroring on an
unknown or unnamed knob.
tulpa_check_control(list(max_iter = 50), c("max_iter", "tol"), "my_fit") try(tulpa_check_control(list(max_itr = 50), c("max_iter", "tol"), "my_fit"))tulpa_check_control(list(max_iter = 50), c("max_iter", "tol"), "my_fit") try(tulpa_check_control(list(max_itr = 50), c("max_iter", "tol"), "my_fit"))
Turn an [n_draws x n_obs] pointwise log-likelihood into the standard
Bayesian goodness-of-fit currency: WAIC, DIC, conditional predictive
ordinates (CPO) and their sum LPML, PSIS-LOO, all from one matrix. PSIS-LOO
reuses the native tulpa_psis() smoothing, so the CPO and LOO numbers are
the same computation (CPO_i = exp(elpd_loo_i), LPML = elpd_loo). The
input may be a matrix or a streaming tulpa_loglik() so EVA-scale fits are
processed in observation blocks.
tulpa_criteria( log_lik, criteria = c("waic", "loo", "cpo", "lpml", "dic"), loglik_at_mean = NULL, group = NULL, chunk_size = NULL, pointwise = FALSE ) ## S3 method for class 'tulpa_criteria' print(x, digits = 1, ...)tulpa_criteria( log_lik, criteria = c("waic", "loo", "cpo", "lpml", "dic"), loglik_at_mean = NULL, group = NULL, chunk_size = NULL, pointwise = FALSE ) ## S3 method for class 'tulpa_criteria' print(x, digits = 1, ...)
log_lik |
An |
criteria |
Which criteria to compute. Any of |
loglik_at_mean |
Optional length- |
group |
Optional length- |
chunk_size |
Number of observation columns to process per block. The default streams the whole matrix at once when materialized, else picks a block sized to a few million entries. |
pointwise |
If |
x |
A |
digits |
Decimal places printed. |
... |
Ignored. |
p_waic is the well-known positively-biased variance estimator at low draw
counts; the result records n_draws and the count of observations with
p_waic_i > 0.4 (the loo heuristic for an unreliable WAIC), and the
PSIS-LOO elpd_loo is the more stable figure to report when that count is
non-trivial.
The LOO unit is whatever one column of log_lik holds. If the consumer
built the matrix with one column per row (plot / visit), the default is
per-row LOO; if a column already carries a whole group's compressed
likelihood, leaving it out drops that group and pareto_k can blow up by
construction. The group argument makes the unit explicit: supply it to
aggregate columns into folds and report leave-one-group-out CV (LOGO-CV)
instead, a different and deliberate estimand.
A tulpa_criteria object: a list with the requested scalar scores
(each estimate paired with its standard error where defined),
n_draws / n_obs, the PSIS pareto_k summary, and – when
pointwise = TRUE – a pointwise data frame.
Vehtari, Gelman & Gabry (2017). Practical Bayesian model evaluation using leave-one-out cross-validation and WAIC. Statistics and Computing 27(5):1413-1432. Watanabe (2010). Spiegelhalter et al. (2002). Geisser & Eddy (1979).
tulpa_psis() for the smoothing core, tulpa_pit() for the
probability-integral-transform companion, compare_models() for
model comparison.
# A draws x observations log-likelihood matrix (here built directly; # in practice extracted from a fitted model's posterior draws). set.seed(1) y <- rnorm(40) mu <- matrix(rnorm(200 * 40, sd = 0.2), 200, 40) ll <- dnorm(matrix(y, 200, 40, byrow = TRUE), mean = mu, log = TRUE) tulpa_criteria(ll) tulpa_criteria(ll, criteria = "waic", pointwise = TRUE)$pointwise[1:3, ]# A draws x observations log-likelihood matrix (here built directly; # in practice extracted from a fitted model's posterior draws). set.seed(1) y <- rnorm(40) mu <- matrix(rnorm(200 * 40, sd = 0.2), 200, 40) ll <- dnorm(matrix(y, 200, 40, byrow = TRUE), mean = mu, log = TRUE) tulpa_criteria(ll) tulpa_criteria(ll, criteria = "waic", pointwise = TRUE)$pointwise[1:3, ]
Model-checking tools based on posterior predictive simulation.
These are native R implementations equivalent to DHARMa's test suite,
with no external dependencies. They work with any model that provides
simulate(), fitted(), and residuals() methods.
The diagnostic functions documented in this family return their
individual results (a test-statistic object, a data frame of residuals, or
a check_model summary); see each function's own help page.
Assembles a fitted model's posterior draws into an [iteration, chain, parameter] array – the layout expected by bayesplot and analogous to
posterior::as_draws_array(). Multiple chains are recognised from a 3D
draws array, a $chain_id row map, or an $n_chains count over chain-major
rows; a single pooled chain yields a one-chain array.
tulpa_draws_array(fit)tulpa_draws_array(fit)
fit |
A |
As with posterior_sample(), a fit that carries no draws returns NULL with
a message naming its backend and the representation it carries instead.
A numeric array with dimensions [n_iter, n_chain, n_param] and the
parameter names on the third dimension, or NULL if the fit carries no
draws. Chains of unequal length are truncated to the shortest.
posterior_sample(), tulpa_posterior_draws(), diagnostics()
set.seed(1) d <- data.frame(x = rnorm(60)) d$y <- rpois(60, exp(0.5 + 0.3 * d$x)) fit <- tulpa(y ~ x, data = d, family = "poisson", mode = "hmc", control = list(n_iter = 400L, warmup = 200L, n_chains = 2L, seed = 1L)) dim(tulpa_draws_array(fit)) # [iteration, chain, parameter]set.seed(1) d <- data.frame(x = rnorm(60)) d$y <- rpois(60, exp(0.5 + 0.3 * d$x)) fit <- tulpa(y ~ x, data = d, family = "poisson", mode = "hmc", control = list(n_iter = 400L, warmup = 200L, n_chains = 2L, seed = 1L)) dim(tulpa_draws_array(fit)) # [iteration, chain, parameter]
Estimate one or more random-effect covariances Sigma by maximizing the
Laplace marginal likelihood over them (plus the hyperprior), then report the
fixed effects conditional on the maximizer. This is the plug-in ("ML-II" /
empirical-Bayes) counterpart of tulpa_re_cov_nested(), which integrates over
Sigma instead of fixing it at the maximizer.
tulpa_eb( y, n_trials = NULL, X, re_terms, family = "binomial", phi = 1, phi2 = NULL, prior_sigma = NULL, eta = NULL, hyperprior = c("proper", "flat"), log_prior_theta = NULL, beta_prior = NULL, offset = NULL, n_quad = 1L, marginal = FALSE, estimate_phi = FALSE, X_zi = NULL, zi_prior_sd = 2.5, control = list() )tulpa_eb( y, n_trials = NULL, X, re_terms, family = "binomial", phi = 1, phi2 = NULL, prior_sigma = NULL, eta = NULL, hyperprior = c("proper", "flat"), log_prior_theta = NULL, beta_prior = NULL, offset = NULL, n_quad = 1L, marginal = FALSE, estimate_phi = FALSE, X_zi = NULL, zi_prior_sd = 2.5, control = list() )
y, n_trials, X, family, phi
|
Passed to |
re_terms |
Either a single random-effect term or a list of them; see
|
phi2 |
Optional second dispersion, threaded into every inner
|
prior_sigma, eta
|
Hyperparameters of the PC + LKJ prior used when
|
hyperprior |
|
log_prior_theta |
Optional |
beta_prior |
Optional Gaussian prior on the fixed effects, threaded into
every inner |
offset |
Optional observation-level offset on the linear predictor
(length |
n_quad |
Quadrature order for the inner marginal. |
marginal |
Report fixed-effect intervals that carry the hyperparameter
uncertainty, instead of the intervals conditional on |
estimate_phi |
Estimate the family's dispersion alongside the
random-effect covariances, instead of conditioning on The dispersion enters unpenalized – the hyperprior covers the covariance
coordinates only – so this is the ML-II estimate of Available for every family carrying a dispersion, which is every front-door
family except Alongside |
X_zi |
Optional zero-inflation design matrix ( |
zi_prior_sd |
Prior SD on |
control |
A named list of numerical knobs: |
Blocks, coordinates and the default hyperprior are exactly those of
tulpa_re_cov_nested() – a correlated (1 + x | g) term is a full
Sigma = L L' in log-Cholesky coordinates, an uncorrelated (1 + x || g)
term is diagonal in log-SD coordinates, and a scalar (1 | g) term is the
degenerate one-coefficient block. Both functions call the same outer
objective and the same optimizer, so tulpa_eb()$theta_hat and
tulpa_re_cov_nested()$theta_hat are the same estimate on the same data –
which requires hyperprior to default the same way on both: "proper", the
PC + LKJ prior (see re_cov_pc_lkj_prior()) at the anchor every scale axis
of the engine defaults to, which at small G keeps a block off the
sigma = 0 boundary. hyperprior = "flat" maximizes the marginal likelihood
alone.
The marginal likelihood is marginal over the fixed effects too: the inner
Laplace approximation integrates beta out together with the random
effects (under beta_prior, or the inner solve's vague N(0, 100^2) default)
rather than profiling it. hyperprior = "flat" is therefore a RESTRICTED,
REML-type estimate, not maximum likelihood: for a gaussian response it is
REML, and for a non-gaussian one it agrees with
glmmTMB(..., REML = TRUE), not with the ML fit of lme4::glmer() or
glmmTMB(..., REML = FALSE). On a binomial random intercept with 40 groups
of 6 the three read RE SDs of 0.7931 (tulpa_eb), 0.7931 (glmmTMB REML) and
0.7594 (ML); the fixed effects at theta_hat match the REML fit's as well.
The restriction is what removes ML's downward bias in the variance
components when beta is estimated from the same data, and it carries over
to n_quad > 1 and to estimate_phi = TRUE. Validating against glmer()
should expect exactly that gap.
The reported fixed-effect covariance is the conditional one at theta_hat
(solve(H_beta)). It does not include the hyperparameter uncertainty that
tulpa_re_cov_nested() integrates over, so EB intervals are narrower –
increasingly so as the number of groups falls. Use the nested integrator when
the variance components themselves, or calibrated fixed-effect intervals, are
the target; use EB when the point estimate is, or as a fast starting fit.
A tulpa_fit with:
mode, H_beta: the fixed-effect (and, on the n_quad = 1 path, random-
effect) mode and the fixed-effect precision at theta_hat, driving
coef/confint/vcov/summary.
map: the Sigma / sigma / rho summary at theta_hat (a single list
for one block, a named list of them for several).
Sigma: the estimated covariance (a matrix for one block, a named list
of matrices for several).
theta_hat, log_marginal, layout, n_blocks, n_coefs.
converged: whether the inner Newton solve at theta_hat converged.
outer_convergence: optim's code for the maximization over Sigma
(0 on success). A non-zero value also warns.
With marginal = TRUE and a correction that formed: cov_marginal (the
widened fixed-effect covariance, which vcov / summary / confint
then report), cov_conditional (the solve(H_beta) they would otherwise
have reported, kept so the two are comparable on one fit), H_theta and
theta_cov (the outer Hessian at theta_hat and its inverse, named by
theta_names), and the marginal_step / marginal_richardson actually
used. All absent when the correction was not requested or could not be
formed, so is.null(fit$cov_marginal) tests whether the reported
intervals are marginal. A requested correction that fails warns and
leaves the conditional covariance in place.
Casella (1985). An introduction to empirical Bayes data analysis. The American Statistician 39(2):83-87. Rue, Martino & Chopin (2009). Approximate Bayesian inference for latent Gaussian models by using integrated nested Laplace approximations. JRSS-B 71(2):319-392.
tulpa_re_cov_nested() to integrate over Sigma rather than fix it;
tulpa_laplace() for the inner solve.
set.seed(1) G <- 30L; per <- 10L; n <- G * per grp <- rep(seq_len(G), each = per); x <- rnorm(n) b <- rnorm(G, 0, 0.8) y <- rpois(n, exp(0.3 + 0.5 * x + b[grp])) re_term <- list(idx = grp, n_groups = G, n_coefs = 1L) fit <- tulpa_eb(y, NULL, cbind(1, x), re_term, family = "poisson") fit$map$sigma # empirical-Bayes RE standard deviationset.seed(1) G <- 30L; per <- 10L; n <- G * per grp <- rep(seq_len(G), each = per); x <- rnorm(n) b <- rnorm(G, 0, 0.8) y <- rpois(n, exp(0.3 + 0.5 * x + b[grp])) re_term <- list(idx = grp, n_groups = G, n_coefs = 1L) fit <- tulpa_eb(y, NULL, cbind(1, x), re_term, family = "poisson") fit$map$sigma # empirical-Bayes RE standard deviation
Generic EM engine. Model packages provide callbacks for the E-step
(latent variable posterior) and the M-step encoding (how to assemble
submodel data from weights). Each M-step submodel block is fit
independently via tulpa_laplace(); the engine reads family and
offset per block so heterogeneous-family mixtures (e.g. a binomial
zero submodel + poisson positive submodel for a hurdle model) work
without engine changes.
tulpa_em_laplace( e_step, m_step_encode, spatial = NULL, re_list = list(), max_iter = 50L, tol = 1e-04, damping = 0.3, correction = c("auto", "mi", "gibbs", "none"), n_imputations = 20L, n_gibbs = 10L, draw_z = NULL, m_step_extra = NULL, beta_prior = NULL, verbose = TRUE, ... )tulpa_em_laplace( e_step, m_step_encode, spatial = NULL, re_list = list(), max_iter = 50L, tol = 1e-04, damping = 0.3, correction = c("auto", "mi", "gibbs", "none"), n_imputations = 20L, n_gibbs = 10L, draw_z = NULL, m_step_extra = NULL, beta_prior = NULL, verbose = TRUE, ... )
e_step |
Callback: |
m_step_encode |
Callback:
|
spatial, re_list
|
Not consumed at the driver level – latent structure
is per-submodel, set as a |
max_iter |
Maximum EM iterations. |
tol |
Convergence tolerance on max relative parameter change. |
damping |
EM damping factor in |
correction |
Post-EM correction. |
n_imputations |
Number of MI draws (default |
n_gibbs |
Length of the Gibbs chain (default |
draw_z |
Optional function |
m_step_extra |
Optional |
beta_prior |
Optional Gaussian prior on the fixed effects, applied to
every block fit via |
verbose |
Print per-iteration progress. |
... |
Forwarded to |
This is an engine block, not a front door: model packages call it
programmatically, so its tuning knobs (max_iter, tol, damping)
sit in the signature rather than in a control list.
A list with:
fits – named list of tulpa_laplace() results, one per block.
weights – final E-step weights.
n_iter – number of EM iterations actually run.
converged – logical.
history – data.frame(iter, delta) of max relative parameter
change per iteration.
correction – the resolved correction mode ("none", "mi",
or "gibbs").
pooled – present when correction %in% c("mi", "gibbs").
Named list of pooled per-submodel summaries from rubins_pool().
draws – present when correction %in% c("mi", "gibbs").
List of per-draw fits with beta / se attached.
weights, not in y
The M-step maximizes the expected complete-data log-likelihood. For a
Bernoulli latent z_i carrying E-step posterior weight w_i that is
a weighted Bernoulli log-likelihood, which carries no binomial
coefficient. Encode it as two rows per unit – y = 1 at weight w_i and
y = 0 at weight 1 - w_i:
list(y = rep(c(1, 0), each = n),
X = rbind(X, X),
weights = c(w, 1 - w),
family = "binomial")
A fractional y on a binomial block is refused, and is not the same
objective: it asks for the exact binomial density at a non-integer
response, whose normalizer lchoose(n, y) is not zero there and depends on
w. On the weighted encoding the block's log_marginal is the Laplace
marginal of Q, an EM objective that increases across iterations.
# Zero-inflated Poisson via EM: the E-step scores the posterior probability # that each zero is non-structural. Those are soft labels, so the occupancy # arm is the weighted Bernoulli above -- two rows per unit -- and the # abundance arm weights each count by the same w. set.seed(1) n <- 200 z <- rbinom(n, 1, 0.7) y <- rpois(n, 4) * z X <- cbind(1, rnorm(n)) e_step <- function(fits, ...) { if (!length(fits)) return(list(weights = pmax(as.numeric(y > 0), 0.5))) psi <- plogis(drop(X %*% fits$occ$mode)) lam <- exp(drop(X %*% fits$abund$mode)) w <- ifelse(y > 0, 1, psi * exp(-lam) / (psi * exp(-lam) + (1 - psi))) list(weights = w) } m_step_encode <- function(weights, ...) { list( occ = list(y = rep(c(1, 0), each = n), X = rbind(X, X), weights = c(weights, 1 - weights), family = "binomial"), abund = list(y = y, X = X, family = "poisson", weights = weights) ) } res <- tulpa_em_laplace(e_step, m_step_encode, verbose = FALSE) res$converged plogis(res$fits$occ$mode[1]) # P(non-structural), truth 0.7 exp(res$fits$abund$mode[1]) # abundance mean, truth 4# Zero-inflated Poisson via EM: the E-step scores the posterior probability # that each zero is non-structural. Those are soft labels, so the occupancy # arm is the weighted Bernoulli above -- two rows per unit -- and the # abundance arm weights each count by the same w. set.seed(1) n <- 200 z <- rbinom(n, 1, 0.7) y <- rpois(n, 4) * z X <- cbind(1, rnorm(n)) e_step <- function(fits, ...) { if (!length(fits)) return(list(weights = pmax(as.numeric(y > 0), 0.5))) psi <- plogis(drop(X %*% fits$occ$mode)) lam <- exp(drop(X %*% fits$abund$mode)) w <- ifelse(y > 0, 1, psi * exp(-lam) / (psi * exp(-lam) + (1 - psi))) list(weights = w) } m_step_encode <- function(weights, ...) { list( occ = list(y = rep(c(1, 0), each = n), X = rbind(X, X), weights = c(weights, 1 - weights), family = "binomial"), abund = list(y = y, X = X, family = "poisson", weights = weights) ) } res <- tulpa_em_laplace(e_step, m_step_encode, verbose = FALSE) res$converged plogis(res$fits$occ$mode[1]) # P(non-structural), truth 0.7 exp(res$fits$abund$mode[1]) # abundance mean, truth 4
Same M-step plumbing as tulpa_em_laplace(): every iteration calls
m_step_encode(weights, ...) to assemble per-submodel blocks and
fits each block via tulpa_laplace(). The difference is the E-step:
instead of computing closed-form weights, an e_step_sample
callback returns n_mc weight draws per iteration. Each draw is
run through the M-step independently and the resulting parameter
estimates are pooled via rubins_pool().
Convergence criterion is the max relative change in pooled M-step
parameter estimates between iterations. To increase Monte-Carlo
accuracy as iterations progress (Booth-Hobert ascent-based MCEM),
set n_mc_growth > 1.
This is an engine block, not a front door: model packages call it
programmatically, so its tuning knobs (max_iter, tol, n_mc)
sit in the signature rather than in a control list.
tulpa_em_mc( e_step_sample, m_step_encode, n_mc = 10L, n_mc_growth = 1, n_mc_max = 200L, max_iter = 30L, tol = 0.001, verbose = TRUE, ... )tulpa_em_mc( e_step_sample, m_step_encode, n_mc = 10L, n_mc_growth = 1, n_mc_max = 200L, max_iter = 30L, tol = 0.001, verbose = TRUE, ... )
e_step_sample |
Function |
m_step_encode |
Function |
n_mc |
Initial number of Monte-Carlo draws per iteration
(default |
n_mc_growth |
Multiplicative growth of |
n_mc_max |
Cap on |
max_iter |
Maximum EM iterations (default |
tol |
Convergence tolerance on max relative change in pooled
parameter estimates (default |
verbose |
Print per-iteration progress (default |
... |
Forwarded to |
A list with:
pooled – named list of pooled per-submodel summaries
(mean, se, V_within, V_between, V_total); see
rubins_pool().
fits – list of per-MC-draw fit lists from the final
iteration, each indexed by submodel name.
n_iter – iterations actually run.
n_mc_final – n_mc value used in the last iteration.
converged – logical.
history – data.frame(iter, n_mc, delta).
Inherits the tier of the inner M-step (Laplace => Tier 2). The
Monte-Carlo E-step itself is exact in the limit n_mc -> infinity,
so as a full pipeline MCEM is asymptotically Tier 1 if
e_step_sample is exact.
Wei, G. C. G., & Tanner, M. A. (1990). A Monte Carlo implementation of the EM algorithm and the poor man's data augmentation algorithms. Journal of the American Statistical Association, 85(411), 699-704.
Booth, J. G., & Hobert, J. P. (1999). Maximizing generalized linear mixed model likelihoods with an automated Monte Carlo EM algorithm. JRSS B, 61(1), 265-285.
tulpa_em_laplace() for the closed-form-weights variant,
rubins_pool() for the pooling rule.
# Two-component Gaussian mixture with unit variances: the latent class z is # sampled in the E-step, and each class mean is an intercept-only Gaussian # block in the M-step. set.seed(1) z <- rbinom(100, 1, 0.4) y <- ifelse(z == 0, rnorm(100, -1), rnorm(100, 2)) e_step_sample <- function(fits, n_mc, ...) { p1 <- if (length(fits) == 0L) { ifelse(y > median(y), 0.8, 0.2) } else { mu0 <- fits[[1]]$comp0$mode[1] mu1 <- fits[[1]]$comp1$mode[1] l0 <- dnorm(y, mu0, log = TRUE) + log(0.6) l1 <- dnorm(y, mu1, log = TRUE) + log(0.4) 1 / (1 + exp(l0 - l1)) } lapply(seq_len(n_mc), function(s) rbinom(length(y), 1, p1)) } m_step_encode <- function(weights, ...) { list(comp0 = list(y = y[weights == 0], X = matrix(1, sum(weights == 0), 1), family = "gaussian"), comp1 = list(y = y[weights == 1], X = matrix(1, sum(weights == 1), 1), family = "gaussian")) } fit <- tulpa_em_mc(e_step_sample, m_step_encode, n_mc = 5L, max_iter = 5L, tol = 5e-3, verbose = FALSE) sapply(fit$pooled, function(p) p$mean) fit$history# Two-component Gaussian mixture with unit variances: the latent class z is # sampled in the E-step, and each class mean is an intercept-only Gaussian # block in the M-step. set.seed(1) z <- rbinom(100, 1, 0.4) y <- ifelse(z == 0, rnorm(100, -1), rnorm(100, 2)) e_step_sample <- function(fits, n_mc, ...) { p1 <- if (length(fits) == 0L) { ifelse(y > median(y), 0.8, 0.2) } else { mu0 <- fits[[1]]$comp0$mode[1] mu1 <- fits[[1]]$comp1$mode[1] l0 <- dnorm(y, mu0, log = TRUE) + log(0.6) l1 <- dnorm(y, mu1, log = TRUE) + log(0.4) 1 / (1 + exp(l0 - l1)) } lapply(seq_len(n_mc), function(s) rbinom(length(y), 1, p1)) } m_step_encode <- function(weights, ...) { list(comp0 = list(y = y[weights == 0], X = matrix(1, sum(weights == 0), 1), family = "gaussian"), comp1 = list(y = y[weights == 1], X = matrix(1, sum(weights == 1), 1), family = "gaussian")) } fit <- tulpa_em_mc(e_step_sample, m_step_encode, n_mc = 5L, max_iter = 5L, tol = 5e-3, verbose = FALSE) sapply(fit$pooled, function(p) p$mean) fit$history
Fits a generalized linear model with a Gaussian coefficient prior by Expectation Propagation: the posterior is approximated by a Gaussian whose per-observation sites match the moments of the tilted distribution (via Gauss-Hermite quadrature). EP is exact for a Gaussian likelihood and typically more accurate than Laplace on skewed likelihoods, since it matches marginal moments rather than the mode curvature.
tulpa_ep( formula, data, family = "binomial", phi = 1, phi2 = NULL, n_trials = NULL, beta_prior = .tulpa_default_beta_prior("ep"), control = list() )tulpa_ep( formula, data, family = "binomial", phi = 1, phi2 = NULL, n_trials = NULL, beta_prior = .tulpa_default_beta_prior("ep"), control = list() )
formula |
Model formula. |
data |
A data frame. |
family |
Character family name (see |
phi |
Dispersion passed to the family, held fixed. One convention at
every door: for Defaulted, it conditions at 1 and says so with a warning, since for a
family that reads a dispersion that is a modelling choice rather than a
neutral value. |
phi2 |
Optional second dispersion (Student-t degrees of freedom for
|
n_trials |
Binomial denominators (length |
beta_prior |
Fixed-effect prior as |
control |
List: An |
A tulpa_fit with means (posterior mean) and
cov (posterior covariance) of the EP Gaussian, which is the posterior
coef(), vcov(), summary.tulpa_fit() and confint() report; draws sampled
from that Gaussian; log_marginal (the EP approximation); converged
(the sweep tolerance was met AND every site's adaptive quadrature mode
search converged AND no site's tilted variance floored – a site that is
not well resolved by the quadrature turns this FALSE even when the
outer sweep loop itself settled); n_site_not_converged,
n_site_floored.
Minka (2001). Expectation Propagation for approximate Bayesian inference. UAI. Rasmussen & Williams (2006). Gaussian Processes for Machine Learning, Algorithm 3.5.
tulpa() (Laplace / sampler tiers), pathfinder() (VI).
set.seed(1) d <- data.frame(x = rnorm(200)) d$y <- rbinom(200, 1, plogis(-0.3 + 0.8 * d$x)) fit <- tulpa_ep(y ~ x, data = d, family = "binomial") coef(fit)set.seed(1) d <- data.frame(x = rnorm(200)) d$y <- rbinom(200, 1, plogis(-0.3 + 0.8 * d$x)) fit <- tulpa_ep(y ~ x, data = d, family = "binomial") coef(fit)
Lightweight constructor for a tulpa_family object that exposes the
contract required by prior_predict() and tulpa_simulate(). Model
packages (tulpaRatio, tulpaObs) register richer families that also link to
C++ likelihoods; this helper is for tests and simple custom families that
only need simulation.
tulpa_family( name, simulate_fn, process_names = "y", extra_params = list(), link_inv = NULL ) ## S3 method for class 'tulpa_family' print(x, ...)tulpa_family( name, simulate_fn, process_names = "y", extra_params = list(), link_inv = NULL ) ## S3 method for class 'tulpa_family' print(x, ...)
name |
Family name (character, length 1). |
simulate_fn |
|
process_names |
Character vector. Defaults to |
extra_params |
Named list of |
link_inv |
List of inverse-link functions per process; defaults to
identity for every process. tulpa passes the raw linear predictor to
|
x |
A |
... |
Ignored. |
A tulpa_family object.
fam <- tulpa_family( name = "poisson", simulate_fn = function(eta, params, n_obs, ...) { rpois(n_obs, exp(eta[[1]])) } )fam <- tulpa_family( name = "poisson", simulate_fn = function(eta, params, n_obs, ...) { rpois(n_obs, exp(eta[[1]])) } )
The constructor summary.tulpa_fit() returns through: everything but the
fixed-effect table is read off the fit the same way for every model. A
model package whose fit stores its coefficients in its own layout (several
arms, no draws) computes that table and hands it here, so its summary()
is the same object, printed and read the same way.
tulpa_fit_summary(object, coefficients, level = 0.95)tulpa_fit_summary(object, coefficients, level = 0.95)
object |
A |
coefficients |
Data frame, one row per coefficient (row names are the
coefficient names), with columns |
level |
Coverage of the interval columns. |
A summary.tulpa_fit object, as described on summary.tulpa_fit().
set.seed(1) df <- data.frame(x = rnorm(60)) df$y <- rpois(60, exp(0.5 + 0.3 * df$x)) fit <- tulpa(y ~ x, data = df, family = "poisson", mode = "laplace") tab <- coef(summary(fit)) identical(coef(tulpa_fit_summary(fit, tab)), tab)set.seed(1) df <- data.frame(x = rnorm(60)) df$y <- rpois(60, exp(0.5 + 0.3 * df$x)) fit <- tulpa(y ~ x, data = df, family = "poisson", mode = "laplace") tab <- coef(summary(fit)) identical(coef(tulpa_fit_summary(fit, tab)), tab)
Parses mixed-model formulas by walking the formula's abstract syntax tree.
R formulas are already parse trees – we do structural recursion to find
random effect terms (| nodes) and separate them from fixed effects.
The formula helpers documented in this family return parsed-formula structures (lists describing the fixed effects, random-effect terms, and latent blocks); see each function's own help page.
Proof-of-concept function demonstrating the tulpa generic interface: the
Gaussian likelihood is supplied as a LikelihoodSpec and fitted by the
production NUTS sampler every other ModelData backend runs (dual-averaged
step size, adapted diagonal mass matrix, reverse-mode AD gradient checked
against finite differences before sampling). Fits
y ~ Normal(X * beta, sigma).
tulpa_gaussian( formula, data, beta_prior = .tulpa_default_beta_prior("gaussian"), control = list() )tulpa_gaussian( formula, data, beta_prior = .tulpa_default_beta_prior("gaussian"), control = list() )
formula |
A formula (e.g., y ~ x1 + x2) |
data |
A data frame |
beta_prior |
Fixed-effect prior as |
control |
List of sampler knobs: |
A tulpa_fit list with draws (post-warmup draws of beta[j] and
log_sigma), means, sigma (exp() of the posterior mean of
log_sigma), accept_rate (the mean NUTS acceptance statistic over the
post-warmup iterations), the per-iteration accept_prob, divergent,
treedepth and log_prob, the adapted step size epsilon, and
formula, N, p.
set.seed(1) d <- data.frame(x = rnorm(40)) d$y <- 1 + 2 * d$x + rnorm(40, 0, 0.6) fit <- tulpa_gaussian(y ~ x, d, control = list(iter = 1000, warmup = 500, seed = 1)) fit$means fit$accept_rateset.seed(1) d <- data.frame(x = rnorm(40)) d$y <- 1 + 2 * d$x + rnorm(40, 0, 0.6) fit <- tulpa_gaussian(y ~ x, d, control = list(iter = 1000, warmup = 500, seed = 1)) fit$means fit$accept_rate
Public API for PG Gibbs sampling. Used by model packages for binomial and negative binomial GLMMs.
tulpa_gibbs( y, n_trials, X, group, n_groups, family = "binomial", beta_prior = .tulpa_default_beta_prior("gibbs"), prior_sigma_scale = 2.5, spatial = NULL, temporal = NULL, control = list() )tulpa_gibbs( y, n_trials, X, group, n_groups, family = "binomial", beta_prior = .tulpa_default_beta_prior("gibbs"), prior_sigma_scale = 2.5, spatial = NULL, temporal = NULL, control = list() )
y |
Response vector |
n_trials |
Trial sizes (binomial) |
X |
Design matrix |
group |
Integer vector of group indices (1-based) |
n_groups |
Number of groups |
family |
Character: "binomial" or "neg_binomial_2" |
beta_prior |
Fixed-effect prior as |
prior_sigma_scale |
Prior scale for RE sigma (statistical; default 2.5). |
spatial |
Optional spatial spec. When supplied the fit routes to the
matching spatial Polya-Gamma Gibbs sampler via
|
temporal |
Optional temporal spec: a validated |
control |
A named list of numerical / tuning knobs (statistical
arguments stay in the signature above): |
For family = "neg_binomial_2" the Polya-Gamma weights are drawn
at the exact real shape PG(y + r, eta) and the dispersion r is
updated by a random-walk Metropolis-Hastings step on log(r) whose
stationary support is bounded to r in [0.1, 500]; data favouring a
dispersion outside that range pile up at the boundary.
Every sampler that moves a latent effect's level through the first
coefficient – the negative-binomial kernels, and the binomial kernels
carrying a spatial or temporal field – leaves eta unchanged only
when the first column of X is an all-ones intercept, and those routes
error on a design without one. An intrinsic field (ICAR, the structured
BYM2 part, RW1) is reported centred with its level in the intercept, and
its sweep carries the intercept's prior through the field mean; a proper
one (the negative-binomial iid block, an NNGP field) has the level it
shares with the intercept drawn from its full conditional, which both
priors define.
A tulpa_fit holding one MCMC chain: draws, the
[n_saved x n_param] matrix of retained draws, with chain_id,
n_chains, means (column means) and param_names. Columns follow the
parameter naming of the ModelData samplers: the fixed effects (the column
names of X, else beta[j]), log_sigma_re and re[g] for the
random-intercept block, then the route's own blocks – log_phi (the
negative-binomial size); log_tau_spatial and phi_spatial[u] (ICAR, and
RSR's projected field); log_sigma_spatial, logit_rho_bym2,
phi_spatial[u] and theta_spatial[u] (BYM2); log_sigma2_gp,
log_phi_gp and gp_w[u] (GP); the _local / _regional counterparts
with gp_local[u] / gp_regional[u] (multiscale GP); log_sigma2_trend,
trend[t], log_sigma2_seasonal, seasonal[s], log_sigma2_short,
short_term[t] and, for an AR1 short-term component, logit_rho_short
(temporal). Scales are stored on the log scale and correlations on the
logit scale, as the samplers store them; a component the model does not
carry has no column.
set.seed(1) G <- 20L; npg <- 15L; n <- G * npg grp <- rep(seq_len(G), each = npg) X <- cbind(1, rnorm(n)) b <- rnorm(G, 0, 0.6) y <- rbinom(n, 1, plogis(X %*% c(-0.2, 0.5) + b[grp])) fit <- tulpa_gibbs(y, rep(1L, n), X, grp, G, family = "binomial", control = list(n_iter = 500L, warmup = 250L)) coef(fit) diagnostics(fit)set.seed(1) G <- 20L; npg <- 15L; n <- G * npg grp <- rep(seq_len(G), each = npg) X <- cbind(1, rnorm(n)) b <- rnorm(G, 0, 0.6) y <- rbinom(n, 1, plogis(X %*% c(-0.2, 0.5) + b[grp])) fit <- tulpa_gibbs(y, rep(1L, n), X, grp, G, family = "binomial", control = list(n_iter = 500L, warmup = 250L)) coef(fit) diagnostics(fit)
Materialises one of tulpa's own default outer-grid axes – the values a
*_grid argument (e.g. sigma_grid, range_grid) takes when a caller
does not supply one. Geometric axes are returned as exp(seq(log(lo), log(hi), length.out = n)); axes declared as explicit nodes are returned
as-is. A default is a decision tulpa makes internally and can change
between releases (gcol33/tulpa#633), so a caller that needs to know the
current default – rather than restate it – should read it here instead
of duplicating tulpa's own grid table.
tulpa_grid_axis(key, n = NULL)tulpa_grid_axis(key, n = NULL)
key |
Name of a default axis, e.g. |
n |
Optional resolution override: same |
Numeric vector of grid nodes.
tulpa_grid_axis("field_sd") tulpa_grid_axis("copy_alpha", n = 9)tulpa_grid_axis("field_sd") tulpa_grid_axis("copy_alpha", n = 9)
The per-cell log quadrature weight (prior mass) of a nested-Laplace outer
grid: every path that turns a fit's log_marginal into posterior cell
weights goes through this one rule, so the prior mass a cell carries is
decided in one place. Rebuilds axis specs from the grid's own columns when
specs is not supplied. Intended for reconstructing a fit's outer-grid
posterior weights (with tulpa_theta_matrix() and
tulpa_normalise_weights_safe()) when the fit doesn't already carry
fit$log_quad.
tulpa_grid_log_quad( theta_grid, specs = NULL, copy_slab = "exponential", close_domain = TRUE, folded_axes = NULL, refining = NULL )tulpa_grid_log_quad( theta_grid, specs = NULL, copy_slab = "exponential", close_domain = TRUE, folded_axes = NULL, refining = NULL )
theta_grid |
A named |
specs |
Optional pre-built per-axis spec list; |
copy_slab |
|
close_domain |
Whether an unbounded axis's outer cells are closed at the grid's own edge rather than left open to infinity. |
folded_axes |
Optional names of axes folded onto |
refining |
Optional refinement tag vector (see
|
Numeric vector of per-cell log quadrature weights, length
nrow(theta_grid), or NULL if theta_grid has no axis names.
tulpa_theta_matrix(), tulpa_normalise_weights_safe(),
tulpa_joint_axis_specs_from_grid()
set.seed(1) S <- 30L # spatial units in a chain nb <- lapply(seq_len(S), function(s) setdiff(c(s - 1L, s + 1L), c(0L, S + 1L))) nn <- lengths(nb) field <- as.numeric(scale(cumsum(rnorm(S, 0, 0.4)))) idx <- rep(seq_len(S), each = 6L); n <- length(idx); x <- rnorm(n) y <- rbinom(n, 1L, plogis(-0.2 + 0.6 * x + field[idx])) prior <- list(type = "icar", n_spatial_units = S, spatial_idx = idx, adj_row_ptr = c(0L, cumsum(nn)), adj_col_idx = unlist(nb) - 1L, n_neighbors = nn, tau_grid = c(0.5, 1, 2, 4, 8)) fit <- tulpa_nested_laplace(y, rep(1L, n), cbind(1, x), prior = prior, family = "binomial", control = list(progress = FALSE)) tg <- tulpa_theta_matrix(fit) lq <- tulpa_grid_log_quad(tg) # The fit's own cell weights, rebuilt from its log marginals: tulpa_normalise_weights_safe(fit$log_marginal, log_quad = lq)set.seed(1) S <- 30L # spatial units in a chain nb <- lapply(seq_len(S), function(s) setdiff(c(s - 1L, s + 1L), c(0L, S + 1L))) nn <- lengths(nb) field <- as.numeric(scale(cumsum(rnorm(S, 0, 0.4)))) idx <- rep(seq_len(S), each = 6L); n <- length(idx); x <- rnorm(n) y <- rbinom(n, 1L, plogis(-0.2 + 0.6 * x + field[idx])) prior <- list(type = "icar", n_spatial_units = S, spatial_idx = idx, adj_row_ptr = c(0L, cumsum(nn)), adj_col_idx = unlist(nb) - 1L, n_neighbors = nn, tau_grid = c(0.5, 1, 2, 4, 8)) fit <- tulpa_nested_laplace(y, rep(1L, n), cbind(1, x), prior = prior, family = "binomial", control = list(progress = FALSE)) tg <- tulpa_theta_matrix(fit) lq <- tulpa_grid_log_quad(tg) # The fit's own cell weights, rebuilt from its log marginals: tulpa_normalise_weights_safe(fit$log_marginal, log_quad = lq)
Validates a copy_slab argument – "exponential" (the default) or
"flat", the two continuum measures tulpa supports for a copy scale's
non-atom mass – defaulting NULL to "exponential" and erroring on
anything else. Intended so a consumer package's own copy_slab argument
stays in sync with tulpa's own accepted choices rather than restating
them.
tulpa_hyper_check_copy_slab(x)tulpa_hyper_check_copy_slab(x)
x |
A |
x, defaulted to "exponential" when NULL.
tulpa_hyper_copy_slab_density()
tulpa_hyper_check_copy_slab("exponential") try(tulpa_hyper_check_copy_slab("uniform"))tulpa_hyper_check_copy_slab("exponential") try(tulpa_hyper_check_copy_slab("uniform"))
The proper density tulpa uses for the copy scale's continuum: an
exponential, the penalized-complexity prior for a scale parameter with its
base model at zero (Simpson et al. 2017). The rate is set by putting 5% of
the prior mass above upper, so a caller that reads this off a fit's own
declared grid gets the exact rate the outer integration used – rather
than restating a PC-prior rate that could silently drift from it.
tulpa_hyper_copy_slab_density(upper)tulpa_hyper_copy_slab_density(upper)
upper |
The largest declared node of the copy-scale axis. |
A function log p(x) = log(lambda) - lambda * x, or NULL if
upper is not a finite positive number.
tulpa_hyper_check_copy_slab(), tulpa_joint_axis_specs_from_grid()
lp <- tulpa_hyper_copy_slab_density(upper = 2) lp(c(0.1, 1, 2))lp <- tulpa_hyper_copy_slab_density(upper = 2) lp(c(0.1, 1, 2))
The hyperparameter half of a draw from the outer-grid mixture: one row per
draw, one column per outer-grid axis. A draw's coordinate on each axis is
sampled from the within-cell density its cell carries under the fit's own
within-cell read, so the columns are a continuous marginal rather than the
handful of grid-node values fit$theta_grid[cells, ] returns.
An axis carrying a declared POINT MASS – the copy scale's alpha = 0, the
"no coupling" model whose prior probability is stated rather than read off a
node count – keeps it: draws in that cell are exactly the level, in the
proportion the fit reports as fit$copy_atom$posterior_mass, and the
continuum above it is continuized on its own partition. The level is a model
and not a cell representative, so spreading it over a box would both put mass
where the axis has none and leave none on the level itself.
Reading the node coordinate directly is what
tulpa_posterior_draws() consumers used to do, and on a 5- to 15-node axis
it makes every draw an atom: a truth between two nodes, or past the
outermost one, has no draw that can land near it. The continuization is the
cell-conditional of the SAME construction the fit reports its interval from
(box_uniform by default, chord when the fit asked for it or its support
does not admit the box read), so the draws reproduce the fit's own
theta_ci_lo / theta_median / theta_ci_hi to Monte Carlo error.
tulpa_hyper_draws(fit, cells = NULL, n = 1000, within = NULL)tulpa_hyper_draws(fit, cells = NULL, n = 1000, within = NULL)
fit |
A nested-Laplace fit ( |
cells |
Integer vector of outer-grid cell indices, one per draw – the
|
n |
Number of draws when |
within |
Within-cell construction, |
A numeric matrix [n x n_axes] with the outer grid's axis names as
columns, or NULL when the fit carries no outer-grid axis. Carries
attr(., "cells") – the cell each row's coordinates were drawn in –
plus attr(., "within_cell") and attr(., "within_cell_declined"), the
per-axis construction that ran and why a requested one did not, and
attr(., "within_cell_copula"), the Gaussian-copula correlation matrix
that ties the axes' within-cell draws together so the draws keep the
grid's own correlation between axes (the identity where the grid's axes
are uncorrelated). Each axis's marginal is the same whatever the copula.
tulpa_posterior_draws(), tulpa_nested_laplace()
set.seed(1) S <- 30L # spatial units in a chain nb <- lapply(seq_len(S), function(s) setdiff(c(s - 1L, s + 1L), c(0L, S + 1L))) nn <- lengths(nb) field <- as.numeric(scale(cumsum(rnorm(S, 0, 0.4)))) idx <- rep(seq_len(S), each = 6L); n <- length(idx); x <- rnorm(n) y <- rbinom(n, 1L, plogis(-0.2 + 0.6 * x + field[idx])) prior <- list(type = "icar", n_spatial_units = S, spatial_idx = idx, adj_row_ptr = c(0L, cumsum(nn)), adj_col_idx = unlist(nb) - 1L, n_neighbors = nn, tau_grid = c(0.5, 1, 2, 4, 8)) fit <- tulpa_nested_laplace(y, rep(1L, n), cbind(1, x), prior = prior, family = "binomial", control = list(progress = FALSE)) th <- tulpa_hyper_draws(fit, n = 200) quantile(th[, "tau"], c(0.025, 0.5, 0.975))set.seed(1) S <- 30L # spatial units in a chain nb <- lapply(seq_len(S), function(s) setdiff(c(s - 1L, s + 1L), c(0L, S + 1L))) nn <- lengths(nb) field <- as.numeric(scale(cumsum(rnorm(S, 0, 0.4)))) idx <- rep(seq_len(S), each = 6L); n <- length(idx); x <- rnorm(n) y <- rbinom(n, 1L, plogis(-0.2 + 0.6 * x + field[idx])) prior <- list(type = "icar", n_spatial_units = S, spatial_idx = idx, adj_row_ptr = c(0L, cumsum(nn)), adj_col_idx = unlist(nb) - 1L, n_neighbors = nn, tau_grid = c(0.5, 1, 2, 4, 8)) fit <- tulpa_nested_laplace(y, rep(1L, n), cbind(1, x), prior = prior, family = "binomial", control = list(progress = FALSE)) th <- tulpa_hyper_draws(fit, n = 200) quantile(th[, "tau"], c(0.025, 0.5, 0.975))
Generic driver for nested-Laplace-style outer integration over a small
hyperparameter block. The user supplies per-axis specs (values + optional
log-prior + log-scale / bounds / refinable metadata) and an inner_fit
callback that, at every hyperparameter cell, returns the inner log marginal
and optionally the fixed-effect posterior mode + marginal covariance.
The driver builds the Cartesian outer grid, normalises the log-marginals to
integration weights, reports per-axis posterior moments and weighted
quantiles, and (when the inner fit supplies them) law-of-total-covariance
fixed-effect posterior + mixture draws.
This factors the per-family outer-grid plumbing in tulpa_nested_laplace()
/ tulpa_nested_laplace_joint() / tulpa_re_cov_nested() into one
callback-driven entry point: downstream consumers (occupancy / N-mixture /
cover hurdle families in tulpaObs, custom user families) drop in their own
per-cell inner fit and get the outer integration for free.
tulpa_hyper_grid( hyper_specs, inner_fit, combine = c("law_of_total_cov", "weighted_mean_only", "none"), n_draws = 2000L, seed = NULL, beta_names = NULL, control = list() )tulpa_hyper_grid( hyper_specs, inner_fit, combine = c("law_of_total_cov", "weighted_mean_only", "none"), n_draws = 2000L, seed = NULL, beta_names = NULL, control = list() )
hyper_specs |
A list of axis specs. Each spec is either a
|
inner_fit |
|
combine |
How to pool per-cell fixed-effect posteriors into a single posterior over the betas. One of:
|
n_draws |
Number of posterior draws of the fixed effects to
synthesise from the cell mixture (default 2000). Used only when
|
seed |
Optional integer seed for the draw synthesis. |
beta_names |
Optional character vector naming the fixed-effect
coordinates. When |
control |
Optional list of refinement / tuning knobs:
|
A list of class c("tulpa_fit", "list") with:
theta_grid – numeric matrix [n_cells x n_axes] of outer-grid
hyperparameter values; columns named after the axes.
theta_names – character vector of axis names.
log_marginal – numeric [n_cells]; per-cell log integrand
inner$log_marginal + log_prior(theta_cell). Non-finite / failed cells
are -Inf.
log_prior – numeric [n_cells]; per-cell log-prior contribution
(the sum across axes of axis$log_prior(theta_cell[axis])). 0 when
all axes have log_prior = NULL.
weights – numeric [n_cells] summing to 1 (or all NA with a
warning when no cell carries finite mass).
theta_mean, theta_sd – named numeric vectors; weighted posterior
mean and SD per axis. Each SD comes from the estimator its axis's own
resolution calls for – the weighted spread of the axis marginal where
it is resolved, the 3-point parabola at the modal node where the
marginal has collapsed onto too few nodes to have a spread.
theta_sd_source, theta_sd_ess, theta_sd_stencil_declined –
per axis, which estimator produced the SD, the quadrature effective
sample size that decided it, and (where the parabola was wanted and
could not be formed) why it declined.
theta_median, theta_ci_lo, theta_ci_hi – named numeric vectors;
weighted-quantile median and 2.5 / 97.5\
(the recommended summary for right-skewed scale-like axes).
beta, beta_cov, draws – fixed-effect posterior, present per
combine:
combine = "law_of_total_cov": weighted mean, total covariance
(E[V] + V[E]), [n_draws x p] mixture draws.
combine = "weighted_mean_only": weighted mean only; beta_cov
and draws are NULL.
combine = "none": all three are NULL.
means, param_names, process_info, n_samples, n_params,
N – the tulpa_fit accessor surface, populated when a
fixed-effect posterior is assembled.
hyper_specs – echoed list of hyper_axis_spec objects (the
normalised form).
combine, n_failed, n_grid, refining_axis – diagnostic fields.
adaptive_grid_info, var_of_means_consistency_info – present when
the corresponding refinement pass fired.
hyper_axis_spec() for the axis-spec constructor;
tulpa_nested_laplace_joint() for the family-specific outer-grid
driver that this helper generalises.
# Integrate a Gaussian linear model's residual SD: inner_fit(hypers) returns # the per-cell log marginal and the conditional fixed-effect posterior, and # the axis spec names the grid. tulpa_nested_laplace() is the packaged driver # built on this. set.seed(1) n <- 40L; X <- cbind(1, rnorm(n)); y <- as.numeric(X %*% c(0.5, -1) + rnorm(n)) XtX <- crossprod(X); Xty <- crossprod(X, y) inner_fit <- function(hypers) { sigma <- as.numeric(hypers["sigma"]) V <- chol2inv(chol(XtX / sigma^2 + diag(1e-4, 2))) mu <- as.numeric(V %*% (Xty / sigma^2)) list(log_marginal = -n * log(sigma) - 0.5 * sum((y - X %*% mu)^2) / sigma^2 + 0.5 * as.numeric(determinant(V)$modulus), beta_mean = stats::setNames(mu, c("b0", "b1")), beta_cov = V) } specs <- list(hyper_axis_spec("sigma", grid = exp(seq(log(0.5), log(2), length.out = 9)), log_scale = TRUE, bounds = c(0, Inf))) res <- tulpa_hyper_grid(specs, inner_fit, n_draws = 500L, seed = 1L) res$theta_median res$beta# Integrate a Gaussian linear model's residual SD: inner_fit(hypers) returns # the per-cell log marginal and the conditional fixed-effect posterior, and # the axis spec names the grid. tulpa_nested_laplace() is the packaged driver # built on this. set.seed(1) n <- 40L; X <- cbind(1, rnorm(n)); y <- as.numeric(X %*% c(0.5, -1) + rnorm(n)) XtX <- crossprod(X); Xty <- crossprod(X, y) inner_fit <- function(hypers) { sigma <- as.numeric(hypers["sigma"]) V <- chol2inv(chol(XtX / sigma^2 + diag(1e-4, 2))) mu <- as.numeric(V %*% (Xty / sigma^2)) list(log_marginal = -n * log(sigma) - 0.5 * sum((y - X %*% mu)^2) / sigma^2 + 0.5 * as.numeric(determinant(V)$modulus), beta_mean = stats::setNames(mu, c("b0", "b1")), beta_cov = V) } specs <- list(hyper_axis_spec("sigma", grid = exp(seq(log(0.5), log(2), length.out = 9)), log_scale = TRUE, bounds = c(0, Inf))) res <- tulpa_hyper_grid(specs, inner_fit, n_draws = 500L, seed = 1L) res$theta_median res$beta
The natural-scale support of every axis in specs that carries one, as a
named list of intervals – the region each axis's outer-grid measure
actually integrates, including levels a refinement pass added. Intended for
recovering an axis's integrated span from a settled grid, e.g. so a
sampled-hyperparameter prior can be derived from what the outer
integration used rather than restated alongside it.
tulpa_hyper_grid_supports(theta_grid, specs, refining = NULL)tulpa_hyper_grid_supports(theta_grid, specs, refining = NULL)
theta_grid |
A named |
specs |
Per-axis spec list, e.g. from
|
refining |
Optional per-cell refinement tag vector (see
|
A named list of natural-scale c(lo, hi) intervals, one per axis
in specs that carries a declared coordinate, or NULL if theta_grid
or specs is NULL.
tulpa_joint_axis_specs_from_grid(), tulpa_hyper_slice_home()
set.seed(1) S <- 30L # spatial units in a chain nb <- lapply(seq_len(S), function(s) setdiff(c(s - 1L, s + 1L), c(0L, S + 1L))) nn <- lengths(nb) field <- as.numeric(scale(cumsum(rnorm(S, 0, 0.4)))) idx <- rep(seq_len(S), each = 6L); n <- length(idx); x <- rnorm(n) y <- rbinom(n, 1L, plogis(-0.2 + 0.6 * x + field[idx])) prior <- list(type = "icar", n_spatial_units = S, spatial_idx = idx, adj_row_ptr = c(0L, cumsum(nn)), adj_col_idx = unlist(nb) - 1L, n_neighbors = nn, tau_grid = c(0.5, 1, 2, 4, 8)) fit <- tulpa_nested_laplace(y, rep(1L, n), cbind(1, x), prior = prior, family = "binomial", control = list(progress = FALSE)) tg <- tulpa_theta_matrix(fit) tulpa_hyper_grid_supports(tg, tulpa_joint_axis_specs_from_grid(tg))set.seed(1) S <- 30L # spatial units in a chain nb <- lapply(seq_len(S), function(s) setdiff(c(s - 1L, s + 1L), c(0L, S + 1L))) nn <- lengths(nb) field <- as.numeric(scale(cumsum(rnorm(S, 0, 0.4)))) idx <- rep(seq_len(S), each = 6L); n <- length(idx); x <- rnorm(n) y <- rbinom(n, 1L, plogis(-0.2 + 0.6 * x + field[idx])) prior <- list(type = "icar", n_spatial_units = S, spatial_idx = idx, adj_row_ptr = c(0L, cumsum(nn)), adj_col_idx = unlist(nb) - 1L, n_neighbors = nn, tau_grid = c(0.5, 1, 2, 4, 8)) fit <- tulpa_nested_laplace(y, rep(1L, n), cbind(1, x), prior = prior, family = "binomial", control = list(progress = FALSE)) tg <- tulpa_theta_matrix(fit) tulpa_hyper_grid_supports(tg, tulpa_joint_axis_specs_from_grid(tg))
The axis whose refinement pass added each cell of a nested-Laplace outer
grid, "" for a cell of the grid as declared. A pass adds levels to an
axis, laid in every row of the others that holds the posterior; the tag
tells the declared levels apart wherever a reader needs them alone, e.g.
rebuilding axis specs from a grid's declared nodes (see
tulpa_joint_axis_specs_from_grid()), whose prior must not move with the
nodes a pass added.
tulpa_hyper_slice_home(refining, n)tulpa_hyper_slice_home(refining, n)
refining |
The |
n |
Number of grid cells; |
A character vector of length n: "" for a declared cell, the
axis name for a cell a refinement pass added.
tulpa_hyper_grid_supports(), tulpa_joint_axis_specs_from_grid()
tulpa_hyper_slice_home(NULL, 3) tulpa_hyper_slice_home(c("", "", "sigma"), 3)tulpa_hyper_slice_home(NULL, 3) tulpa_hyper_slice_home(c("", "", "sigma"), 3)
Get or set the symplectic splitting integrator that the exact-MCMC tier (HMC / NUTS) uses to build its trajectory proposals. The integrator is backed by the SIMP library, which supplies leapfrog and the higher-order Yoshida members from one triple-jump composition.
tulpa_integrator(name, mts_substeps = 4L)tulpa_integrator(name, mts_substeps = 4L)
name |
Integrator name: |
mts_substeps |
Number of inner prior-force substeps per trajectory step
for the |
The default, "leapfrog", reproduces tulpa's historical trajectory step
exactly.
"minerror2" is a two-stage order-two scheme whose coefficient is tuned to
cancel the leading energy error on a Gaussian target. Since mass adaptation
drives a posterior toward an isotropic Gaussian, it conserves energy well
near the adapted optimum and adapts to larger step sizes, at two gradient
evaluations per step. It is the recommended choice when leapfrog's step size
is limited by energy error on a near-Gaussian posterior.
"yoshida4" is an order-4 scheme that also samples reliably (three gradient
evaluations per step). "yoshida6" and "yoshida8" are available but
experimental for NUTS: high-order composition integrators have a sharp
step-size stability threshold that the dual-averaging adaptation pushes
against, so "yoshida6" needs a high adapt_delta (0.95 or more) and
"yoshida8" often fails to adapt. For sampling, prefer "leapfrog",
"minerror2", or "yoshida4".
"adaptive2" and "adaptive3" are step-adapted versions of the two- and
three-stage schemes. Rather than fixing the coefficient in advance, each NUTS
chain resolves it at the end of warmup for its own operating point: the
coefficient that minimizes the worst-case energy error over the band of
dimensionless steps the chain actually takes, read off from the adapted mass
matrix and the local posterior curvature. Where "minerror2" is optimal only
in the small-step limit, "adaptive2" tracks the chain's realised step band;
"adaptive3" spends a third gradient per step to hold a small error over a
wider band. Both run a fixed placeholder of the same stage family during
warmup, so the dual-averaged step size carries over. Step-adaptation applies
to NUTS (the default sampler); the fixed-trajectory HMC path uses the
placeholder.
"mts" is a multiple-time-stepping (RESPA) integrator. It splits the force
into a stiff but cheap prior part – the Gaussian latent structure – and a
smooth but expensive likelihood part. Each NUTS trajectory step takes
mts_substeps inner leapfrog substeps against the prior force while
evaluating the likelihood gradient only once, so the outer step can be larger
without the stiff prior forcing it small. It pays one full gradient per step
(as leapfrog does) plus mts_substeps cheap prior gradients, and helps most
when the latent field is stiff relative to a comparatively flat likelihood.
Like the other schemes it applies to NUTS.
The choice is process-global (like the gradient mode): set it once before
fitting. It is read on the main thread at the start of sampling. Because it
is process-global, a caller that changes it owns restoring it – and an error
between the two calls would leave the process on the other integrator.
with_tulpa_integrator() does both, restoring on error as well as on
success.
If name is omitted, the current integrator name. If name is
given, the previous name is returned invisibly.
tulpa_integrator() # current integrator old <- tulpa_integrator("yoshida4") tulpa_integrator(old) # restoretulpa_integrator() # current integrator old <- tulpa_integrator("yoshida4") tulpa_integrator(old) # restore
Test whether a one-sided formula carries a single varying-coefficient
grouping bar of the form ~ 1 + w || node (independent fields, ||) or
~ 1 + w | node (correlated fields, |). This is the same grammar
spatial() and the inline temporal() field constructor accept, and the
grammar tulpa_bar_field_specs() expands. A downstream package can use it to
branch on "is this term a spatial / temporal varying-coefficient bar?" before
calling tulpa_bar_field_specs().
tulpa_is_spatial_bar(x)tulpa_is_spatial_bar(x)
x |
A one-sided formula (e.g. |
A single logical: TRUE when x is (or wraps) a | / || bar,
FALSE otherwise (a plain formula such as ~ 1 + w, a non-bar term, or a
non-formula / non-language input).
tulpa_bar_field_specs() for expanding the bar into per-column
field specs.
tulpa_is_spatial_bar(~ 1 + w || cell) # TRUE tulpa_is_spatial_bar(~ 1 + w | cell) # TRUE (correlated) tulpa_is_spatial_bar(~ 1 + w) # FALSE (no bar)tulpa_is_spatial_bar(~ 1 + w || cell) # TRUE tulpa_is_spatial_bar(~ 1 + w | cell) # TRUE (correlated) tulpa_is_spatial_bar(~ 1 + w) # FALSE (no bar)
Rebuilds each axis's declared spec (support, log/linear coordinate,
prior) from a theta_grid's columns, keyed off column names – the same
metadata the multi-block joint driver recovers rather than carrying a
second description of the axes it already assembled. A column holding one
value across every cell is treated as a fixed setting rather than an axis
(e.g. a fixed NB-size node) and dropped. Intended for a caller that needs
an axis's declared span, coordinate or prior off a settled grid without
restating tulpa's own hyperprior declarations alongside it.
tulpa_joint_axis_specs_from_grid( theta_grid, copy_slab = "exponential", folded_axes = NULL, logchol = .hp_logchol_designs(theta_grid) )tulpa_joint_axis_specs_from_grid( theta_grid, copy_slab = "exponential", folded_axes = NULL, logchol = .hp_logchol_designs(theta_grid) )
theta_grid |
A named |
copy_slab |
|
folded_axes |
Optional names of axes folded onto |
logchol |
Free-covariance block design(s) as returned by
|
A named list of per-axis spec lists, or NULL if theta_grid has
no axis names.
tulpa_theta_matrix(), tulpa_grid_log_quad(),
tulpa_hyper_grid_supports()
set.seed(1) S <- 30L # spatial units in a chain nb <- lapply(seq_len(S), function(s) setdiff(c(s - 1L, s + 1L), c(0L, S + 1L))) nn <- lengths(nb) field <- as.numeric(scale(cumsum(rnorm(S, 0, 0.4)))) idx <- rep(seq_len(S), each = 6L); n <- length(idx); x <- rnorm(n) y <- rbinom(n, 1L, plogis(-0.2 + 0.6 * x + field[idx])) prior <- list(type = "icar", n_spatial_units = S, spatial_idx = idx, adj_row_ptr = c(0L, cumsum(nn)), adj_col_idx = unlist(nb) - 1L, n_neighbors = nn, tau_grid = c(0.5, 1, 2, 4, 8)) fit <- tulpa_nested_laplace(y, rep(1L, n), cbind(1, x), prior = prior, family = "binomial", control = list(progress = FALSE)) specs <- tulpa_joint_axis_specs_from_grid(tulpa_theta_matrix(fit)) specs[[1]]$log_scaleset.seed(1) S <- 30L # spatial units in a chain nb <- lapply(seq_len(S), function(s) setdiff(c(s - 1L, s + 1L), c(0L, S + 1L))) nn <- lengths(nb) field <- as.numeric(scale(cumsum(rnorm(S, 0, 0.4)))) idx <- rep(seq_len(S), each = 6L); n <- length(idx); x <- rnorm(n) y <- rbinom(n, 1L, plogis(-0.2 + 0.6 * x + field[idx])) prior <- list(type = "icar", n_spatial_units = S, spatial_idx = idx, adj_row_ptr = c(0L, cumsum(nn)), adj_col_idx = unlist(nb) - 1L, n_neighbors = nn, tau_grid = c(0.5, 1, 2, 4, 8)) fit <- tulpa_nested_laplace(y, rep(1L, n), cbind(1, x), prior = prior, family = "binomial", control = list(progress = FALSE)) specs <- tulpa_joint_axis_specs_from_grid(tulpa_theta_matrix(fit)) specs[[1]]$log_scale
Fits B responses that share one design (arms, latent blocks, outer grid
layout, solver settings) and differ only in their responses, their arm
dispersions and the nodes of any dispersion axis, through one fused grid
solve (cpp_nested_laplace_joint_multi_batch): one design pass per outer-
grid cell for all B responses, B block-diagonal Newton solves, instead
of B independent fits each repeating the design pass.
tulpa_joint_grid_batch(fits)tulpa_joint_grid_batch(fits)
fits |
A non-empty list of functions of no arguments, each performing one ordinary joint nested-Laplace fit. |
Each element of fits is a function of no arguments that performs one
ordinary fit (a tulpa_nested_laplace_joint() call, or a consumer front
door that makes one). The fits run twice: the first run stops each fit at
its main outer-grid solve and keeps the kernel request; the fused solve
answers every request at once; the second run replays each fit with its
main grid solve served from the fused result. Every other kernel call a
fit makes (placement, refinement, diagnostics) runs as it always does, so
each returned fit is the object its ordinary fit would have returned,
built by the same code.
The fits replay one after another from the random number state the batch was called at, so they draw the same stream the same fits called in sequence would draw; each capture starts from that state too, and a fit whose grid request depended on a draw taken before it is refused at its replay rather than served. Warnings and messages a capture run raises are muffled; the replay raises them again.
Raises a tulpa_grid_batch_ineligible condition (catchable with
tryCatch(..., tulpa_grid_batch_ineligible = ...)) when the fits do not
share a design or a request uses a setting the fused driver does not
carry, and says so on the first fit's capture when the setting is the
request's own. The cheap-pass screen is one: it prunes each response's grid
to its own kept set, which one fused grid walk cannot, and it is on by
default, so fits meant for the fused solve pass control$prune = FALSE.
The random number state is restored to where the batch was called
before that condition leaves, so a caller that falls back to fitting the
responses one at a time draws the stream it would have drawn without the
batch.
A list of fit results, one per element of fits, in order.
# The fused solve serves coupled-arm families (a model package's joint # occupancy-cover fit, say); for designs it does not carry it signals # `tulpa_grid_batch_ineligible`, and the caller fits one at a time. set.seed(1) n_s <- 20L; N <- 100L s <- sample.int(n_s, N, replace = TRUE); x <- rnorm(N) nb <- lapply(seq_len(n_s), function(i) setdiff(c(i - 1L, i + 1L), c(0L, n_s + 1L))) prior <- list(type = "icar", n_spatial_units = n_s, adj_row_ptr = as.integer(c(0L, cumsum(lengths(nb)))), adj_col_idx = as.integer(unlist(nb)) - 1L, n_neighbors = lengths(nb), sigma_grid = c(0.5, 1)) fit_one <- function(y) function() tulpa_nested_laplace_joint( responses = list(occ = list(y = y, n_trials = rep(1L, N), X = cbind(1, x), spatial_idx = s, family = "binomial")), prior = prior, control = list(progress = FALSE)) fits <- lapply(1:2, function(b) fit_one(rbinom(N, 1, plogis(0.3 * x)))) res <- tryCatch(tulpa_joint_grid_batch(fits), tulpa_grid_batch_ineligible = function(e) lapply(fits, function(f) f())) length(res)# The fused solve serves coupled-arm families (a model package's joint # occupancy-cover fit, say); for designs it does not carry it signals # `tulpa_grid_batch_ineligible`, and the caller fits one at a time. set.seed(1) n_s <- 20L; N <- 100L s <- sample.int(n_s, N, replace = TRUE); x <- rnorm(N) nb <- lapply(seq_len(n_s), function(i) setdiff(c(i - 1L, i + 1L), c(0L, n_s + 1L))) prior <- list(type = "icar", n_spatial_units = n_s, adj_row_ptr = as.integer(c(0L, cumsum(lengths(nb)))), adj_col_idx = as.integer(unlist(nb)) - 1L, n_neighbors = lengths(nb), sigma_grid = c(0.5, 1)) fit_one <- function(y) function() tulpa_nested_laplace_joint( responses = list(occ = list(y = y, n_trials = rep(1L, N), X = cbind(1, x), spatial_idx = s, family = "binomial")), prior = prior, control = list(progress = FALSE)) fits <- lapply(1:2, function(b) fit_one(rbinom(N, 1, plogis(0.3 * x)))) res <- tryCatch(tulpa_joint_grid_batch(fits), tulpa_grid_batch_ineligible = function(e) lapply(fits, function(f) f())) length(res)
Runs the same per-outer-grid-cell selected-inversion the joint
nested-Laplace driver's own fixed-effect retention uses internally
(extract_inner_vcov_block_cell(), see the note above
.joint_attach_grid_fixed()), for a caller-chosen index set. A consumer
package computing its own standard-error correction on a subset of a
joint fit's latent coordinates – a different idx than whatever the fit
itself retained on $cov_block_per_grid – reruns the extraction through
this door instead of reimplementing the sparse selected inversion.
tulpa_joint_inner_vcov_blocks( Q_p_per_grid, Q_i_per_grid, Q_x_per_grid, n_x, idx, n_dense, A_cols_list, field_marginal = TRUE, n_threads = 1L )tulpa_joint_inner_vcov_blocks( Q_p_per_grid, Q_i_per_grid, Q_x_per_grid, n_x, idx, n_dense, A_cols_list, field_marginal = TRUE, n_threads = 1L )
Q_p_per_grid, Q_i_per_grid, Q_x_per_grid
|
Per-outer-grid-cell CSC
storage of the latent precision |
n_x |
Integer, the latent dimension |
idx |
Integer vector (1-based) of latent coordinates to extract the sub-block for. |
n_dense |
Integer, the number of leading |
A_cols_list |
A list of integer vectors, one per sum-to-zero constraint group, each the (1-based) latent coordinates that group averages to zero. |
field_marginal |
Logical; if |
n_threads |
Integer, grid cells processed concurrently. |
When a field is present (n_dense < length(idx)), the
extraction takes a cheap selected-inversion path: the dense
(fixed-effect) block and the fixed-effect x field cross terms are
exact; the field x field block is either its marginal diagonal
(field_marginal = TRUE) or left at zero (FALSE) – never the full
field x field covariance.
A list of length n_grid (one per outer-grid cell), each element
either NULL (the cell stored no Q, or its sparse Cholesky failed)
or a dense length(idx) x length(idx) matrix: the constrained
covariance sub-block for idx, with the sum-to-zero constraints in
A_cols_list conditioned out by the same kriging correction the fit's
own posterior draws use.
# Two outer-grid cells of a latent precision over two fixed effects and a # ten-node random-walk field (sum-to-zero), each stored as the lower-triangle # CSC a joint fit keeps per cell. make_Q <- function(tau) { Q <- matrix(0, 12, 12) Q[1:2, 1:2] <- diag(2, 2) + 0.1 Q[3:12, 3:12] <- tau * crossprod(diff(diag(10))) + diag(0.3, 10) Q[1, 3:12] <- Q[3:12, 1] <- 0.1 L <- Matrix::tril(methods::as(methods::as(Q, "generalMatrix"), "CsparseMatrix")) list(p = L@p, i = L@i, x = L@x) } cells <- list(make_Q(1), make_Q(4)) blocks <- tulpa_joint_inner_vcov_blocks( lapply(cells, `[[`, "p"), lapply(cells, `[[`, "i"), lapply(cells, `[[`, "x"), n_x = 12L, idx = 1:12, n_dense = 2L, A_cols_list = list(3:12)) blocks[[1]][1:2, 1:2] # fixed-effect covariance in the first cell diag(blocks[[2]])[3:12] # field marginal variances in the second# Two outer-grid cells of a latent precision over two fixed effects and a # ten-node random-walk field (sum-to-zero), each stored as the lower-triangle # CSC a joint fit keeps per cell. make_Q <- function(tau) { Q <- matrix(0, 12, 12) Q[1:2, 1:2] <- diag(2, 2) + 0.1 Q[3:12, 3:12] <- tau * crossprod(diff(diag(10))) + diag(0.3, 10) Q[1, 3:12] <- Q[3:12, 1] <- 0.1 L <- Matrix::tril(methods::as(methods::as(Q, "generalMatrix"), "CsparseMatrix")) list(p = L@p, i = L@i, x = L@x) } cells <- list(make_Q(1), make_Q(4)) blocks <- tulpa_joint_inner_vcov_blocks( lapply(cells, `[[`, "p"), lapply(cells, `[[`, "i"), lapply(cells, `[[`, "x"), n_x = 12L, idx = 1:12, n_dense = 2L, A_cols_list = list(3:12)) blocks[[1]][1:2, 1:2] # fixed-effect covariance in the first cell diag(blocks[[2]])[3:12] # field marginal variances in the second
The law-of-total-covariance mixture over the outer grid,
with C_k the same per-cell constrained block
tulpa_joint_inner_vcov_blocks() returns, accumulated inside the engine
without holding one block per cell. Memory is one length(idx) square
result whatever the grid size, so this is the door for an idx that
carries a field.
tulpa_joint_inner_vcov_mixture( Q_p_per_grid, Q_i_per_grid, Q_x_per_grid, n_x, idx, n_dense, A_cols_list, weights, modes = NULL, field_marginal = TRUE, n_threads = 1L )tulpa_joint_inner_vcov_mixture( Q_p_per_grid, Q_i_per_grid, Q_x_per_grid, n_x, idx, n_dense, A_cols_list, weights, modes = NULL, field_marginal = TRUE, n_threads = 1L )
Q_p_per_grid, Q_i_per_grid, Q_x_per_grid
|
Per-outer-grid-cell CSC
storage of the latent precision |
n_x |
Integer, the latent dimension |
idx |
Integer vector (1-based) of latent coordinates to extract the sub-block for. |
n_dense |
Integer, the number of leading |
A_cols_list |
A list of integer vectors, one per sum-to-zero constraint group, each the (1-based) latent coordinates that group averages to zero. |
weights |
Numeric, one non-negative weight per outer-grid cell (normalized here). A cell of zero weight is never extracted. |
modes |
Optional |
field_marginal |
Logical; if |
n_threads |
Integer, grid cells processed concurrently. |
A list: vcov, the mixture covariance; mean, the weighted mean
of modes (NULL without them); dense_blocks, one
n_dense x n_dense block per cell (NULL where the cell was not
extracted); failed, the cells of positive weight whose block is
missing (no stored precision, or its Cholesky failed). Such a cell keeps
its weight and contributes its between-cell term only. With
field_marginal = TRUE the field x field part of the within term is its
diagonal.
# Two outer-grid cells of a latent precision over two fixed effects and a # ten-node random-walk field (sum-to-zero), each stored as the lower-triangle # CSC a joint fit keeps per cell, with the cells' modes and weights. make_Q <- function(tau) { Q <- matrix(0, 12, 12) Q[1:2, 1:2] <- diag(2, 2) + 0.1 Q[3:12, 3:12] <- tau * crossprod(diff(diag(10))) + diag(0.3, 10) Q[1, 3:12] <- Q[3:12, 1] <- 0.1 L <- Matrix::tril(methods::as(methods::as(Q, "generalMatrix"), "CsparseMatrix")) list(p = L@p, i = L@i, x = L@x) } cells <- list(make_Q(1), make_Q(4)) set.seed(1) modes <- matrix(rnorm(2 * 12, sd = 0.2), 2, 12) mix <- tulpa_joint_inner_vcov_mixture( lapply(cells, `[[`, "p"), lapply(cells, `[[`, "i"), lapply(cells, `[[`, "x"), n_x = 12L, idx = 1:12, n_dense = 2L, A_cols_list = list(3:12), weights = c(0.7, 0.3), modes = modes) mix$mean[1:2] mix$vcov[1:2, 1:2]# Two outer-grid cells of a latent precision over two fixed effects and a # ten-node random-walk field (sum-to-zero), each stored as the lower-triangle # CSC a joint fit keeps per cell, with the cells' modes and weights. make_Q <- function(tau) { Q <- matrix(0, 12, 12) Q[1:2, 1:2] <- diag(2, 2) + 0.1 Q[3:12, 3:12] <- tau * crossprod(diff(diag(10))) + diag(0.3, 10) Q[1, 3:12] <- Q[3:12, 1] <- 0.1 L <- Matrix::tril(methods::as(methods::as(Q, "generalMatrix"), "CsparseMatrix")) list(p = L@p, i = L@i, x = L@x) } cells <- list(make_Q(1), make_Q(4)) set.seed(1) modes <- matrix(rnorm(2 * 12, sd = 0.2), 2, 12) mix <- tulpa_joint_inner_vcov_mixture( lapply(cells, `[[`, "p"), lapply(cells, `[[`, "i"), lapply(cells, `[[`, "x"), n_x = 12L, idx = 1:12, n_dense = 2L, A_cols_list = list(3:12), weights = c(0.7, 0.3), modes = modes) mix$mean[1:2] mix$vcov[1:2, 1:2]
Splits the data into K folds, refits the model on each K - 1 training
partition (via the fit's stored tulpa() call), and accumulates the held-out
fold's pointwise log predictive density
over the training
posterior draws. The summed elpd_kfold is directly comparable to the
elpd_loo from tulpa_criteria() – the exact refit counterpart to the
PSIS-LOO approximation, for when the Pareto k-hat gate flags LOO as
unreliable.
Fixed-effect / GLMM fits only: subsetting the observations breaks a spatial or
temporal field, so those fits are rejected (use PSIS-LOO via
tulpa_criteria()). Held-out random-effect groups contribute at their prior
mean (population-level held-out prediction), matching stats::predict().
tulpa_kfold(object, data, K = 10L, folds = NULL, n_trials = NULL, seed = NULL)tulpa_kfold(object, data, K = 10L, folds = NULL, n_trials = NULL, seed = NULL)
object |
A |
data |
The data frame the model was fit to. |
K |
Number of folds (default 10). |
folds |
Optional integer vector of fold ids, length |
n_trials |
Optional binomial denominators (length |
seed |
Optional seed for the random partition. |
A list with elpd_kfold (summed held-out elpd), se_elpd_kfold (its
standard error), pointwise (per-observation held-out elpd), folds, and
K.
tulpa_criteria() for PSIS-LOO / WAIC on a single fit.
set.seed(1) d <- data.frame(x = rnorm(120)) d$y <- rpois(120, exp(0.4 + 0.6 * d$x)) fit <- tulpa(y ~ x, data = d, family = "poisson", mode = "laplace") cv <- tulpa_kfold(fit, data = d, K = 5, seed = 1) cv$elpd_kfoldset.seed(1) d <- data.frame(x = rnorm(120)) d$y <- rpois(120, exp(0.4 + 0.6 * d$x)) fit <- tulpa(y ~ x, data = d, family = "poisson", mode = "laplace") cv <- tulpa_kfold(fit, data = d, K = 5, seed = 1) cv$elpd_kfold
General-purpose Laplace approximation for latent Gaussian models. Finds the mode of the latent field (beta + random effects) and returns the Laplace-approximated marginal likelihood.
This is the public API for model packages (tulpaGlmm, tulpaObs, etc.)
to call tulpa's Laplace engine. As the low-level engine entry point (not a
front-door fitter), it keeps its numerical controls (max_iter, tol,
n_threads, return_hessian) inline in the signature rather than in a
control list, so callers assembling many solves pass them positionally.
tulpa_laplace( y, n_trials, X, re_list = list(), family = "binomial", phi = 1, phi2 = NULL, spatial = NULL, weights = NULL, offset = NULL, max_iter = 100L, tol = 1e-06, n_threads = 1L, return_hessian = TRUE, beta_prior = NULL, return_re_cov = FALSE, X_zi = NULL, zi_prior_sd = 2.5, return_joint_hessian = FALSE, compute_skew = FALSE, skew_idx = NULL, debias = NULL )tulpa_laplace( y, n_trials, X, re_list = list(), family = "binomial", phi = 1, phi2 = NULL, spatial = NULL, weights = NULL, offset = NULL, max_iter = 100L, tol = 1e-06, n_threads = 1L, return_hessian = TRUE, beta_prior = NULL, return_re_cov = FALSE, X_zi = NULL, zi_prior_sd = 2.5, return_joint_hessian = FALSE, compute_skew = FALSE, skew_idx = NULL, debias = NULL )
y |
Response vector (integer for binomial/poisson/negbin, numeric for gaussian) |
n_trials |
Trial sizes (integer vector, used for binomial only) |
X |
Fixed-effects design matrix |
re_list |
List of RE specifications. Each element is a list with:
|
family |
Character: |
phi |
Dispersion passed to the family, held fixed. One convention at
every door: for Defaulted, it conditions at 1 and says so with a warning, since for a
family that reads a dispersion that is a modelling choice rather than a
neutral value. |
phi2 |
Optional second dispersion: the Student-t degrees of freedom
( |
spatial |
Optional spatial specification (tulpa_spatial object) |
weights |
Optional observation weights (numeric vector, length |
offset |
Optional observation-level offset on the linear predictor
(numeric vector, length |
max_iter |
Maximum Newton iterations (default 100) |
tol |
Convergence tolerance (default 1e-6) |
n_threads |
Number of threads (default 1) |
return_hessian |
Logical: return the fixed-effect Hessian block? (default TRUE) |
beta_prior |
Optional Gaussian prior on the fixed effects. |
return_re_cov |
If |
X_zi |
Optional zero-inflation design matrix ( |
zi_prior_sd |
Prior SD on the zero-inflation coefficients,
|
return_joint_hessian |
If |
compute_skew |
If |
skew_idx |
1-based latent indices to probe (in the |
debias |
Subspace debias: a list with |
A list with:
mode: full mode vector (beta, then RE values per term)
log_marginal: Laplace-approximated log-marginal likelihood
n_iter: number of Newton iterations
converged: logical, whether the stopping rule was met
score_max: largest absolute component of the joint penalized score at
the returned mode – the residual the solve actually achieved, which is a
different question from converged and the one anything differentiating
through the mode depends on
log_det_Q: log-determinant of the Hessian
H_beta: fixed-effect block of the Hessian (if return_hessian = TRUE)
H_joint, H_latent: the joint latent precision, at the kernel's and
at the observed curvature (if return_joint_hessian = TRUE)
cov_blocks: list of per-group posterior covariance matrices, one per
(RE term, group) in term-major then group order (if return_re_cov = TRUE)
set.seed(1) n <- 200L X <- cbind(1, rnorm(n)) eta <- X %*% c(-0.3, 0.8) y <- rbinom(n, 1, plogis(eta)) fit <- tulpa_laplace(y, rep(1L, n), X, family = "binomial") fit$mode # posterior mode of the fixed effectsset.seed(1) n <- 200L X <- cbind(1, rnorm(n)) eta <- X %*% c(-0.3, 0.8) y <- rbinom(n, 1, plogis(eta)) fit <- tulpa_laplace(y, rep(1L, n), X, family = "binomial") fit$mode # posterior mode of the fixed effects
Thin wrapper around tulpa_laplace() for family = "beta". The
Laplace engine treats phi as fixed per fit (same contract as gamma
and neg_binomial_2); this wrapper does an outer 1-D optimisation of
the Laplace-approximated log-marginal over phi, then refits at the
optimum to return betas and Hessian.
The mean-precision parameterisation is y ~ Beta(mu * phi, (1 - mu) * phi)
with default logit link; y must be strictly in (0, 1).
tulpa_laplace_beta( y, X, re_list = list(), spatial = NULL, weights = NULL, offset = NULL, max_iter = 100L, tol = 1e-06, n_threads = 1L, beta_prior = NULL, phi_init = NULL, phi_bounds = c(0.1, 10000), outer_tol = 1e-04, mode = c("laplace", "nuts"), control = list() )tulpa_laplace_beta( y, X, re_list = list(), spatial = NULL, weights = NULL, offset = NULL, max_iter = 100L, tol = 1e-06, n_threads = 1L, beta_prior = NULL, phi_init = NULL, phi_bounds = c(0.1, 10000), outer_tol = 1e-04, mode = c("laplace", "nuts"), control = list() )
y |
Response in |
X |
Fixed-effects design matrix. |
re_list, spatial, weights, offset, max_iter, tol, n_threads, beta_prior
|
Passed to |
phi_init |
Optional starting value for the precision. If |
phi_bounds |
Numeric length-2 vector with lower/upper bounds on
|
outer_tol |
Tolerance for the outer optimisation. Default 1e-4. |
mode |
Inference method (the method is an argument, not a parallel
verb): |
control |
Passed to |
For mode = "laplace", the list returned by tulpa_laplace() at the
optimum, augmented with phi (the optimised precision), phi_trace (the
optimisation trace: a matrix with one row per objective evaluation and
columns phi and log_marginal) and phi_converged (TRUE when the
Brent search reported convergence). For mode = "nuts", the draws object
returned by tulpa_nuts_beta().
set.seed(1) n <- 200L X <- cbind(1, rnorm(n)) mu <- plogis(X %*% c(0.2, 0.7)); phi <- 8 y <- rbeta(n, mu * phi, (1 - mu) * phi) fit <- tulpa_laplace_beta(y, X) fit$modeset.seed(1) n <- 200L X <- cbind(1, rnorm(n)) mu <- plogis(X %*% c(0.2, 0.7)); phi <- 8 y <- rbeta(n, mu * phi, (1 - mu) * phi) fit <- tulpa_laplace_beta(y, X) fit$mode
Specify latent factors to capture shared unmeasured confounders between model processes. Latent factors are particularly useful when you suspect that both processes are driven by common unmeasured variables.
The constructor documented in this family (latent_factor())
returns a tulpa_latent specification object consumed by ratio /
multi-arm model packages built on tulpa (e.g. tulpaRatio), where the
shared factor enters two or more linear predictors and cancels in the
derived ratio. It is a multi-arm construct: the single-response tulpa()
front door does not read it (for an in-formula latent Gaussian block on a
single response, use latent(tgmrf(...))).
Wrap a pointwise log-likelihood for tulpa_criteria() without materializing
the whole [n_draws x n_obs] matrix. A plain matrix is wrapped directly; a
block generator (a function of an integer column vector returning the
[n_draws x length(cols)] submatrix) lets the criteria accumulators stream
over observation blocks, so an EVA-scale [200 x 1.16M] log-likelihood is
consumed a few thousand columns at a time.
tulpa_loglik(x, n_obs = NULL, n_draws = NULL)tulpa_loglik(x, n_obs = NULL, n_draws = NULL)
x |
Either a numeric |
n_obs, n_draws
|
Required when |
A tulpa_loglik object: a list with get(cols), n_obs,
n_draws, and materialized.
ll <- tulpa_loglik(matrix(rnorm(20 * 5, -1), nrow = 20)) # 20 draws x 5 obs c(ll$n_draws, ll$n_obs)ll <- tulpa_loglik(matrix(rnorm(20 * 5, -1), nrow = 20)) # 20 draws x 5 obs c(ll$n_draws, ll$n_obs)
Fits a baseline-category multinomial logit model: for a K-level unordered
response, classes 1..K-1 each get their own linear predictor and class K
is the baseline. The coupled multinomial likelihood is solved by a Newton
step to the penalized mode (a Gaussian ridge prior on the coefficients) and
summarized by a Laplace approximation, reusing the native multinomial kernel.
tulpa_multinomial( formula, data, beta_prior = .tulpa_default_beta_prior("multinomial"), control = list() ) ## S3 method for class 'tulpa_multinomial' print(x, ...)tulpa_multinomial( formula, data, beta_prior = .tulpa_default_beta_prior("multinomial"), control = list() ) ## S3 method for class 'tulpa_multinomial' print(x, ...)
formula |
Model formula; the response must be a factor (or coercible to one) with >= 3 levels. The baseline is the last level. |
data |
A data frame. |
beta_prior |
Fixed-effect prior as |
control |
List of numerical knobs: |
x |
A |
... |
Ignored. |
A tulpa_fit (subclass tulpa_multinomial) with means (the
posterior mode, named class:term) and cov of the Laplace Gaussian, which
is the posterior coef(), vcov(), summary.tulpa_fit() and confint() report;
draws sampled from that Gaussian; log_marginal, classes, baseline,
and the standard generic-method support.
tulpa() for single-process GLMMs.
set.seed(1) n <- 300L; x <- rnorm(n) eta <- cbind(0.5 + 1.0 * x, -0.3 - 0.8 * x) # classes 1, 2 vs baseline 3 P <- cbind(exp(eta), 1); P <- P / rowSums(P) y <- factor(apply(P, 1, function(pr) sample.int(3L, 1L, prob = pr))) fit <- tulpa_multinomial(y ~ x, data = data.frame(y = y, x = x)) coef(fit)set.seed(1) n <- 300L; x <- rnorm(n) eta <- cbind(0.5 + 1.0 * x, -0.3 - 0.8 * x) # classes 1, 2 vs baseline 3 P <- cbind(exp(eta), 1); P <- P / rowSums(P) y <- factor(apply(P, 1, function(pr) sample.int(3L, 1L, prob = pr))) fit <- tulpa_multinomial(y ~ x, data = data.frame(y = y, x = x)) coef(fit)
The single weight normaliser for every outer hyperparameter grid: softmax
of lm (optionally plus a per-cell log quadrature weight) into weights
summing to 1. Non-finite nodes (an inner Newton diverging in a grid
corner) are dropped from the max-shift and zeroed before renormalising
over the finite cells; an all-non-finite grid returns all-NA with a
warning rather than NaN. Intended for reconstructing a fit's outer-grid
posterior weights (with tulpa_theta_matrix() and
tulpa_grid_log_quad()) when the fit doesn't already carry fit$weights.
tulpa_normalise_weights_safe(lm, what = "grids / data", log_quad = NULL)tulpa_normalise_weights_safe(lm, what = "grids / data", log_quad = NULL)
lm |
Numeric vector of per-cell log-marginal-likelihood values. |
what |
Label for the grid, used only in the degenerate-case warning. |
log_quad |
Optional per-cell log quadrature weight (prior mass) of
the outer grid, e.g. from |
Numeric vector of posterior cell weights summing to 1, the same
length as lm, or all-NA if no cell carries finite mass.
tulpa_theta_matrix(), tulpa_grid_log_quad()
tulpa_normalise_weights_safe(c(-1, 0, -2)) # With per-cell log quadrature weights (cell prior masses): tulpa_normalise_weights_safe(c(-1, 0, -2), log_quad = log(c(0.2, 0.5, 0.3)))tulpa_normalise_weights_safe(c(-1, 0, -2)) # With per-cell log quadrature weights (cell prior masses): tulpa_normalise_weights_safe(c(-1, 0, -2), log_quad = log(c(0.2, 0.5, 0.3)))
Bayesian counterpart to tulpa_laplace_beta(). Routes the Beta GLM
through tulpa's full NUTS backend via the generic LikelihoodSpec
interface, sampling the precision phi jointly with the regression
coefficients on the log scale. NUTS performs exact marginalisation
over phi, replacing the Brent outer-opt in tulpa_laplace_beta()
and the deterministic nested-Laplace grid that would otherwise
integrate over the Beta dispersion.
Mean-precision parameterisation, default logit link:
y_i ~ Beta(mu_i * phi, (1 - mu_i) * phi) with
mu_i = 1 / (1 + exp(-eta_i)) and eta_i = X_i %*% beta.
tulpa_nuts_beta( y, X, beta_prior = .tulpa_default_beta_prior("beta_nuts"), log_phi_prior_sd = 3, log_phi_init = 0, control = list() )tulpa_nuts_beta( y, X, beta_prior = .tulpa_default_beta_prior("beta_nuts"), log_phi_prior_sd = 3, log_phi_init = 0, control = list() )
y |
Response vector, strictly in |
X |
Fixed-effects design matrix. |
beta_prior |
Fixed-effect prior as |
log_phi_prior_sd |
Prior SD on |
log_phi_init |
Starting value for |
control |
A named list of numerical / sampler knobs (statistical
arguments stay in the signature): |
A tulpa_fit object with:
draws – n_samples x (p + 1) matrix of post-warmup draws,
columns named from colnames(X) (falling back to beta[1] ... beta[p]) then log_phi.
means – posterior means.
phi_summary – posterior mean / median / quantiles of
phi = exp(log_phi).
accept_prob, divergent, treedepth, epsilon – NUTS
diagnostics from the underlying chain.
tulpa_laplace_beta() for the Laplace + Brent point
estimate; tulpa_laplace() for the underlying Laplace engine.
set.seed(1) n <- 150L X <- cbind(1, rnorm(n)) mu <- plogis(X %*% c(0.2, 0.7)); phi <- 8 y <- rbeta(n, mu * phi, (1 - mu) * phi) fit <- tulpa_nuts_beta(y, X, control = list(n_iter = 500L, n_warmup = 250L)) colMeans(fit$draws)set.seed(1) n <- 150L X <- cbind(1, rnorm(n)) mu <- plogis(X %*% c(0.2, 0.7)); phi <- 8 y <- rbeta(n, mu * phi, (1 - mu) * phi) fit <- tulpa_nuts_beta(y, X, control = list(n_iter = 500L, n_warmup = 250L)) colMeans(fit$draws)
Bayesian counterpart to fit_spde() / laplace_spde_at(). Routes the
SPDE-augmented GLM through tulpa's full NUTS backend via the generic
LikelihoodSpec interface. Two modes:
Legacy / fixed-hyper (joint = FALSE, the default): conditions on
the supplied (range, sigma). Samples the latent block
(beta, w_mesh, log_phi) jointly. Useful as the inner step of an outer
CCD / nested-Laplace integration over hypers.
Joint hypers (joint = TRUE): samples (beta, z_mesh, log_kappa, log_tau, log_phi) jointly via the non-centered transform
w = L(theta)^{-T} z. A PC prior (Fuglstad et al. 2019 JASA) on
(range, sigma) is taken from prior_range, prior_sigma. Returns
additional w_draws, range_draws, sigma_draws, kappa_draws,
tau_draws columns/vectors on top of the raw parameter draws.
tulpa_nuts_spde( y, X, spatial, family = c("gaussian", "poisson", "binomial", "gamma", "neg_binomial_2", "beta"), n_trials = NULL, joint = FALSE, range = NULL, sigma = NULL, prior_range = NULL, prior_sigma = NULL, log_kappa_init = NULL, log_tau_init = NULL, beta_prior = .tulpa_default_beta_prior("spde_nuts"), log_phi_prior_sd = 3, log_phi_init = 0, control = list() )tulpa_nuts_spde( y, X, spatial, family = c("gaussian", "poisson", "binomial", "gamma", "neg_binomial_2", "beta"), n_trials = NULL, joint = FALSE, range = NULL, sigma = NULL, prior_range = NULL, prior_sigma = NULL, log_kappa_init = NULL, log_tau_init = NULL, beta_prior = .tulpa_default_beta_prior("spde_nuts"), log_phi_prior_sd = 3, log_phi_init = 0, control = list() )
y |
Response vector. Family-specific:
|
X |
Fixed-effects design matrix. |
spatial |
A |
family |
One of |
n_trials |
Integer vector for |
joint |
Logical. |
range, sigma
|
Fixed-hyper mode only. Matern range and marginal SD
on the field. Default to the SPDE prior medians
( |
prior_range, prior_sigma
|
Joint mode only. PC prior anchors as
|
log_kappa_init, log_tau_init
|
Joint mode only. Initial values for
the hyper slots. Default to the value implied by the PC anchor's
|
beta_prior |
Fixed-effect prior as |
log_phi_prior_sd |
Prior SD on
|
log_phi_init |
Starting value for |
control |
A named list of numerical / sampler knobs (statistical
arguments stay in the signature): |
A list with draws (matrix n_samples x n_params), means,
phi_summary (where applicable), accept_prob, divergent,
treedepth, epsilon, joint_hypers, plus the supplied spatial
spec. In joint = TRUE mode additionally:
w_draws (transformed z -> w per draw), range_draws,
sigma_draws, kappa_draws, tau_draws, and range_summary,
sigma_summary (mean/median/5%/95% quantiles).
fit_spde() for the Laplace counterpart and the nested-Laplace
path over (range, sigma).
# Gaussian response with a smooth spatial trend, sampled at fixed # (range, sigma) on a coarse mesh. set.seed(1) n <- 60L coords <- cbind(runif(n), runif(n)) x <- runif(n, -1, 1) y <- 0.5 - 0.8 * x + 0.4 * sin(3 * coords[, 1]) + rnorm(n, 0, 0.3) spec <- spatial_spde(coords, max_edge = c(0.3, 0.6), cutoff = 0.1, prior_range = c(0.5, 0.5), prior_sigma = c(1, 0.5)) fit <- tulpa_nuts_spde(y, cbind(1, x), spatial = spec, family = "gaussian", range = 0.4, sigma = 0.5, log_phi_init = log(0.3), control = list(n_iter = 300L, n_warmup = 150L, seed = 1L)) colMeans(fit$draws[, c("beta[1]", "beta[2]")]) fit$phi_summary# Gaussian response with a smooth spatial trend, sampled at fixed # (range, sigma) on a coarse mesh. set.seed(1) n <- 60L coords <- cbind(runif(n), runif(n)) x <- runif(n, -1, 1) y <- 0.5 - 0.8 * x + 0.4 * sin(3 * coords[, 1]) + rnorm(n, 0, 0.3) spec <- spatial_spde(coords, max_edge = c(0.3, 0.6), cutoff = 0.1, prior_range = c(0.5, 0.5), prior_sigma = c(1, 0.5)) fit <- tulpa_nuts_spde(y, cbind(1, x), spatial = spec, family = "gaussian", range = 0.4, sigma = 0.5, log_phi_init = log(0.3), control = list(n_iter = 300L, n_warmup = 150L, seed = 1L)) colMeans(fit$draws[, c("beta[1]", "beta[2]")]) fit$phi_summary
Fits a proportional-odds cumulative-logit model for an ordered factor
response: P(y <= j | x) = plogis(c_j - x'beta) with ordered cutpoints
c_1 < ... < c_{K-1}. The penalized mode (a ridge prior on the coefficients
and cutpoints) is found by L-BFGS and summarized by a Laplace approximation.
There is no separate intercept – the cutpoints carry the baseline levels.
tulpa_ordinal( formula, data, link = c("logit", "probit"), beta_prior = .tulpa_default_beta_prior("ordinal"), cut_prior_sd = 10, control = list() ) ## S3 method for class 'tulpa_ordinal' coef(object, ...) ## S3 method for class 'tulpa_ordinal' print(x, ...)tulpa_ordinal( formula, data, link = c("logit", "probit"), beta_prior = .tulpa_default_beta_prior("ordinal"), cut_prior_sd = 10, control = list() ) ## S3 method for class 'tulpa_ordinal' coef(object, ...) ## S3 method for class 'tulpa_ordinal' print(x, ...)
formula |
Model formula; the response must be an ordered (or coercible)
factor with >= 3 levels, each observed at least once. An intercept in
|
data |
A data frame. |
link |
Cumulative link: |
beta_prior |
Fixed-effect prior as |
cut_prior_sd |
SD of the mean-zero Gaussian ridge prior on the cutpoint parameters (default 10). |
control |
List of numerical knobs: |
object |
A |
... |
Ignored. |
x |
A |
A tulpa_fit (subclass tulpa_ordinal) with coefficients
(covariate effects) and cutpoints at the posterior mode, draws (the
Laplace Gaussian mapped to ordered cutpoints), log_marginal, levels.
The draws are the posterior coef(), vcov(), summary.tulpa_fit() and
confint() report; coef() omits the cutpoints, as MASS::polr does.
coef() returns the covariate effects without the cutpoints, as
MASS::polr() omits its zeta; vcov() and summary() keep them.
tulpa_multinomial() for the nominal (unordered) case.
set.seed(1) n <- 400L; x <- rnorm(n) cuts <- c(-1, 0.5, 2); eta <- 0.8 * x Fm <- plogis(outer(-eta, cuts, "+")); P <- cbind(Fm, 1) - cbind(0, Fm) y <- ordered(apply(P, 1, function(pr) sample.int(4L, 1L, prob = pr))) fit <- tulpa_ordinal(y ~ x, data = data.frame(y = y, x = x)) fit$coefficients; fit$cutpointsset.seed(1) n <- 400L; x <- rnorm(n) cuts <- c(-1, 0.5, 2); eta <- 0.8 * x Fm <- plogis(outer(-eta, cuts, "+")); P <- cbind(Fm, 1) - cbind(0, Fm) y <- ordered(apply(P, 1, function(pr) sample.int(4L, 1L, prob = pr))) fit <- tulpa_ordinal(y ~ x, data = data.frame(y = y, x = x)) fit$coefficients; fit$cutpoints
A nested-Laplace fit integrates its hyperparameters on an outer grid: each
cell is one hyperparameter value, carries an integration weight, and holds
the inner Laplace solve at that value. This returns that grid, cell by cell,
so the integration can be inspected or reused (a discrete hyperparameter
posterior for sbc_discrete(), a grid-marginalized latent field).
The per-cell latent modes average to the grid-marginalized posterior mean of
any latent coordinate: colSums(g$weights * g$modes). Columns
seq_len(g$n_fixed) are the fixed effects and the rest the latent field and
random effects, in the order the fit's latent vector stores them.
tulpa_outer_grid(fit)tulpa_outer_grid(fit)
fit |
A nested-Laplace fit: |
A list:
nodesnumeric matrix [n_cells x n_axes], the hyperparameter
value of each cell, with the axis names as column names.
weightsthe cells' integration weights, normalized to sum to one.
log_marginaleach cell's log marginal likelihood, hyperprior
density included; with the cell's quadrature volume it gives the
weight. NULL when the fit does not keep it.
supportnamed list of natural-scale c(lo, hi) intervals, the
span each axis's outer measure integrates (including levels a
refinement pass added); NULL when the fit records none.
modesnumeric matrix [n_cells x n_latent], each cell's inner
Laplace mode, or NULL when the fit keeps none.
n_fixedhow many leading columns of modes are fixed effects.
tulpa_hyper_draws() for continuous draws of the hyperparameters,
tulpa_posterior_draws() for draws of the latent field, and
summary(fit)$hyperparameters for their marginal summaries.
set.seed(1) S <- 30L nb <- lapply(seq_len(S), function(s) setdiff(c(s - 1L, s + 1L), c(0L, S + 1L))) nn <- lengths(nb) field <- as.numeric(scale(cumsum(rnorm(S, 0, 0.4)))) idx <- rep(seq_len(S), each = 6L); n <- length(idx); x <- rnorm(n) y <- rbinom(n, 1L, plogis(-0.2 + 0.6 * x + field[idx])) prior <- list(type = "icar", n_spatial_units = S, spatial_idx = idx, adj_row_ptr = c(0L, cumsum(nn)), adj_col_idx = unlist(nb) - 1L, n_neighbors = nn, tau_grid = c(0.5, 1, 2, 4, 8)) fit <- tulpa_nested_laplace(y, rep(1L, n), cbind(1, x), prior = prior, family = "binomial", control = list(progress = FALSE)) g <- tulpa_outer_grid(fit) cbind(g$nodes, weight = g$weights) field_hat <- colSums(g$weights * g$modes)[-seq_len(g$n_fixed)]set.seed(1) S <- 30L nb <- lapply(seq_len(S), function(s) setdiff(c(s - 1L, s + 1L), c(0L, S + 1L))) nn <- lengths(nb) field <- as.numeric(scale(cumsum(rnorm(S, 0, 0.4)))) idx <- rep(seq_len(S), each = 6L); n <- length(idx); x <- rnorm(n) y <- rbinom(n, 1L, plogis(-0.2 + 0.6 * x + field[idx])) prior <- list(type = "icar", n_spatial_units = S, spatial_idx = idx, adj_row_ptr = c(0L, cumsum(nn)), adj_col_idx = unlist(nb) - 1L, n_neighbors = nn, tau_grid = c(0.5, 1, 2, 4, 8)) fit <- tulpa_nested_laplace(y, rep(1L, n), cbind(1, x), prior = prior, family = "binomial", control = list(progress = FALSE)) g <- tulpa_outer_grid(fit) cbind(g$nodes, weight = g$weights) field_hat <- colSums(g$weights * g$modes)[-seq_len(g$n_fixed)]
Decomposes a formula into fixed effects and random effects by walking the formula's abstract syntax tree. This is structural recursion with pattern matching on bar terms – no string manipulation.
tulpa_parse_formula(formula) ## S3 method for class 'tulpa_parsed_formula' print(x, ...)tulpa_parse_formula(formula) ## S3 method for class 'tulpa_parsed_formula' print(x, ...)
formula |
A formula object (e.g., |
x |
A |
... |
Ignored. |
A list with:
response: character, the response variable label (NULL if none)
response_expr: language object, the LHS of ~ (NULL if none)
fixed_formula: formula, the fixed-effects-only formula
random_effects: list of parsed RE specifications
n_re_terms: integer, number of RE terms
latent_blocks: list of evaluated user-defined latent block objects
(typically tgmrf S3 objects), one per latent(...) term in the
original formula. Empty list when no latent(...) terms are present.
n_latent_blocks: integer, number of latent(...) terms
original: the original formula
pf <- tulpa_parse_formula(y ~ x1 + x2 + (1 | group) + (x1 || site)) pf$response # "y" pf$fixed_formula # y ~ x1 + x2pf <- tulpa_parse_formula(y ~ x1 + x2 + (1 | group) + (x1 || site)) pf$response # "y" pf$fixed_formula # y ~ x1 + x2
The generic, family-agnostic half of a PIT residual check: the model package
supplies the posterior-predictive CDF evaluated at each observation (a
[n_draws x n_obs] matrix, or a draw-averaged [n_obs] vector), and this
returns the PIT value per observation. For a discrete or mixed response
(a hurdle has a point mass at zero) supply the left limit cdf_lower
(P(Y < y)); the randomized PIT then draws one uniform per observation and
interpolates F(y^-) + U (F(y) - F(y^-)), which is uniform under a correct
model. With cdf_lower = NULL the response is treated as continuous and the
PIT is the draw-averaged CDF.
tulpa_pit( cdf, cdf_lower = NULL, jitter = TRUE, log_lik = NULL, tail_points = NULL, n_threads = 1L )tulpa_pit( cdf, cdf_lower = NULL, jitter = TRUE, log_lik = NULL, tail_points = NULL, n_threads = 1L )
cdf |
Posterior-predictive CDF at the observed value, |
cdf_lower |
Optional left-limit CDF |
jitter |
If |
log_lik |
Optional |
tail_points |
Optional override for the PSIS tail size used by the
leave-one-out weighting (see |
n_threads |
Number of threads for the leave-one-out weighting (one
observation per thread). Ignored unless |
Supplying log_lik switches to the leave-one-out PIT (as in INLA's
cpo$pit or loo::psis_loo()'s LOO-PIT): each observation's CDF limits are
averaged over draws with PSIS leave-one-out weights (from that
observation's own pointwise log-likelihood) instead of equal weights, so
the PIT does not use the observation to predict itself. A column whose
importance ratio is not all finite falls back to the equal-weight average
for that observation.
Numeric vector of length n_obs of PIT values in [0, 1].
tulpa_criteria(), tulpa_psis()
set.seed(1) Fl <- matrix(runif(200 * 10, 0, 0.4), 200, 10) # per-draw CDF limits Fu <- Fl + 0.3 tulpa_pit(Fu, cdf_lower = Fl)set.seed(1) Fl <- matrix(runif(200 * 10, 0, 0.4), 200, 10) # per-draw CDF limits Fu <- Fl + 0.3 tulpa_pit(Fu, cdf_lower = Fl)
Draw from the outer-grid mixture posterior of a nested-Laplace fit – the
engine analogue of inla.posterior.sample(). Each draw picks an outer-grid
cell k ~ Categorical(weights) and then samples that cell's inner Gaussian,
so the draws are i.i.d. samples from sum_k w_k N(m_k, V_k).
Sampling the mixture is the faithful primitive for marginalizing nonlinear
derived quantities (e.g. plogis(eta_2) - plogis(eta_1), expected-cover
products p * mu): compute the derived quantity per draw, then summarize.
Collapsing the grid to a single moment-matched Gaussian biases skewed or
multimodal-over-grid posteriors.
tulpa_posterior_draws(fit, idx = NULL, n = 1000, ...) ## Default S3 method: tulpa_posterior_draws(fit, idx = NULL, n = 1000, ...) ## S3 method for class 'tulpa_nested_laplace' tulpa_posterior_draws(fit, idx = NULL, n = 1000, ...)tulpa_posterior_draws(fit, idx = NULL, n = 1000, ...) ## Default S3 method: tulpa_posterior_draws(fit, idx = NULL, n = 1000, ...) ## S3 method for class 'tulpa_nested_laplace' tulpa_posterior_draws(fit, idx = NULL, n = 1000, ...)
fit |
A nested-Laplace fit ( |
idx |
Optional integer vector of 1-based indices into whatever a draw
covers for this backend (see above); |
n |
Number of posterior draws (default 1000). |
... |
Unused; for S3 compatibility. |
A numeric matrix [n x length(idx)], one row per draw. Carries
attr(., "draws_kind") = "iid" (consistent with the draws-provenance
gate), attr(., "cells") – the outer-grid cell index each row was drawn
from – and attr(., "scope"), which of the two representations above the
columns are. attr(., "theta") is the HYPERPARAMETER half of the same
rows, one column per outer-grid axis, continuized within each row's own
cell by tulpa_hyper_draws(); reading fit$theta_grid[attr(., "cells"), ]
instead returns the bare node coordinate, which is an atom rather than a
marginal.
It depends on which representation the backend retained, and the returned
matrix says so in its scope attribute.
tulpa_nested_laplace_jointthe FULL latent vector – per-arm fixed
effects, per-arm random effects, then the latent field(s) – because the
joint fit retains each cell's sparse precision over that vector
(control$store_Q = TRUE). scope is "latent".
tulpa_nested_laplace (single-block)the FIXED-EFFECT block. This
backend inverts each cell's precision into the marginal fixed-effect
block and releases the precision itself, so the latent field is not part
of the retained per-cell Gaussian and cannot be sampled from the fit.
scope is "fixed".
tulpa_hyper_draws(), tulpa_nested_laplace(),
tulpa_nested_laplace_joint(), posterior_sample()
set.seed(1) S <- 30L # spatial units in a chain nb <- lapply(seq_len(S), function(s) setdiff(c(s - 1L, s + 1L), c(0L, S + 1L))) nn <- lengths(nb) field <- as.numeric(scale(cumsum(rnorm(S, 0, 0.4)))) idx <- rep(seq_len(S), each = 6L); n <- length(idx); x <- rnorm(n) y <- rbinom(n, 1L, plogis(-0.2 + 0.6 * x + field[idx])) prior <- list(type = "icar", n_spatial_units = S, spatial_idx = idx, adj_row_ptr = c(0L, cumsum(nn)), adj_col_idx = unlist(nb) - 1L, n_neighbors = nn, tau_grid = c(0.5, 1, 2, 4, 8)) fit <- tulpa_nested_laplace(y, rep(1L, n), cbind(1, x), prior = prior, family = "binomial", control = list(progress = FALSE)) dr <- tulpa_posterior_draws(fit, n = 200) colMeans(dr) # fixed effects, mixed over the outer grid summary(attr(dr, "theta")) # the hyperparameter half of the same drawsset.seed(1) S <- 30L # spatial units in a chain nb <- lapply(seq_len(S), function(s) setdiff(c(s - 1L, s + 1L), c(0L, S + 1L))) nn <- lengths(nb) field <- as.numeric(scale(cumsum(rnorm(S, 0, 0.4)))) idx <- rep(seq_len(S), each = 6L); n <- length(idx); x <- rnorm(n) y <- rbinom(n, 1L, plogis(-0.2 + 0.6 * x + field[idx])) prior <- list(type = "icar", n_spatial_units = S, spatial_idx = idx, adj_row_ptr = c(0L, cumsum(nn)), adj_col_idx = unlist(nb) - 1L, n_neighbors = nn, tau_grid = c(0.5, 1, 2, 4, 8)) fit <- tulpa_nested_laplace(y, rep(1L, n), cbind(1, x), prior = prior, family = "binomial", control = list(progress = FALSE)) dr <- tulpa_posterior_draws(fit, n = 200) colMeans(dr) # fixed effects, mixed over the outer grid summary(attr(dr, "theta")) # the hyperparameter half of the same draws
The tulpa_posterior_draws() method for a joint nested-Laplace fit
(tulpa_nested_laplace_joint()). Each draw picks an outer-grid cell
k ~ Categorical(weights) and then samples the inner latent vector from the
constrained Gaussian N(m_k, V_k) at that cell, where m_k is the cell's
inner mode and V_k is the inner-Laplace covariance with the ICAR / BYM2
sum-to-zero field constraint imposed (conditioning by kriging). The draws
are i.i.d. samples from sum_k weights_k * N(m_k, V_k).
## S3 method for class 'tulpa_nested_laplace_joint' tulpa_posterior_draws(fit, idx = NULL, n = 1000, ...)## S3 method for class 'tulpa_nested_laplace_joint' tulpa_posterior_draws(fit, idx = NULL, n = 1000, ...)
fit |
A |
idx |
Optional integer vector of 1-based latent indices to return.
|
n |
Number of posterior draws (default 1000). |
... |
Unused; for S3 compatibility. |
The constrained draw at cell k uses the sparse Cholesky of the stored
precision Q_k and the conditioning-by-kriging correction
where z ~ N(0, Q_k^{-1}) and A stacks the field sum-to-zero rows (one for
an ICAR / CAR field; two – structured and unstructured – for a BYM2 field;
one per spatial block in a multi-block fit). The returned draw is
m_k + z_c, restricted to idx. Because m_k already satisfies the
constraint (the inner solve centres the field), the mean is left unchanged
and only the covariance is constrained, so the per-cell marginal matches the
inner-Laplace constrained covariance exactly.
Cells with zero outer-grid weight (e.g. pruned cells) or no stored Q are
dropped and the remaining weights renormalized. A degenerate single-cell
grid (quadrature ESS 1) returns draws from that cell's N(m_1, V_1).
A numeric matrix [n x length(idx)] of latent draws, one row per
draw, columns named x<idx>. Carries attr(., "draws_kind") = "iid"
(consistent with the draws-provenance gate), attr(., "cells") – the
outer-grid cell index each row was drawn from – attr(., "theta"), the
hyperparameter half of the same rows continuized within each row's own cell
(tulpa_hyper_draws()) – and attr(., "scope") = "latent".
tulpa_posterior_draws(), tulpa_nested_laplace_joint(),
posterior_sample()
Local power-scaling sensitivity (Kallioinen et al. 2024): how much each
fixed-effect posterior moves when the prior or the likelihood is raised to a
power alpha near 1. The existing draws are importance-reweighted by
exp((alpha - 1) * log_component) (PSIS-smoothed via tulpa_psis(); no
refits), and the sensitivity is the gradient of the cumulative
Jensen-Shannon distance between the base and power-scaled posteriors with
respect to log2(alpha).
Values above threshold flag sensitivity. High on both the prior and
likelihood components indicates potential prior-data conflict; high prior
with low likelihood indicates a strong prior / weak likelihood.
When the fit recorded per-draw hyperparameter log-prior values at
draw-synthesis time ($hyper_log_prior_draws, stored by the nested-Laplace
mixture paths such as tulpa_re_cov_nested() and the tulpa() random-slope
redirect), a hyperparameter column reports the power-scaling sensitivity
of the hyperparameter prior by the same reweighting; NA otherwise.
tulpa_powerscale_sensitivity( fit, prior = NULL, lower_alpha = 0.99, upper_alpha = 1.01, threshold = 0.05 )tulpa_powerscale_sensitivity( fit, prior = NULL, lower_alpha = 0.99, upper_alpha = 1.01, threshold = 0.05 )
fit |
A |
prior |
The Gaussian fixed-effect prior the fit used, as
|
lower_alpha, upper_alpha
|
Power-scaling grid endpoints for the gradient (defaults 0.99 / 1.01, as in priorsense). |
threshold |
Sensitivity flag threshold (default 0.05). |
A data frame with one row per fixed-effect parameter and columns
variable, prior, hyperparameter, likelihood, diagnosis.
Kallioinen, Paananen, Buerkner & Vehtari (2024). Detecting and diagnosing prior and likelihood sensitivity with power-scaling. Statistics and Computing 34:57. Nguyen & Vreeken (2015). Non-parametric Jensen-Shannon divergence. ECML PKDD.
tulpa_psis(), tulpa_criteria().
set.seed(1) d <- data.frame(x = rnorm(150)) d$y <- rpois(150, exp(0.5 + 0.7 * d$x)) fit <- tulpa(y ~ x, data = d, family = "poisson", mode = "laplace", beta_prior = list(mean = 0, sd = 5)) tulpa_powerscale_sensitivity(fit, prior = list(mean = 0, sd = 5))set.seed(1) d <- data.frame(x = rnorm(150)) d$y <- rpois(150, exp(0.5 + 0.7 * d$x)) fit <- tulpa(y ~ x, data = d, family = "poisson", mode = "laplace", beta_prior = list(mean = 0, sd = 5)) tulpa_powerscale_sensitivity(fit, prior = list(mean = 0, sd = 5))
Specify priors for model parameters. Supports both PC (penalized complexity) priors for variance components and standard distributions for other parameters.
A tulpa_priors object is what the simulation tools draw parameters from:
prior_predict() and tulpa_simulate() with theta = NULL. The model
fitters do not read one. tulpa() carries its own defaults, set per backend
through its beta_prior, re_prior and hyperprior arguments:
Fixed effects: prior_normal(0, 2.5) on every backend – the same default
as beta here.
Random-effect SDs: a PC prior with P(sigma > 3) = 0.01 (plus an LKJ(2)
prior on a correlated block's correlations) on the routes that integrate or
maximize over them (nested Laplace, re_cov_nested, mode = "eb");
half-Cauchy(2.5) on the samplers (re_prior$sigma_re_scale, and
re_prior$prior_sigma_scale for mode = "gibbs"); an inverse-Wishart
under control$re_cov = "gibbs".
The sigma, phi, rho_temporal and rho_spatial defaults below are
therefore the simulation defaults, not the priors a fit ran under.
tulpa_priors( beta = NULL, sigma = NULL, phi = NULL, rho_temporal = NULL, rho_spatial = NULL ) ## S3 method for class 'tulpa_priors' print(x, ...) ## S3 method for class 'tulpa_prior' print(x, ...) ## S3 method for class 'tulpa_prior_normal' format(x, indent = "", ...) ## S3 method for class 'tulpa_prior_half_normal' format(x, indent = "", ...) ## S3 method for class 'tulpa_prior_half_cauchy' format(x, indent = "", ...) ## S3 method for class 'tulpa_prior_gamma' format(x, indent = "", ...) ## S3 method for class 'tulpa_prior_exponential' format(x, indent = "", ...) ## S3 method for class 'tulpa_prior_beta' format(x, indent = "", ...) ## S3 method for class 'tulpa_prior_pc' format(x, indent = "", ...) ## S3 method for class 'tulpa_prior' format(x, indent = "", ...)tulpa_priors( beta = NULL, sigma = NULL, phi = NULL, rho_temporal = NULL, rho_spatial = NULL ) ## S3 method for class 'tulpa_priors' print(x, ...) ## S3 method for class 'tulpa_prior' print(x, ...) ## S3 method for class 'tulpa_prior_normal' format(x, indent = "", ...) ## S3 method for class 'tulpa_prior_half_normal' format(x, indent = "", ...) ## S3 method for class 'tulpa_prior_half_cauchy' format(x, indent = "", ...) ## S3 method for class 'tulpa_prior_gamma' format(x, indent = "", ...) ## S3 method for class 'tulpa_prior_exponential' format(x, indent = "", ...) ## S3 method for class 'tulpa_prior_beta' format(x, indent = "", ...) ## S3 method for class 'tulpa_prior_pc' format(x, indent = "", ...) ## S3 method for class 'tulpa_prior' format(x, indent = "", ...)
beta |
Prior for fixed effects. Default: |
sigma |
Prior for random effect SDs. Default: PC prior with P(sigma > 1) = 0.01. |
phi |
Prior for overdispersion parameter. Default: PC prior with P(phi > 10) = 0.01. |
rho_temporal |
Prior for temporal autocorrelation. Default:
|
rho_spatial |
Prior for spatial proportion (BYM2). Default:
|
x |
A |
... |
Ignored. |
indent |
String prefixed to each formatted line. |
PC priors (Simpson et al., 2017) provide principled regularization that:
Favors simpler models (smaller variance components)
Has interpretable parameters (tail probabilities)
Prevents overfitting with sparse data
For other parameters, standard distributions are available via helper
functions: prior_normal(), prior_half_normal(), prior_half_cauchy(),
prior_gamma(), prior_beta(), prior_exponential().
A tulpa_priors object
format() returns the prior as a character string.
Simpson, D., Rue, H., Riebler, A., Martins, T. G., & Sorbye, S. H. (2017). Penalising model component complexity: A principled, practical approach to constructing priors. Statistical Science, 32(1), 1-28.
# Default priors tulpa_priors() # Custom fixed effect prior tulpa_priors(beta = prior_normal(0, 1)) # Tighter random effect prior tulpa_priors(sigma = prior_pc(U = 0.5, alpha = 0.01)) # Half-Cauchy for random effect SD tulpa_priors(sigma = prior_half_cauchy(2.5)) # Informative prior for temporal correlation tulpa_priors(rho_temporal = prior_beta(5, 2)) # Prior mode at ~0.8# Default priors tulpa_priors() # Custom fixed effect prior tulpa_priors(beta = prior_normal(0, 1)) # Tighter random effect prior tulpa_priors(sigma = prior_pc(U = 0.5, alpha = 0.01)) # Half-Cauchy for random effect SD tulpa_priors(sigma = prior_half_cauchy(2.5)) # Informative prior for temporal correlation tulpa_priors(rho_temporal = prior_beta(5, 2)) # Prior mode at ~0.8
Times a fit one solver phase at a time – the inner Newton solve's eta, scatter (the Hessian and gradient assembly), factorize (Cholesky), line search and final pass, the nested-Laplace outer grid's cell solves, and the NUTS sampler's iterations and gradient evaluations – and returns the breakdown as a data frame. The accumulator aggregates across the parallel outer-grid worker threads and across concurrently run chains, so the reported times cover the whole fit rather than only the calling thread (and can therefore exceed its wall time).
tulpa_profile(expr, sort = TRUE)tulpa_profile(expr, sort = TRUE)
expr |
An expression that runs a fit (for example a call to
|
sort |
Logical; order rows by descending time. Default |
Use it to settle where a fit spends its time, e.g. whether a slow joint
occu_cover() fit is bound by the assembly scatter or the Cholesky
factorize:
p <- tulpa_profile(
tulpa_nested_laplace_joint(..., control = list(integration = "ccd"))
)
print(p) # rows ordered by time; scatter vs factorize at top
fit <- attr(p, "value")
Timing is off outside tulpa_profile(): an ordinary fit pays one
flag read per phase scope and nothing else.
A data frame with one row per phase and columns phase,
seconds, calls, ms_per_call (mean wall time per phase
call), and share (fraction of the total timed seconds of the leaf
phases; NA for an enclosing phase). The fit result is attached as
the "value" attribute.
The single-response Laplace solve behind tulpa_laplace(),
tulpa(mode = "laplace") and every per-cell inner solve of
tulpa_nested_laplace() / tulpa(mode = "nested_laplace"):
phases eta, scatter, factorize,
line_search, log_det, log_lik_prior,
hessian_extract and inner_diagnostics.
The joint solver behind tulpa_nested_laplace_joint(), on both
its dense and its sparse path (the sparse one adds
pattern_build and prep).
The nested-Laplace outer grid, single-response and joint:
outer_grid_cell, one call per solved cell.
The samplers: nuts_warmup and nuts_sampling, one call
per NUTS iteration per chain, and gradient, one call per
log-density gradient evaluation (NUTS leapfrog steps, static HMC, the
step-size search and the other samplers that evaluate the gradient
through the engine).
An expression that reaches none of these – a Gibbs or quadrature backend
that runs neither an engine Newton solve nor an engine gradient, or no fit
at all – records nothing, and tulpa_profile() then warns rather than returning an
all-zero table as if it were a measurement.
outer_grid_cell, nuts_warmup and nuts_sampling
ENCLOSE the leaf phases timed inside them (a cell's inner Newton phases, an
iteration's gradient evaluations), so their seconds overlap the leaves'
rather than adding to them. Their share is NA; the shares of
the leaf phases sum to one.
set.seed(1) n <- 200L; X <- cbind(1, rnorm(n)) y <- rbinom(n, 1, plogis(X %*% c(0, 0.5))) tulpa_profile(tulpa_laplace(y, rep(1L, n), X, family = "binomial"))set.seed(1) n <- 200L; X <- cbind(1, rnorm(n)) y <- rbinom(n, 1, plogis(X %*% c(0, 0.5))) tulpa_profile(tulpa_laplace(y, rep(1L, n), X, family = "binomial"))
Smooths a set of importance ratios by replacing the largest weights with the
order statistics of a generalized-Pareto fit to their upper tail, and
returns the Pareto shape diagnostic pareto_k together with the smoothed
(normalized) log weights and their importance-sampling effective sample
size. pareto_k estimates the number of finite moments of the raw weight
distribution: < 0.5 is good (finite variance). The usable upper boundary is
sample-size dependent, min(1 - 1/log10(S), 0.7) for S draws (Vehtari et al.
2024): about 0.565 at S = 200, reaching the 0.7 cap only past S ~ 2154.
Above it the proposal cannot be reliably corrected to the target.
tulpa_psis(log_ratios, tail_points = NULL)tulpa_psis(log_ratios, tail_points = NULL)
log_ratios |
Numeric vector of (unnormalized) log importance ratios
|
tail_points |
Number of upper-tail order statistics for the
generalized-Pareto fit, or |
A list with pareto_k (the tail shape, NA if the sample is too
small to fit), is_ess (importance-sampling effective sample size,
1 / sum(w^2) on the normalized smoothed weights), log_weights
(the normalized smoothed log weights, one per element of log_ratios and
in the same order; with fewer than 5 draws the raw ratios, normalized),
tail_len (the tail size used, 0 when no tail was fitted), and
tail_smoothed (FALSE when the tail kept its raw log ratios because the
generalized-Pareto fit was not attempted or returned a shape / scale the
quantile function is undefined at; pareto_k then reports the attempted fit
and does not describe the returned weights).
Vehtari, Simpson, Gelman, Yao & Gabry (2024). Pareto smoothed importance sampling. JMLR 25(72):1-58.
diagnostics() for the fit-level diagnostic front door.
set.seed(1) # Well-behaved importance ratios: k-hat is small. ps <- tulpa_psis(rnorm(2000)) ps$pareto_k ps$is_essset.seed(1) # Well-behaved importance ratios: k-hat is small. ps <- tulpa_psis(rnorm(2000)) ps$pareto_k ps$is_ess
Refines a generalized linear mixed model's fixed effects and random-effect
covariance by replacing the per-group Laplace integral with n_quad-point
adaptive Gauss-Hermite quadrature (AGHQ). At n_quad = 1 this is the joint
Laplace (glmer nAGQ = 1); higher n_quad reduces the small-cluster
attenuation of the variance components for binary / count data. Unlike
agq_fit() (intercept-only RE, built-in binomial/poisson/gaussian
likelihoods), this engine is callback-driven: the caller supplies the
per-group conditional likelihood, so a custom marginal (e.g. a
latent-state-integrated occupancy / detection likelihood, or the
latent-abundance-integrated N-mixture marginal) refines through the same
quadrature.
This is an engine block, not a front door: model packages call it
programmatically, so its tuning knobs (max_iter, n_quad, keep)
sit in the signature rather than in a control list.
The engine is structure-agnostic. It integrates the per-group marginal
where is the group's random-effect vector (dimension
over the RE terms) and is the group's
conditional log-likelihood when its linear predictors are perturbed by
. How enters the likelihood – through one linear
predictor, or through several coupled arms at different observation
granularities (e.g. a per-site abundance arm and a per-visit detection arm
sharing a species grouping) – lives entirely in the callback. The engine
only needs, per group, the value / gradient / Hessian of in
(for the mode) and at the quadrature nodes (for the
sum). The fixed parameters theta and the log-Cholesky coordinates of
are optimized jointly on ; standard errors
come from the exact-marginal Hessian.
Two callback forms select the structure (supply exactly one):
make_site – the common single-arm, per-row-separable case
( for one
linear predictor ). The engine builds the oracle from the
per-row marginal and the RE design Z itself.
make_group – the general / multi-arm case. The caller supplies
the per-group b-space oracle directly, so non-separable units (e.g. the
visits of an N-mixture site coupled through the shared latent count) and
random effects on several arms at once are handled with no engine change.
Scope: one shared grouping factor across all RE terms (the per-group integral
factorizes). The total RE dimension per group should be small (the quadrature
grid is n_quad^dim).
tulpa_re_aghq( theta0, re_terms, Sigma0, make_site = NULL, make_group = NULL, oracle = NULL, n_obs = NULL, keep = NULL, n_quad = 9L, lkj_eta = 1, theta_prior_sd = Inf, sigma_prior = NULL, gradient = c("fd", "analytic"), max_iter = 200L )tulpa_re_aghq( theta0, re_terms, Sigma0, make_site = NULL, make_group = NULL, oracle = NULL, n_obs = NULL, keep = NULL, n_quad = 9L, lkj_eta = 1, theta_prior_sd = Inf, sigma_prior = NULL, gradient = c("fd", "analytic"), max_iter = 200L )
theta0 |
Initial fixed-parameter vector. The engine optimizes these jointly with the RE covariance; the callback interprets them. |
re_terms |
A list of RE term specs (or one spec), each defining a
covariance block: |
Sigma0 |
List of initial per-term covariance matrices (the EM estimate). |
make_site |
|
make_group |
|
oracle |
Optional prebuilt native (compiled) oracle, an external pointer
to a |
n_obs |
Number of observations (length of each term's |
keep |
Optional logical/integer mask of observations to include
(default all; |
n_quad |
Quadrature nodes per RE dimension. Either a single integer
(default 9; |
lkj_eta |
LKJ shape for an optional correlation penalty on each
correlated block (log-density |
theta_prior_sd |
Optional Gaussian ridge SD on the fixed parameters
|
sigma_prior |
Optional Penalized-Complexity prior on the marginal
standard deviations of one or more RE covariance blocks, added to the
objective (and hence the marginal Hessian). |
gradient |
How |
max_iter |
Optimizer iteration cap (default 200). |
A list with: theta (refined fixed parameters), Sigma_list
(refined per-term covariance), blup / blup_var (per-term n_groups x n_coefs posterior mean / variance of the RE), group_ok (logical, length
n_groups: FALSE where that group's mode search or precision
factorization failed, which is what the NA rows of blup, blup_var,
blup_cov_g and blup_cross_g mean – a caller conditions its per-group
reads on this rather than on trapping the accompanying warning),
blup_cross (per-term
n_groups x n_theta x n_coefs array: the mode/theta cross-Hessian block
Bf, -d^2 ell_g / d theta db at each group's mode, in the same
negative-Hessian sign convention as the posterior precision underlying
blup_var – so a joint draw of theta and a group's RE b_g uses
b_g | theta ~ N(blup_g - Cinv_g %*% t(Bf_g) %*% (theta_draw - theta), Cinv_g) with Cinv_g the group's n_coefs x n_coefs posterior
covariance block. NA throughout when blup_cross_available is FALSE:
the cross-Hessian needs the oracle's analytic theta_score, which the
R-closure bridge (make_site / make_group) does not supply – only a
prebuilt native oracle carries it), blup_cov_g / blup_cross_g
(per-group lists, length n_groups, of the FULL joint posterior
covariance (d x d, d = every RE term's width combined) and mode/theta
cross-Hessian (n_theta x d) across ALL RE terms sharing that group –
the superset blup_var/blup_cross reduce to a per-term diagonal block
of when a group carries more than one term, since a group's terms are
found jointly and can carry real posterior covariance BETWEEN terms
(e.g. an abundance-arm and a detection-arm term sharing one grouping
factor); same NA-when-unavailable rule as blup_cross), theta_cov /
theta_se
(fixed-parameter covariance / SE from the marginal Hessian), re_par /
re_par_cov / re_par_se (the RE-covariance coordinates the optimizer
carried – log-Cholesky for a full block, log-SD for a diagonal one – with
their block of the same inverse Hessian, so SE(log sigma) is available
for a boundary test on a weakly-identified variance component),
re_par_layout (per block: label, nc, full, the index range into
re_par and the coord names, so a caller does not reconstruct the
packing), joint_cov (the whole (n_theta + n_chol) inverse Hessian),
log_marginal
(the AGHQ marginal log-likelihood at the optimum, excluding any ridge),
n_quad, lkj_eta, converged, and counts (stats::optim's own
function / gradient evaluation counts, so a caller reporting how much
work the fit took has a number to report rather than NA). RE terms that
do not share one
grouping factor are an input error and stop. Three conditions warn and
return NULL (caller keeps its prior fit): a singular / non-finite
optimum, an objective that is already undefined at the starting parameters
(some group's solve fails there, so there is nothing to descend), and an
optimum whose objective is the failure sentinel rather than an attained
marginal likelihood – the last two report which groups failed.
Pinheiro & Bates (1995). Approximations to the log-likelihood function in the nonlinear mixed-effects model. Journal of Computational and Graphical Statistics 4(1):12-35. Lewandowski, Kurowicka & Joe (2009). Generating random correlation matrices based on vines and extended onion method. Journal of Multivariate Analysis 100(9):1989-2001.
# A per-row-separable binomial GLMM marginal supplied through `make_site`. l1pe <- function(x) ifelse(x > 0, x + log1p(exp(-x)), log1p(exp(x))) make_binom_site <- function(X, y, nt) function(theta) { eta_fixed <- as.numeric(X %*% theta) list(eta_re = eta_fixed, deriv = function(rows, eta) { p <- plogis(eta) list(logL = y[rows] * eta - nt[rows] * l1pe(eta), d1 = y[rows] - nt[rows] * p, d2 = -nt[rows] * p * (1 - p)) }, lmat = function(rows, ETA) y[rows] * ETA - nt[rows] * l1pe(ETA)) } set.seed(1) ng <- 30L; npg <- 8L; n <- ng * npg g <- rep(seq_len(ng), each = npg); x <- rnorm(n) X <- cbind(1, x); nt <- rep(3L, n); u <- rnorm(ng, 0, 0.9) y <- rbinom(n, nt, plogis(0.3 + 0.7 * x + u[g])) fit <- tulpa_re_aghq(theta0 = c(0, 0), re_terms = list(list(idx = g, n_groups = ng, n_coefs = 1L)), Sigma0 = list(matrix(0.25, 1, 1)), make_site = make_binom_site(X, y, nt), n_obs = n, n_quad = 5L) sqrt(fit$Sigma_list[[1]][1, 1]) # adaptive-GHQ RE standard deviation# A per-row-separable binomial GLMM marginal supplied through `make_site`. l1pe <- function(x) ifelse(x > 0, x + log1p(exp(-x)), log1p(exp(x))) make_binom_site <- function(X, y, nt) function(theta) { eta_fixed <- as.numeric(X %*% theta) list(eta_re = eta_fixed, deriv = function(rows, eta) { p <- plogis(eta) list(logL = y[rows] * eta - nt[rows] * l1pe(eta), d1 = y[rows] - nt[rows] * p, d2 = -nt[rows] * p * (1 - p)) }, lmat = function(rows, ETA) y[rows] * ETA - nt[rows] * l1pe(ETA)) } set.seed(1) ng <- 30L; npg <- 8L; n <- ng * npg g <- rep(seq_len(ng), each = npg); x <- rnorm(n) X <- cbind(1, x); nt <- rep(3L, n); u <- rnorm(ng, 0, 0.9) y <- rbinom(n, nt, plogis(0.3 + 0.7 * x + u[g])) fit <- tulpa_re_aghq(theta0 = c(0, 0), re_terms = list(list(idx = g, n_groups = ng, n_coefs = 1L)), Sigma0 = list(matrix(0.25, 1, 1)), make_site = make_binom_site(X, y, nt), n_obs = n, n_quad = 5L) sqrt(fit$Sigma_list[[1]][1, 1]) # adaptive-GHQ RE standard deviation
For one or more random-effects terms (e.g. (1 + x | g), (1 + x || g), or
several terms together), estimate the random-effect covariances Sigma by
sampling the exact joint posterior p(beta, {b}, {Sigma} | y) rather than
fixing each Sigma at the Laplace mode. This removes the Laplace / PQL
"approximation" bias that shrinks variance components low for binary and
low-count responses with small groups.
tulpa_re_cov_gibbs( y, n_trials = NULL, X, re_terms, family = "binomial", phi = 1, prior_df = NULL, prior_scale = NULL, beta_prior = .tulpa_default_beta_prior("re_cov_gibbs"), control = list() )tulpa_re_cov_gibbs( y, n_trials = NULL, X, re_terms, family = "binomial", phi = 1, prior_df = NULL, prior_scale = NULL, beta_prior = .tulpa_default_beta_prior("re_cov_gibbs"), control = list() )
y, n_trials, X, family, phi
|
Passed to the likelihood and to
|
re_terms |
Either a single random-effect term or a list of them. Each
term is a list with |
prior_df |
Inverse-Wishart prior degrees of freedom. Applied to every
correlated block (default |
prior_scale |
Inverse-Wishart prior scale matrix. Used for a block when
its dimension matches (default |
beta_prior |
Gaussian fixed-effect prior as |
control |
A named list of numerical / tuning knobs (statistical arguments stay in the signature above). Recognized entries:
|
Metropolis-within-Gibbs targeting p(beta, {b_m}, {Sigma_m} | y):
b_{m,g} | beta, Sigma, y – random-walk Metropolis per (term, group)
(groups are conditionally independent given beta and the covariances). The
proposal shape is the Laplace per-group posterior covariance block
(return_re_cov from tulpa_laplace()); the Metropolis acceptance is
computed against the exact group log-likelihood plus the Gaussian RE
log-prior, which corrects the non-Gaussianity the Laplace approximation
misses. The linear predictor holds every other term's contribution fixed.
beta | b, Sigma, y – random-walk Metropolis, proposal shape from the
Laplace fixed-effect Hessian block H_beta.
Sigma_m | b_m – exact conjugate draw. A correlated block draws the
full matrix from IW(prior_df + G_m, prior_scale + sum_g b_{m,g} b_{m,g}');
an uncorrelated (diagonal) block draws each variance from its scalar
conjugate (inverse-gamma). No linearization enters this step.
A single Laplace solve provides the starting values (beta, b) and the
proposal shapes; the random-walk scales for the beta block and the per-term
b blocks are adapted toward their target acceptance during burn-in
(Robbins-Monro), then held fixed for the recorded sweeps. The covariance
summary marginalizes the derived scale / correlation parameters over the
posterior draws via the same machinery as tulpa_re_cov_nested().
For family = "gaussian" the response is already conditionally Gaussian, so
there is no Laplace bias to remove; phi (the residual variance) is treated
as known. The sampler still runs and is useful as a reference / for Sigma
uncertainty.
A list with:
posterior: data frame with one row per parameter (sigma_i, rho_ij,
Sigma_ij; prefixed by block label when there are several terms; diagonal
blocks report no rho) and columns mean, sd, median, ci_lo,
ci_hi.
Sigma_mean: the posterior mean of Sigma (a matrix for one block, a
named list of matrices for several).
Sigma_draws: list of recorded draws; each element is the per-block list
of Sigma matrices for that sweep.
beta_draws / draws: matrix of recorded beta draws; draws (with
means, param_names, process_info) drives the generic tulpa_fit
methods.
re: matrix of recorded random-effect draws, n_kept rows by one column
per (block, group, coefficient) in that order – the exact per-group
posterior the sweep samples. Row-aligned with beta_draws, so a draw is
a joint (beta, b) state. This is what ranef() summarizes and what
posterior_predict() adds to the linear predictor.
accept: list with beta and b acceptance rates over recorded sweeps.
n_kept, n_coefs (vector of per-block c), prior: bookkeeping.
Lewandowski, Kurowicka & Joe (2009). Generating random correlation matrices based on vines and extended onion method. Journal of Multivariate Analysis 100(9):1989-2001.
tulpa_re_cov_nested() for the grid-integration (summary-bias) fix;
tulpa_laplace() for the pilot solve and per-group covariance blocks.
set.seed(1) G <- 20L; per <- 12L; n <- G * per grp <- rep(seq_len(G), each = per); x <- rnorm(n) b <- cbind(rnorm(G, 0, 0.7), rnorm(G, 0, 0.5)) # random intercept + slope eta <- -0.2 + 0.5 * x + b[grp, 1] + b[grp, 2] * x y <- rbinom(n, 1L, plogis(eta)) re_term <- list(idx = grp, n_groups = G, n_coefs = 2L, Z = cbind(1, x), correlated = TRUE) fit <- tulpa_re_cov_gibbs(y, rep(1L, n), cbind(1, x), re_term, family = "binomial", control = list(n_iter = 300L, warmup = 150L)) fit$Sigma_mean # exact-debias RE covariance posterior meanset.seed(1) G <- 20L; per <- 12L; n <- G * per grp <- rep(seq_len(G), each = per); x <- rnorm(n) b <- cbind(rnorm(G, 0, 0.7), rnorm(G, 0, 0.5)) # random intercept + slope eta <- -0.2 + 0.5 * x + b[grp, 1] + b[grp, 2] * x y <- rbinom(n, 1L, plogis(eta)) re_term <- list(idx = grp, n_groups = G, n_coefs = 2L, Z = cbind(1, x), correlated = TRUE) fit <- tulpa_re_cov_gibbs(y, rep(1L, n), cbind(1, x), re_term, family = "binomial", control = list(n_iter = 300L, warmup = 150L)) fit$Sigma_mean # exact-debias RE covariance posterior mean
For one or more random-effects terms (e.g. (1 + x | g), (1 + x || g), or
several terms together), integrate the Laplace marginal likelihood over the
random-effect covariances Sigma instead of fixing them at point estimates.
Reports weighted posterior summaries (mean, SD, median, 2.5\
Sigma and its derived scale (sigma_i) and correlation (rho_ij)
parameters, marginalizing the joint posterior over a Sigma-grid.
This corrects the plug-in-MAP ("summary") bias: the mode of a skewed variance-component marginal is biased low relative to its median, so the headline summary should be the marginalized median, not the mode.
tulpa_re_cov_nested( y, n_trials = NULL, X, re_terms, family = "binomial", phi = 1, phi2 = NULL, prior_sigma = NULL, eta = NULL, hyperprior = c("proper", "flat"), log_prior_theta = NULL, beta_prior = NULL, offset = NULL, weights = NULL, n_quad = 1L, X_zi = NULL, zi_prior_sd = 2.5, control = list() )tulpa_re_cov_nested( y, n_trials = NULL, X, re_terms, family = "binomial", phi = 1, phi2 = NULL, prior_sigma = NULL, eta = NULL, hyperprior = c("proper", "flat"), log_prior_theta = NULL, beta_prior = NULL, offset = NULL, weights = NULL, n_quad = 1L, X_zi = NULL, zi_prior_sd = 2.5, control = list() )
y, n_trials, X, family, phi
|
Passed to |
re_terms |
Either a single random-effect term or a list of them. Each
term is a list with |
phi2 |
Optional second dispersion, threaded into every inner
|
prior_sigma, eta
|
Hyperparameters of the PC + LKJ prior used when
|
hyperprior |
|
log_prior_theta |
Optional |
beta_prior |
Optional Gaussian prior on the fixed effects, threaded into
every inner |
offset |
Optional observation-level offset on the linear predictor
(length |
weights |
Optional non-negative observation weights (length
|
n_quad |
Quadrature order for the inner marginal. |
X_zi |
Optional zero-inflation design matrix ( |
zi_prior_sd |
Prior SD on |
control |
A named list of numerical / tuning knobs (statistical arguments stay in the signature above). Recognized entries:
|
Each term is one covariance block. A correlated block ((1 + x | g)) is
a full Sigma = L L' parameterized by its lower Cholesky factor in
log-Cholesky coordinates (the log-diagonal and the strictly-lower entries of
L, c(c+1)/2 values for a c-coefficient block), which keeps Sigma
positive definite for every coordinate. An uncorrelated block
((1 + x || g)) is a diagonal Sigma parameterized by its c log standard
deviations. A scalar (1 | g) term is the degenerate c = 1 block. Several
blocks stack their parameters into one integration vector; a single-term
model is the length-1 case.
Integration nodes live in the whitened stacked-parameter space, centred at
the joint marginal-likelihood mode and rotated/scaled by the Cholesky of the
mode's posterior covariance (solve(Hessian)), so points track the posterior
ridge. Two node layouts are available via integration:
"ccd" (default): a central-composite design (ccd_grid()) of
1 + 2k + 2^(k-q) points for the total k = sum_blocks parameters, with
the corrected R-INLA design weights (ccd_weights()). Scales polynomially
in k, where the tensor grid is exponential.
"grid": the full n_per_axis^k tensor product with uniform cell
weights – denser and more robust to a non-Gaussian whitened posterior,
but only tractable for small k.
Each node k contributes integration weight proportional to
Delta_k * exp(log_marginal(Sigma_k) + log_prior_theta(theta_k)), following
the INLA convention int ~ sum_k Delta_k pi(theta_k).
The two layouts also decide how the reported median and 2.5\
of each derived quantity are read off the nodes. A tensor grid's uniform
cells discretize the posterior density, so the cumulative node weights are a
CDF and the summary is the weighted quantile. A CCD is a moment rule: its
nodes sit where they reproduce the integrand's first two moments and carry no
probability mass of their own, so the summary is moment-matched instead –
the first two weighted moments on each quantity's own coordinate (log for a
scale or a variance, atanh for a correlation, the identity for a covariance)
define a Gaussian there whose quantiles are mapped back. Scale intervals are
therefore positive and asymmetric, and correlation intervals stay inside
(-1, 1). The mean and sd columns are the weighted moments under either
layout.
By default (hyperprior = "proper") log_prior_theta is the
weakly-informative PC + LKJ hyperprior, built per block by
re_cov_pc_lkj_prior() and summed over blocks (PC prior on each marginal SD
via prior_sigma, LKJ prior on each correlated block's correlation matrix via
eta), expressed in the same parameterization with the exact
change-of-variables Jacobian. hyperprior = "flat" makes it the zero
function, flat in log(theta). The likelihood it multiplies is the inner
Laplace marginal, which integrates the fixed effects out together with the
random effects rather than profiling them, so under "flat" the node centre
theta_hat is a restricted (REML-type) estimate – REML for a gaussian
response, agreeing with glmmTMB(..., REML = TRUE) rather than with the ML
fit of lme4::glmer() otherwise (see tulpa_eb()). Supply a custom log_prior_theta
function to override either default (then prior_sigma / eta /
hyperprior are ignored); it must act on the full stacked parameter vector.
tulpa_eb() shares this same objective and the same default, so
tulpa_eb()$theta_hat and tulpa_re_cov_nested()$theta_hat stay the same
estimate on the same data under either setting.
A list with:
posterior: data frame with one row per parameter and columns mean,
sd, median, ci_lo, ci_hi. Parameter names are sigma_i, rho_ij,
Sigma_ij for a single block, prefixed by the block label
(g.sigma_1, ...) when there are several blocks. Diagonal blocks report
no rho.
map: the plug-in-mode summary at theta_hat (a single list(Sigma, sigma, rho) for one block, or a named list of them).
Sigma_mean: the weighted posterior mean of Sigma (a matrix for one
block, or a named list of matrices).
beta, draws, means, param_names, process_info: the fixed-effect
posterior from the node mixture (drives coef/confint/vcov/summary).
re_nodes, re_var_nodes: the per-group random-effect posterior at each
integration node – conditional mean and marginal variance of every
(block, group, coefficient), one row per node. ranef() reports the
weights-mixture of them. NULL at n_quad > 1, whose inner marginal
integrates each group out instead of conditioning on it; that fit carries
ranef_unavailable (the reason) in their place.
re_debias_draws, re_debias_idx: present when the subspace debias
selected a random-effect coordinate. The sampled draws of those
coordinates on the node mixture the fixed-effect draws use, and their
positions within the random-effect block. ranef() reports those rows
empirically and the rest from the Gaussian mixture, recording which in
its source column.
theta_hat, theta_grid, weights, log_marginal, n_grid, layout,
n_blocks, n_coefs (vector of per-block c).
subspace_debias: present only when control$subspace_debias was set.
idx are the corrected latent coordinates, bands the per-probed-index
reliability table they were read from, closure_added what the coupling
closure added, and accept the per-node Metropolis acceptance rate.
Rue, Martino & Chopin (2009). Approximate Bayesian inference for latent Gaussian models by using integrated nested Laplace approximations. JRSS-B 71(2):319-392. Lewandowski, Kurowicka & Joe (2009). Generating random correlation matrices based on vines and extended onion method. Journal of Multivariate Analysis 100(9):1989-2001.
tulpa_laplace() for the inner solve; tulpa_nested_laplace() for
the analogous outer integration over spatial / temporal prior
hyperparameters.
set.seed(1) G <- 20L; per <- 12L; n <- G * per grp <- rep(seq_len(G), each = per); x <- rnorm(n) b <- cbind(rnorm(G, 0, 0.7), rnorm(G, 0, 0.5)) # random intercept + slope eta <- -0.2 + 0.5 * x + b[grp, 1] + b[grp, 2] * x y <- rbinom(n, 1L, plogis(eta)) re_term <- list(idx = grp, n_groups = G, n_coefs = 2L, Z = cbind(1, x), correlated = TRUE) fit <- tulpa_re_cov_nested(y, rep(1L, n), cbind(1, x), re_term, family = "binomial") fit$Sigma_mean # marginalized RE covarianceset.seed(1) G <- 20L; per <- 12L; n <- G * per grp <- rep(seq_len(G), each = per); x <- rnorm(n) b <- cbind(rnorm(G, 0, 0.7), rnorm(G, 0, 0.5)) # random intercept + slope eta <- -0.2 + 0.5 * x + b[grp, 1] + b[grp, 2] * x y <- rbinom(n, 1L, plogis(eta)) re_term <- list(idx = grp, n_groups = G, n_coefs = 2L, Z = cbind(1, x), correlated = TRUE) fit <- tulpa_re_cov_nested(y, rep(1L, n), cbind(1, x), re_term, family = "binomial") fit$Sigma_mean # marginalized RE covariance
PSIS-LOO with exact refits where the importance sampling is unreliable:
observations whose Pareto k-hat exceeds k_threshold are re-scored by
refitting the model without that observation (through the fit's stored
tulpa() call, as in tulpa_kfold()) and evaluating the exact held-out
log predictive density. All other observations keep their PSIS-LOO value,
so the cost is one refit per flagged observation rather than per fold.
The same restrictions as tulpa_kfold() apply: fixed-effect / GLMM fits
only (subsetting breaks a spatial / temporal field), and held-out
random-effect groups contribute at their prior mean.
tulpa_reloo( object, data, k_threshold = .nl_diag("k_usable"), n_trials = NULL, ndraws = NULL )tulpa_reloo( object, data, k_threshold = .nl_diag("k_usable"), n_trials = NULL, ndraws = NULL )
object |
A |
data |
The data frame the model was fit to. |
k_threshold |
Pareto k-hat above which an observation is refit (default 0.7, the standard PSIS reliability gate). |
n_trials |
Optional binomial denominators (length |
ndraws |
Number of posterior draws used for the PSIS-LOO baseline (defaults to all stored draws, or 400 on the draw-free Laplace tier). |
A list with elpd_loo (corrected), se_elpd_loo, looic,
pointwise (per-observation elpd, exact at the refit observations),
reloo_idx (indices refit), pareto_k (the original k-hat values), and
k_threshold.
tulpa_kfold() for the full refit-CV; tulpa_criteria() for
PSIS-LOO / WAIC without refits.
set.seed(1) d <- data.frame(x = rnorm(120)) d$y <- rpois(120, exp(0.4 + 0.6 * d$x)) fit <- tulpa(y ~ x, data = d, family = "poisson", mode = "laplace") rl <- tulpa_reloo(fit, data = d) rl$elpd_looset.seed(1) d <- data.frame(x = rnorm(120)) d$y <- rpois(120, exp(0.4 + 0.6 * d$x)) fit <- tulpa(y ~ x, data = d, family = "poisson", mode = "laplace") rl <- tulpa_reloo(fit, data = d) rl$elpd_loo
Vectorized PG(b, z) draws via the Polson, Scott & Windle (2013) sampler
(tulpa::rpg_vec()), the kernel every Polya-Gamma Gibbs fitter in this
package draws its auxiliary weights from. Exposed as a door for consumer
packages fitting their own Polya-Gamma Gibbs models, which cannot reach an
RNG through LinkingTo the way a likelihood kernel would.
tulpa_rpg(b, z)tulpa_rpg(b, z)
b |
Integer vector of PG shape parameters (trial counts); one draw per element. |
z |
Numeric vector of tilting parameters, the same length as |
A numeric vector of PG(b, z) draws, the same length as b.
tulpa_rpg(rep(1L, 5), rnorm(5))tulpa_rpg(rep(1L, 5), rnorm(5))
Generic simulator that dispatches through a tulpa_family's simulate_fn.
Given a model spec (formula + family + data) and a parameter vector or a
fitted model, generate one or more synthetic response datasets.
Used internally by prior_predict() and exposed for posterior predictive
checks, simulation-based calibration, and what-if analyses with fixed
parameters.
tulpa_simulate( formula, family, data, theta = NULL, n_sims = 1L, priors = NULL, seed = NULL, ... ) ## S3 method for class 'tulpa_simulate' print(x, ...)tulpa_simulate( formula, family, data, theta = NULL, n_sims = 1L, priors = NULL, seed = NULL, ... ) ## S3 method for class 'tulpa_simulate' print(x, ...)
formula |
A model formula, or list of formulas keyed by process name. |
family |
A |
data |
Data frame with covariates and grouping factors. The response column may be absent or NA; it is not read. |
theta |
One of:
|
n_sims |
Number of simulated datasets. When |
priors |
Used only if |
seed |
Optional integer seed. |
... |
Passed to |
x |
A |
A tulpa_simulate object: list with y (length-n_sims list of
simulated responses), theta (parameters used per sim), linpred,
family, n_sims, n_obs.
fam <- tulpa_family( name = "gaussian", simulate_fn = function(eta, params, n_obs, ...) { rnorm(n_obs, eta[[1]], params$sigma_y) }, extra_params = list(sigma_y = prior_half_normal(1)) ) df <- data.frame(y = rep(0, 20), x = rnorm(20)) theta <- list( beta = list(y = c(0.5, 1.0)), u = list(y = list()), extras = list(sigma_y = 0.5) ) sim <- tulpa_simulate(y ~ x, fam, df, theta = theta, n_sims = 3, seed = 1) length(sim$y) # 3fam <- tulpa_family( name = "gaussian", simulate_fn = function(eta, params, n_obs, ...) { rnorm(n_obs, eta[[1]], params$sigma_y) }, extra_params = list(sigma_y = prior_half_normal(1)) ) df <- data.frame(y = rep(0, 20), x = rnorm(20)) theta <- list( beta = list(y = c(0.5, 1.0)), u = list(y = list()), extras = list(sigma_y = 0.5) ) sim <- tulpa_simulate(y ~ x, fam, df, theta = theta, n_sims = 3, seed = 1) length(sim$y) # 3
Functions to specify spatial random effects for tulpa models. Spatial effects are shared between processes by default, which helps prevent bias from spatially-structured unmeasured confounders.
The spatial constructors documented in this family
(spatial_car(), spatial_bym2(), spatial_gp(), and the others) each
return a tulpa_spatial (or related) specification object to pass to the
spatial argument of tulpa().
The SPDE field's hyperprior on (range, sigma), evaluated as a density on
(log range, log sigma) – the coordinates every SPDE outer integrator
works in (grid, CCD and k-hat alike, gcol33/tulpa#731). Intended for a
caller running its own outer-grid loop over (range, sigma) around a
coupled model tulpa's own SPDE front doors do not fit directly.
tulpa_spde_log_hyperprior(range, sigma, sp, hyperprior = "proper")tulpa_spde_log_hyperprior(range, sigma, sp, hyperprior = "proper")
range, sigma
|
Numeric vectors of range / marginal-SD nodes (natural scale, recycled against each other). |
sp |
A |
hyperprior |
|
Numeric vector of log-density values, one per (range, sigma)
pair.
tulpa_spde_precision_Q(), spatial_spde()
n <- 6L G <- Matrix::bandSparse(n, k = c(-1, 0, 1), diagonals = list(rep(-1, n - 1), c(1, rep(2, n - 2), 1), rep(-1, n - 1))) sp <- spatial_spde_custom(Matrix::Diagonal(n), G, Matrix::Diagonal(n), prior_range = c(1, 0.5), prior_sigma = c(1, 0.01)) tulpa_spde_log_hyperprior(range = c(0.5, 1, 2), sigma = 1, sp = sp)n <- 6L G <- Matrix::bandSparse(n, k = c(-1, 0, 1), diagonals = list(rep(-1, n - 1), c(1, rep(2, n - 2), 1), rep(-1, n - 1))) sp <- spatial_spde_custom(Matrix::Diagonal(n), G, Matrix::Diagonal(n), prior_range = c(1, 0.5), prior_sigma = c(1, 0.01)) tulpa_spde_log_hyperprior(range = c(0.5, 1, 2), sigma = 1, sp = sp)
Rebuilds the sparse precision matrix Q(kappa, tau) of an SPDE/Matern
field from its mesh geometry and a (kappa, tau) hyperparameter pair,
mirroring the compiled builder (src/spde_qbuilder.h) that
tulpa_nuts_spde() and fit_spde() use internally. Intended for a caller
running its own outer-grid loop over (range, sigma) around a coupled
model tulpa's own SPDE front doors (fit_spde(), spatial_spde()) do not
fit directly – e.g. a per-species community layer sharing one spatial
field.
tulpa_spde_precision_Q(spatial, kappa, tau_spde)tulpa_spde_precision_Q(spatial, kappa, tau_spde)
spatial |
A |
kappa |
Spatial frequency |
tau_spde |
SPDE precision scale (see |
A sparse n_mesh x n_mesh precision matrix.
tulpa_spde_log_hyperprior(), fit_spde(), spatial_spde()
n <- 6L G <- Matrix::bandSparse(n, k = c(-1, 0, 1), diagonals = list(rep(-1, n - 1), c(1, rep(2, n - 2), 1), rep(-1, n - 1))) sp <- spatial_spde_custom(Matrix::Diagonal(n), G, Matrix::Diagonal(n), prior_range = c(1, 0.5), prior_sigma = c(1, 0.01)) Q <- tulpa_spde_precision_Q(sp, kappa = sqrt(8) / 1, tau_spde = 1) dim(Q)n <- 6L G <- Matrix::bandSparse(n, k = c(-1, 0, 1), diagonals = list(rep(-1, n - 1), c(1, rep(2, n - 2), 1), rep(-1, n - 1))) sp <- spatial_spde_custom(Matrix::Diagonal(n), G, Matrix::Diagonal(n), prior_range = c(1, 0.5), prior_sigma = c(1, 0.01)) Q <- tulpa_spde_precision_Q(sp, kappa = sqrt(8) / 1, tau_spde = 1) dim(Q)
Functions to specify temporal random effects for tulpa models. Temporal effects are shared between processes by default, which helps prevent bias from temporally-structured unmeasured confounders.
The temporal constructors documented in this family
(temporal_rw1(), temporal_rw2(), temporal_ar1(), and the others) each
return a tulpa_temporal specification object to pass to the temporal
argument of tulpa().
One front door for inference over a user-defined tgmrf() latent block's
hyperparameter vector theta. mode selects the inference engine, all of
which share the same Laplace body for (beta, z) | theta:
"imh" (default) – Tier-1 exact MCMC: the Laplace body plus an
independence-Metropolis bias correction over theta (the
"Laplace + MH debias" composition).
"nuts" – Tier-1 exact MCMC: NUTS over the marginal theta posterior.
"vi" – Tier-2 structured: a single-path Pathfinder Gaussian fit of the
theta posterior (no bias correction).
"nuts_joint" – Tier-1 exact MCMC sampling the FULL joint
(beta, z, theta) rather than the Laplace-marginalized theta (requires a
C++-backend block, see tgmrf_cpp()).
The inference method is an argument, not a parallel verb.
tulpa_tgmrf( y, n_trials, X, block, family = "binomial", phi = 1, re_idx = NULL, n_re_groups = 0L, sigma_re = 1, mode = c("imh", "nuts", "vi", "nuts_joint"), control = list(), ... )tulpa_tgmrf( y, n_trials, X, block, family = "binomial", phi = 1, re_idx = NULL, n_re_groups = 0L, sigma_re = 1, mode = c("imh", "nuts", "vi", "nuts_joint"), control = list(), ... )
y, n_trials, X
|
Response, binomial trial counts (or |
block |
A |
family, phi
|
Observation family and its dispersion. |
re_idx, n_re_groups, sigma_re
|
Optional scalar random-intercept structure. |
mode |
Inference engine: |
control |
A list of numerical / tuning knobs for the chosen |
... |
Individual |
A tulpa_fit object; $backend and $mode record
the engine used.
tgmrf() for the block, tgmrf_cpp() for the compiled-block form.
# An iid random intercept written as a tgmrf block over 15 groups; the # inference method is an argument, not a parallel verb. See # vignette("tgmrf"). set.seed(3) J <- 15L; g <- rep(seq_len(J), each = 6L); n <- length(g); x <- rnorm(n) y <- rpois(n, exp(0.3 + 0.5 * x + rnorm(J, 0, 0.6)[g])) blk <- tgmrf( Q = function(theta) Matrix::Diagonal(J, exp(-2 * theta[1])), prior = function(theta) stats::dnorm(theta[1], 0, 1, log = TRUE), init = c(log_sigma = 0), obs_idx = g) fit <- tulpa_tgmrf(y, rep(1L, n), cbind(1, x), block = blk, family = "poisson", mode = "imh", control = list(n_iter = 300L, warmup = 150L)) mean(exp(fit$draws[, "log_sigma"])) # posterior mean group SD# An iid random intercept written as a tgmrf block over 15 groups; the # inference method is an argument, not a parallel verb. See # vignette("tgmrf"). set.seed(3) J <- 15L; g <- rep(seq_len(J), each = 6L); n <- length(g); x <- rnorm(n) y <- rpois(n, exp(0.3 + 0.5 * x + rnorm(J, 0, 0.6)[g])) blk <- tgmrf( Q = function(theta) Matrix::Diagonal(J, exp(-2 * theta[1])), prior = function(theta) stats::dnorm(theta[1], 0, 1, log = TRUE), init = c(log_sigma = 0), obs_idx = g) fit <- tulpa_tgmrf(y, rep(1L, n), cbind(1, x), block = blk, family = "poisson", mode = "imh", control = list(n_iter = 300L, warmup = 150L)) mean(exp(fit$draws[, "log_sigma"])) # posterior mean group SD
Coerces a nested-Laplace fit's theta_grid to matrix form with named axis
columns. A single-axis grid is stored on the fit as a bare vector named by
theta_names; every downstream reader that keys an axis by name needs the
named matrix form, so this is the one place that coercion happens.
Intended for reconstructing a fit's outer-grid posterior cell weights (see
tulpa_grid_log_quad(), tulpa_normalise_weights_safe()) for a fit or
path whose driver doesn't already carry fit$log_quad.
tulpa_theta_matrix(res)tulpa_theta_matrix(res)
res |
A |
res$theta_grid as an [n_cells x n_axes] matrix with axis names
as column names, or NULL if the fit has no grid.
tulpa_grid_log_quad(), tulpa_normalise_weights_safe()
set.seed(1) S <- 30L # spatial units in a chain nb <- lapply(seq_len(S), function(s) setdiff(c(s - 1L, s + 1L), c(0L, S + 1L))) nn <- lengths(nb) field <- as.numeric(scale(cumsum(rnorm(S, 0, 0.4)))) idx <- rep(seq_len(S), each = 6L); n <- length(idx); x <- rnorm(n) y <- rbinom(n, 1L, plogis(-0.2 + 0.6 * x + field[idx])) prior <- list(type = "icar", n_spatial_units = S, spatial_idx = idx, adj_row_ptr = c(0L, cumsum(nn)), adj_col_idx = unlist(nb) - 1L, n_neighbors = nn, tau_grid = c(0.5, 1, 2, 4, 8)) fit <- tulpa_nested_laplace(y, rep(1L, n), cbind(1, x), prior = prior, family = "binomial", control = list(progress = FALSE)) tulpa_theta_matrix(fit)set.seed(1) S <- 30L # spatial units in a chain nb <- lapply(seq_len(S), function(s) setdiff(c(s - 1L, s + 1L), c(0L, S + 1L))) nn <- lengths(nb) field <- as.numeric(scale(cumsum(rnorm(S, 0, 0.4)))) idx <- rep(seq_len(S), each = 6L); n <- length(idx); x <- rnorm(n) y <- rbinom(n, 1L, plogis(-0.2 + 0.6 * x + field[idx])) prior <- list(type = "icar", n_spatial_units = S, spatial_idx = idx, adj_row_ptr = c(0L, cumsum(nn)), adj_col_idx = unlist(nb) - 1L, n_neighbors = nn, tau_grid = c(0.5, 1, 2, 4, 8)) fit <- tulpa_nested_laplace(y, rep(1L, n), cbind(1, x), prior = prior, family = "binomial", control = list(progress = FALSE)) tulpa_theta_matrix(fit)
Visual and numerical checks comparing observed data to posterior predictive distributions. Essential for assessing model fit.
The functions documented in this family return a ggplot object
(pp_check()) or a tulpa_prior_predict object (prior_predict()); see
each function's own help page.
Computes the empirical semivariogram in distance bins for visual assessment of remaining spatial structure in residuals.
tulpa_variogram( object, coords, n_bins = 15L, max_dist = NULL, resid_type = "pearson" ) ## S3 method for class 'tulpa_variogram' plot(x, ...) ## S3 method for class 'tulpa_variogram' print(x, ...)tulpa_variogram( object, coords, n_bins = 15L, max_dist = NULL, resid_type = "pearson" ) ## S3 method for class 'tulpa_variogram' plot(x, ...) ## S3 method for class 'tulpa_variogram' print(x, ...)
object |
A fitted model, or a numeric vector of residuals |
coords |
N x 2 coordinate matrix (required) |
n_bins |
Number of distance bins (default 15) |
max_dist |
Maximum distance (default: half the maximum pairwise distance) |
resid_type |
Residual type if extracting from model (default |
x |
A |
... |
Passed to |
A tulpa_variogram data.frame with columns dist, gamma, n_pairs
plot() draws the semivariance against distance, point size scaled
by the pair count, and returns x invisibly.
set.seed(1) coords <- cbind(runif(60), runif(60)) tulpa_variogram(rnorm(60), coords, n_bins = 5)set.seed(1) coords <- cbind(runif(60), runif(60)) tulpa_variogram(rnorm(60), coords, n_bins = 5)
Extract posterior distributions of temporally-varying coefficients (TVCs) from a fitted tulpa model with TVC specification.
tvc(object, terms = NULL, summary = FALSE, probs = c(0.025, 0.5, 0.975), ...) ## S3 method for class 'tulpa_fit' tvc(object, terms = NULL, summary = FALSE, probs = c(0.025, 0.5, 0.975), ...) ## S3 method for class 'tulpa_tvc_posterior' print(x, ...) ## S3 method for class 'tulpa_tvc_summary' print(x, ...) ## S3 method for class 'tulpa_tvc_posterior' summary(object, probs = c(0.025, 0.5, 0.975), ...)tvc(object, terms = NULL, summary = FALSE, probs = c(0.025, 0.5, 0.975), ...) ## S3 method for class 'tulpa_fit' tvc(object, terms = NULL, summary = FALSE, probs = c(0.025, 0.5, 0.975), ...) ## S3 method for class 'tulpa_tvc_posterior' print(x, ...) ## S3 method for class 'tulpa_tvc_summary' print(x, ...) ## S3 method for class 'tulpa_tvc_posterior' summary(object, probs = c(0.025, 0.5, 0.975), ...)
object |
A |
terms |
Which TVC terms to extract. If NULL (default), extracts all. |
summary |
Logical; if TRUE, return summary statistics instead of full posterior draws. |
probs |
Quantiles to compute if |
... |
Ignored |
x |
A |
A tulpa_tvc_posterior object containing:
draws: Array of posterior draws (draws x times x terms)
time_levels: Time point labels
term_names: Names of TVC terms
summary() on a tulpa_tvc_posterior returns a tulpa_tvc_summary
data frame with one row per time point and term, holding the posterior
mean, SD and requested quantiles of each temporally varying coefficient.
temporal_tvc(), plot.tulpa_tvc_posterior()
set.seed(160) n_t <- 10L; reps <- 5L walk <- cumsum(rnorm(n_t, 0, 0.35)); walk <- walk - mean(walk) year <- rep(seq_len(n_t), each = reps) df <- data.frame(year = year, x = rnorm(length(year))) df$count <- rpois(nrow(df), exp(0.3 + (0.5 + walk[year]) * df$x)) # The slope on `x` walks in time; TVC is exact-mode only. fit <- tulpa( count ~ x, data = df, family = "poisson", temporal = temporal_tvc("year", terms = ~ x - 1, structure = "rw1"), mode = "exact", control = list(n_iter = 200L, n_warmup = 100L, seed = 1L) ) tvc_post <- tvc(fit) summary(tvc_post) plot(tvc_post, "x")set.seed(160) n_t <- 10L; reps <- 5L walk <- cumsum(rnorm(n_t, 0, 0.35)); walk <- walk - mean(walk) year <- rep(seq_len(n_t), each = reps) df <- data.frame(year = year, x = rnorm(length(year))) df$count <- rpois(nrow(df), exp(0.3 + (0.5 + walk[year]) * df$x)) # The slope on `x` walks in time; TVC is exact-mode only. fit <- tulpa( count ~ x, data = df, family = "poisson", temporal = temporal_tvc("year", terms = ~ x - 1, structure = "rw1"), mode = "exact", control = list(n_iter = 200L, n_warmup = 100L, seed = 1L) ) tvc_post <- tvc(fit) summary(tvc_post) plot(tvc_post, "x")
Fills in the neighbor structure (spatial_gp()) or structures
(spatial_multiscale()) that tulpa_gibbs() and tulpa_laplace() need
from a spatial = spec, since those design-matrix doors take no data
argument to validate one themselves. tulpa() calls this internally, so it
only needs to be called directly when fitting through a design-matrix door.
validate_gp(gp, data)validate_gp(gp, data)
gp |
A |
data |
Data frame containing the coordinate columns named in |
Updated spatial object with computed neighbor structure.
validate_temporal_multiscale(), spatial_gp(),
spatial_multiscale()
set.seed(1) d <- data.frame(lon = runif(30), lat = runif(30)) gp <- validate_gp(spatial_gp(~ lon + lat, nn = 5), d) gp$n_spatialset.seed(1) d <- data.frame(lon = runif(30), lat = runif(30)) gp <- validate_gp(spatial_gp(~ lon + lat, nn = 5), d) gp$n_spatial
Ensures the fit object was created with the expected inference mode. Useful for enforcing mode requirements in downstream analysis.
A fit matches a mode when the mode names either the backend that ran
(fit$backend, e.g. "laplace", "mala", "re_cov_gibbs") or the tier it
ran in (fit$inference_mode: "exact", "structured", "optimized"). So a
fit made with mode = "laplace" validates against both "laplace" and
"structured", and a fit made with mode = "auto" validates against the
backend and the tier auto chose. A request that tulpa() overrode (see
fit$mode_overridden) validates against what ran, not what was asked for.
validate_mode(fit, expected_mode, error = TRUE)validate_mode(fit, expected_mode, error = TRUE)
fit |
A tulpa_fit object |
expected_mode |
Mode(s) that should have been used: a character vector of backend and / or tier names, any one of which is accepted. |
error |
If TRUE (default), error on mismatch. If FALSE, return logical. |
If error = FALSE, returns TRUE/FALSE. Otherwise errors on mismatch.
set.seed(1) d <- data.frame(x = rnorm(60)) d$y <- rpois(60, exp(0.5 + 0.3 * d$x)) fit <- tulpa(y ~ x, data = d, family = "poisson", mode = "hmc", control = list(n_iter = 400L, warmup = 200L, n_chains = 2L, seed = 1L)) validate_mode(fit, "exact") # the tier hmc runs in validate_mode(fit, "structured", error = FALSE) # FALSEset.seed(1) d <- data.frame(x = rnorm(60)) d$y <- rpois(60, exp(0.5 + 0.3 * d$x)) fit <- tulpa(y ~ x, data = d, family = "poisson", mode = "hmc", control = list(n_iter = 400L, warmup = 200L, n_chains = 2L, seed = 1L)) validate_mode(fit, "exact") # the tier hmc runs in validate_mode(fit, "structured", error = FALSE) # FALSE
Fills in the time index and point count that tulpa_gibbs() needs from a
temporal = spec built with temporal_multiscale(), since that
design-matrix door takes no data argument to validate one itself.
tulpa() calls this internally, so it only needs to be called directly
when fitting through a design-matrix door.
validate_temporal_multiscale(temporal, data)validate_temporal_multiscale(temporal, data)
temporal |
A |
data |
Data frame containing the time column named in |
Updated object with indices computed.
validate_gp(), temporal_multiscale()
tm <- temporal_multiscale("t", trend = "rw1", seasonal = 12) tm <- validate_temporal_multiscale(tm, data.frame(t = 1:36)) class(tm)tm <- temporal_multiscale("t", trend = "rw1", seasonal = 12) tm <- validate_temporal_multiscale(tm, data.frame(t = 1:36)) class(tm)
Summarize the random-effect covariance of a fit: the standard deviation of each random-effect coefficient, the correlations between them within a term, and the covariance matrix itself.
Whether the covariance was estimated, sampled, or merely conditioned on is
reported alongside it, because the three are different claims. A fit from
mode = "laplace" conditions on sigma_re, so its values are the ones
supplied; mode = "eb" estimates them; a sampler tier integrates them.
VarCorr(x, sigma = 1, ...) ## S3 method for class 'tulpa_fit' VarCorr(x, sigma = 1, ...)VarCorr(x, sigma = 1, ...) ## S3 method for class 'tulpa_fit' VarCorr(x, sigma = 1, ...)
x |
A |
sigma |
Ignored, for compatibility with the |
... |
Ignored. |
A data frame with one row per random-effect coefficient: term,
coef, sd, and source (one of "estimated", "sampled",
"conditioned"). A gaussian fit adds a final term = "Residual" row
(coef NA) with the residual SD sqrt(phi), labelled "estimated"
under estimate_phi = TRUE and "conditioned" otherwise; it has no entry
in "cov". Correlated terms additionally carry the covariance
matrices in the "cov" attribute, one per term, each with a "correlation"
attribute. Returns an empty data frame when the fit has no random effects.
ranef() for the per-level deviations, tulpa_eb() to estimate the
covariance rather than condition on it.
set.seed(1) G <- 30L; per <- 10L; n <- G * per grp <- rep(seq_len(G), each = per); x <- rnorm(n) b <- rnorm(G, 0, 0.8) d <- data.frame(y = rpois(n, exp(0.3 + 0.5 * x + b[grp])), x = x, g = factor(grp)) fit <- tulpa(y ~ x + (1 | g), data = d, family = "poisson", mode = "eb") VarCorr(fit)set.seed(1) G <- 30L; per <- 10L; n <- G * per grp <- rep(seq_len(G), each = per); x <- rnorm(n) b <- rnorm(G, 0, 0.8) d <- data.frame(y = rpois(n, exp(0.3 + 0.5 * x + b[grp])), x = x, g = factor(grp)) fit <- tulpa(y ~ x + (1 | g), data = d, family = "poisson", mode = "eb") VarCorr(fit)
Variance-covariance matrix of the fixed effects
## S3 method for class 'tulpa_fit' vcov(object, ...)## S3 method for class 'tulpa_fit' vcov(object, ...)
object |
A |
... |
Ignored. |
Fixed-effect variance-covariance matrix (empirical for sampler tiers,
H_beta^-1 for the Laplace tier, the stated covariance on a fit whose
reported posterior is a closed-form Gaussian).
Watanabe's (2013) WBIC: the expected negative log-likelihood under the
posterior tempered at inverse temperature beta = 1 / log(n),
It estimates the Bayes free energy -log p(y), the negative log evidence,
and shares its asymptotic expansion up to O_p(1) terms in singular models
as well as regular ones – mixtures, reduced-rank and latent-factor
structure, a variance component at zero – where BIC's (d / 2) log n
penalty does not describe the evidence. Lower is better.
wbic(object, ...) ## Default S3 method: wbic(object, ...) ## S3 method for class 'tulpa_fit' wbic(object, ...) ## S3 method for class 'tulpa_wbic' print(x, digits = 2, ...)wbic(object, ...) ## Default S3 method: wbic(object, ...) ## S3 method for class 'tulpa_fit' wbic(object, ...) ## S3 method for class 'tulpa_wbic' print(x, digits = 2, ...)
object |
A |
... |
Unused. |
x |
A |
digits |
Decimal places printed. |
No draw from the posterior itself can supply the expectation: it is taken
under a different distribution, and importance-reweighting posterior draws
down to beta degenerates as n grows. A tulpa_fit therefore carries the
tempered draws only when its sampler visited that distribution, which the
SMC path does when asked: fit with mode = "smc" and
control = list(wbic = TRUE). The population is then carried from the
reference through p(theta) L(theta)^beta, recorded there, and tempered on
to the posterior, so the fit's own $draws are posterior draws as usual.
A fit without the tempered draws is refused.
The log-likelihood is the normalized pointwise density pointwise_loglik()
returns, the input of loo::waic() and loo::loo() too, so WBIC is on the
scale of -log p(y) and comparable across fits of the same response. The
parameter vector tempered is the one the sampler samples – fixed effects,
latent effects and hyperparameters – with the likelihood conditional on
it, so the free energy estimated is that of the marginal likelihood
integrating all of them.
The default method takes an [n_draws x n_obs] pointwise log-likelihood
whose rows are draws from the posterior tempered at 1 / log(n_obs). A
model package with its own likelihood draws them through the tulpa_smc_fit
C entry with bridge_end = 1 / log(n) and evaluates its density there.
A tulpa_wbic object: a list with wbic, the inverse temperature
beta, n_obs and n_draws.
Watanabe, S. (2013). A widely applicable Bayesian information criterion. Journal of Machine Learning Research 14:867-897.
loo::waic(), dic() and compare_models() for the other
criteria; tulpa_criteria().
set.seed(1) d <- data.frame(x = rnorm(80)) d$y <- rpois(80, exp(0.3 + 0.5 * d$x)) fit <- tulpa(y ~ x, data = d, family = "poisson", mode = "smc", control = list(wbic = TRUE, n_particles = 500L, seed = 1L)) wbic(fit)set.seed(1) d <- data.frame(x = rnorm(80)) d$y <- rpois(80, exp(0.3 + 0.5 * d$x)) fit <- tulpa(y ~ x, data = d, family = "poisson", mode = "smc", control = list(wbic = TRUE, n_particles = 500L, seed = 1L)) wbic(fit)
Evaluate expr with the integrator set to name, then restore whatever was
selected before – on an error as well as on success. The integrator
selection is process-global, so a bare tulpa_integrator() call leaves every
later fit in the session on the new scheme, and an error between setting and
restoring leaves it there permanently.
with_tulpa_integrator(name, expr, mts_substeps = 4L)with_tulpa_integrator(name, expr, mts_substeps = 4L)
name |
Integrator name, as for |
expr |
Expression to evaluate. Evaluated in the caller's environment. |
mts_substeps |
Inner prior-force substeps for |
The value of expr.
with_tulpa_integrator("yoshida4", tulpa_integrator()) tulpa_integrator() # unchangedwith_tulpa_integrator("yoshida4", tulpa_integrator()) tulpa_integrator() # unchanged