| Title: | Templated Unified Library for Posterior Approximation in Bayesian Hierarchical Models |
| Version: | 0.7.5 |
| 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. |
| License: | MIT + file LICENSE |
| Encoding: | UTF-8 |
| Language: | en-US |
| SystemRequirements: | C++17 |
| Depends: | R (≥ 4.1.0) |
| Imports: | Rcpp (≥ 1.0.12), Matrix, tulpaMesh (≥ 0.1.3), generics, stats, tools, utils, methods, graphics, grDevices |
| Suggests: | testthat (≥ 3.0.0), bayesplot, coda, ggplot2, sf, terra, knitr, rmarkdown, posterior (≥ 1.5.0), loo (≥ 2.7.0), rstantools, lme4, nlme, lmtest, numDeriv, spdep, MASS, betareg, glmmTMB, pscl, tweedie, fmesher, patchwork, stars, statmod, withr |
| LinkingTo: | Rcpp, RcppEigen, Matrix |
| Config/testthat/edition: | 3 |
| VignetteBuilder: | knitr |
| URL: | https://github.com/gcol33/tulpa, https://gillescolling.com/tulpa/ |
| BugReports: | https://github.com/gcol33/tulpa/issues |
| Config/roxygen2/version: | 8.1.0 |
| NeedsCompilation: | yes |
| Packaged: | 2026-10-19 14:14:50 UTC; Gilles Colling |
| Author: | Gilles Colling |
| Maintainer: | Gilles Colling <gilles.colling051@gmail.com> |
| Repository: | CRAN |
| Date/Publication: | 2026-10-19 16:00:02 UTC |
tulpa: 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.
Author(s)
Maintainer: Gilles Colling gilles.colling051@gmail.com (ORCID) [copyright holder]
Authors:
Gilles Colling gilles.colling051@gmail.com (ORCID) [copyright holder]
Other contributors:
Frances Y. Kuo (Sobol direction numbers in src/sobol_direction_numbers.h, BSD-3-clause) [contributor, copyright holder]
Stephen Joe (Sobol direction numbers in src/sobol_direction_numbers.h, BSD-3-clause) [contributor, copyright holder]
Arm Limited (exp and log routines in src/fastmath.h and src/fastmath_data.cpp, MIT) [copyright holder]
The Walnutpie Developers (WALNUTS transition in inst/include/tulpa/walnuts.h, ported from walnutpie, MIT) [copyright holder]
See Also
Useful links:
Report bugs at https://github.com/gcol33/tulpa/issues
Information criteria on a tulpa fit
Description
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".
Usage
## S3 method for class 'tulpa_fit'
AIC(object, ..., k = 2)
## S3 method for class 'tulpa_fit'
BIC(object, ...)
Arguments
object |
A |
... |
Further fits. |
k |
Penalty per parameter. |
Value
As stats::AIC() / stats::BIC() for maximised log-likelihoods;
errors otherwise.
Random-effect variances and correlations
Description
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.
Usage
VarCorr(x, sigma = 1, ...)
## S3 method for class 'tulpa_fit'
VarCorr(x, sigma = 1, ...)
Arguments
x |
A |
sigma |
Ignored, for compatibility with the |
... |
Ignored. |
Value
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.
See Also
ranef() for the per-level deviations, tulpa_eb() to estimate the
covariance rather than condition on it.
Examples
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)
Construct a spatial adjacency graph for areal models
Description
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:
- a
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.
- an
sfobject polygon contiguity from shared boundaries (queen = share a point or edge; rook = share an edge). Requires sf.
- a
SpatRaster(terra) orstarsobject raster 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().
Usage
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, ...)
Arguments
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 |
Value
A tulpa_adjacency object: a list with
adjacencythe
[n x n]symmetric sparse adjacency matrix (dgCMatrix, 0/1, zero diagonal) to pass asgraph/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, orNAfor polygons.typethe contiguity rule used.
See Also
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.
Examples
# 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
Adaptive Gauss-Hermite quadrature for one-RE GLMMs
Description
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.
Usage
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
)
Arguments
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 |
Value
A list with class tulpa_fit carrying:
-
means: named vectorc(beta, log_sigma_re). -
mode: same asmeans(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
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.
References
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.
See Also
tulpa_laplace() for the joint-mode Laplace path
(handles multi-RE and spatial structure).
Examples
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)
Posterior draws in the posterior package's format
Description
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.
Usage
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, ...)
Arguments
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. |
Details
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.
Value
A posterior draws object of the requested shape.
See Also
tulpa_draws_array() for the base R array without the dependency,
posterior_sample() for the raw matrix.
Examples
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))
}
Mark an outer-grid setting as a default rather than a pin
Description
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
priorblock (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'slogchol_grid,tgmrf'stheta_grid_built);an entry of
tulpa_nested_laplace_joint()'sphi_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()'sn_grid_spatial,tau_upper, ...);a
prior_sigmahyperprior 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.
Usage
auto_grid(x, place = TRUE)
Arguments
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? |
Value
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.
Declaring a default the engine must integrate as written
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.
See Also
is_auto_grid(), auto_grid_place(),
tulpa_nested_laplace_joint(), fit_st_nested()
Examples
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))
May the placement pass move a marked outer-grid axis?
Description
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.
Usage
auto_grid_place(x)
Arguments
x |
Any object. |
Value
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.
See Also
Examples
auto_grid_place(auto_grid(c(0.5, 1, 2)))
auto_grid_place(auto_grid(c(0.5, 1, 2), place = FALSE))
Bayesian R-squared
Description
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).
Usage
bayes_R2(object, ...)
## S3 method for class 'tulpa_fit'
bayes_R2(
object,
ndraws = NULL,
summary = TRUE,
probs = c(0.025, 0.975),
seed = NULL,
...
)
Arguments
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. |
Value
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.
References
Gelman, Goodrich, Gabry & Vehtari (2019). R-squared for Bayesian regression models. The American Statistician 73(3):307-309.
Examples
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)
Bridge sampling for marginal likelihood
Description
Estimate the log marginal likelihood \log Z = \log p(y) 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.
Usage
bridge_sampling(
draws,
log_posterior,
n_proposal = nrow(draws),
max_iter = 1000L,
tol = 1e-10,
split = TRUE,
verbose = FALSE
)
Arguments
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 |
Value
A list with:
-
log_marginal: the bridge-sampling estimate of\log Z. -
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 proposallist(mean, cov).
Tier
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.
References
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.
Examples
# 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)
Observation-level accessors for categorical fits
Description
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 deviationsqrt(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 predictorx' 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.
Usage
## 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, ...)
Arguments
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). |
Value
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.
See Also
tulpa_multinomial(), tulpa_ordinal()
Central composite design (CCD) grid for nested-Laplace integration
Description
Produces a structured set of standardised hyperparameter points
z \in \mathbb{R}^k for use as integration nodes in a nested
Laplace approximation when k \ge 3. CCD scales much better than
the full tensor expand.grid() used by the 1D and 2D backends:
a CCD has 1 + 2k + 2^{k - q} points (1 centre, 2k axial,
2^{k - q} factorial), versus m^k for an m-per-axis
tensor product.
Point layout (with centre+axial+factorial scaling f_0):
1 centre point at the origin;
2k axial points at
\pm f_0along each coordinate axis;-
2^{k - q}factorial points at corners of the hypercube, scaled to lie on a sphere of radiusf_0.
For k \le 6 the factorial portion is the full 2^k
design. For k \ge 7 a half-fraction (q = 1) using the
defining word x_1 \cdots x_k keeps the point count reasonable
while preserving Resolution V.
Used by tulpa_nested_laplace() for higher-dimensional hyperparameter
blocks. The standardised z-coordinates are mapped to physical
hyperparameters \theta via ccd_to_theta().
Usage
ccd_grid(k, f_0 = sqrt(k))
Arguments
k |
Number of hyperparameters (length of theta). Must be >= 1. |
f_0 |
Radius of the design sphere (default |
Value
A list with components:
-
z: numeric matrix[n_points x k]of standardised hyperparameter coordinates. -
n_points: integer; total grid size. -
kind: character vector labelling each point as"center","axial", or"factorial". -
f_0: the sphere radius used.
See Also
ccd_to_theta() to map z-coordinates to physical theta.
Examples
g <- ccd_grid(3)
g$n_points
table(g$kind)
Map standardised CCD coordinates to physical hyperparameters
Description
Converts standardised CCD z-coordinates (in \mathbb{R}^k) to
physical hyperparameters \theta via the affine map
\theta = \hat\theta + L \cdot z with optional log-scale
transform per component. L is typically a Cholesky factor of the
negative Hessian inverse evaluated at the (working) mode \hat\theta
– i.e. it scales z to one posterior standard deviation per axis.
Usage
ccd_to_theta(z, theta_hat, L, log_scale = FALSE)
Arguments
z |
Matrix |
theta_hat |
Numeric vector of length |
L |
Numeric |
log_scale |
Logical (or logical vector of length k). If |
Value
Numeric matrix [n_points x k] of physical theta-values.
Examples
g <- ccd_grid(2)
ccd_to_theta(g$z, theta_hat = c(0, 1), L = diag(c(0.5, 0.2)))
Corrected R-INLA CCD integration weights
Description
Per-point design weights for a ccd_grid() used as nested-Laplace integration
nodes. Implements the corrected R-INLA central-composite-design weights: the
formula in Rue, Martino and Chopin (2009) has a typo, corrected by the R-INLA
team to give a positive central weight. With m hyperparameters, np design
points and standardized scaling f0, every non-centre point gets
w = 1 / ((np - 1)(1 + e^{-m f0^2 / 2}(f0^2 - 1)))
and the centre point gets w_0 = 1 - (np - 1) w. Used as
\Delta_k: the integration weight of node k is
\Delta_k \exp(\log\,\mathrm{marginal}_k), renormalized. On a standardized
(whitened) hyperparameter posterior these reproduce the Gaussian moments
exactly.
Usage
ccd_weights(ccd)
Arguments
ccd |
A |
Value
Numeric vector of length ccd$n_points: the design weight for each
point (the centre weight w0 for the central point, w otherwise).
See Also
Examples
ccd_weights(ccd_grid(3))
Validate a spatial adjacency matrix
Description
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.
Usage
check_adjacency(adjacency, ids = NULL)
## S3 method for class 'tulpa_adjacency_check'
print(x, ...)
Arguments
adjacency |
A matrix (dense or sparse |
ids |
Optional cell identifiers; if supplied, their length must match
|
x |
A |
... |
Ignored. |
Value
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.
See Also
adjacency() to construct a graph, node_index().
Examples
adj <- matrix(0, 4, 4)
for (i in 1:3) adj[i, i + 1] <- adj[i + 1, i] <- 1
check_adjacency(adj)
Quick convergence check
Description
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.
Usage
check_diagnostics(
fit,
rhat_threshold = 1.01,
ess_threshold = 400,
quiet = FALSE
)
Arguments
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). |
Value
Invisibly, TRUE if all checks pass, FALSE if any fail, or NA
for a non-chain (approximation) fit where Rhat/ESS do not apply.
See Also
diagnostic_summary(), diagnostics(), n_divergent()
Examples
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)
Diagnostic panel plot
Description
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)
Usage
check_model(object, ...)
## Default S3 method:
check_model(object, coords = NULL, nsim = 250L, seed = 123L, ...)
Arguments
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) |
Value
Invisible list with ks_p, disp_ratio, moran (if spatial)
Examples
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
Description
Fixed-effect coefficients
Usage
## S3 method for class 'tulpa_fit'
coef(object, ...)
Arguments
object |
A |
... |
Ignored. |
Value
Named numeric vector of fixed-effect posterior means (the Laplace
mode for the Laplace tier). Random effects come from ranef().
Compare models by information criteria
Description
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.
Usage
compare_models(..., criterion = c("waic", "loo", "wbic", "loglik"))
Arguments
... |
Named |
criterion |
|
Value
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).
See Also
model_average() for model-averaged predictions, tulpa_criteria()
and tulpa_psis() for the native criteria layer.
Examples
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")
Compute nearest neighbors for NNGP
Description
Compute the k nearest neighbors for each observation using Euclidean distance. Returns in a format suitable for the NNGP likelihood.
Usage
compute_nngp_neighbors(coords, k)
Arguments
coords |
N x d matrix of coordinates |
k |
Number of nearest neighbors |
Value
List with:
-
nn_idx: N x k matrix of neighbor indices (0 for obs with fewer neighbors) -
nn_dist: N x k matrix of distances to neighbors -
nn_order: Ordering of observations for NNGP (by coordinate)
Credible intervals for the fixed effects
Description
Credible intervals for the fixed effects
Usage
## S3 method for class 'tulpa_fit'
confint(object, parm = NULL, level = 0.95, ...)
Arguments
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. |
Value
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().
DIC, CPO, WAIC and PSIS-LOO on a fit
Description
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.
Usage
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, ...)
Arguments
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- |
Details
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.
Value
dic() and cpo() return a tulpa_criteria object. waic()
returns a loo waic object and loo() a loo psis_loo
object.
See Also
tulpa_criteria() for every criterion at once and for what the LOO
unit means; compare_models() to rank several fits.
Examples
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)
Comprehensive diagnostic summary
Description
Provides a comprehensive diagnostic report for a tulpa model, combining convergence metrics, divergence information, and actionable recommendations.
Usage
diagnostic_summary(fit, quiet = FALSE)
## S3 method for class 'tulpa_diagnostic_summary'
print(x, ...)
Arguments
fit |
A |
quiet |
Logical; if TRUE, suppress printed output (default: FALSE). |
x |
A |
... |
Ignored. |
Value
A list with class tulpa_diagnostic_summary containing:
- status
Overall status: "PASS", "WARN", or "FAIL"
- n_divergent
Number of divergent transitions
- divergent_pct
Percentage of divergent transitions
- worst_rhat
Data frame of parameters with worst Rhat
- worst_ess
Data frame of parameters with worst ESS
- e_bfmi
E-BFMI value (HMC only)
- pareto_k, quad_ess
approximation fits only: the outer PSIS k-hat, or the grid quadrature ESS when no k-hat was produced
- hyper_share_min, hyper_share_max
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.NAwhen the fit retains no per-cell pieces to decompose- pareto_k_declined
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"- inner_skew_max, inner_skew_declined
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)- axis_fields_dropped
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
- recommendations
Character vector of recommendations
See Also
check_diagnostics(), diagnostics(), plot_diagnostics()
Examples
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)
Posterior diagnostics for a fitted model
Description
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:
- MCMC chain draws
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.
- i.i.d. approximation draws
the approximation-reliability table – the PSIS tail-shape
pareto_kscored against the exact inner-Laplace marginal and the outer-grid quadrature effective sample size (the OUTER hyperparameter-grid integration layer), the inner-Laplace skewness diagnosticgamma_3when computed (the INNER Gaussian approximation to the latent field, a separate layerpareto_kdoes 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.- point summaries
no sample to diagnose; returns
NULLwith 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.
Usage
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, ...)
Arguments
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 |
Value
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.
Extending
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.
Calibration alongside reliability
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.
Approximation reliability (i.i.d. draws)
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.
Approximation-reliability scope
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.
Approximation-reliability table columns
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 (
NAif not computed).pareto_k_band"good"/"ok"/"unreliable"/NA.pareto_k_declined,pareto_k_declined_notewhen
pareto_kisNA, WHY:"not_requested","not_applicable","unguessable_axis"(naming the axis – a permanent limitation of that family, so readess_gridinstead),"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_drawsthe 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_roundsthe outcome of a
control$k_qualityrequest; present when one was made.inner_skew,inner_skew_idxthe per-index inner-Laplace skewness
gamma_3and the latent indices it was probed at; present when the probe ran.ess_grid,n_grid,rel_ess_grid,max_weightgrid quadrature reliability.
n_gridcounts the cells SOLVED, so a posterior sharp enough to underflow all but one of them readsess_grid = 1.00 of 124 cellsrather than of one, andrel_ess_gridis 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_sidesfor 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 statementgrid_railed_axescannot 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 (
NAmeans 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_notewhether 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_textwhat 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 itsreliabilityisNA.inner_skew_maxthe largest
|gamma_3|among the scored latent indices (NAifcontrol$diagnose_skew = FALSEor nothing scored).inner_skew_band"good"/"ok"/"unreliable"/NA, banded oninner_skew_maxby the general skewness-magnitude convention (Bulmer 1979) – not a Rue-Martino-Chopin-specific cutoff.inner_skew_scored,inner_skew_probedhow many of the probed latent indices returned a finite
gamma_3vs how many were probed.inner_skew_declined,inner_skew_arms_declined,inner_skew_declined_notewhen 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_bandthe 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_3cannot score.inner_pareto_k_rel_ess,inner_pareto_k_is_essthe 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_uniformTRUEwhen 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_probedhow many probed indices returned a finite k-hat vs how many were probed.
inner_pareto_k_declined,inner_pareto_k_declined_notewhen 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.
References
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.
See Also
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()
Examples
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)
Durbin-Watson test for temporal autocorrelation
Description
Tests first-order autocorrelation in temporally-ordered residuals.
Usage
durbin_watson(
object,
alternative = c("two.sided", "greater", "less"),
time = NULL,
resid_type = "pearson"
)
Arguments
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
|
Value
An htest object with DW statistic, lag-1 r, and p-value
Examples
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)
Energy Bayesian Fraction of Missing Information (E-BFMI)
Description
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.
Usage
energy_bfmi(energy, chain = NULL)
Arguments
energy |
Numeric vector of per-iteration energies. |
chain |
Chain index of each entry of |
Value
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.
Examples
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_chain
Supported R-level family names
Description
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 accepted as well and not listed here.
Usage
family_names()
Value
A character vector of family names.
Examples
family_names()
Spatial field effects per unit
Description
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.
Usage
field_effects(object)
Arguments
object |
A |
Details
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 combinedsigma * (sqrt(rho) * s * phi + sqrt(1 - rho) * theta)at that draw'ssigmaandrho(sthe scaling factor). A unit that no observation falls in contributes nothing to the linear predictor and is reported asNA.A Polya-Gamma Gibbs fit (
mode = "gibbs") stores the field it samples (phi_spatial[k]); for BYM2 each draw's two unit-scale components are combined into the samesigma * (sqrt(rho) * s * phi + sqrt(1 - rho) * theta)the sampler path reports. 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()); atulpa()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 thesigma = 1,rho = 0.5the 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().
Value
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".
See Also
smooth_effects() for an s(x) term, ranef() for random
effects, temporal() for a temporal field.
Examples
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")
Find all bar terms in a formula's parse tree
Description
Recursively walks the formula AST and collects all | and || nodes
found inside parentheses. These are the random effect specifications.
Usage
findbars(term)
Arguments
term |
A language object (formula term) |
Value
A list of language objects, each a | or || call
Examples
findbars(y ~ x + (1 | g) + (1 + x | h))
Fit a spatial model using SPDE Laplace approximation
Description
Fits a GLM with a Matern spatial field via the SPDE approach. Uses CHOLMOD sparse solver with optional nested Laplace for hyperparameter integration.
Usage
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()
)
Arguments
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:
|
Value
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-\hat{k}and its importance-sampling ESS (NAwhendiagnose_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)
References
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.
Examples
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
}
Fit an additive spatiotemporal GLM by nested Laplace
Description
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.
Usage
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, ...)
Arguments
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. |
Value
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.
See Also
tulpa() (front door), tulpa_nested_laplace() (single field).
Examples
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")
Fitted values (population level)
Description
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.
Usage
## S3 method for class 'tulpa_fit'
fitted(object, ...)
Arguments
object |
A |
... |
Ignored. |
Value
Numeric vector of fitted mean responses, length nobs.
Fixed-effect coefficients (lme4-compatible)
Description
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.
Usage
fixef(object, ...)
## S3 method for class 'tulpa_fit'
fixef(object, ...)
Arguments
object |
A |
... |
Ignored. |
Details
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().
Value
Named numeric vector of fixed-effect estimates.
See Also
Examples
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)
Geweke convergence test
Description
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.
Usage
geweke_test(fit, frac1 = 0.1, frac2 = 0.5, pars = NULL)
## S3 method for class 'tulpa_geweke'
print(x, ...)
Arguments
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. |
Details
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.
Value
A data frame of class tulpa_geweke with columns chain,
parameter, z_score and p_value, one row per chain and parameter.
See Also
diagnostics(), check_diagnostics()
Examples
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)
Description
Model-level summary statistics (broom-compatible)
Usage
## S3 method for class 'tulpa_fit'
glance(x, ...)
Arguments
x |
A |
... |
Ignored. |
Value
Single-row data frame.
Examples
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)
Describe one outer-grid hyperparameter axis
Description
Builds a single axis spec for tulpa_hyper_grid(). Carries the candidate
values, the optional log-prior, and the metadata (log-scale, bounds,
refinable flag) that the generic refinement / consistency passes need.
Usage
hyper_axis_spec(
name,
grid,
log_prior = NULL,
log_scale = FALSE,
bounds = NULL,
refinable = FALSE,
atom_mass = NULL,
slab_bounds = NULL,
log_prior_coord = c("integration", "natural"),
extend = TRUE
)
## S3 method for class 'tulpa_hyper_axis_spec'
print(x, ...)
Arguments
name |
Character. Axis label, used as the column name of the grid matrix and in posterior summaries. |
grid |
Numeric vector of length >= 1. The per-axis candidate values (the outer integration nodes on this axis). The full outer grid is the Cartesian product across axes. |
log_prior |
Optional |
log_scale |
Logical. Does the axis live naturally on a log scale
( |
bounds |
Numeric vector of length 2 giving the natural support
|
refinable |
Logical. When |
atom_mass |
Numeric in |
slab_bounds |
Numeric |
log_prior_coord |
One of |
extend |
Logical. On a refinable axis, may the passes place a node
beyond the outermost value in |
x |
A |
... |
Ignored. |
Value
An object of class tulpa_hyper_axis_spec (a validated list with
the fields listed above).
See Also
Examples
hyper_axis_spec("sigma", grid = c(0.5, 1, 2), log_scale = TRUE,
bounds = c(0, Inf))
# A zero level on a log-scale axis is a point mass with a declared share:
hyper_axis_spec("alpha", grid = c(0, 0.5, 1), log_scale = TRUE,
bounds = c(0, Inf), atom_mass = 0.5)
Independence Metropolis-Hastings with Laplace proposal
Description
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.
Usage
imh_laplace(
log_posterior,
mode,
hessian,
n_iter = 2000L,
warmup = n_iter%/%2L,
scale = 1,
init = NULL,
thin = 1L,
seed = NULL,
verbose = FALSE
)
Arguments
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 |
Value
A list with class tulpa_fit carrying:
-
draws:(n_iter - warmup) / thinxdmatrix 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
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.
See Also
tulpa_laplace() for the mode + Hessian,
bridge_sampling() for marginal-likelihood estimation on the
resulting draws.
Examples
# 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)
Print inference mode information
Description
Displays information about the inference modes available in tulpa, including their tiers, guarantees, and appropriate use cases.
Usage
inference_mode_info()
Value
NULL, invisibly. Called for the side effect of printing the
available inference modes, their tiers, guarantees, and use cases to the
console.
Examples
inference_mode_info()
Is an outer-grid setting marked as a default?
Description
Is an outer-grid setting marked as a default?
Usage
is_auto_grid(x)
Arguments
x |
Any object. |
Value
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().
See Also
auto_grid(), auto_grid_place()
Examples
is_auto_grid(auto_grid(c(0.5, 1, 2)))
is_auto_grid(c(0.5, 1, 2))
Mark an expression as a latent block in a tulpa formula
Description
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.
Usage
latent(block)
Arguments
block |
A |
Value
Returns block invisibly. Outside a formula context the call is
a no-op pass-through.
Examples
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_blocks
Create a latent factor specification
Description
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).
Usage
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, ...)
Arguments
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. |
Details
Why Use Latent Factors?
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.
Mathematical Model
For observation i with K latent factors, on each of two model arms (processes 1 and 2):
\eta^{(1)}_i = X^{(1)}_i \beta^{(1)} + \sum_{k=1}^{K} f_{ik} \sigma_k + \ldots
\eta^{(2)}_i = X^{(2)}_i \beta^{(2)} + \sum_{k=1}^{K} f_{ik} \sigma_k + \ldots
where:
-
f_{ik} \sim N(0, 1)are standardized factor scores -
\sigma_kare factor standard deviations with PC prior Identifiability:
\sum_i f_{ik} = 0for each k
Because factors enter both linear predictors identically (when shared), they cancel in derived quantities (e.g., ratios, differences):
\eta^{(1)}_i - \eta^{(2)}_i
This means factors capture shared effects that would otherwise bias derived quantities.
Choosing n_factors
Start with
n_factors = 1for simple unmeasured confoundingUse
n_factors = 2-3if you suspect multiple independent confoundersMore than 3 factors is rarely needed and risks overfitting
The PC prior provides regularization, shrinking unneeded factors toward zero
Relationship to Random Effects
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.
Value
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).
See Also
prior_pc() for prior specification; latent() / tgmrf() for a
single-response in-formula latent Gaussian block
Examples
# 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
Description
Extract latent factor posteriors from fit
Usage
latent_factors(fit, ...)
## Default S3 method:
latent_factors(fit, summary = TRUE, probs = c(0.025, 0.5, 0.975), ...)
Arguments
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). |
Value
If summary = TRUE, a data frame with columns for observation,
factor, and summary statistics. If summary = FALSE, a matrix of
posterior draws.
Examples
# 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))
The fit's log-scale goodness quantity
Description
What this returns depends on what the fit computed, and the returned object
names it in a quantity attribute:
Usage
## S3 method for class 'tulpa_fit'
logLik(object, ...)
Arguments
object |
A |
... |
Ignored. |
Details
"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, asigma_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. Theconditioned_onattribute 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, withdfthe 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.
Value
A logLik object with quantity and conditioned_on attributes,
and a declined attribute naming the reason when no value could be read.
Metropolis-adjusted Langevin algorithm (MALA)
Description
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).
Usage
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
)
Arguments
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). |
Value
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
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).
References
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.
Examples
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.574
MCMC chain draws from a fit
Description
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().
Usage
mcmc_draws(fit)
Arguments
fit |
A |
Value
The chain draws matrix/array, or NULL for a non-chain fit.
See Also
posterior_sample(), diagnostics()
Examples
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))
Model-averaged predictions
Description
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.
Usage
model_average(
...,
weights = c("loo", "waic", "pbma", "pbma+"),
fitted_fn = fitted
)
Arguments
... |
Named |
weights |
|
fitted_fn |
Function extracting a length- |
Value
A list with averaged (the weighted fitted vector), weights (the
named model weights), and comparison (the compare_models() table).
References
Yao, Vehtari, Simpson & Gelman (2018). Using stacking to average Bayesian predictive distributions. Bayesian Analysis 13(3):917-1007.
See Also
Examples
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$weights
Moran's I test for spatial autocorrelation in residuals
Description
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).
Usage
moran_i(
object,
coords,
weights = c("inverse", "knn"),
k = 10L,
resid_type = "pearson",
alternative = c("two.sided", "greater", "less")
)
Arguments
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 |
|
Value
An htest object with Moran's I, expected I, and p-value
Examples
set.seed(1)
coords <- cbind(runif(50), runif(50))
resid <- rnorm(50)
moran_i(resid, coords)
Number of divergent transitions
Description
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.
Usage
n_divergent(fit)
Arguments
fit |
A |
Value
Integer count of divergent transitions (0 if none are recorded).
See Also
plot_divergences(), check_diagnostics()
Examples
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)
Remove all bar terms from a formula's parse tree
Description
Recursively rewrites the formula AST, removing any | or || nodes
found inside parentheses. Returns the fixed-effects-only formula.
Usage
nobars(term)
Arguments
term |
A language object (formula term) |
Value
A language object with all bar terms removed, or NULL if nothing remains
Examples
nobars(y ~ x + (1 | g) + (1 + x | h))
Number of observations in a tulpa fit
Description
Number of observations in a tulpa fit
Usage
## S3 method for class 'tulpa_fit'
nobs(object, ...)
Arguments
object |
A |
... |
Ignored. |
Value
Integer observation count.
Map cell identifiers to graph node indices
Description
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.
Usage
node_index(graph, ids)
Arguments
graph |
A |
ids |
A vector of cell identifiers to look up (matched against
|
Value
An integer vector the same length as ids, giving each one's node
index in graph (NA for identifiers absent from the graph).
See Also
Examples
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)
obs
Pathfinder: variational warm-start via L-BFGS + ELBO scoring
Description
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.
Usage
pathfinder(
log_posterior,
init,
grad_log_posterior = NULL,
n_draws = 1000L,
max_iter = 100L,
tol = 1e-06,
seed = NULL,
verbose = FALSE
)
Arguments
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). |
Value
A list with class tulpa_fit carrying:
-
draws:n_draws x dmatrix 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 ofE_q[log p - log q]. -
n_iter: L-BFGS iterations. -
converged: logical. -
inference_mode:"structured". -
inference_tier:2L. -
backend:"pathfinder".
Tier
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.
References
Zhang, L., Carpenter, B., Gelman, A., & Vehtari, A. (2022). Pathfinder: parallel quasi-Newton variational inference. Journal of Machine Learning Research, 23(306), 1-49.
See Also
imh_laplace() for an exact-tier MH using the Pathfinder
Gaussian as proposal; bridge_sampling() for marginal-likelihood
estimation on the resulting draws.
Examples
# 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
PIT (probability integral transform) residuals
Description
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).
Usage
pit_residuals(object, ...)
## Default S3 method:
pit_residuals(object, observed = NULL, nsim = 250L, seed = 123L, ...)
Arguments
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) |
Details
For integer-valued responses, a randomisation step avoids discrete artefacts: the residual is drawn uniformly between P(sim < obs) and P(sim <= obs).
Value
Numeric vector of length n_obs with values in [0, 1]
Examples
# 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))
Plot fixed-effect posteriors
Description
Plot fixed-effect posteriors
Usage
## S3 method for class 'tulpa_fit'
plot(x, type = c("density", "trace", "pairs", "smooth"), term = NULL, ...)
Arguments
x |
A |
type |
One of |
term |
For |
... |
Passed to plotting functions. |
Value
The input x, returned invisibly. Called for the side effect of
producing base-graphics plots of the fixed-effect posteriors.
Plot method for tulpa_prior_predict
Description
Density overlay of prior predictive draws. Requires bayesplot; falls back to base graphics matplot of a subset of draws otherwise.
Usage
## S3 method for class 'tulpa_prior_predict'
plot(x, process = 1L, max_draws = 50L, ...)
Arguments
x |
A tulpa_prior_predict object |
process |
Process index or name (multi-process families) |
max_draws |
Maximum draws to overlay. Default 50. |
... |
Passed through. |
Value
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
Description
Plot method for spatiotemporal effects
Usage
## S3 method for class 'tulpa_st_summary'
plot(x, type = "heatmap", ...)
Arguments
x |
Spatiotemporal effects object |
type |
Plot type: |
... |
Additional arguments passed to plotting functions |
Value
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
Description
Plot method for tulpa_svc_posterior
Usage
## S3 method for class 'tulpa_svc_posterior'
plot(x, term = 1, type = "mean", ...)
Arguments
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 |
Value
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
Description
Plot method for tulpa_temporal_posterior
Usage
## S3 method for class 'tulpa_temporal_posterior'
plot(x, component = NULL, type = "ribbon", ...)
Arguments
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 |
Value
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
Description
Plot method for tulpa_tvc_posterior
Usage
## S3 method for class 'tulpa_tvc_posterior'
plot(x, term = 1, type = "ribbon", ...)
Arguments
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 |
Value
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.
Plot autocorrelation functions
Description
Creates autocorrelation function (ACF) plots for selected parameters. High autocorrelation indicates slow mixing and low effective sample size.
Usage
plot_acf(fit, pars = NULL, lags = 25, n_pars = 6)
Arguments
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). |
Details
Ideal ACF plots show rapid decay to zero. Slow decay indicates high autocorrelation, which reduces effective sample size and may indicate poor mixing.
Value
A ggplot object (if ggplot2 available) or base R plot (invisible).
See Also
Examples
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)
Diagnostic plotting functions for tulpa models
Description
Visual diagnostic tools for MCMC convergence assessment. All functions provide base R fallbacks when ggplot2/bayesplot are unavailable.
Creates a combined diagnostic figure with Rhat, ESS, trace plot, and
energy/ACF panels. Requires the patchwork package for layout.
Usage
plot_diagnostics(fit, pars = NULL)
Arguments
fit |
A |
pars |
Character vector of parameter names for trace plot. If NULL, uses the parameter with worst Rhat. |
Details
Creates a 2x2 grid:
Top left: Rhat plot
Top right: ESS plot
Bottom left: Trace for worst parameter
Bottom right: Energy (HMC) or ACF (other)
Value
A combined plot (ggplot + patchwork) or NULL if requirements not met.
See Also
diagnostic_summary(), plot_rhat(), plot_ess()
Examples
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_diagnostics(fit)
Plot divergent transitions
Description
Creates visualizations to investigate divergent transitions. Parallel coordinates and scatter plots highlight where in parameter space divergences occur.
Usage
plot_divergences(fit, pars = NULL, type = c("parcoord", "scatter"))
Arguments
fit |
A |
pars |
Character vector of parameter names. If NULL, uses variance parameters. |
type |
Plot type: "parcoord" (parallel coordinates) or "scatter". |
Details
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
Value
A ggplot object or base R plot (invisible).
See Also
Examples
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")
Plot energy diagnostic (E-BFMI)
Description
Creates overlaid histograms of marginal energy and energy transition, along with the E-BFMI statistic. Low E-BFMI indicates poor exploration of the posterior.
Usage
plot_energy(fit)
Arguments
fit |
A |
Details
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.
Value
A ggplot object (if ggplot2 available) or base R plot.
See Also
diagnostic_summary(), check_diagnostics()
Examples
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)
Plot effective sample size diagnostic
Description
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.
Usage
plot_ess(fit, type = c("bulk", "tail"), threshold = 400, pars = NULL)
Arguments
fit |
A |
type |
Type of ESS: "bulk" (default) or "tail". |
threshold |
Minimum acceptable ESS (default: 400). |
pars |
Character vector of parameter names to include. |
Details
ESS/iter ratio interpretation:
Green: ESS >= threshold
Yellow: threshold/2 <= ESS < threshold
Red: ESS < threshold/2
Value
A ggplot object (if ggplot2 available) or base R plot (invisible).
See Also
plot_rhat(), diagnostic_summary(), diagnostics()
Examples
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")
Plot spatial predictions as a map
Description
Create publication-ready maps from a tulpa spatial fit: the fitted response surface or the prediction uncertainty (credible-interval width), on the response scale.
Usage
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,
...
)
Arguments
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. |
Details
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().
Value
A ggplot2 object that can be further customized.
Required packages
This function requires ggplot2. For raster-style maps, stars and sf
are also needed. Install with:
install.packages(c("ggplot2", "stars", "sf"))
See Also
predict.tulpa_fit() for predictions at new locations
Examples
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
}
Plot multiple maps in a grid
Description
Create a multi-panel figure with the fitted-value map and the prediction-uncertainty map side by side.
Usage
plot_map_panel(x, newdata = NULL, ncol = 2, ...)
Arguments
x |
A |
newdata |
Optional data frame with prediction locations. |
ncol |
Number of columns in the grid. Default 2. |
... |
Additional arguments passed to |
Value
A patchwork object (if patchwork is installed) or a list of ggplots.
Examples
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)
}
Plot bivariate parameter posteriors (pairs plot)
Description
Creates a pairs plot showing bivariate relationships between parameters. Divergent transitions (if present) are highlighted to help identify problematic posterior regions.
Usage
plot_pairs(
fit,
pars = NULL,
highlight_divergent = TRUE,
n_pars = 5,
alpha = 0.3
)
Arguments
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). |
Details
Pairs plots help identify:
Strong correlations between parameters (potential non-identifiability)
Multimodality
Regions where divergences cluster (indicating problematic geometry)
Value
A ggplot object (if ggplot2/GGally available) or base R plot.
See Also
plot_divergences(), diagnostics()
Examples
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)
Plot Rhat convergence diagnostic
Description
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.
Usage
plot_rhat(fit, threshold = 1.01, pars = NULL)
Arguments
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). |
Details
Color coding:
Green: Rhat < 1.01 (converged)
Yellow: 1.01 <= Rhat < 1.05 (borderline)
Red: Rhat >= 1.05 (not converged)
Value
A ggplot object (if ggplot2 available) or base R plot (invisible).
See Also
plot_ess(), diagnostic_summary(), diagnostics()
Examples
# 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)
Fit a post-hoc linear model on estimated parameters
Description
Useful for exploring drivers of occupancy/detection/abundance variation after model fitting. Fits a weighted linear model and optionally generates bootstrap confidence intervals.
Usage
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, ...)
Arguments
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. |
Value
A list of class "post_hoc_lm" with:
- summary
data.frame of coefficient estimates and CIs
- lm_fit
the underlying
lmobject- boot_coefs
matrix of bootstrap coefficient samples (if
n_boot > 0)- R2
R-squared from the fitted model
Examples
# 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
Posterior predictive replicates
Description
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 withcontrol$fitted_var = FALSEcarries nofitted_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 fromN(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).
Usage
posterior_predict(object, ...)
## S3 method for class 'tulpa_fit'
posterior_predict(
object,
newdata = NULL,
ndraws = NULL,
n_trials = NULL,
seed = NULL,
...
)
Arguments
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). |
Value
A ndraws x n_obs numeric matrix of replicated responses.
See Also
pp_check(), which uses these replicates; simulate.tulpa_fit().
Examples
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 100
Posterior parameter sample from a fit
Description
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().
Usage
posterior_sample(fit)
Arguments
fit |
A |
Details
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.
Value
The posterior draws matrix/array, or NULL if the fit carries none.
See Also
mcmc_draws(), tulpa_posterior_draws(), diagnostics()
Examples
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))
Posterior predictive check
Description
Generate posterior predictive checks for a fitted tulpa model. Compares observed data to replicated data from the posterior predictive distribution.
Usage
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,
...
)
Arguments
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) |
Value
A ggplot object
Examples
# 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")
}
Predict at new covariate values (population level)
Description
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.
Usage
## S3 method for class 'tulpa_fit'
predict(
object,
newdata = NULL,
type = c("link", "response"),
se.fit = FALSE,
level = 0.95,
include_field = TRUE,
...
)
Arguments
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. |
Value
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.
Summarize and print a tulpa fit
Description
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.
Usage
## 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), ...)
Arguments
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. |
Details
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.
Value
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,familyas the fit records them,
NULLwhere it records none.inferencehow the fit was produced: a list of
backend,inference_tier,inference_modeandselection_reason(whymode = "auto"chose that backend).n_obs,n_drawsthe observation count and the number of posterior draws the fixed-effect summaries read (
NULLwithout draws).levelthe interval level of
coefficients.coefficientsdata frame, one row per fixed effect named by the term:
estimate,std.errorand the lower / upper credible bounds, labelled asconfint.tulpa_fit()labels them. Carries theinterval_source,interval_declined,retained_massandskew_appliedattributes described in Details. Also returned bycoef()on the summary.randomthe random-effect standard deviations as
VarCorr()reports them, with their estimated / sampled / conditioned label;NULLon a fit with no random-effect terms.hyperparametersdata frame of the integrated hyperparameters, one row each:
parameter,mean,sd,medianand the 2.5% / 97.5% boundsci_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.NULLwhere the fit integrated none.sigmathe residual / dispersion scale a fit reports as one number, else
NULL.smooth_termsthe fit's
s(...)term metadata,NULLwithout smoothers;smooth_effects()returns the fitted curves.reliabilitya list:
kind("chain","iid"or"point", the draws provenancediagnostics()routes on); on a chainrhat_max,ess_bulk_minandess_tail_minover the parameters; on i.i.d. drawsband, the one-row headlinediagnostics()attaches as itssummaryattribute (outerpareto_kand its band, grid quadrature ESS, inner-Laplace scores,reliability), andmoment_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 combinedreliabilityverdict on i.i.d. draws,NAotherwise);convergence, the fit's own convergence record; and the backend-specific quality numbersmean_accept(Metropolis acceptance rate),n_divergent(divergent transitions) andelbo(the variational objective), eachNULLwhere 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);
NULLwhenever every supplied axis was used.diagnostic_summary()reads the same record in sentences.priora list whose
betais the Gaussian fixed-effect prior the fit ran under, aslist(mean, sd): the engine defaultprior_normal(0, 2.5)whenever the caller supplied none, andNULLon the paths that express no Gaussian prior on the fixed effects.
coef.summary.tulpa_fit() returns the coefficients data frame.
See Also
coef.tulpa_fit(), confint.tulpa_fit(), VarCorr(),
diagnostics(), tulpa_outer_grid()
Examples
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$backend
Nested Laplace approximation for latent Gaussian models
Description
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) ofD^{-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).
Usage
## 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()
)
Arguments
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
|
Details
print() shows the integrated hyperparameters, the outer-grid
size, the outer Pareto-\hat{k} 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.
Value
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-\hat{k}and its importance-sampling ESS (NAwhen not computed for the grid; seecontrol$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 (seecontrol$diagnose_skew). -
timing: named numeric of wall-clock seconds (total,setup,grid,postproc,diagnostics); thegridphase is the inner Laplace pass that scales with grid size. Surfaced one-line inprint. -
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 isprune_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.
References
Rue, Martino & Chopin (2009). Approximate Bayesian inference for latent Gaussian models by using integrated nested Laplace approximations. JRSS-B 71(2):319-392.
Examples
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 precision
Joint multi-likelihood nested Laplace approximation
Description
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)withQ = D - rho_car * W.
Other backends (NNGP, HSGP, RW1/2, AR1) follow the same interface and land under Phase 3.
Usage
## 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()
)
Arguments
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
|
Value
A list of class c("tulpa_nested_laplace_joint", "tulpa_nested_laplace", "list") with:
-
theta_grid,theta_names– outer-grid hyperparameter values (includes thealphaaxis whencopyis set). -
log_marginal,weights– per-grid-point log-marginal and integration weights (sum to 1). -
theta_mean,theta_sd– posterior moments per hyperparameter, includingalphawhencopyis set. -
theta_median,theta_ci_lo,theta_ci_hi– weighted-quantile median and 2.5/97.5 empirical CI per hyperparameter axis (same names astheta_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 andmean +/- 1.96 sdmis-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_declinedsays 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 readsNA.fitted_eta_varis its per-cell within-cell variance, present undercontrol$fitted_var(the default). Together these are the per-cell Gaussianposterior_predict()draws a replicate from. -
n_iter– inner Newton iterations per grid point. -
arm_layout– list with per-armbeta_start,re_start, spatial offset(s) andn_xfor decoding modes. -
prior,responses,copy– echoed inputs. -
timing– named numeric of wall-clock seconds:totalplus thesetup(validation / encoding / grid construction),grid(inner Laplace solves, including adaptive-refinement and consistency passes),postproc(weight / moment / marginal assembly) anddiagnostics(outer Pareto-\hat{k}) phases. Thegridphase is the one that scales with grid size and core count. Surfaced one-line inprint. -
pareto_k,pareto_k_is_ess,pareto_k_scope– outer Pareto-\hat{k}accuracy diagnostic and its importance-sampling ESS (bothNAwhencontrol$diagnose_k = FALSEor the fit declines; see thediagnose_kcontrol knob).pareto_k < 0.7indicates the nested integration is reliable;>= 0.7that the (skewed / heavy- tailed) hyperparameter posterior is misfit by the Gaussian grid and the fit should escalate to an exact debias. -
pareto_k_declined– whenpareto_kisNA, WHY:"not_requested"(control$diagnose_k = FALSE; nothing is wrong),"unguessable_axis: <axis>"(a support the engine will not guess, e.g. car_proper'srho_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).NAon a fit whose\hat{k}WAS computed. -
pareto_k_proposal_source– how the outer importance proposal the\hat{k}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.NAwhen the diagnostic is off or declines. The mode-Hessian source keeps the\hat{k}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 withcontrol$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 – forkequally spaced nodes that isk / (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 intervalaxis_supportreports),refine(the mode refinement ran under:"none"/"densify"/"extend") andn_nodes(initial and final continuum node counts). -
outer_grid_placement–"fixed"(the defaultsigma_gridaxis was used as-is) or"auto_recentered"when a railedsigmaaxis triggered the mode-Hessian recenter-and-refit (see thepriorargument above).outer_grid_recenter_attempts(integer) andouter_grid_prior_added(logical: whether the light default PC(U=3, alpha=0.01) sigma prior was applied) are set alongside it, plusouter_grid_prior_declined = "prior_pinned"when a second attempt ran but the caller's pinnedprior_sigmaheld 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 pinnedsigma_grid; mark it withauto_grid()if it is a default rather than a choice),"default_axis_pinned"(a wrapper package declared the nodes withauto_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 whoserho_caraxis 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 saysauto_recenteredand nothing about the axis that stayed – which is the axisgrid_coarsest_axisthen names. Written by the field-SD passes forsigma(b<k>.sigmaon a copy block) and by the per-arm dispersion pass forphi_<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 whencontrol$recenter_pilotran: 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 ownregime,ess_gridandedge_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\hat{k}above the good band triggered the skew-normal rescue pass. It is the EXPLANATION for an inflated\hat{k}: a symmetric Gaussian proposal against a right-skewed variance-component marginal has a heavy importance-ratio tail whatever the integration's quality.NULLwhen 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-\hat{k}uncertainty, present whenever the diagnostic ran.pareto_k_se_bootis the bootstrap SE of the k-hat andpareto_k_ci_low/pareto_k_ci_highits 2.5\ quantiles – the estimator's sampling spread GIVEN the proposal, not a posterior credible interval;pareto_k_se_formulais the closed-form GPD-shape MLE asymptotic SE cross-check.pareto_k_tail_pointsis the GPD tail size used andpareto_k_tail_points_requestedthek_tail_pointsrequest (NAwhen automatic).pareto_k_band_confidentis TRUE iff the bootstrap CI lies within one reliability band,NAwhen the bootstrap was off or could not fit.diagnose_drawsanddiagnose_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 withcontrol$diagnose_k = "by_arm". Named (by arm) outer Pareto-\hat{k}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 isNAwhen 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_pointsandpareto_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 thecontrol$k_qualityintent.k_quality_requestedechoes the intent;k_quality_bestis 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, whichk_quality_k_tracecarries;k_quality_reachedisTRUE/FALSEfor an"ok"/"good"target (NAfor"report"/"none"), never silently downgraded;k_quality_missclassifies a miss as"resolution"(the\hat{k}confidently outside the band – a grid deficiency) or"precision"(the bootstrap CI straddling a boundary – the estimator's variance),NAwhen there is no miss to classify, and is what the escalation reads once refinement is exhausted;k_quality_reasonrecords why it stopped;k_quality_roundsis the number of escalation re-fits performed (0when the first fit sufficed or escalation was off); andk_quality_k_traceis the outer\hat{k}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-\hat{k}one. -
adaptive_grid_info– whenadaptive_grid = TRUE, a list withtriggered_axes(character) andn_points_added(integer) describing the refinement passes. NULL otherwise. -
local_ccd_info– whenlocal_ccdengaged (ork_refine = "ccd"), a list withn_cells_refined,n_nodes_added, the refinedcells, then_cells_before/n_cells_aftergrid 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) andcell_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 readingskew_maxgates on, withmisfit_declined/cells_declinedfor 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) andmode_gain(the same displacement in the cell's own curvature units,0.5 * g' (-H)^-1 gin nats, read off the same fit's quadratic coefficients and comparable across cells whose curvature differs;NAwhere-His 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) andmax_node_weight(the share the single largest node takes of its own cell's refined mass). Each carries a_declinedtwin for the cells the gate put back, whose nodes were evaluated before the score was read.misfitcertifies 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,< 4axes, an activephi_grid, or no peaked interior cell). -
integration_requested,integration_declined– whatintegrationasked for, and why the requested CCD or adaptive lattice did not run.$integrationnames 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_declinedisNA_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_properrho_caror a non-BYM2rho),"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, seecopy_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 theccd_budgetcontrol knob). Underintegration = "grid_adaptive"it is one of"adaptive_small_grid"(fewer outer cells thanadaptive_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 thanadaptive_grid_max_fracof 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$integrationto 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 offintegration. -
theta_interval_read,theta_interval_design_mass– what the reported per-axistheta_median/theta_ci_lo/theta_ci_hiwere 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_mass0),"moment_rule"an interval from the moments a central-composite design delivers (design_mass1), 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 bothdesign_massregimes; 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 intheta_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 apositiveaxis and a bound above 1 on aunitone. 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_declinedisNAwhere 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 questionouter_grid_railed_axesreports. -
outer_grid_resolution_declined– per axis, why that axis carries no ratio, from the closed vocabularytoo_few_nodes,mode_at_edge,coord_not_finite,spacing_not_finite,stencil_degenerate,curvature_not_negative;NAon an axis that scored. The reasons are not interchangeable:mode_at_edgesays 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-gridresolvedverdict 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 whenprune = TRUEand 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 isprune_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 havelog_marginal = -Infso 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–TRUEon 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 underOMP_NUM_THREADS=1withn_threads_outer = 10runs 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 withincontrol$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_resolvecounts 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 innonconverged_cellswith the posterior mass such cells carry innonconverged_mass, and is named bydiagnostics(). Past the screen's mass gate the fit warns.
(sigma, alpha) parameterization
Each arm's linear predictor reads
\eta_{arm} = X_{arm} \beta_{arm} + \sigma_{arm} \cdot z_s,
where z_s is a unit-precision latent (ICAR(tau=1) for ICAR/BYM2,
or the BYM2 mix; CAR_proper uses the structure of
D - \rho_{car} W). All arms share the donor amplitude
\sigma from the outer-grid sigma_grid axis. The copy arm's
amplitude is \sigma_{arm} = \alpha \cdot \sigma, where \alpha
is a direct outer-grid axis taken from copy$alpha_grid. The Cartesian
product is over (sigma, [rho/rho_car,] alpha[, phi]). Direct \alpha
as an outer-grid axis (rather than a post-hoc ratio
\alpha = \sigma_{pos} / \sigma_{occ}) avoids plug-in bias on the
weakly-identified ratio at small n_pos and lets a regularizing
hyperprior land on \alpha directly.
References
Rue, Martino & Chopin (2009). Approximate Bayesian inference for latent Gaussian models by using integrated nested Laplace approximations. JRSS-B 71(2):319-392.
See Also
tulpa_nested_laplace() for the single-arm engine.
Examples
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 arms
Extract temporal effects from a fitted model
Description
Extract posterior distributions of temporal effects from a fitted tulpa model with temporal specification.
Usage
## 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, ...)
Arguments
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 |
Details
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.
Value
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.
See Also
temporal_multiscale(), temporal_rw1()
Examples
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)
Beta prior
Description
Specify a beta prior for a parameter bounded in (0, 1).
Usage
prior_beta(alpha = 1, beta = 1)
Arguments
alpha |
First shape parameter. Must be positive. |
beta |
Second shape parameter. Must be positive. |
Details
alpha = beta = 1: Uniform(0, 1)
alpha = beta = 2: Symmetric, peaked at 0.5
alpha > beta: Skewed toward 1
alpha < beta: Skewed toward 0
Value
A tulpa_prior object
Examples
prior_beta(1, 1) # Uniform
prior_beta(2, 2) # Symmetric, centered at 0.5
prior_beta(5, 2) # Skewed toward 1 (for high autocorrelation)
Exponential prior
Description
Specify an exponential prior for a positive parameter.
Usage
prior_exponential(rate = 1)
Arguments
rate |
Rate parameter (lambda). Must be positive. |
Value
A tulpa_prior object
Examples
prior_exponential(1)
Build a prior list for tulpa_nested_laplace() from a tulpa spec object
Description
Validates 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_temporalwithtype in {"rw1", "rw2", "ar1"} -
tulpa_spatial, arealtype 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– plusscale_factorfor bym2 and the eigenvalue-derivedrho_boundsfor proper CAR. No grid field is returned for any areal type: the outer axes are built later by the family's registrydefaults()closure, and proper CAR's(tau, rho)grid is built there from those bounds. -
tulpa_gp/tulpa_hsgp, continuoustype in {"gp", "nngp", "hsgp"}: validated againstdataand routed to thenngp/hsgpnested 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.
Usage
prior_from_spec(spec, data)
Arguments
spec |
A |
data |
Data frame the spec resolves time/group/site indices against. |
Value
A prior list ready for tulpa_nested_laplace().
Examples
d <- data.frame(year = rep(1:10, each = 3))
pr <- prior_from_spec(temporal_ar1("year"), d)
str(pr)
Gamma prior
Description
Specify a gamma prior for a positive parameter.
Usage
prior_gamma(shape = 2, rate = 0.1)
Arguments
shape |
Shape parameter (alpha). Must be positive. |
rate |
Rate parameter (beta). Must be positive. |
Details
Mean = shape/rate, Variance = shape/rate^2.
Value
A tulpa_prior object
Examples
prior_gamma(2, 0.1) # Mean = 20, weakly informative
prior_gamma(1, 1) # Exponential(1)
Half-Cauchy prior
Description
Specify a half-Cauchy prior for a positive parameter. Has heavier tails than half-normal, often used for variance parameters.
Usage
prior_half_cauchy(scale = 2.5)
Arguments
scale |
Scale parameter. Must be positive. |
Value
A tulpa_prior object
Examples
prior_half_cauchy(2.5)
Half-normal prior
Description
Specify a half-normal (truncated at 0) prior for a positive parameter.
Usage
prior_half_normal(sd = 1)
Arguments
sd |
Scale parameter. Must be positive. |
Value
A tulpa_prior object
Examples
prior_half_normal(1)
Normal prior
Description
Specify a normal prior for a parameter.
Usage
prior_normal(mean = 0, sd = 2.5)
Arguments
mean |
Prior mean. Default 0. |
sd |
Prior standard deviation. Must be positive. |
Value
A tulpa_prior object
Examples
prior_normal(0, 2.5)
prior_normal(0, 1)
Penalized complexity (PC) prior
Description
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.
Usage
prior_pc(U = 1, alpha = 0.01)
Arguments
U |
Upper bound. P(x > U) = alpha. |
alpha |
Tail probability. Default 0.01. |
Details
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.
Value
A tulpa_prior object
References
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.
Examples
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.05
Prior predictive simulation
Description
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.
Usage
prior_predict(
formula,
family,
data,
priors = NULL,
n_draws = 100,
seed = NULL,
...
)
## S3 method for class 'tulpa_prior_predict'
print(x, ...)
Arguments
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 |
Value
A tulpa_prior_predict object: a list with
-
y: list of lengthn_draws, each element the simulated response for that draw (matching whatever shapefamily$simulate_fnreturns). -
theta: list of lengthn_drawsof parameter draws (beta,sigma, RE coefficientsu, family-specific extras). -
linpred: list of lengthn_draws, each a list of linear predictor vectors per process. -
family: the family used. -
n_draws,n_obs.
Examples
# 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
Show default priors for a tulpa family
Description
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()).
Usage
priors_default(family = NULL, spatial = FALSE, temporal = FALSE)
Arguments
family |
|
spatial |
Logical; if TRUE, include spatial priors. Default FALSE. |
temporal |
Logical; if TRUE, include temporal priors. Default FALSE. |
Details
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(largerphiis less overdispersion;phi -> Infis the Poisson limit). The default keepsphifinite, 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.
Value
Invisibly returns a tulpa_priors object with the defaults.
Primarily called for its side effect of printing.
See Also
tulpa_priors() for creating custom priors
Examples
# 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
Description
Random-effect summaries
Usage
ranef(object, ...)
## S3 method for class 'tulpa_fit'
ranef(object, ...)
Arguments
object |
A |
... |
Ignored. |
Details
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 bytulpa(..., control = list(re_cov = "gibbs"))) draw the random effects jointly with everything else, soestimate/sd/ bounds are the empirical posterior summaries.The RE-covariance integrator (
tulpa_re_cov_nested()) carries a Gaussian per-group posterior at eachSigmanode; the reported summaries are the exact moments and quantiles of the weighted mixture of those, so they carry both the within-node curvature and theSigmauncertainty. 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 (
sdand the bounds areNA), 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.
Value
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).
See Also
field_effects() for a spatial field, smooth_effects() for an
s(x) term.
Examples
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)
PC + LKJ hyperprior for a random-effect covariance
Description
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.
Usage
re_cov_pc_lkj_prior(n_coefs, prior_sigma = NULL, eta = NULL, correlated = TRUE)
Arguments
n_coefs |
Number of coefficients |
prior_sigma |
|
eta |
LKJ shape. |
correlated |
|
Details
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.)
Value
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().
See Also
Examples
# 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))
Objects exported from other packages
Description
These objects are imported from other packages. Follow the links below to see their documentation.
Residuals from a tulpa fit
Description
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.
Usage
## S3 method for class 'tulpa_fit'
residuals(object, type = c("pearson", "response"), ...)
Arguments
object |
A |
type |
|
... |
Ignored. |
Value
Numeric vector of length nobs(object).
Pool multiple imputation draws via Rubin's rules
Description
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
\kappa_3 = E[\kappa_3(X | k)] + 3\,\mathrm{Cov}(\mu_k, \sigma_k^2) + \kappa_3(\mu_k),
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.
Usage
rubins_pool(draws)
Arguments
draws |
List of K draws. Each draw is a named list of submodel
results, each with |
Value
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).
Examples
# 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
Simulation-based calibration
Description
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).
Usage
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, ...)
Arguments
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. |
Value
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.
The two experiments
"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 posteriorpi(theta | y, y_obs). That is ordinary SBC withpi(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.
What is reported
Three reads of the same PIT sample, per (arm, quantity):
- raw
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.
- folded
2 |u - 1/2|, also uniform, and where a symmetric over- or under-dispersion shows after cancelling in the raw ECDF.- CRPS
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.
The callback contract
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').
Two guards
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.
References
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.
See Also
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).
Examples
# 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")
Predictive shapes an SBC fitter reports
Description
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.
Usage
sbc_mixture(mu, var, w = NULL)
sbc_normal(mean, sd)
sbc_discrete(support, probs)
sbc_rank(rank, n_ref)
sbc_draws(x)
Arguments
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. |
Details
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.
Value
A list carrying a kind tag and that shape's parameters.
See Also
Examples
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))
Select the "main" model parameters for diagnostic display
Description
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.
Usage
select_main_params(param_names)
Arguments
param_names |
Character vector of parameter names. |
Value
The subset of param_names that are not bracketed-index entries
(or the full vector if that would be empty).
Examples
select_main_params(c("beta[1]", "beta[2]", "log_sigma_re", "re[1]", "re[2]"))
Simulate responses from a fitted tulpa model
Description
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.
Usage
## S3 method for class 'tulpa_fit'
simulate(object, nsim = 1, seed = NULL, ...)
Arguments
object |
A |
nsim |
Number of simulated datasets (default 1). |
seed |
Optional integer seed (RNG state is restored on exit). |
... |
Ignored. |
Value
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.
Extract fitted covariate smooths
Description
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.
Usage
smooth_effects(object, term = 1L)
Arguments
object |
A |
term |
Which smoother: index or covariate name. Default 1. |
Value
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".
See Also
field_effects() for a spatial field, ranef() for random
effects.
Examples
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")
Skew-normal CDF
Description
Cumulative distribution function of the univariate skew-normal,
F(q; \xi, \omega, \alpha) = \Phi(z) - 2 T(z, \alpha),
where z = (q - \xi) / \omega and T is Owen's T function.
Owen's T is evaluated by base R numerical quadrature; no extra
dependencies are required.
Usage
sn_cdf(q, sn)
Arguments
q |
Numeric vector of quantiles. |
sn |
Skew-normal parameter list from |
Value
Numeric vector of CDF values, same length as q.
Match three cumulants to a skew-normal parameterisation
Description
Inverts the skew-normal moment formulas to convert
(mu, sigma, gamma) into (xi, omega, alpha) parameters.
Returns NULL with a warning when |gamma| exceeds the
skew-normal ceiling (~0.9953) – in that regime the third cumulant
cannot be matched by any skew-normal and the caller should fall back
to direct-quadrature quantiles.
Usage
sn_match(mu, sigma, gamma)
Arguments
mu |
Posterior mean (numeric, length 1). |
sigma |
Posterior standard deviation (positive numeric, length 1). |
gamma |
Posterior skewness (numeric, length 1). |
Value
Named list with elements xi, omega, alpha;
or NULL if |gamma| >= .SN_GAMMA_MAX.
References
Azzalini, A. (1985). A class of distributions which includes the normal ones. Scand. J. Statist. 12: 171-178.
Skew-normal quantile
Description
Inverse of the skew-normal CDF via Newton iteration on
sn_cdf() with a Brent-style bracket fallback when Newton fails to
converge.
Usage
sn_quantile(p, sn, tol = 1e-10, max_iter = 60L)
Arguments
p |
Numeric vector of probabilities in |
sn |
Skew-normal parameter list from |
tol |
Absolute tolerance on the CDF residual (default |
max_iter |
Maximum Newton iterations (default |
Value
Numeric vector of quantiles, same length as p.
Areal spatially varying coefficient field
Description
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:
\eta_i = \ldots + \sum_c X_{ic}\, z^{(c)}_{g_i},
where g_i is the graph node of observation i, z^{(c)} is
the CAR field for design column c, and X_{ic} is that column's
value at observation i. 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)
Usage
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, ...)
Arguments
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. |
Details
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.
Value
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.
See Also
spatial_car() for a single areal field passed via the
spatial = argument, spatial_svc() for the coordinate-based
(Gaussian-process) spatially varying coefficient.
Examples
# 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)
BYM2 spatial structure
Description
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)
Usage
spatial_bym2(
adjacency,
level = c("group", "obs"),
group_var = NULL,
shared = NULL,
scale_factor = NULL,
parameterization = c("standard", "collapsed")
)
Arguments
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 |
|
Value
A tulpa_spatial object
References
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.
Examples
# 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)
CAR / ICAR spatial structure
Description
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().
Usage
spatial_car(
adjacency,
level = c("group", "obs"),
group_var = NULL,
proper = FALSE,
shared = NULL,
parameterization = c("standard", "collapsed")
)
Arguments
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 |
|
Value
A tulpa_spatial object with type = "car" (or
"car_proper" when proper = TRUE).
See Also
spatial_car_proper(), spatial_bym2().
Examples
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")
Proper CAR spatial structure
Description
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.
Usage
spatial_car_proper(
adjacency,
level = c("group", "obs"),
group_var = NULL,
shared = NULL
)
Arguments
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). |
Value
A tulpa_spatial object with type = "car_proper".
See Also
spatial_car() for ICAR (rho fixed at 1),
spatial_bym2() for the BYM2 decomposition.
Examples
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)
Gaussian process spatial structure (NNGP)
Description
Specify a Gaussian-process spatial random effect, approximated with a nearest-neighbour GP (NNGP) for scalability. Captures smooth spatial variation from point-referenced coordinates.
Usage
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, ...)
Arguments
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. |
Value
A tulpa_gp object (also of class tulpa_spatial).
See Also
spatial_car(), spatial_bym2() for areal spatial effects.
Examples
# GP spatial specification from coordinate columns
spatial_gp(~ lon + lat)
spatial_gp(~ lon + lat, cov = "matern", nu = 1.5)
Multi-scale Gaussian process spatial structure
Description
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.
Usage
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, ...)
Arguments
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. |
Details
The multi-scale model decomposes spatial variation additively:
\eta(s) = X\beta + w_{local}(s) + w_{regional}(s)
where each component follows an independent Gaussian process:
w_{local}(s) \sim GP(0, \sigma^2_{local} C(\phi_{local}))
w_{regional}(s) \sim GP(0, \sigma^2_{regional} C(\phi_{regional}))
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.
Value
A tulpa_multiscale object
See Also
spatial_gp() for single-scale GP, temporal_multiscale() for
multi-scale temporal effects
Examples
# 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)
Extract spatial range and variance from a fitted spatial model
Description
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.
Usage
spatial_range(object, probs = c(0.025, 0.975))
Arguments
object |
A |
probs |
Quantile probabilities for the summary (default 0.025, 0.975). |
Details
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.
Value
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).
Examples
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)
Restricted spatial regression (RSR)
Description
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.
Usage
spatial_rsr(spatial, restrict_to)
## S3 method for class 'tulpa_rsr'
print(x, ...)
Arguments
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. |
Details
The RSR approach (Reich et al., 2006; Hodges & Reich, 2010) modifies the spatial random effect to be orthogonal to the fixed effect design matrix:
w_{RSR} = (I - P_X) w
where P_X = X(X'X)^{-1}X' 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.
Value
A modified spatial specification with RSR enabled
References
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.
See Also
Examples
# 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)
SPDE spatial field (Matern via triangular mesh)
Description
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.
Usage
spatial_spde(
coords,
data = NULL,
mesh = NULL,
boundary = NULL,
max_edge = NULL,
cutoff = 0,
nu = 1,
prior_range = NULL,
prior_sigma = NULL
)
Arguments
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, |
Value
A tulpa_spatial object with type "spde".
Examples
set.seed(42)
coords <- cbind(runif(50), runif(50))
spec <- spatial_spde(coords)
print(spec)
SPDE spatial field from custom matrices
Description
Specify a continuous Matern spatial field using externally-provided FEM matrices. Use this with meshes from fmesher, rSPDE, or any other source.
Usage
spatial_spde_custom(
C,
G,
A,
nu = 1,
prior_range = NULL,
prior_sigma = NULL,
coords = NULL
)
Arguments
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 |
Value
A tulpa_spatial object with type "spde".
Examples
# 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
Spatially varying coefficient structure
Description
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).
Usage
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, ...)
Arguments
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. |
Value
A tulpa_svc object (also of class tulpa_spatial).
See Also
spatial_gp() for a spatial random effect (rather than a varying
coefficient).
Examples
# Intercept that varies smoothly over space
spatial_svc(~ lon + lat)
Spatiotemporal interaction specifications for tulpa
Description
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().
Usage
spatiotemporal(
spatial,
temporal,
type = c("I", "II", "III", "IV", "iid", "separable"),
shared = NULL
)
Arguments
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). |
Details
Spatiotemporal interactions extend the basic additive model:
\eta_{st} = X\beta + f_s(space) + f_t(time)
to include interactions:
\eta_{st} = X\beta + f_s(space) + f_t(time) + \delta_{st}
where \delta_{st} captures space-time interactions.
Interaction Types (following Knorr-Held, 2000):
-
Type I: Unstructured interaction - IID
\delta_{st} \sim N(0, \sigma^2) -
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
C_{st} = C_s \otimes C_t -
Non-separable: GP with joint space-time metric
Type I (IID)
Independent random effect for each space-time combination:
\delta_{st} \stackrel{iid}{\sim} N(0, \sigma^2_\delta)
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:
\delta_{\cdot t}^{(s)} \sim RW(\sigma^2)
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:
\delta_{s \cdot}^{(t)} \sim ICAR(\tau)
This captures time-specific spatial patterns but assumes independence across time points.
Type IV (Full structure)
Kronecker product of spatial and temporal precision matrices:
Q_\delta = Q_s \otimes Q_t
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:
C(\mathbf{s}_1, t_1; \mathbf{s}_2, t_2) = C_s(\mathbf{s}_1, \mathbf{s}_2) \cdot C_t(t_1, t_2)
Value
Nothing: the call always signals an error.
References
Knorr-Held, L. (2000). Bayesian modelling of inseparable space-time variation in disease risk. Statistics in Medicine, 19(17-18), 2555-2567.
See Also
spatial_car(), spatial_gp(), temporal_rw1(), temporal_ar1()
Examples
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 spatiotemporal effects from fitted model
Description
Extract posterior distributions of spatiotemporal interaction effects from a fitted tulpa model.
Usage
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, ...)
Arguments
object |
A fit carrying a |
format |
Output format: |
probs |
Quantiles to compute if |
... |
Ignored |
x |
A |
Value
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.
Examples
# 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"))
Non-separable spatiotemporal GP
Description
Specify a non-separable Gaussian Process for spatiotemporal effects.
Unlike separable models where the covariance factors as C_s \otimes C_t,
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).
Usage
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
)
Arguments
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. |
Details
The non-separable covariance functions allow for more flexible space-time dependence:
Gneiting class:
C(h, u) = \frac{\sigma^2}{(a|u|^{2\alpha} + 1)^{\tau}} \exp\left(-\frac{c\|h\|^{2\gamma}}{(a|u|^{2\alpha} + 1)^{\beta\gamma}}\right)
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.
Value
Nothing: the call always signals an error.
References
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.
Examples
# No backend fits a joint space-time covariance, so the constructor refuses:
try(spatiotemporal_gp(~ lon + lat, "year", nonsep_type = "gneiting"))
Extract spatially-varying coefficients from a fitted model
Description
Extract posterior distributions of spatially-varying coefficients (SVCs) from a fitted tulpa model with SVC specification.
Usage
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, ...)
Arguments
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 |
Value
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.
See Also
spatial_svc(), plot.tulpa_svc_posterior()
Examples
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)
AR(p) temporal latent field (general-order autoregressive)
Description
A stationary autoregressive process of order p,
w_t = \sum_{j=1}^{p} \phi_j w_{t-j} + \varepsilon_t, 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.
Usage
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, ...)
Arguments
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 |
Value
A tgmrf / tulpa_latent_block object.
See Also
temporal_ar2() (the p = 2 shorthand), temporal_ar1() for the
first-order formula-integrated process, tgmrf() for the general
user-defined GMRF interface.
Examples
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")
AR1 temporal structure (first-order autoregressive)
Description
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.
Usage
temporal_ar1(time_var, group_var = NULL, shared = NULL, rho_prior = NULL)
Arguments
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 |
Details
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.
Value
A tulpa_temporal object
Examples
# 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)
AR(2) temporal latent field (second-order autoregressive)
Description
A stationary second-order autoregressive process
w_t = \phi_1 w_{t-1} + \phi_2 w_{t-2} + \varepsilon_t 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).
Usage
temporal_ar2(
time_idx,
n_times = NULL,
prior_tau_sd = 2,
prior_psi_sd = 1.5,
name = "ar2"
)
Arguments
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. |
Value
A tgmrf / tulpa_latent_block object.
See Also
temporal_ar1() for the first-order (formula-integrated) process;
tgmrf() for the general user-defined GMRF interface.
Examples
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")
Extract temporal correlation parameters from a fitted model
Description
Returns posterior summary for temporal hyperparameters (tau, rho for AR1, sigma/lengthscale for temporal GP).
Usage
temporal_corr(object, probs = c(0.025, 0.975))
Arguments
object |
A |
probs |
Quantile probabilities (default 0.025, 0.975). |
Value
A data.frame with rows for each temporal hyperparameter.
Examples
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)
Gaussian process temporal structure
Description
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
Usage
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, ...)
Arguments
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. |
Details
The GP temporal model adds a time-correlated random effect:
\eta(t) = X\beta + f(t)
where f(t) follows a Gaussian process:
f(t) \sim GP(0, \sigma^2 C(|t - t'|; \phi))
The correlation function C(d; \phi) depends on time distance d:
-
Exponential:
C(d) = \exp(-d/\phi)- continuous but not differentiable -
Matern: Smooth with tunable roughness via
\nu -
Gaussian:
C(d) = \exp(-(d/\phi)^2)- infinitely differentiable -
Periodic:
C(d) = \exp(-2\sin^2(\pi d/p)/\phi^2)- 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.
Value
A tulpa_temporal_gp object
See Also
temporal_rw1(), temporal_ar1() for equally-spaced temporal effects,
spatial_gp() for spatial GP effects
Examples
# 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)
)
Multi-scale temporal structure
Description
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.
Usage
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, ...)
Arguments
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. |
Details
At least one of trend, seasonal, or short_term must be active.
Value
A tulpa_temporal_multiscale object.
See Also
temporal_rw1(), temporal_rw2(), temporal_ar1() for
single-scale temporal priors.
Examples
# Trend + annual seasonal cycle + AR1 short-term component on monthly data
temporal_multiscale("month", trend = "rw2", seasonal = 12, short_term = "ar1")
Restricted temporal regression (RTR)
Description
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, u_{RTR} = (I - P_X) u.
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.
Usage
temporal_rtr(temporal, restrict_to)
Arguments
temporal |
A |
restrict_to |
A one-sided formula giving the covariate space the temporal
effect is made orthogonal to, e.g. |
Value
Nothing: the call always signals an error.
See Also
spatial_rsr(), temporal_rw1(), temporal_ar1()
Examples
# No backend fits a restricted temporal field, so the constructor refuses:
try(temporal_rtr(temporal_rw1("year"), ~ x))
RW1 temporal structure (first-order random walk)
Description
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).
Usage
temporal_rw1(time_var, group_var = NULL, cyclic = FALSE, shared = NULL)
Arguments
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. |
Details
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().
Value
A tulpa_temporal object.
See Also
temporal_rw2(), temporal_ar1() for other temporal priors.
Examples
# Create temporal RW1 specification
temporal_rw1("year")
temporal_rw1("month", cyclic = TRUE)
RW2 temporal structure (second-order random walk)
Description
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).
Usage
temporal_rw2(time_var, group_var = NULL, cyclic = FALSE, shared = NULL)
Arguments
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. |
Details
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).
Value
A tulpa_temporal object
Examples
# 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)
Time-varying coefficient structure
Description
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)).
Usage
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, ...)
Arguments
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. |
Value
A tulpa_tvc object.
See Also
temporal_rw1(), temporal_rw2(), temporal_ar1() for the
underlying temporal priors; temporal_gp() for a GP over time that is not
a varying coefficient.
Examples
# 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")
Test for over- or underdispersion
Description
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().
Usage
test_dispersion(object, ...)
## Default S3 method:
test_dispersion(
object,
observed = NULL,
nsim = 250L,
seed = 123L,
alternative = c("two.sided", "greater", "less"),
...
)
Arguments
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 |
|
Value
An htest object with dispersion ratio and p-value
Examples
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)
Test for outliers (simulation envelope)
Description
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().
Usage
test_outliers(object, ...)
## Default S3 method:
test_outliers(object, observed = NULL, nsim = 250L, seed = 123L, ...)
Arguments
object |
A fitted model with |
... |
Passed to methods. |
observed |
Observed response vector (optional) |
nsim |
Number of simulations (default 250) |
seed |
Random seed (default 123) |
Value
An htest object (binomial test)
Examples
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)
Test uniformity of PIT residuals
Description
If the model is correct, PIT residuals should follow Uniform(0, 1).
Applies a Kolmogorov-Smirnov test against that null.
Equivalent to DHARMa::testUniformity().
Usage
test_uniformity(
object,
observed = NULL,
nsim = 250L,
seed = 123L,
plot = FALSE
)
Arguments
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 |
Value
An htest object (KS test result)
Examples
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)
Test for zero inflation
Description
Compares the number of zeros in observed data to the distribution
expected under the fitted model. Equivalent to DHARMa::testZeroInflation().
Usage
test_zero_inflation(object, ...)
## Default S3 method:
test_zero_inflation(object, observed = NULL, nsim = 250L, seed = 123L, ...)
Arguments
object |
A fitted model with |
... |
Passed to methods. |
observed |
Observed response vector (optional) |
nsim |
Number of simulations (default 250) |
seed |
Random seed (default 123) |
Value
An htest object with zero-inflation ratio and p-value
Examples
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)
User-defined GMRF latent block
Description
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.
Usage
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, ...)
Arguments
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 |
Details
For a Gaussian latent block z ~ N(mu(theta), Q(theta)^{-1}):
\log p(z\mid\theta) = \tfrac{1}{2}\log\det Q(\theta) - \tfrac{1}{2}(z - \mu(\theta))^\top Q(\theta)(z - \mu(\theta)) + \mathrm{const}
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.
Value
An object of class c("tgmrf", "tulpa_latent_block") with
components:
-
Q,prior,mu– the user closures (orNULLformu). -
init,theta_names,theta_dim– hyperparameter metadata. -
n_latent– inferred fromQ(init). -
pattern– captured sparsity pattern as adgCMatrixof 1s. -
graph– user-supplied pattern, validated againstpatternif given. -
bounds,name– passed through.
summary() prints the block, its log prior at init and its bounds,
and returns object invisibly.
See Also
latent() for the formula slot that consumes a tgmrf block.
Examples
# 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)
User-defined GMRF latent block, compiled C++ backend
Description
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.
Usage
tgmrf_cpp(
cpp_file,
id,
init,
mu = NULL,
graph = NULL,
bounds = NULL,
obs_idx = NULL,
name = NULL,
cache_dir = tulpa_cache_dir(),
rebuild = FALSE
)
Arguments
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 |
Value
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.
See Also
tgmrf() for the R-closure path; latent() for the formula
slot that consumes a tgmrf block.
Examples
# 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)
Description
Tidy fixed-effect table (broom-compatible)
Usage
## S3 method for class 'tulpa_fit'
tidy(x, conf.level = 0.95, ...)
Arguments
x |
A |
conf.level |
Interval level (default 0.95). |
... |
Ignored. |
Value
Data frame: term, estimate, std.error, conf.low, conf.high.
Examples
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)
Fit a tulpa model
Description
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.
Usage
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(),
...
)
Arguments
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 |
Value
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.
Coverage
-
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_reto condition on, so the RE covarianceSigmais integrated rather than fixed. This covers correlated terms ((1 + x | g), a fullSigma), uncorrelated terms ((1 + x || g), a diagonalSigma), 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 atsigma_re = 1).mode = "laplace"routes to the nested-LaplaceSigmaintegrator (tulpa_re_cov_nested(), CCD design, PC + LKJ hyperprior by default – seehyperprior);control$re_cov = "gibbs"switches to the exact Metropolis-within-Gibbs debias (tulpa_re_cov_gibbs()), andcontrol$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; seen_quadintulpa_re_cov_nested()). Both also run on the sampler path (mode = "mala"/"pathfinder"). -
mode = "gibbs"(Polya-Gamma) fits a single random-intercept model forfamily = "binomial"or"neg_binomial_2", and samples the RE sd rather than conditioning onsigma_re; tune it viare_prior$prior_sigma_scaleand a mean-zerobeta_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 – calltulpa_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.esscarries 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.
References
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.
See Also
inference_mode_info(), tulpa_laplace(), mala(), pathfinder()
Examples
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)
Environment variables tulpa reads
Description
Switches read from the process environment rather than from a fitter's
control list, because each one changes how the compiled kernels partition
work or which tests run rather than what model is fitted.
Parallel scatter
TULPA_SCATTER_FORCE_PARALLEL=1Takes the per-thread parallel fill of an uncoupled arm's per-observation scatter whatever its size, instead of only where the fill work outweighs the per-thread buffers. Read on every scatter call, so it can be set within a session.
The cell-coupled scatter needs no switch: its cells are split into a fixed number of chunks set by the cell count alone, so a fit returns the same numbers at every thread count.
Test-suite tiers
TULPA_FAST, TULPA_SLOW_TESTS and TULPA_FULL_RECOVERY select which cost
tier of the test suite runs; they are read by the suite, not by the package,
and are described in tests/testthat/README.md.
Replicate an areal graph across the levels of a factor (replicated CAR)
Description
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.
Usage
tulpa_bar_field_replicate(adjacency, node, by)
Arguments
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 |
Value
A list:
adjacencythe
[L*n_node x L*n_node]block-diagonal Kronecker adjacencyI_L (x) Q(the base graph forL == 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.
See Also
tulpa_bar_field_specs() for the coefficient-column expansion,
spatial() for the inline areal field constructor whose by = argument
this powers.
Examples
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 4
Expand a varying-coefficient bar into per-column field specs
Description
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).
Usage
tulpa_bar_field_specs(formula, data)
Arguments
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 |
Value
A list with one element per design-matrix column, each a list:
column_namecharacter;
"Intercept"for the intercept column, otherwise themodel.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), orNULLfor the intercept column (which is the all-ones, unweighted field).is_interceptlogical;
TRUEfor 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 ||).
See Also
tulpa_is_spatial_bar() for the recognizer, spatial() for the
inline areal field constructor that consumes this expansion.
Examples
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 for a batched-target grid-integrated fit
Description
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.
Usage
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
)
Arguments
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 |
Value
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").
See Also
Examples
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_k
Build model matrices from a parsed formula
Description
Takes a parsed formula and data frame, and constructs:
The fixed-effects design matrix X
An offset vector (or NULL) extracted from
offset(...)termsRE group index vectors
RE slope matrices (if applicable)
Usage
tulpa_build_model_data(parsed, data)
Arguments
parsed |
A |
data |
A data frame |
Details
Missing values pass through (na.action = na.pass), because a prior
predictive may have no response; the fitting doors refuse them. Unused
levels of a grouping factor are dropped, so n_groups counts the groups
that have data.
Value
A list with:
-
y: response vector (or NULL). Acbind(successes, failures)response is resolved to the successes column. -
n_trials: binomial denominators when the response was written ascbind(successes, failures), otherwise NULL -
X: fixed-effects design matrix -
offset: numeric vector or NULL -
re_terms: list of RE data structures (group indices, slope matrices)
Examples
d <- data.frame(y = rnorm(6), x = 1:6, g = rep(1:2, 3))
parsed <- tulpa_parse_formula(y ~ x + (1 | g))
md <- tulpa_build_model_data(parsed, d)
dim(md$X)
md$n_re_terms
Remove compiled blocks from the tgmrf_cpp() cache
Description
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.
Usage
tulpa_cache_clear(older_than = 0)
Arguments
older_than |
Remove only entries whose last modification is more than
this many days ago. |
Value
The number of entries removed, invisibly.
See Also
tulpa_cache_dir() for the location.
Examples
# 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
}
Default cache directory for tgmrf_cpp()-compiled DLLs
Description
Reports 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.
Usage
tulpa_cache_dir(create = FALSE)
Arguments
create |
Create the directory if it is missing. |
Value
Absolute path (character) to the cache directory.
See Also
tulpa_cache_clear() to remove cached objects.
Examples
# Where compiled tgmrf_cpp() blocks are cached. Reporting creates nothing.
tulpa_cache_dir()
Validate a control = list() surface against its canonical key set
Description
Front-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.
Usage
tulpa_check_control(control, allowed, where)
Arguments
control |
The |
allowed |
Character vector of accepted knob names. |
where |
Name of the calling fitter, used in the error message. |
Details
Consumer packages (tulpaRatio, tulpaObs) call this with their own key
registry rather than reimplementing the check.
Value
invisible(NULL), called for the side effect of erroring on an
unknown or unnamed knob.
Examples
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"))
Model criteria from a pointwise log-likelihood
Description
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.
Usage
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, ...)
Arguments
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. |
Details
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.
Value
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.
References
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).
See Also
tulpa_psis() for the smoothing core, tulpa_pit() for the
probability-integral-transform companion, compare_models() for
model comparison.
Examples
# 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, ]
Simulation-based diagnostics for tulpa models
Description
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.
Value
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.
Posterior draws as a 3D array
Description
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.
Usage
tulpa_draws_array(fit)
Arguments
fit |
A |
Details
As with posterior_sample(), a fit that carries no draws returns NULL with
a message naming its backend and the representation it carries instead.
Value
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.
See Also
posterior_sample(), tulpa_posterior_draws(), diagnostics()
Examples
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]
Empirical-Bayes random-effect covariances
Description
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.
Usage
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()
)
Arguments
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: |
Details
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.
Value
A tulpa_fit with:
-
mode,H_beta: the fixed-effect (and, on then_quad = 1path, random- effect) mode and the fixed-effect precision attheta_hat, drivingcoef/confint/vcov/summary. -
map: theSigma/sigma/rhosummary attheta_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 attheta_hatconverged. -
outer_convergence:optim's code for the maximization overSigma(0on success). A non-zero value also warns. With
marginal = TRUEand a correction that formed:cov_marginal(the widened fixed-effect covariance, whichvcov/summary/confintthen report),cov_conditional(thesolve(H_beta)they would otherwise have reported, kept so the two are comparable on one fit),H_thetaandtheta_cov(the outer Hessian attheta_hatand its inverse, named bytheta_names), and themarginal_step/marginal_richardsonactually used. All absent when the correction was not requested or could not be formed, sois.null(fit$cov_marginal)tests whether the reported intervals are marginal. A requested correction that fails warns and leaves the conditional covariance in place.
References
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.
See Also
tulpa_re_cov_nested() to integrate over Sigma rather than fix it;
tulpa_laplace() for the inner solve.
Examples
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 deviation
Fit a latent-variable model via EM + Laplace approximation
Description
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.
Usage
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,
...
)
Arguments
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 |
Details
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.
Value
A list with:
-
fits– named list oftulpa_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 whencorrection %in% c("mi", "gibbs"). Named list of pooled per-submodel summaries fromrubins_pool(). -
draws– present whencorrection %in% c("mi", "gibbs"). List of per-draw fits withbeta/seattached.
Soft latent labels go in 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
Q = \sum_i [\, w_i \log p_i + (1 - w_i) \log (1 - p_i) \,],
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.
Examples
# 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
Generic Monte-Carlo EM driver
Description
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.
Usage
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,
...
)
Arguments
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 |
Value
A list with:
-
pooled– named list of pooled per-submodel summaries (mean,se,V_within,V_between,V_total); seerubins_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_mcvalue used in the last iteration. -
converged– logical. -
history–data.frame(iter, n_mc, delta).
Tier
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.
References
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.
See Also
tulpa_em_laplace() for the closed-form-weights variant,
rubins_pool() for the pooling rule.
Examples
# 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
Expectation-Propagation fit for a GLM
Description
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.
Usage
tulpa_ep(
formula,
data,
family = "binomial",
phi = 1,
phi2 = NULL,
n_trials = NULL,
beta_prior = .tulpa_default_beta_prior("ep"),
control = list()
)
Arguments
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 |
Value
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.
References
Minka (2001). Expectation Propagation for approximate Bayesian inference. UAI. Rasmussen & Williams (2006). Gaussian Processes for Machine Learning, Algorithm 3.5.
See Also
tulpa() (Laplace / sampler tiers), pathfinder() (VI).
Examples
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)
Construct a minimal tulpa_family for simulation
Description
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.
Usage
tulpa_family(
name,
simulate_fn,
process_names = "y",
extra_params = list(),
link_inv = NULL
)
## S3 method for class 'tulpa_family'
print(x, ...)
Arguments
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. |
Value
A tulpa_family object.
Examples
fam <- tulpa_family(
name = "poisson",
simulate_fn = function(eta, params, n_obs, ...) {
rpois(n_obs, exp(eta[[1]]))
}
)
Build the summary of a tulpa fit around a coefficient table
Description
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.
Usage
tulpa_fit_summary(object, coefficients, level = 0.95)
Arguments
object |
A |
coefficients |
Data frame, one row per coefficient (row names are the
coefficient names), with columns |
level |
Coverage of the interval columns. |
Value
A summary.tulpa_fit object, as described on summary.tulpa_fit().
See Also
Examples
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)
Formula parsing for tulpa models
Description
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.
Value
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.
Fit a Gaussian linear model via tulpa's generic engine
Description
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).
Usage
tulpa_gaussian(
formula,
data,
beta_prior = .tulpa_default_beta_prior("gaussian"),
control = list()
)
Arguments
formula |
A formula (e.g., y ~ x1 + x2) |
data |
A data frame |
beta_prior |
Fixed-effect prior as |
control |
List of sampler knobs: |
Value
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.
Examples
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_rate
Fit via Polya-Gamma Gibbs sampler
Description
Public API for PG Gibbs sampling. Used by model packages for binomial and negative binomial GLMMs.
Usage
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()
)
Arguments
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): |
Details
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.
Value
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.
Examples
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)
Read a default nested-Laplace outer-grid axis
Description
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.
Usage
tulpa_grid_axis(key, n = NULL)
Arguments
key |
Name of a default axis, e.g. |
n |
Optional resolution override: same |
Value
Numeric vector of grid nodes.
Examples
tulpa_grid_axis("field_sd")
tulpa_grid_axis("copy_alpha", n = 9)
Per-cell log quadrature weight of an outer grid
Description
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.
Usage
tulpa_grid_log_quad(
theta_grid,
specs = NULL,
copy_slab = "exponential",
close_domain = TRUE,
folded_axes = NULL,
refining = NULL
)
Arguments
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
|
Value
Numeric vector of per-cell log quadrature weights, length
nrow(theta_grid), or NULL if theta_grid has no axis names.
See Also
tulpa_theta_matrix(), tulpa_normalise_weights_safe(),
tulpa_joint_axis_specs_from_grid()
Examples
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)
Validate/default a copy-scale slab measure choice
Description
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.
Usage
tulpa_hyper_check_copy_slab(x)
Arguments
x |
A |
Value
x, defaulted to "exponential" when NULL.
See Also
tulpa_hyper_copy_slab_density()
Examples
tulpa_hyper_check_copy_slab("exponential")
try(tulpa_hyper_check_copy_slab("uniform"))
The copy-scale axis's PC-prior density
Description
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.
Usage
tulpa_hyper_copy_slab_density(upper)
Arguments
upper |
The largest declared node of the copy-scale axis. |
Value
A function log p(x) = log(lambda) - lambda * x, or NULL if
upper is not a finite positive number.
See Also
tulpa_hyper_check_copy_slab(), tulpa_joint_axis_specs_from_grid()
Examples
lp <- tulpa_hyper_copy_slab_density(upper = 2)
lp(c(0.1, 1, 2))
Hyperparameter draws from a nested-Laplace fit
Description
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.
Usage
tulpa_hyper_draws(fit, cells = NULL, n = 1000, within = NULL)
Arguments
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, |
Value
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.
See Also
tulpa_posterior_draws(), tulpa_nested_laplace()
Examples
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))
Outer hyperparameter-grid integration with a user-supplied inner fit
Description
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.
Usage
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()
)
Arguments
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:
|
Value
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 integrandinner$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 ofaxis$log_prior(theta_cell[axis])).0when all axes havelog_prior = NULL. -
weights– numeric[n_cells]summing to 1 (or allNAwith 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 percombine:-
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_covanddrawsareNULL. -
combine = "none": all three areNULL.
-
-
means,param_names,process_info,n_samples,n_params,N– thetulpa_fitaccessor surface, populated when a fixed-effect posterior is assembled. -
hyper_specs– echoed list ofhyper_axis_specobjects (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.
See Also
hyper_axis_spec() for the axis-spec constructor;
tulpa_nested_laplace_joint() for the family-specific outer-grid
driver that this helper generalises.
Examples
# 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
Per-axis integrated support of an outer grid
Description
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.
Usage
tulpa_hyper_grid_supports(theta_grid, specs, refining = NULL)
Arguments
theta_grid |
A named |
specs |
Per-axis spec list, e.g. from
|
refining |
Optional per-cell refinement tag vector (see
|
Value
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.
See Also
tulpa_joint_axis_specs_from_grid(), tulpa_hyper_slice_home()
Examples
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))
Per-cell refinement tag of an outer grid
Description
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.
Usage
tulpa_hyper_slice_home(refining, n)
Arguments
refining |
The |
n |
Number of grid cells; |
Value
A character vector of length n: "" for a declared cell, the
axis name for a cell a refinement pass added.
See Also
tulpa_hyper_grid_supports(), tulpa_joint_axis_specs_from_grid()
Examples
tulpa_hyper_slice_home(NULL, 3)
tulpa_hyper_slice_home(c("", "", "sigma"), 3)
Select the symplectic integrator for HMC and NUTS
Description
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.
Usage
tulpa_integrator(name, mts_substeps = 4L)
Arguments
name |
Integrator name: |
mts_substeps |
Number of inner prior-force substeps per trajectory step
for the |
Details
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.
Value
If name is omitted, the current integrator name. If name is
given, the previous name is returned invisibly.
Examples
tulpa_integrator() # current integrator
old <- tulpa_integrator("yoshida4")
tulpa_integrator(old) # restore
Recognize an inline varying-coefficient bar
Description
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().
Usage
tulpa_is_spatial_bar(x)
Arguments
x |
A one-sided formula (e.g. |
Value
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).
See Also
tulpa_bar_field_specs() for expanding the bar into per-column
field specs.
Examples
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)
Progress reporter for a counted R-side fitting loop
Description
Build a progress reporter for a counted R loop (an EM iteration, a
per-species outer loop, ...) – the R-side counterpart of the C++
tulpa_progress::GridProgress reporter, sharing its wire format so a
detached reader sees the same heartbeat file regardless of which kind of
loop produced it. Config comes from the scoped tulpa.nl_progress option
(progress, progress_file, progress_every, progress_throttle); a
caller outside tulpa's own control$progress machinery sets the same
option to opt in.
Two independently gated channels: a console bar, emitted when progress
is set on the option, and a heartbeat file, written whenever
progress_file is non-empty regardless of progress – a file write
survives a detached Start-Process / nohup stdout buffer where a
console flush does not, so it is the only liveness signal on a headless
box.
Usage
tulpa_iter_progress(label, total, unit = "iter", threads = 1L)
Arguments
label |
Short name for the loop, shown in the console line. |
total |
Iteration denominator (e.g. |
unit |
Name for one step in the console line ( |
threads |
Active outer-thread count. When |
Value
A list of two closures: tick(), called once per completed step,
and finish(), called once after the loop (forces a final
100%/heartbeat emit). When neither channel is requested both are
zero-overhead no-ops.
Rebuild axis specs from an already-assembled outer grid
Description
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.
Usage
tulpa_joint_axis_specs_from_grid(
theta_grid,
copy_slab = "exponential",
folded_axes = NULL,
logchol = .hp_logchol_designs(theta_grid)
)
Arguments
theta_grid |
A named |
copy_slab |
|
folded_axes |
Optional names of axes folded onto |
logchol |
Free-covariance block design(s) as returned by
|
Value
A named list of per-axis spec lists, or NULL if theta_grid has
no axis names.
See Also
tulpa_theta_matrix(), tulpa_grid_log_quad(),
tulpa_hyper_grid_supports()
Examples
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_scale
Fit multiple joint nested-Laplace models through one fused grid solve
Description
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.
Usage
tulpa_joint_grid_batch(fits)
Arguments
fits |
A non-empty list of functions of no arguments, each performing one ordinary joint nested-Laplace fit. |
Details
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.
Value
A list of fit results, one per element of fits, in order.
See Also
Examples
# 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)
Per-cell constrained inverse-precision sub-block for a joint fit
Description
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.
Usage
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
)
Arguments
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. |
Details
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.
Value
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.
Examples
# 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
Outer-grid posterior covariance of a joint fit's latent coordinates
Description
The law-of-total-covariance mixture over the outer grid,
V = \sum_k w_k [C_k + (m_k - \bar m)(m_k - \bar m)^T],
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.
Usage
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
)
Arguments
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. |
Value
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.
Examples
# 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]
K-fold cross-validation for a tulpa fit
Description
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
\log \frac{1}{S}\sum_s p(y_i \mid \eta_i^{(s)}) 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().
Usage
tulpa_kfold(object, data, K = 10L, folds = NULL, n_trials = NULL, seed = NULL)
Arguments
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. |
Value
A list with elpd_kfold (summed held-out elpd), se_elpd_kfold (its
standard error), pointwise (per-observation held-out elpd), folds, and
K.
See Also
tulpa_criteria() for PSIS-LOO / WAIC on a single fit.
Examples
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_kfold
Fit a model via Laplace approximation
Description
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.
Usage
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
)
Arguments
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 |
Value
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 fromconvergedand 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)
Examples
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 effects
Fit a beta-regression model via Laplace, estimating the precision
Description
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).
Usage
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()
)
Arguments
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 |
Value
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().
Examples
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$mode
Latent factor specification for unmeasured confounders
Description
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.
Value
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(...))).
Streaming pointwise log-likelihood
Description
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.
Usage
tulpa_loglik(x, n_obs = NULL, n_draws = NULL)
Arguments
x |
Either a numeric |
n_obs, n_draws |
Required when |
Value
A tulpa_loglik object: a list with get(cols), n_obs,
n_draws, and materialized.
See Also
Examples
ll <- tulpa_loglik(matrix(rnorm(20 * 5, -1), nrow = 20)) # 20 draws x 5 obs
c(ll$n_draws, ll$n_obs)
Multinomial (nominal K-class) logistic regression via Laplace
Description
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.
Usage
tulpa_multinomial(
formula,
data,
beta_prior = .tulpa_default_beta_prior("multinomial"),
control = list()
)
## S3 method for class 'tulpa_multinomial'
print(x, ...)
Arguments
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. |
Value
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.
See Also
tulpa() for single-process GLMMs.
Examples
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)
Normalise outer-grid log-marginals to posterior cell weights
Description
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.
Usage
tulpa_normalise_weights_safe(lm, what = "grids / data", log_quad = NULL)
Arguments
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 |
Value
Numeric vector of posterior cell weights summing to 1, the same
length as lm, or all-NA if no cell carries finite mass.
See Also
tulpa_theta_matrix(), tulpa_grid_log_quad()
Examples
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)))
Fit a beta-regression model via NUTS (joint sampling of beta + log_phi)
Description
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.
Usage
tulpa_nuts_beta(
y,
X,
beta_prior = .tulpa_default_beta_prior("beta_nuts"),
log_phi_prior_sd = 3,
log_phi_init = 0,
control = list()
)
Arguments
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): |
Value
A tulpa_fit object with:
-
draws–n_samples x (p + 1)matrix of post-warmup draws, columns named fromcolnames(X)(falling back tobeta[1] ... beta[p]) thenlog_phi. -
means– posterior means. -
phi_summary– posterior mean / median / quantiles ofphi = exp(log_phi). -
accept_prob,divergent,treedepth,epsilon– NUTS diagnostics from the underlying chain.
See Also
tulpa_laplace_beta() for the Laplace + Brent point
estimate; tulpa_laplace() for the underlying Laplace engine.
Examples
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)
Sample an SPDE GLM via NUTS, optionally jointly over Matern hypers
Description
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 transformw = L(theta)^{-T} z. A PC prior (Fuglstad et al. 2019 JASA) on(range, sigma)is taken fromprior_range,prior_sigma. Returns additionalw_draws,range_draws,sigma_draws,kappa_draws,tau_drawscolumns/vectors on top of the raw parameter draws.
Usage
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()
)
Arguments
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): |
Value
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).
See Also
fit_spde() for the Laplace counterpart and the nested-Laplace
path over (range, sigma).
Examples
# 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
Ordinal (ordered K-class) cumulative-logit regression via Laplace
Description
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.
Usage
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, ...)
Arguments
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 |
Value
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.
See Also
tulpa_multinomial() for the nominal (unordered) case.
Examples
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$cutpoints
The outer hyperparameter grid of a nested-Laplace fit
Description
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.
Usage
tulpa_outer_grid(fit)
Arguments
fit |
A nested-Laplace fit: |
Value
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.
NULLwhen 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);NULLwhen the fit records none.modesnumeric matrix
[n_cells x n_latent], each cell's inner Laplace mode, orNULLwhen the fit keeps none.n_fixedhow many leading columns of
modesare fixed effects.
See Also
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.
Examples
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)]
Parse a mixed-model formula
Description
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.
Usage
tulpa_parse_formula(formula)
## S3 method for class 'tulpa_parsed_formula'
print(x, ...)
Arguments
formula |
A formula object (e.g., |
x |
A |
... |
Ignored. |
Value
A list with:
-
response: character, the response variable label (NULLif none) -
response_expr: language object, the LHS of~(NULLif 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 (typicallytgmrfS3 objects), one perlatent(...)term in the original formula. Empty list when nolatent(...)terms are present. -
n_latent_blocks: integer, number oflatent(...)terms -
original: the original formula
Examples
pf <- tulpa_parse_formula(y ~ x1 + x2 + (1 | group) + (x1 || site))
pf$response # "y"
pf$fixed_formula # y ~ x1 + x2
Probability integral transform from a predictive CDF
Description
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.
Usage
tulpa_pit(
cdf,
cdf_lower = NULL,
jitter = TRUE,
log_lik = NULL,
tail_points = NULL,
n_threads = 1L
)
Arguments
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 |
Details
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.
Value
Numeric vector of length n_obs of PIT values in [0, 1].
See Also
tulpa_criteria(), tulpa_psis()
Examples
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)
Posterior draws from a nested-Laplace fit
Description
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.
Usage
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, ...)
Arguments
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. |
Value
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.
What a draw covers
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).scopeis"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.
scopeis"fixed".
See Also
tulpa_hyper_draws(), tulpa_nested_laplace(),
tulpa_nested_laplace_joint(), posterior_sample()
Examples
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 draws
Posterior draws from a joint nested-Laplace fit
Description
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).
Usage
## S3 method for class 'tulpa_nested_laplace_joint'
tulpa_posterior_draws(fit, idx = NULL, n = 1000, ...)
Arguments
fit |
A |
idx |
Optional integer vector of 1-based latent indices to return.
|
n |
Number of posterior draws (default 1000). |
... |
Unused; for S3 compatibility. |
Details
The constrained draw at cell k uses the sparse Cholesky of the stored
precision Q_k and the conditioning-by-kriging correction
z_c = z - Q_k^{-1} A^\top (A Q_k^{-1} A^\top)^{-1} A z,
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).
Value
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".
See Also
tulpa_posterior_draws(), tulpa_nested_laplace_joint(),
posterior_sample()
Power-scaling prior / likelihood sensitivity
Description
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.
Usage
tulpa_powerscale_sensitivity(
fit,
prior = NULL,
lower_alpha = 0.99,
upper_alpha = 1.01,
threshold = 0.05
)
Arguments
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). |
Value
A data frame with one row per fixed-effect parameter and columns
variable, prior, hyperparameter, likelihood, diagnosis.
References
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.
See Also
tulpa_psis(), tulpa_criteria().
Examples
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))
Prior specification for tulpa models
Description
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 asbetahere.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, andre_prior$prior_sigma_scaleformode = "gibbs"); an inverse-Wishart undercontrol$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.
Usage
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 = "", ...)
Arguments
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. |
Details
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().
Value
A tulpa_priors object
format() returns the prior as a character string.
References
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.
Examples
# 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
Profile a fit by solver phase
Description
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).
Usage
tulpa_profile(expr, sort = TRUE)
Arguments
expr |
An expression that runs a fit (for example a call to
|
sort |
Logical; order rows by descending time. Default |
Details
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.
Value
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.
Which fits are timed
The single-response Laplace solve behind
tulpa_laplace(),tulpa(mode = "laplace")and every per-cell inner solve oftulpa_nested_laplace()/tulpa(mode = "nested_laplace"): phaseseta,scatter,factorize,line_search,log_det,log_lik_prior,hessian_extractandinner_diagnostics.The joint solver behind
tulpa_nested_laplace_joint(), on both its dense and its sparse path (the sparse one addspattern_buildandprep).The nested-Laplace outer grid, single-response and joint:
outer_grid_cell, one call per solved cell.The samplers:
nuts_warmupandnuts_sampling, one call per NUTS iteration per chain, andgradient, 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.
Enclosing phases
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.
Examples
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"))
Pareto-smoothed importance sampling
Description
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.
Usage
tulpa_psis(log_ratios, tail_points = NULL)
Arguments
log_ratios |
Numeric vector of (unnormalized) log importance ratios
|
tail_points |
Number of upper-tail order statistics for the
generalized-Pareto fit, or |
Value
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).
References
Vehtari, Simpson, Gelman, Yao & Gabry (2024). Pareto smoothed importance sampling. JMLR 25(72):1-58.
See Also
diagnostics() for the fit-level diagnostic front door.
Examples
set.seed(1)
# Well-behaved importance ratios: k-hat is small.
ps <- tulpa_psis(rnorm(2000))
ps$pareto_k
ps$is_ess
Adaptive Gauss-Hermite refinement of a grouped random-effect covariance
Description
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
M_g = \int \exp\{\ell_g(b_g)\}\, N(b_g; 0, \Sigma)\, db_g,
where b_g is the group's random-effect vector (dimension
\sum_m c_m over the RE terms) and \ell_g(b_g) is the group's
conditional log-likelihood when its linear predictors are perturbed by
b_g. How b_g 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 \ell_g in
b_g (for the mode) and \ell_g at the quadrature nodes (for the
sum). The fixed parameters theta and the log-Cholesky coordinates of
\Sigma are optimized jointly on \sum_g \log M_g; 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 (\ell_g(b_g) = \sum_{i \in g} \log f_i(\eta_i + Z_i b_g)for one linear predictor\eta). The engine builds the oracle from the per-row marginal and the RE designZitself. -
make_group– the general / multi-arm case. The caller supplies the per-groupb-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).
Usage
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
)
Arguments
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). |
Value
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.
References
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.
Examples
# 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
Gibbs estimation of random-effect covariances (exact-target debias)
Description
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.
Usage
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()
)
Arguments
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:
|
Details
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 givenbetaand the covariances). The proposal shape is the Laplace per-group posterior covariance block (return_re_covfromtulpa_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 blockH_beta. -
Sigma_m | b_m– exact conjugate draw. A correlated block draws the full matrix fromIW(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.
Value
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 norho) and columnsmean,sd,median,ci_lo,ci_hi. -
Sigma_mean: the posterior mean ofSigma(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 ofSigmamatrices for that sweep. -
beta_draws/draws: matrix of recordedbetadraws;draws(withmeans,param_names,process_info) drives the generictulpa_fitmethods. -
re: matrix of recorded random-effect draws,n_keptrows by one column per (block, group, coefficient) in that order – the exact per-group posterior the sweep samples. Row-aligned withbeta_draws, so a draw is a joint(beta, b)state. This is whatranef()summarizes and whatposterior_predict()adds to the linear predictor. -
accept: list withbetaandbacceptance rates over recorded sweeps. -
n_kept,n_coefs(vector of per-blockc),prior: bookkeeping.
References
Lewandowski, Kurowicka & Joe (2009). Generating random correlation matrices based on vines and extended onion method. Journal of Multivariate Analysis 100(9):1989-2001.
See Also
tulpa_re_cov_nested() for the grid-integration (summary-bias) fix;
tulpa_laplace() for the pilot solve and per-group covariance blocks.
Examples
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 mean
Nested-Laplace integration over random-effect covariances
Description
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.
Usage
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()
)
Arguments
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:
|
Details
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()) of1 + 2k + 2^(k-q)points for the totalk = sum_blocksparameters, with the corrected R-INLA design weights (ccd_weights()). Scales polynomially ink, where the tensor grid is exponential. -
"grid": the fulln_per_axis^ktensor product with uniform cell weights – denser and more robust to a non-Gaussian whitened posterior, but only tractable for smallk.
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.
Value
A list with:
-
posterior: data frame with one row per parameter and columnsmean,sd,median,ci_lo,ci_hi. Parameter names aresigma_i,rho_ij,Sigma_ijfor a single block, prefixed by the block label (g.sigma_1, ...) when there are several blocks. Diagonal blocks report norho. -
map: the plug-in-mode summary attheta_hat(a singlelist(Sigma, sigma, rho)for one block, or a named list of them). -
Sigma_mean: the weighted posterior mean ofSigma(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 (drivescoef/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 theweights-mixture of them.NULLatn_quad > 1, whose inner marginal integrates each group out instead of conditioning on it; that fit carriesranef_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 itssourcecolumn. -
theta_hat,theta_grid,weights,log_marginal,n_grid,layout,n_blocks,n_coefs(vector of per-blockc). -
subspace_debias: present only whencontrol$subspace_debiaswas set.idxare the corrected latent coordinates,bandsthe per-probed-index reliability table they were read from,closure_addedwhat the coupling closure added, andacceptthe per-node Metropolis acceptance rate.
References
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.
See Also
tulpa_laplace() for the inner solve; tulpa_nested_laplace() for
the analogous outer integration over spatial / temporal prior
hyperparameters.
Examples
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 covariance
Selective refit of high-Pareto-k observations (reloo)
Description
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.
Usage
tulpa_reloo(
object,
data,
k_threshold = .nl_diag("k_usable"),
n_trials = NULL,
ndraws = NULL
)
Arguments
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). |
Value
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.
See Also
tulpa_kfold() for the full refit-CV; tulpa_criteria() for
PSIS-LOO / WAIC without refits.
Examples
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_loo
Draw Polya-Gamma random variates
Description
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.
Usage
tulpa_rpg(b, z)
Arguments
b |
Integer vector of PG shape parameters (trial counts); one draw per element. |
z |
Numeric vector of tilting parameters, the same length as |
Value
A numeric vector of PG(b, z) draws, the same length as b.
Examples
tulpa_rpg(rep(1L, 5), rnorm(5))
Simulate data from a tulpa model
Description
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.
Usage
tulpa_simulate(
formula,
family,
data,
theta = NULL,
n_sims = 1L,
priors = NULL,
seed = NULL,
...
)
## S3 method for class 'tulpa_simulate'
print(x, ...)
Arguments
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 |
Value
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.
Examples
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) # 3
Spatial structure specifications for tulpa
Description
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.
Value
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().
SPDE field hyperprior log density on (range, sigma)
Description
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.
Usage
tulpa_spde_log_hyperprior(range, sigma, sp, hyperprior = "proper")
Arguments
range, sigma |
Numeric vectors of range / marginal-SD nodes (natural scale, recycled against each other). |
sp |
A |
hyperprior |
|
Value
Numeric vector of log-density values, one per (range, sigma)
pair.
See Also
tulpa_spde_precision_Q(), spatial_spde()
Examples
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)
Rebuild the SPDE/Matern field precision matrix
Description
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.
Usage
tulpa_spde_precision_Q(spatial, kappa, tau_spde)
Arguments
spatial |
A |
kappa |
Spatial frequency |
tau_spde |
SPDE precision scale (see |
Value
A sparse n_mesh x n_mesh precision matrix.
See Also
tulpa_spde_log_hyperprior(), fit_spde(), spatial_spde()
Examples
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)
Temporal structure specifications for tulpa
Description
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.
Value
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().
Fit a custom tgmrf latent block
Description
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 overtheta(the "Laplace + MH debias" composition). -
"nuts"– Tier-1 exact MCMC: NUTS over the marginalthetaposterior. -
"vi"– Tier-2 structured: a single-path Pathfinder Gaussian fit of thethetaposterior (no bias correction). -
"nuts_joint"– Tier-1 exact MCMC sampling the FULL joint(beta, z, theta)rather than the Laplace-marginalizedtheta(requires a C++-backend block, seetgmrf_cpp()).
The inference method is an argument, not a parallel verb.
Usage
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(),
...
)
Arguments
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 |
Value
A tulpa_fit object; $backend and $mode record
the engine used.
See Also
tgmrf() for the block, tgmrf_cpp() for the compiled-block form.
Examples
# An iid random intercept written as a tgmrf block over 15 groups; the
# inference method is an argument, not a parallel verb. See the
# custom-process article at https://gillescolling.com/tulpa/articles/tgmrf.html.
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
A fit's outer grid as a named matrix
Description
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.
Usage
tulpa_theta_matrix(res)
Arguments
res |
A |
Value
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.
See Also
tulpa_grid_log_quad(), tulpa_normalise_weights_safe()
Examples
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)
Posterior predictive checks for tulpa models
Description
Visual and numerical checks comparing observed data to posterior predictive distributions. Essential for assessing model fit.
Value
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.
Empirical semivariogram of residuals
Description
Computes the empirical semivariogram in distance bins for visual assessment of remaining spatial structure in residuals.
Usage
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, ...)
Arguments
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 |
Value
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.
Examples
set.seed(1)
coords <- cbind(runif(60), runif(60))
tulpa_variogram(rnorm(60), coords, n_bins = 5)
Extract temporally-varying coefficients from a fitted model
Description
Extract posterior distributions of temporally-varying coefficients (TVCs) from a fitted tulpa model with TVC specification.
Usage
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), ...)
Arguments
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 |
Value
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.
See Also
temporal_tvc(), plot.tulpa_tvc_posterior()
Examples
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")
Validate a GP spatial specification against data
Description
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.
Usage
validate_gp(gp, data)
Arguments
gp |
A |
data |
Data frame containing the coordinate columns named in |
Value
Updated spatial object with computed neighbor structure.
See Also
validate_temporal_multiscale(), spatial_gp(),
spatial_multiscale()
Examples
set.seed(1)
d <- data.frame(lon = runif(30), lat = runif(30))
gp <- validate_gp(spatial_gp(~ lon + lat, nn = 5), d)
gp$n_spatial
Validate that a fit used the expected mode
Description
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.
Usage
validate_mode(fit, expected_mode, error = TRUE)
Arguments
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. |
Value
If error = FALSE, returns TRUE/FALSE. Otherwise errors on mismatch.
Examples
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) # FALSE
Validate a multi-scale temporal specification against data
Description
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.
Usage
validate_temporal_multiscale(temporal, data)
Arguments
temporal |
A |
data |
Data frame containing the time column named in |
Value
Updated object with indices computed.
See Also
validate_gp(), temporal_multiscale()
Examples
tm <- temporal_multiscale("t", trend = "rw1", seasonal = 12)
tm <- validate_temporal_multiscale(tm, data.frame(t = 1:36))
class(tm)
Variance-covariance matrix of the fixed effects
Description
Variance-covariance matrix of the fixed effects
Usage
## S3 method for class 'tulpa_fit'
vcov(object, ...)
Arguments
object |
A |
... |
Ignored. |
Value
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).
Widely applicable Bayesian information criterion (WBIC)
Description
Watanabe's (2013) WBIC: the expected negative log-likelihood under the
posterior tempered at inverse temperature beta = 1 / log(n),
\mathrm{WBIC} = -E_\beta\left[\sum_{i=1}^n \log p(y_i \mid \theta)\right],
\qquad p_\beta(\theta) \propto p(\theta) \prod_{i=1}^n p(y_i \mid \theta)^\beta.
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.
Usage
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, ...)
Arguments
object |
A |
... |
Unused. |
x |
A |
digits |
Decimal places printed. |
Details
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.
Value
A tulpa_wbic object: a list with wbic, the inverse temperature
beta, n_obs and n_draws.
References
Watanabe, S. (2013). A widely applicable Bayesian information criterion. Journal of Machine Learning Research 14:867-897.
See Also
loo::waic(), dic() and compare_models() for the other
criteria; tulpa_criteria().
Examples
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)
Run an expression under a chosen symplectic integrator
Description
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.
Usage
with_tulpa_integrator(name, expr, mts_substeps = 4L)
Arguments
name |
Integrator name, as for |
expr |
Expression to evaluate. Evaluated in the caller's environment. |
mts_substeps |
Inner prior-force substeps for |
Value
The value of expr.
See Also
Examples
with_tulpa_integrator("yoshida4", tulpa_integrator())
tulpa_integrator() # unchanged