| Title: | Bayesian Occupancy, Abundance and Detection Models via 'tulpa' |
|---|---|
| Description: | Fits Bayesian models for species observed with imperfect detection: single-season and dynamic site occupancy (MacKenzie et al. (2002) <doi:10.1890/0012-9658(2002)083[2248:ESORWD]2.0.CO;2>), N-mixture abundance from replicated counts (Royle (2004) <doi:10.1111/j.0006-341X.2004.00142.x>), distance sampling, removal and double-observer counts, false-positive and time-to-detection occupancy, integrated models combining several data sources, multi-species and joint species distribution models, and a hurdle model for vegetation cover. Linear predictors take fixed, random, spatial and temporal effects. Each model supplies its observation likelihood to the 'tulpa' engine, which fits it by Laplace approximation, by nested Laplace integration over the hyperparameters, or by Hamiltonian Monte Carlo with the No-U-Turn Sampler (NUTS) using automatic differentiation. Simulation, prediction and diagnostic functions (posterior predictive checks, leave-one-out cross-validation, simulation-based calibration) accompany each model. |
| Authors: | Gilles Colling [aut, cre, cph] (ORCID: <https://orcid.org/0000-0003-3070-6066>) |
| Maintainer: | Gilles Colling <[email protected]> |
| License: | MIT + file LICENSE |
| Version: | 0.6.1 |
| Built: | 2026-10-18 11:34:02 UTC |
| Source: | https://github.com/gcol33/tulpaobs |
Allows accessing spOccupancy-style fields (e.g., $beta.samples,
$psi.samples) on tobs_fit objects. Since tobs stores actual posterior
draws, this is a thin remapping layer.
## S3 method for class 'tobs_fit' x$name## S3 method for class 'tobs_fit' x$name
x |
A |
name |
Field name to access. |
The requested field value.
sim <- simulate_occu(N = 100, J = 3, n.occ.covs = 1, n.det.covs = 1, seed = 1) fit <- tobs(~ occ_cov1, data = sim$data, family = occu(), detection = ~ det_cov1, y = sim$y, method = "laplace", control = list(verbose = FALSE, progress = FALSE)) dim(fit$beta.samples) dim(fit$alpha.samples)sim <- simulate_occu(N = 100, J = 3, n.occ.covs = 1, n.det.covs = 1, seed = 1) fit <- tobs(~ occ_cov1, data = sim$data, family = occu(), detection = ~ det_cov1, y = sim$y, method = "laplace", control = list(verbose = FALSE, progress = FALSE)) dim(fit$beta.samples) dim(fit$alpha.samples)
Latent Poisson (or NB) abundance with binomial detection per visit (Royle 2004).
abun(K.max = NULL, mixture = c("poisson", "negbin", "zip", "zinb"))abun(K.max = NULL, mixture = c("poisson", "negbin", "zip", "zinb"))
K.max |
upper bound for the latent-abundance marginal sum (the exact
integration over |
mixture |
latent-abundance distribution, both fitted via tulpa's
closed-form marginal Laplace engine. |
Royle's (2004) N-mixture model: latent abundance N_i ~ Poisson(lambda_i)
with log lambda_i = X_lambda beta_lambda, and counts
y_ij | N_i ~ Binomial(N_i, p_ij) with logit p_ij = X_p beta_p. The
abundance formula is the tobs() formula; the per-visit detection formula
is detection. The marginal likelihood integrates N out exactly, so no
route runs an EM over N.
A tobs_family object.
method = "laplace" – a direct Laplace approximation over the closed-form
marginal with an observed-Fisher covariance; a grouped random effect on one
arm is integrated by adaptive Gauss-Hermite quadrature.
method = "nested_laplace" – an areal icar() / bym2() / car_proper()
field on the abundance arm, its hyperparameters integrated on an outer grid
(grid-integrated coefficient covariance).
method = "nuts" – the in-tree C++ NUTS sampler over the same marginal,
Poisson or negative binomial (log_r sampled), warm-started at the Laplace
mode; with an areal term a fixed-hyper non-centered field on the abundance
arm (icar() / car_proper() / bym2()); "zip" / "zinb" are
"laplace"-only, and a field does not combine with a random effect here.
sim <- simulate_abun(N = 120, J = 4, n.abund.covs = 1, n.det.covs = 1, seed = 1) fit <- tobs(~ abund_cov1, data = sim$data, family = abun(), detection = ~ det_cov1, y = sim$y, method = "laplace") summary(fit)sim <- simulate_abun(N = 120, J = 4, n.abund.covs = 1, n.det.covs = 1, seed = 1) fit <- tobs(~ abund_cov1, data = sim$data, family = abun(), detection = ~ det_cov1, y = sim$y, method = "laplace") summary(fit)
The tulpa::check_model() method for tobs_fit: the criteria, the
posterior-predictive check and the goodness-of-fit tests in one call,
printed as a report and drawn as the diagnostic panel.
## S3 method for class 'tobs_fit' check_model( object, coords = NULL, n.samples = 250, nsim = NULL, seed = NULL, plot = TRUE, ... )## S3 method for class 'tobs_fit' check_model( object, coords = NULL, n.samples = 250, nsim = NULL, seed = NULL, plot = TRUE, ... )
object |
A |
coords |
Optional |
n.samples |
Posterior samples for simulation tests. |
nsim |
The same budget under the name |
seed |
Optional random seed for the simulation tests; the caller's RNG stream is restored afterwards. |
plot |
Draw the panel. |
... |
Must be empty; an unrecognised argument is an error. |
The engine's default method builds its panel from fitted() and
residuals() as numeric vectors, which a latent-state fit does not have –
fitted() returns list(psi, p, z) and residuals() one series per
process, so the response cannot be resolved. This method reads the same
quantities through the family's own doors instead: pit_residuals(),
ppc(), test_dispersion() and tulpa::moran_i().
Invisibly, the diagnostic results: waic, dic, cpo, ppc,
zero_inflation, dispersion, pit, uniformity, and moran when
coords is supplied.
sim <- simulate_occu(N = 100, J = 3, n.occ.covs = 1, n.det.covs = 1, seed = 1) fit <- tobs(~ occ_cov1, data = sim$data, family = occu(), detection = ~ det_cov1, y = sim$y, method = "laplace", control = list(verbose = FALSE, progress = FALSE)) check_model(fit, seed = 1, plot = FALSE)sim <- simulate_occu(N = 100, J = 3, n.occ.covs = 1, n.det.covs = 1, seed = 1) fit <- tobs(~ occ_cov1, data = sim$data, family = occu(), detection = ~ det_cov1, y = sim$y, method = "laplace", control = list(verbose = FALSE, progress = FALSE)) check_model(fit, seed = 1, plot = FALSE)
Per-species coefficients from a batched occu_cover fit.
## S3 method for class 'tobs_batch' coef(object, ...)## S3 method for class 'tobs_batch' coef(object, ...)
object |
A |
... |
Passed to each per-species |
A named list of per-species coefficient vectors.
sims <- lapply(1:2, function(s) simulate_occu_cover(N = 40, J = 3, seed = s)) y <- lapply(sims, function(s) s$y) y_pos <- lapply(sims, function(s) { p <- s$y_pos; p[is.na(p)] <- 0; p }) fit <- tobs(~ occ_cov1, data = sims[[1]]$data, family = occu_cover("lognormal"), detection = ~ 1, y = y, y.pos = y_pos, species = c("a", "b"), method = "laplace") coef(fit)sims <- lapply(1:2, function(s) simulate_occu_cover(N = 40, J = 3, seed = s)) y <- lapply(sims, function(s) s$y) y_pos <- lapply(sims, function(s) { p <- s$y_pos; p[is.na(p)] <- 0; p }) fit <- tobs(~ occ_cov1, data = sims[[1]]$data, family = occu_cover("lognormal"), detection = ~ 1, y = y, y.pos = y_pos, species = c("a", "b"), method = "laplace") coef(fit)
The fixed-effect estimates as one named numeric vector, named
<arm>_<term> as stats::vcov() and stats::confint() name them: psi_(Intercept),
p_det_cov1, lambda_elev, ... The visit-level detection coefficients
(p_visit_<cov>) belong to the detection arm. A coordinate that enters no
single arm, such as the log_r overdispersion, keeps its own name.
## S3 method for class 'tobs_fit' coef(object, arm = NULL, ...)## S3 method for class 'tobs_fit' coef(object, arm = NULL, ...)
object |
A |
arm |
Optional name of one linear predictor ( |
... |
Ignored. |
A named numeric vector.
sim <- simulate_occu(N = 100, J = 3, n.occ.covs = 1, n.det.covs = 1, seed = 1) fit <- tobs(~ occ_cov1, data = sim$data, family = occu(), detection = ~ det_cov1, y = sim$y, method = "laplace", control = list(verbose = FALSE, progress = FALSE)) coef(fit) coef(fit, arm = "psi")sim <- simulate_occu(N = 100, J = 3, n.occ.covs = 1, n.det.covs = 1, seed = 1) fit <- tobs(~ occ_cov1, data = sim$data, family = occu(), detection = ~ det_cov1, y = sim$y, method = "laplace", control = list(verbose = FALSE, progress = FALSE)) coef(fit) coef(fit, arm = "psi")
The public accessor for whether a fit converged, with one return shape across
every tobs() family. Every family's optimiser / EM / sampler verdict is
recorded under fit$convergence, and a Laplace-family fit that stops
without meeting its criterion warns once, naming control$max.iter.
convergence() returns the full record (converged, n_iter,
sla_status when the simplified-Laplace marginals were used, and the
per-group AGHQ solve status on the families fitted by that engine), and
converged() returns the single logical.
convergence(object, ...) ## S3 method for class 'tobs_fit' convergence(object, ...) converged(object, ...) ## S3 method for class 'tobs_fit' converged(object, ...)convergence(object, ...) ## S3 method for class 'tobs_fit' convergence(object, ...) converged(object, ...) ## S3 method for class 'tobs_fit' converged(object, ...)
object |
A fitted |
... |
Ignored. |
convergence(): a list with converged (logical), n_iter
(integer, NA for grid / closed-form fits with no iteration count), and
sla_status (character, when present). A fit whose random effects were
integrated by adaptive Gauss-Hermite quadrature also carries group_ok
(logical, one entry per group: FALSE where that group's posterior solve
failed and its estimates are NA), groups_failed (their indices) and
groups_failed_names (their labels where the family names its groups –
species, for the community models). converged is FALSE whenever any
group failed, since the quantities that group contributes to are not
estimates of anything; filter on these rather than on the text of the
warning the engine raises. A single-species fit whose grouped random
effects were integrated that way carries re_boundary: one record per
random-effect SD (sigma_<g>_<coef>) and, on a correlated block, per
correlation (cor_<g>_<ci>_<cj>), with the boundary Wald statistic, its
critical value and distinguishable; FALSE means the SD cannot be told
from zero or the correlation from +-1, and the fit says so in one message
("boundary (singular) fit").
converged(): a single TRUE / FALSE.
sim <- simulate_occu(N = 100, J = 3, n.occ.covs = 1, n.det.covs = 1, seed = 1) fit <- tobs(~ occ_cov1, data = sim$data, family = occu(), detection = ~ det_cov1, y = sim$y, method = "laplace", control = list(verbose = FALSE, progress = FALSE)) converged(fit) convergence(fit)$n_itersim <- simulate_occu(N = 100, J = 3, n.occ.covs = 1, n.det.covs = 1, seed = 1) fit <- tobs(~ occ_cov1, data = sim$data, family = occu(), detection = ~ det_cov1, y = sim$y, method = "laplace", control = list(verbose = FALSE, progress = FALSE)) converged(fit) convergence(fit)$n_iter
A generalized linear model on an observed count (or continuous) response
directly, with no detection sub-model and no latent abundance to marginalize.
The abundance analogue of jsdm(): where abun() fits Royle's N-mixture
(latent N plus imperfect detection), count() fits the observed response
as a plain GLMM. This is the "relative abundance" model of spAbundance
(abund): one value per site, log-link Poisson / negative-binomial counts
or an identity-link Gaussian response.
count(response = c("poisson", "negbin", "gaussian", "binomial"))count(response = c("poisson", "negbin", "gaussian", "binomial"))
response |
The response distribution: |
The response is a numeric vector (one value per site), supplied via y = or
on a two-sided formula left-hand side (count.value ~ predictors).
A tobs_family object.
method = "laplace" – one tulpa GLMM block; the negative-binomial size
and the Gaussian residual variance are estimated by an outer dispersion
loop.
method = "nested_laplace" – a plain areal field (icar() /
car_proper()) on the formula as a latent GMRF, integrated over its
hyperparameters by the nested-Laplace EM; the areal term is required on
this route.
abun() (N-mixture: latent abundance + detection), jsdm()
(occurrence GLMM, no detection).
f <- count("poisson") ff <- count("poisson") f
Latent presence (Bernoulli) plus conditional positive cover (beta or
lognormal). Does not share the replicate-detection assumption of the other
families – see vignette("families") for the conceptual caveat.
cover( response = c("beta", "beta_oi", "lognormal", "lognormal_trunc", "ordinal", "gaussian"), breaks = NULL )cover( response = c("beta", "beta_oi", "lognormal", "lognormal_trunc", "ordinal", "gaussian"), breaks = NULL )
response |
likelihood for the positive cover part:
|
breaks |
for |
A tobs_family object.
method = "laplace" – two independent tulpa_laplace() fits, one per
arm (presence, positive), non-spatial; the default.
method = "laplace_sla" – the same two fits with simplified-Laplace
skew-corrected marginals.
method = "nested_laplace" – the joint nested-Laplace engine: shared or
arm-specific areal fields, weighted trend fields, temporal() and re()
blocks, every hyperparameter integrated on an outer grid; the route the
"lognormal_trunc" and "ordinal" arms require.
method = "nested_laplace_sla" – the nested route with skew-corrected
marginals.
method = "nuts" – the non-spatial sampler over the exact two-arm
coefficient vector c(beta_presence, beta_positive, log_disp), giving
calibrated intervals and a per-draw pointwise likelihood for WAIC / LOO;
"beta", "lognormal" and "gaussian" arms only, and any structured
term errors with a pointer to "nested_laplace".
The cover response is a single length-N vector, so it may sit on the top
formula left-hand side and y = is dropped. The two
calls are equivalent:
# response on the LHS (y = omitted)
tobs(cover.flat ~ time.sc + habitat +
spatial(~ 1 + time.sc || cell_idx, graph = adj),
data = dat, family = cover(response = "beta"),
method = "nested_laplace")
# the same fit with a one-sided formula and an explicit y =
tobs(~ time.sc + habitat +
spatial(~ 1 + time.sc || cell_idx, graph = adj),
y = dat$cover.flat, data = dat, family = cover(response = "beta"),
method = "nested_laplace")
Naming the response makes the per-arm spatial labels read naturally: cover()
splits cover.flat into a presence arm and a positive arm, the arm names
that per-arm formulas and share() address. The LHS is evaluated against data
(then the calling environment), so it may be a bare column or an expression.
When fitted with method = "nested_laplace" and an areal spatial term in
the latent-presence formula (bym2(graph = adj), or car() /
car_proper()), the engine identifies a single latent field z per region
and parameterises the two arms as eta_occ = X beta_occ + sigma * z
and eta_pos = X beta_pos + alpha * sigma * z. Both BYM2 sub-blocks
(phi, theta) are subject to hard sum-to-zero constraints (see
.joint_inner_var() for the math): the cover-arm intercept
beta_pos[1] is identified as the population mean of eta_pos
under mean(z) = 0. Any simulator generating data for this engine
must demean both phi_f and theta_f before scaling, otherwise the
estimator targets beta_pos_0_truth + alpha * mean(w_s_sim) and
coverage of the population truth collapses with alpha. See
simulate_cover_joint() for a ready-made demeaned simulator.
A spatially varying trend is model structure, so it is declared in the
formula as a SECOND, weighted areal term on the same graph as the intercept
field, not through control:
~ time.sc + icar(graph = adj, group.var = "cell_idx") + icar(graph = adj, weight = time.sc, group.var = "cell_idx")
The unweighted term is the shared intercept field; the weighted term
icar(..., weight = col) is the trend field, whose contribution to each
arm's predictor is weight_i * z[cell_i], coupled onto the cover arm with
its own scale (alpha_trend, reported in fit$alpha_trend /
fit$sigma_trend) integrated over the outer grid. The umbrella spelling
spatial(graph = adj, model = "icar", weight = col) resolves identically.
The trend block inherits the intercept block's amplitude; give it its own
with share(spatial(), terms = list(intercept = ..., trend = ...)).
Requires method = "nested_laplace". A coupled
trend cannot currently combine with temporal() / re() blocks in the same
fit.
The axis the amplitude rides carries prior structure – a point mass at
alpha = 0 ("no coupling") and a log-spaced slab above it – so it is set
in one of two ways. share(alpha = grid(...)) STATES its nodes, and with them
that structure. share(alpha = grid(n = 9)) states a RESOLUTION: the engine
re-reads its own axis with that many slab nodes, point mass and slab bounds
unchanged, so sharpening the axis never restates its structure. terms =
gives either, per block. The resolution is the only way to raise this axis,
because it does not densify when the donor control$sigma.grid does: on an
informative data set the outer grid's quadrature effective sample size
saturates on the copy amplitude while every other axis tracks the request
(measured engine-side, NOTES_measurements.md). One block takes one of the
two; giving both is an error. control$alpha.grid[.trend] and
control$alpha.n[.trend] are the wire format these compile into and are
refused as user input.
Set it when the fit's hyperparameter intervals are reported. On the coupled
SBC fixture the declared resolution leaves the field SD miscalibrated once
the data are informative – at 10 visits per site its uniformity p-value is
9.1e-05, against 0.17 at 3 visits – and control$alpha.n = 21 returns every
scored quantity to nominal at both. Thirteen slab nodes do not (the field SD
still reads 4.5e-03), and the price is a 2.3-3x longer fit, since the outer
grid is a tensor. Point estimates are not affected the way the intervals are;
the measurement is in NOTES_measurements.md.
The intercept field plus its weighted trend field can also be written as one
spatial() term carrying an lme4-style coefficient formula:
~ time.sc + habitat + spatial(~ 1 + time.sc || cell_idx, graph = adj)
The bar left-hand side spells the coefficient fields: the intercept column
(1) is the unweighted field; each covariate column (time.sc) is a
weight-scaled coefficient field (weight_i * z[cell_i]). The bar right-hand
side (cell_idx) is the graph node index (the areal group.var); ||
requests independent intercept and slope fields, a single | makes them
correlated. This desugars to exactly the two-term weighted-areal form above,
so the two spellings give the same fit.
placement and share():
The cover hurdle's two arms are presence (the y > 0 Bernoulli arm) and
positive (the y | y > 0 arm); summary() and the coefficient output print
these same labels. A field is placed on an arm by writing it in that arm's
per-arm formula (presence = ~ ..., positive = ~ ...), and shared across
arms with share(). A field in the single shared formula reaches both arms.
Both arms (shared / copied). A field in the shared formula is one
presence-anchored latent copied onto the positive arm with an estimated
coupling per coefficient field (alpha for the intercept field, alpha_trend
for the trend field), marginalized on the outer grid:
# one latent field, shared by both arms
tobs(cover.flat ~ time.sc + habitat +
spatial(~ 1 + time.sc || cell_idx, graph = adj),
data = dat, family = cover(response = "beta"), method = "nested_laplace")
presence: eta_presence = ... + u_cell + time.sc * s_cell positive: eta_positive = ... + alpha * u_cell + alpha_trend * time.sc * s_cell
Per-arm formulas make the shared field explicit: place it on presence and
copy it onto positive. share(spatial()) estimates the coupling on the
default axis; share(spatial(), alpha = grid(c(...))) integrates it over
supplied nodes, alpha = grid(n = 9) over the engine's own axis read at n
slab nodes, and alpha = 0.5 fixes it. share(spatial(), prior = list(...))
regularizes the coefficient itself; a fit copying both the intercept and a
weighted trend block is refused it, since one prior reaches the engine per
fit.
tobs(presence = ~ time.sc + habitat +
spatial(~ 1 + time.sc || cell_idx, graph = adj),
positive = ~ time.sc + habitat + share(spatial()),
data = dat, family = cover(response = "beta"), method = "nested_laplace")
One arm only (free / separate). A field written in one arm's formula is a separate latent on that arm alone, with its own precision and no cross-arm coupling. A field in each arm's formula gives two independent latents:
tobs(presence = ~ time.sc + spatial(~ 1 + time.sc || cell_idx, graph = adj),
positive = ~ time.sc + spatial(~ 1 + time.sc || cell_idx, graph = adj),
data = dat, family = cover(response = "beta"), method = "nested_laplace")
presence: eta_presence = ... + u_presence_cell + time.sc * s_presence_cell positive: eta_positive = ... + u_positive_cell + time.sc * s_positive_cell
The free fit reports sigma_armspecific. Arm-specific fields are their own
spatial structure: they do not combine with a shared field, a weighted trend,
or temporal() / re() in the same formula, and at most one targets each arm.
The || and | axis is separate from shared / free: || makes the intercept
and slope fields independent, while a single | makes them correlated (a free
cross-covariance, MCAR). A correlated | bar shared across both arms (in the
shared formula, or on presence with share() on positive) is copied with
one estimated amplitude; placed on one arm's formula it is a free-Sigma
correlated field on that arm alone, no cross-arm copy.
field in the shared formula, no share() one presence-arm latent, pinned at alpha = 0 field + share() (either spelling) one shared / copied latent (presence anchor, coupling estimated) a field in each arm's formula separate / free latents, no coupling || independent intercept and slope coefficient fields | correlated (MCAR) coefficient fields, copy-only
Without a share() (and without control$alpha.grid) the presence field is
NOT carried onto the cover arm: the amplitude is pinned at alpha = 0 and the
field rides the presence arm alone. Structure nobody wrote is not in the
model, the reading occu_cover() and occu_multiscale_cover() also have.
Write + share(spatial()) in the shared formula, or place the field on
presence and share(spatial()) on positive, to couple the two arms. A
correlated | bar is the exception: that spelling is copy-only by
definition, so the bar itself states the coupling.
A bare lme4 bar in the formula – (1 | cell), (1 + x | cell),
(x || cell) – is a grouped random effect, not a spatial field, even when the
grouping factor names the areal graph nodes. The engine's inline-MCAR call
tulpa::spatial(graph, ~ 1 + x | cell) reads | cell as a separable spatial
field, but the same spelling inside a cover() formula is parsed as an IID
random effect on cell. For a spatial field on the cells write either the
compact spatial() bar of the section above,
spatial(~ 1 + x || cell, graph = adj), or the two-term weighted-areal form,
icar(graph = adj, group.var = "cell") +
icar(graph = adj, weight = x, group.var = "cell"). When a formula carries a
bar whose grouping factor is also an areal term's group.var, cover() emits
a one-time message noting the bar is fitted as a random effect; suppress it
with base::suppressMessages().
A spatial cover-hurdle fit integrates over a large outer hyperparameter grid
and can run for hours. control$checkpoint = list(path = "fit.ckpt", resume = TRUE) makes the joint engine append each completed grid cell to path; a
resume = TRUE run loads the finished cells and solves only the rest,
reproducing the from-scratch fit, so a killed or rebooted fit resumes instead
of restarting. resume = FALSE starts a fresh file. Forwarded to
tulpa::tulpa_nested_laplace_joint().
sim <- simulate_cover(N = 200, seed = 1) fit <- tobs(~ x, data = sim$data, family = cover("beta"), y = sim$y) summary(fit)sim <- simulate_cover(N = 200, seed = 1) fit <- tobs(~ x, data = sim$data, family = cover("beta"), y = sim$y) summary(fit)
Builds an opt-in fixed-effect prior for cover() models fit with a Laplace
method. The cover hurdle has two arms with their own coefficients – an
occurrence arm (Bernoulli presence) and a positive-cover arm (beta or
lognormal) – each with an intercept and a slope bucket. The penalty is the
quadratic + sum((beta_j - mu_j)^2 / (2 sd_j^2)) added to each arm's
negative log-likelihood, the same mechanism occu_priors() uses.
cover_priors( occ.intercept = list(mean = 0, sd = 2), occ.slope = list(mean = 0, sd = 2.5), pos.intercept = list(mean = 0, sd = 3), pos.slope = list(mean = 0, sd = 2.5) )cover_priors( occ.intercept = list(mean = 0, sd = 2), occ.slope = list(mean = 0, sd = 2.5), pos.intercept = list(mean = 0, sd = 3), pos.slope = list(mean = 0, sd = 2.5) )
occ.intercept |
Prior on the occurrence (presence) intercept, logit
scale. |
occ.slope |
Prior on occurrence slopes. Default
|
pos.intercept |
Prior on the positive-cover intercept (logit scale for
|
pos.slope |
Prior on positive-cover slopes. Default
|
Cover priors are opt-in: without priors, a cover fit is unpenalised. The
main use is regularising perfect separation in the occurrence arm at small
N. Set any sd = Inf to drop that component.
A cover_priors object, ready to pass to tobs(..., priors = ...).
Applied on the separate-Laplace path
(method = "laplace" / "laplace_sla") when the formula has no spatial
term. Both arms are penalised: the occurrence and lognormal-positive arms
through tulpa::tulpa_laplace(), and the beta-positive arm through
tulpa::tulpa_laplace_beta(). The joint nested-Laplace path
(method = "nested_laplace" / "nested_laplace_sla", used for spatial
cover formulas) also threads the priors, as a per-arm beta_prior_mean /
beta_prior_prec on the joint engine's occurrence and positive responses.
The separate-Laplace path keeps rejecting a prior alongside a spatial term
(that solver carries its own); add the spatial term through the
nested-Laplace path to combine the two.
# regularise the occurrence arm, leave the positive arm unpenalised priors <- cover_priors(pos.intercept = list(mean = 0, sd = Inf), pos.slope = list(mean = 0, sd = Inf)) priors sim <- simulate_cover(N = 150, seed = 1) fit <- tobs(cover ~ x, data = sim$data, family = cover(response = "lognormal"), method = "laplace", priors = priors) coef(fit)# regularise the occurrence arm, leave the positive arm unpenalised priors <- cover_priors(pos.intercept = list(mean = 0, sd = Inf), pos.slope = list(mean = 0, sd = Inf)) priors sim <- simulate_cover(N = 150, seed = 1) fit <- tobs(cover ~ x, data = sim$data, family = cover(response = "lognormal"), method = "laplace", priors = priors) coef(fit)
Latent abundance N_i ~ Poisson(lambda_i) (or negative binomial) in a covered
region, observed through a half-normal or hazard-rate detection function over
distance bins. With B bins the detected counts are multinomial over
(bin 1, ..., bin B, undetected) with cell probabilities
pi_b = integral_bin g(x; sigma) f(x) dx and 1 - sum_b pi_b, where f(x) is
the distance density (uniform for a line transect, proportional to distance for
a point transect). The latent N is summed out in closed form (truncation
K.max), so the fit is a direct Laplace approximation (no EM), with a NUTS
path over the same marginal; the per-bin detection integrals are evaluated by
Gauss-Legendre quadrature.
distance( key = c("halfnorm", "hazard"), transect = c("line", "point"), cutpoints = NULL, K.max = NULL, mixture = c("poisson", "negbin") )distance( key = c("halfnorm", "hazard"), transect = c("line", "point"), cutpoints = NULL, K.max = NULL, mixture = c("poisson", "negbin") )
key |
detection-function key. |
transect |
|
cutpoints |
numeric distance-bin edges, length |
K.max |
upper bound for the latent-abundance marginal sum (the exact
integration over |
mixture |
latent-abundance distribution, both fitted via tulpa's
closed-form marginal Laplace engine. |
The tobs() formula is the abundance (log lambda) model; detection is
the site-level detection-scale (log sigma) model. The response y is an
n_sites x n_bins integer matrix of per-bin detected counts. The bin edges and
transect geometry travel with the family: distance(cutpoints = ...).
A tobs_family object.
method = "laplace" – a direct Laplace approximation over the closed-form
marginal (Poisson or negative binomial); a grouped random effect on the
abundance arm is integrated by adaptive Gauss-Hermite quadrature.
method = "nested_laplace" – an areal icar() / car_proper() / bym2()
field on the abundance arm, optionally composed with a temporal() field,
through the shared areal-BFGS driver; a hazard-rate fit recovers the shape
alongside the field.
method = "nuts" – the in-tree C++ NUTS sampler over the same marginal,
with a fixed-hyper areal or temporal field block on the abundance arm (one
of the two, not both); the hazard-rate log-shape rides alongside.
Buckland, S. T., Anderson, D. R., Burnham, K. P., Laake, J. L., Borchers, D. L., Thomas, L. (2001). Introduction to Distance Sampling. Oxford. Royle, J. A., Dawson, D. K., Bates, S. (2004). Modeling abundance effects in distance sampling. Ecology 85, 1591-1597.
sim <- simulate_distance(N = 200, key = "halfnorm", transect = "line", n.abund.covs = 1, n.sigma.covs = 1, seed = 1) fit <- tobs(~ abund_cov1, data = sim$data, family = distance(key = "halfnorm", transect = "line", cutpoints = sim$cutpoints), detection = ~ sigma_cov1, y = sim$y, method = "laplace") summary(fit)sim <- simulate_distance(N = 200, key = "halfnorm", transect = "line", n.abund.covs = 1, n.sigma.covs = 1, seed = 1) fit <- tobs(~ abund_cov1, data = sim$data, family = distance(key = "halfnorm", transect = "line", cutpoints = sim$cutpoints), detection = ~ sigma_cov1, y = sim$y, method = "laplace") summary(fit)
The unmarked distsampOpen model: a Dail-Madsen open N-mixture (as
dyn_abun()) observed by distance sampling at each primary period – the
open-population counterpart of the single-season gdistremoval(). Initial
abundance N_1 ~ Poisson(lambda), then N_t = Binomial(N_{t-1}, omega) + Poisson(gamma) (apparent survival omega + recruitment gamma); at each
primary period the detected birds are distance-sampled into bins.
distsamp_open( transect = c("line", "point"), cutpoints = NULL, K.max = NULL, mixture = c("poisson", "negbin", "zip", "zinb"), dynamics = c("constant", "notrend", "trend", "autoreg", "ricker", "gompertz") )distsamp_open( transect = c("line", "point"), cutpoints = NULL, K.max = NULL, mixture = c("poisson", "negbin", "zip", "zinb"), dynamics = c("constant", "notrend", "trend", "autoreg", "ricker", "gompertz") )
transect |
Transect geometry: |
cutpoints |
Distance-bin edges, length |
K.max |
Truncation for the latent abundance HMM. Defaults to
|
mixture |
Initial-abundance mixing distribution. |
dynamics |
Population-dynamics form for the transition
|
The distance-band allocation is conditional on the period total detected, so it
factors out of the abundance HMM (the gdistremoval() trick): the marginal is
the dyn_abun() forward recursion with the detection probability set to the
overall distance detection pdist, plus the per-period band multinomials.
Four site-level arms: log abundance lambda (the tobs() formula), log
distance scale sigma (detection), logit survival omega (omega = ~ ...),
and log recruitment gamma (gamma = ~ ...), the last two intercept-only by
default.
A tobs_family object.
y is a 3D array [n_sites x n_bins x n_seasons] of per-distance-band counts
at each primary period (secondary occasions absorbed into the period total).
method = "laplace" – the forward-HMM marginal maximised (BFGS) with an
observed-information covariance; the only route (no field, no sampler).
Dail, D., Madsen, L. (2011). Models for estimating abundance from repeated counts of an open metapopulation. Biometrics 67, 577-587. Sollmann, R., Gardner, B., Chandler, R. B., Royle, J. A., Sillett, T. S. (2015). An open-population hierarchical distance sampling model. Ecology 96, 325-331.
gdistremoval() (single-season), dyn_abun() (open N-mixture),
distance().
f <- distsamp_open(transect = "line", cutpoints = c(0, 10, 20, 30, 40), dynamics = "notrend") ff <- distsamp_open(transect = "line", cutpoints = c(0, 10, 20, 30, 40), dynamics = "notrend") f
Abundance from a double-observer protocol (unmarked multinomPois with a
double-observer pi-function). Site abundance N ~ Poisson(lambda) is surveyed
by two observers with detection p1 / p2. The state formula models
log lambda; detection the shared site-level per-observer detection design
(observers 1 and 2 carry separate coefficients). By Poisson-multinomial
thinning the observable cell counts are independent Poissons, so the marginal
is closed form with no latent-abundance summation.
double_observer(type = c("independent", "dependent"))double_observer(type = c("independent", "dependent"))
type |
|
A tobs_family object for tobs().
type = "independent" (the default): the two observers detect independently and
each individual is recorded by observer 1 only, observer 2 only, or both, so y
is an N x 3 matrix of cell counts in that column order and the three cells are
pi = (p1(1 - p2), (1 - p1)p2, p1 p2).
type = "dependent": a removal-style protocol where a primary observer
records what it detects and a secondary observer records only what the primary
missed, so y is an N x 2 matrix (primary-detected, secondary-only) with
cells pi = (p_pri, (1 - p_pri)p_sec). A single fixed primary observer gives
only two cells for three parameters (lambda, p1, p2), a ridge that does
not separate the two observer detections; observer role-swapping identifies
them – pass primary = (a length-N vector in {1, 2} naming each site's
primary observer), and with observer 1 primary at some sites and observer 2 at
others (p_pri, p_sec) alternates between (p1, p2) and (p2, p1), giving the
four cell means that recover (lambda, p1, p2). With p1 = p2 (an
interchangeable pair) the dependent protocol reduces to a two-pass removal()
model.
method = "laplace" – the closed-form Poisson-multinomial marginal
maximised (BFGS) with an observed-information covariance; the only route
(no field, no sampler).
sim <- simulate_double_observer(N = 200, seed = 1) fit <- tobs(~ abund_cov1, data = sim$data, family = double_observer(), detection = ~ det_cov1, y = sim$y, control = list(verbose = FALSE)) coef(fit) # Dependent (role-swapping) protocol: sd <- simulate_double_observer(N = 300, type = "dependent", seed = 1) fd <- tobs(~ abund_cov1, data = sd$data, family = double_observer("dependent"), detection = ~ det_cov1, y = sd$y, primary = sd$primary, control = list(verbose = FALSE)) coef(fd)sim <- simulate_double_observer(N = 200, seed = 1) fit <- tobs(~ abund_cov1, data = sim$data, family = double_observer(), detection = ~ det_cov1, y = sim$y, control = list(verbose = FALSE)) coef(fit) # Dependent (role-swapping) protocol: sd <- simulate_double_observer(N = 300, type = "dependent", seed = 1) fd <- tobs(~ abund_cov1, data = sd$data, family = double_observer("dependent"), detection = ~ det_cov1, y = sd$y, primary = sd$primary, control = list(verbose = FALSE)) coef(fd)
Latent abundance evolves across primary seasons via apparent survival and
recruitment (Dail & Madsen 2011): N_1 ~ Poisson(lambda); for t >= 2,
N_t = S_t + G_t with survivors S_t ~ Binomial(N_{t-1}, omega) and recruits
G_t ~ Poisson(gamma); observed via Binomial(N_t, p) over secondary visits.
The latent abundance sequence is summed out by an exact HMM forward recursion
(it is not closed form, unlike the static abun()); analytic gradients come
from forward-mode differentiation of the scaled forward algorithm, so the fit
is a direct maximum-likelihood / Laplace fit with a NUTS path over the same
marginal.
dyn_abun(K.max = NULL, mixture = c("poisson", "negbin", "zip", "zinb"))dyn_abun(K.max = NULL, mixture = c("poisson", "negbin", "zip", "zinb"))
K.max |
abundance-state truncation for the forward recursion (states
|
mixture |
initial-abundance distribution: |
Four arms: initial abundance lambda (the tobs() formula) and detection
p (detection) are site-level; apparent survival omega (the omega
argument, default ~ 1) and recruitment gamma (the gamma argument,
default ~ 1) span the T - 1 transition intervals. A constant omega /
gamma is shared across a site's seasons; supplying a season-varying
covariate (a [n_sites x (T - 1)] matrix column of data, one column per
transition interval) on omega / gamma gives interval-specific vital
rates. The response y is a 3D array [n_sites x max_visits x n_seasons] (or
a list of per-season count matrices); missing visits are NA.
A tobs_family object.
method = "laplace" – analytic-gradient BFGS over the forward marginal
with an observed-information covariance; a grouped random effect on the
initial-abundance arm is integrated by adaptive Gauss-Hermite quadrature.
method = "nested_laplace" – an areal icar() / car_proper() field on
the initial-abundance arm (BFGS over the forward marginal plus the CAR
prior), optionally composed with a temporal() field, through the shared
areal-BFGS driver.
method = "nuts" – the in-tree C++ NUTS sampler over the same forward
marginal, with a fixed-hyper areal field block on the initial-abundance
arm; a temporal() term is not sampled.
Dail, D., Madsen, L. (2011). Models for estimating abundance from repeated counts of an open metapopulation. Biometrics 67, 577-587.
f <- dyn_abun(mixture = "negbin") ff <- dyn_abun(mixture = "negbin") f
Dynamic (multi-season) occupancy observed by several detection sources
(spOccupancy tIntPGOcc): the product of a dynamic
occupancy HMM (season-1 occupancy psi1, colonization gamma, extinction
eps) and integrated occupancy (a per-season emission that pools multiple
detection sources). Pooling sources across seasons is how colonization /
extinction estimates come out of individually-sparse opportunistic data. The
latent occupancy sequence integrates out by the two-state HMM forward
recursion; the exact marginal is maximised with an observed-information vcov.
dyn_int_occu()dyn_int_occu()
y is a length-S list of [sites x visits x seasons] detection arrays
(0/1/NA). The state formula models logit psi1; colonization = ~ ... and
extinction = ~ ... the site-level transitions (required, as in dyn_occu());
detection the shared per-source detection design (each source carries its own
coefficients). v1: every source covers all sites and the same season grid,
constant transitions, site-level detection.
A tobs_family object for tobs().
method = "laplace" – the non-spatial fit: the HMM-forward marginal
maximised with analytic forward-backward gradients and an
observed-information covariance.
method = "nested_laplace" – a shared icar() field on the first-season
occupancy formula (spOccupancy stIntPGOcc) through the shared areal-BFGS
driver, the field hyperparameters integrated on an outer grid; that field
is the only structured term accepted and is required on this route.
sim <- simulate_dyn_int_occu(N = 200, T.seasons = 4, S = 2, seed = 1) fit <- tobs(~ 1, data = sim$data, family = dyn_int_occu(), detection = ~ 1, colonization = ~ 1, extinction = ~ 1, y = sim$y, sources = sim$sources, control = list(verbose = FALSE)) coef(fit)sim <- simulate_dyn_int_occu(N = 200, T.seasons = 4, S = 2, seed = 1) fit <- tobs(~ 1, data = sim$data, family = dyn_int_occu(), detection = ~ 1, colonization = ~ 1, extinction = ~ 1, y = sim$y, sources = sim$sources, control = list(verbose = FALSE)) coef(fit)
Bernoulli occupancy state with colonisation + extinction transitions across
seasons (the MacKenzie et al. dynamic model). Colonisation / extinction are
constant across a site's seasons by default; a season-varying rate is supplied
as a [n_sites x (T - 1)] matrix column of data (one column per transition
interval) on colonization / extinction. Detection is site-level by default;
a [n_sites x T] matrix column of data (one column per primary season) on
the detection formula gives per-season detection. All season-varying paths
run under method = "laplace" (the exact HMM-forward marginal refine
calibrates the coefficients); they are gated under "nuts" with a pointer.
dyn_occu()dyn_occu()
A tobs_family object.
method = "laplace" – EM + Laplace over the exact HMM-forward marginal,
the default and the route every season-varying path runs on.
method = "laplace_sla" – the Laplace fit with simplified-Laplace
skew-corrected marginals.
method = "laplace_gibbs" / method = "laplace_mi" – the Laplace fit
with a post-EM Gibbs / multiple-imputation variance correction.
method = "nested_laplace" – an areal field on the occupancy arm through
the nested-Laplace EM, its hyperparameters integrated on an outer grid.
method = "nuts" – the unified C++ NUTS sampler over the HMM marginal;
season-varying colonisation / extinction / detection error with a pointer.
f <- dyn_occu() ff <- dyn_occu() f
Fitted values (occupancy and detection probabilities)
## S3 method for class 'tobs_fit' fitted(object, ...)## S3 method for class 'tobs_fit' fitted(object, ...)
object |
A |
... |
Ignored. |
A list with psi (occupancy probabilities), p (detection
probabilities), and z (posterior state probability at posterior-mean
parameters). For an int_occu() fit p is a named list, one detection
probability vector per source (matching y's names), since each source
carries its own detection design and, when present, its own detection-arm
spatial field. For single-season, int_occu() and community models z
is the per-site Bayes posterior P(z=1 | y) (an int_occu() site is
occupied with certainty if ANY source detected it, and otherwise
pools every source's non-detection evidence); for dynamic models z is
the forward-backward (HMM smoothing) state posterior P(z_t=1 | y_{1:T})
as an [n_sites x n_seasons] matrix.
sim <- simulate_occu(N = 100, J = 3, n.occ.covs = 1, n.det.covs = 1, seed = 1) fit <- tobs(~ occ_cov1, data = sim$data, family = occu(), detection = ~ det_cov1, y = sim$y, method = "laplace", control = list(verbose = FALSE, progress = FALSE)) str(fitted(fit))sim <- simulate_occu(N = 100, J = 3, n.occ.covs = 1, n.det.covs = 1, seed = 1) fit <- tobs(~ occ_cov1, data = sim$data, family = occu(), detection = ~ det_cov1, y = sim$y, method = "laplace", control = list(verbose = FALSE, progress = FALSE)) str(fitted(fit))
A binomial N-mixture dataset (Royle 2004) of repeated counts at 100 sites
over 3 visits, drawn from a known generative model. Latent abundance
increases with shrub cover and decreases with elevation; per-visit
detection increases with survey effort. Synthetic, not field data; the
generative coefficients and the realised latent abundances are in truth.
foray_countsforay_counts
A list with components:
100 x 3 integer matrix of counts.
Data frame of site covariates: elevation, shrub (both
standardised).
Named list with one 100 x 3 visit covariate matrix,
effort (standardised).
List of generative quantities: beta_lambda, beta_p, the
realised latent abundance N, and per-site lambda.
abun(), tobs(), simulate_abun()
data(foray_counts) summary(as.vector(foray_counts$y)) fit <- tobs(~ elevation + shrub, data = foray_counts$occ.covs, family = abun(), detection = ~ effort, y = foray_counts$y, visits = foray_counts$det.covs) coef(fit)data(foray_counts) summary(as.vector(foray_counts$y)) fit <- tobs(~ elevation + shrub, data = foray_counts$occ.covs, family = abun(), detection = ~ effort, y = foray_counts$y, visits = foray_counts$det.covs) coef(fit)
Occupancy with both false negatives and false positives in the detection
process (e.g. acoustic classifiers, citizen-science misidentification), using
the Miller et al. (2011) confirmed-detection design that makes the model
robustly identifiable. Each visit yields a detection state y in {0, 1, 2}:
0 = no detection, 1 = ambiguous detection (a true detection OR a false
positive), 2 = certain / confirmed detection (only possible when the site is
truly occupied). The latent occupancy z marginalises in closed form (two
states), so the fit maximises the exact marginal likelihood directly
(analytic-gradient BFGS, observed-information covariance) with a NUTS path over
the same marginal.
fp_occu()fp_occu()
Four site-level logit arms: occupancy psi (the tobs() formula), true
detection p11 (detection), false-positive rate p10, and the probability a
true detection is certain b. The p10 and b predictors default to
intercept-only and are set with the p10 = ~ ... and certainty = ~ ...
arguments to tobs() (certainty is the b arm). The response y is an
n_sites x J integer matrix in {0, 1, 2} (NA visits dropped).
A tobs_family object.
method = "laplace" – analytic-gradient BFGS over the exact marginal with
an observed-information covariance; a grouped random effect on the psi
or p11 arm is integrated by adaptive Gauss-Hermite quadrature.
method = "nested_laplace" – an areal icar() / car_proper() field on
the occupancy arm (BFGS over the marginal plus the CAR prior), optionally
composed with a temporal() field, through the shared areal-BFGS driver.
method = "nuts" – the in-tree C++ NUTS sampler over the same marginal,
with a fixed-hyper areal or temporal field block on the occupancy arm (one
of the two, not both).
Miller, D. A. W., Nichols, J. D., McClintock, B. T., Grant, E. H. C., Bailey, L. L., Weir, L. A. (2011). Improving occupancy estimation when two types of observational error occur. Ecology 92, 1422-1428. Royle, J. A., Link, W. A. (2006). Generalized site occupancy models allowing for false positive and false negative errors. Ecology 87, 835-841.
f <- fp_occu() ff <- fp_occu() f
The unmarked gdistremoval model (Amundson et al. 2014): a single-season
point-count design in which the detected individuals are recorded two ways at
once – their distance band (distance sampling) and the removal period of first
detection (removal sampling). This is NOT the open-population distance model
(distsampOpen); the abundance is static.
gdistremoval(transect = c("line", "point"), cutpoints = NULL)gdistremoval(transect = c("line", "point"), cutpoints = NULL)
transect |
Transect geometry: |
cutpoints |
Distance-bin edges, length |
Site abundance N_i ~ Poisson(lambda_i); the detected birds are
cross-classified by a distance band and a removal period. Writing pdist_i for
the overall distance detection, prem_i for the overall removal detection, the
total detected is a binomial thinning of N_i, and Poisson is closed under
binomial thinning, so the marginal is closed-form:
with the distance-band counts and removal-period counts as two conditional
multinomials – the double_observer() Poisson-multinomial pattern, here with
a distance multinomial (half-normal key band integrals) and a depleting-removal
multinomial pi_k = r (1 - r)^{k-1}.
Three site-level arms: log abundance lambda (the tobs() formula), log
distance scale sigma (detection), and logit per-period removal capture r
(removal = ~ ..., default intercept-only).
A tobs_family object.
y is an n_sites x n_bins integer matrix of per-distance-band counts;
y.rem an n_sites x n_periods integer matrix of per-removal-period counts.
The per-site row totals must match (the same detected birds cross-classified).
method = "laplace" – the closed-form marginal maximised (BFGS) with an
observed-information covariance; the only route (no field, no sampler).
Amundson, C. L., Royle, J. A., Handel, C. M. (2014). A hierarchical model combining distance sampling and time removal to estimate detection probability during avian point counts. The Auk 131, 476-494.
distance(), removal(), double_observer().
f <- gdistremoval(transect = "point", cutpoints = c(0, 10, 20, 30, 40)) ff <- gdistremoval(transect = "point", cutpoints = c(0, 10, 20, 30, 40)) f
Every family, and every method within a family, glances to one column set:
nobs, df, logLik, n_fixed, n_samples, n_divergent,
mean_accept and converged, the sampler columns NA on a fit with no
draws. The joint nested-Laplace fits add the outer grid placement and, when
requested, the outer Pareto-k diagnostic. The joint-coupled families
(occu_cover(), cover(), occu() spatial, occu_multiscale_cover())
carry both at the fit top level.
## S3 method for class 'tobs_fit' glance(x, ...)## S3 method for class 'tobs_fit' glance(x, ...)
x |
A fitted |
... |
Ignored. |
The one-row glance data frame. A joint-coupled fit adds two
outer-grid placement columns, reported whether or not the grid moved so an
inert auto-recenter is visible in a batch summary:
outer_grid_placement"fixed" (the grid the fit was given) or
"auto_recentered" (the engine moved it onto the hyperparameter mode).
outer_grid_recenter_declinedOn a "fixed" placement, why the
recenter did not apply – for instance a user-pinned axis, or
"auto_recenter_disabled". NA when the grid was recentered.
A fit that also requested the Pareto-k diagnostic (control$diagnose.k = TRUE, off by default) adds three more:
pareto_kThe outer importance-sampling for the
hyperparameter Gaussian summary; < 0.7 indicates a reliable summary,
NA when the diagnostic did not run or the proposal was degenerate.
pareto_k_is_essThe importance-sampling effective sample size on
the PSIS-smoothed weights (numeric); pareto_k_is_ess / control$k.samples
is the relative IS efficiency.
pareto_k_proposal_sourceHow the importance proposal was
built: "mode_hessian" from the Laplace curvature at
the hyperparameter mode – curvature-backed, so the stays
trustworthy even when a sharp posterior collapses the integration grid to
~1 cell; "grid_moment" from the grid-weighted covariance of the
integration nodes (with "moment_matched" its refinement); or
"grid_mixture", the local-bump-per-cell mixture matching what the engine
samples on a spread grid.
sim <- simulate_occu(N = 100, J = 3, n.occ.covs = 1, n.det.covs = 1, seed = 1) fit <- tobs(~ occ_cov1, data = sim$data, family = occu(), detection = ~ det_cov1, y = sim$y, method = "laplace", control = list(verbose = FALSE, progress = FALSE)) glance(fit)sim <- simulate_occu(N = 100, J = 3, n.occ.covs = 1, n.det.covs = 1, seed = 1) fit <- tobs(~ occ_cov1, data = sim$data, family = occu(), detection = ~ det_cov1, y = sim$y, method = "laplace", control = list(verbose = FALSE, progress = FALSE)) glance(fit)
Multiple data sources informing a shared latent occupancy state, each with its own detection process.
int_occu()int_occu()
A continuous-mesh spde() term on the detection formula loads a
spatially-structured detection field, broadcast to every source, under
method = "laplace". The per-source fields are reported as a named list in
fit$spatial_field_det. All sources must share one detection formula; areal
terms belong on the state arm.
A tobs_family object.
method = "laplace" – EM + Laplace over the multi-source two-state
marginal, the default.
method = "laplace_sla" – the Laplace fit with simplified-Laplace
skew-corrected marginals.
method = "laplace_gibbs" / method = "laplace_mi" – the Laplace fit
with a post-EM Gibbs / multiple-imputation variance correction.
method = "nested_laplace" – an areal field on the state arm through the
nested-Laplace EM, its hyperparameters integrated on an outer grid.
method = "nuts" – the unified C++ NUTS sampler over the same marginal.
f <- int_occu() ff <- int_occu() f
Multivariate occurrence with shared latent factors. No observation replication – treats observed presence/absence as the response.
jsdm()jsdm()
A tobs_family object.
method = "laplace" – the community Laplace-EM shared with ms_count()
(a logit-link community GLMM); latent() factors alone (lfJSDM) run here
through the block-coordinate driver.
method = "nuts" – samples the exact joint community posterior
(community means, per-species deviations, community covariance) over the
Bernoulli response, warm-started at the Laplace-EM mode; non-spatial, an
areal or latent() term errors with a pointer.
method = "pg_gibbs" – a per-species Polya-Gamma conjugate Gibbs with
community mean and Inverse-Gamma variance updates, giving a calibrated
community-variance posterior; non-spatial.
method = "nested_laplace" – a shared areal field (icar() /
car_proper() / bym2()), with or without latent() factors (sfJSDM),
by block coordinate ascent; the areal term is required on this route.
f <- jsdm() ff <- jsdm() f
The value and degrees of freedom are the engine's (the tulpa_fit
method); the "nobs" attribute is nobs.tobs_fit() on the same fit, so
stats::BIC() and anything else reading the attribute counts the
observations nobs() reports.
## S3 method for class 'tobs_fit' logLik(object, ...)## S3 method for class 'tobs_fit' logLik(object, ...)
object |
A |
... |
Passed on to the next method. |
A logLik object.
sim <- simulate_occu(N = 100, J = 3, n.occ.covs = 1, n.det.covs = 1, seed = 1) fit <- tobs(~ occ_cov1, data = sim$data, family = occu(), detection = ~ det_cov1, y = sim$y, method = "laplace", control = list(verbose = FALSE, progress = FALSE)) logLik(fit)sim <- simulate_occu(N = 100, J = 3, n.occ.covs = 1, n.det.covs = 1, seed = 1) fit <- tobs(~ occ_cov1, data = sim$data, family = occu(), detection = ~ det_cov1, y = sim$y, method = "laplace", control = list(verbose = FALSE, progress = FALSE)) logLik(fit)
A vegetation cover-hurdle dataset for 150 grassland plots surveyed across
three years, drawn from a known generative model: the species occurs with a
probability driven by moisture, and where it occurs, its conditional cover
(a Beta-distributed proportion in (0, 1)) increases with moisture and
decreases with grazing. Roughly half the plots record zero cover. year_c
is the centred survey year, so within_between() can split the cross-plot
baseline from the within-plot temporal deviation. Synthetic, not field data;
the generative coefficients are attached as attr(meadow_cover, "truth").
meadow_covermeadow_cover
A data frame with 150 rows (one per plot) and columns:
Plot identifier (factor).
Survey year (2018-2020).
Survey year centred on its mean.
Standardised soil-moisture index.
Standardised grazing-pressure index.
Recorded cover as a proportion in [0, 1] (0 where absent).
cover(), tobs(), within_between(), simulate_cover()
data(meadow_cover) mean(meadow_cover$cover > 0) fit <- tobs(cover ~ moisture, data = meadow_cover, family = cover(response = "beta")) coef(fit)data(meadow_cover) mean(meadow_cover$cover > 0) fit <- tobs(cover ~ moisture, data = meadow_cover, family = cover(response = "beta")) coef(fit)
Per-species N-mixture with shared community-level hyperparameters.
ms_abun(K.max = NULL, mixture = c("poisson", "negbin", "zip", "zinb"))ms_abun(K.max = NULL, mixture = c("poisson", "negbin", "zip", "zinb"))
K.max |
upper bound for the latent-abundance marginal sum (the exact
integration over |
mixture |
Abundance mixing distribution: |
A tobs_family object.
method = "laplace" – the in-tree C++ community Laplace-EM (per-species
coefficient random effects, closed-form community covariance M-step, AGHQ
variance debias), the default; latent() factors run here through the
block-coordinate driver.
method = "nested_laplace" – a shared areal field (icar() / bym2() /
car_proper()) on the abundance arm (the spAbundance sfMsNMix model),
Poisson or grid-integrated negative-binomial size; with latent() factors
beside the field the spatial-factor model by block coordinate ascent.
method = "nuts" – the non-spatial community sampler over the closed-form
per-(species, site) marginal, sampling the community means, per-species
deviations and community covariances jointly (Poisson, or per-species
negative-binomial log_r_s), warm-started at the Laplace-EM mode; with a
shared car_proper() field (Poisson) a fixed-hyper non-centered field
joins the sampler, while icar() / bym2(), temporal() and re()
terms stay on "nested_laplace".
Under mixture = "negbin" the community mean mu_log_r reaches
coef() / vcov() / confint() with a marginal Wald SE, and the variance
component sigma_log_r is reported on fit$ms_dispersion. That interval is
calibrated conditional on the variance component being recovered, and the
two are not independent: sigma_log_r is a scalar variance over species and
at few species it can settle near its lower boundary, which shifts
mu_log_r and narrows its SE at the same time.
Measured on simulated data (39 laplace fits at 8 and 36 species,
sigma.logr = 0.5): where sigma_log_r came back at
least 0.30, the nominal 95% interval covered 33 of 34 with a
sqrt(mean(z^2)) of 0.88; where it came back below, it covered 2 of 5, the
point estimate was 2.2x further from the truth and the SE 28% narrower. So
read fit$ms_dispersion$sigma_log_r before quoting a mu_log_r interval: a
value near zero, well under what the data should support, means the interval
is not trustworthy even though the fit converged and reported no warning.
Those thresholds are the ones this fixture separated on and are not a general
rule; what transfers is the conditioning, not the number.
Away from that boundary the interval is calibrated, and calibrated at every
group count measured. Over 97 fits at 8 / 18 / 36 species the scale
sd(mu_log_r) / mean(se) reads 1.066 / 1.265 / 0.981, and the middle figure
is the seed block rather than the estimator: mu_log_r is a POPULATION mean,
each seed draws S log-dispersions around it, and that draw supplies about
two thirds of the across-seed spread. The 18-species blocks drew theirs
18-21% wider than sigma.logr / sqrt(S). Put the draw at its expectation and
the scale is 1.077 / 1.101 / 0.977; rebuild the SE at the simulated sigma as
well and it is 0.990 / 1.035 / 0.963 (NOTES_measurements.md). The residual
there is the attenuation of
sigma_log_r itself (0.418 / 0.448 / 0.487 against 0.5), which shrinks as
species are added and which the SE inherits, since
se^2 = (sigma_log_r^2 + c) / S.
A simulation study measuring this needs simulate_ms_abun()'s
truth$mu_log_r_real – the mean of the log-dispersions that seed actually
drew – beside truth$mu_log_r. Score coverage of a community mean against
the constant; score point recovery and interval SCALE against the realized
mean, or the draw is charged to the estimator.
control$logr.sigma.prior puts a Penalized-Complexity prior on that variance,
which adds curvature at sigma_log_r -> 0. Measured on the same fixture at
c(1, 0.05), it is worth reaching for when a fit reports a sigma_log_r near
zero or fails outright with a singular marginal Hessian – one seed that
errored under pure maximum likelihood converged and covered under the prior,
and variance components near 0.01-0.06 lifted by an order of magnitude. It
does not repair coverage: on 19 paired seeds the nominal 95% interval
covered 17 either way, with the same two seeds missing, because the prior
bites near the boundary while those misses sit at sigma_log_r around 0.2.
It is off by default because the fits that were already calibrated pick up a
small systematic shift under it (mu_log_r by -0.006, p = 0.001).
f <- ms_abun(mixture = "negbin") ff <- ms_abun(mixture = "negbin") f
The community analogue of count(): a per-species GLMM on an observed count
or continuous response with Gaussian community hyperpriors on the per-species
coefficients (the spAbundance msAbund model). No detection and no latent
state – the abundance counterpart of ms_occu() without the occupancy layer.
Poisson / negative-binomial (log link) or Gaussian (identity). y is an
n_sites x n_species matrix (or a named list of n_species count vectors),
one value per site per species; species names the columns.
ms_count(response = c("poisson", "negbin", "gaussian", "binomial"))ms_count(response = c("poisson", "negbin", "gaussian", "binomial"))
response |
One of |
As with the other community Laplace-EM families (ms_occu(), ms_occu_cover()),
the community means are recovered essentially unbiased for the Gaussian
(identity link, exact Laplace) and Poisson responses; the negative-binomial
slope carries a mild first-order-Laplace (PQL) attenuation of order the
community variance (a few percent, shrinking with the number of species and
observations per species). The community-mean Wald intervals are calibrated to
the package rubric (pooled coverage at least 0.85).
method = "nuts" samples the exact joint posterior (community means,
per-species deviations, and the community covariance) for all three responses
– the negative binomial carrying a per-species dispersion random effect, the
Gaussian a per-species free residual variance – which removes the
Laplace-EM's negative-binomial attenuation and returns calibrated,
non-Gaussian community intervals. A shared areal field or latent factors are
available through method = "nested_laplace" / the latent() term; see the
package overview for the spatial and factor variants.
A tobs_family object.
method = "laplace" – the shared community Laplace-EM, the default;
latent() factors alone run here through the block-coordinate driver.
method = "nested_laplace" – a shared areal field icar() on the
abundance formula (the spAbundance sfMsAbund model), with or without
latent() factors, by block coordinate ascent; the field is required on
this route.
method = "nuts" – the exact joint community posterior sampled by the
in-tree C++ target, warm-started at the Laplace-EM mode, as described
above; non-spatial.
method = "pg_gibbs" – a per-species Polya-Gamma conjugate Gibbs with
community mean and Inverse-Gamma variance updates, for the logistic
"binomial" response only (another response errors with a pointer);
non-spatial.
count() (single species), ms_occu(), ms_abun()
sim <- simulate_ms_count(N = 120, n.species = 8, seed = 1) fit <- tobs(~ x, data = sim$data, family = ms_count(), y = sim$y, species = colnames(sim$y), method = "laplace") summary(fit)sim <- simulate_ms_count(N = 120, n.species = 8, seed = 1) fit <- tobs(~ x, data = sim$data, family = ms_count(), y = sim$y, species = colnames(sim$y), method = "laplace") summary(fit)
The community analogue of distance(): per-species binned distance sampling
with Gaussian community hyperpriors on the per-species abundance and
detection-scale coefficients, so rare species borrow strength from common ones
through the shared community means and covariances – the same pooling that
stabilises ms_abun(), on the distance-sampling response.
ms_distance( key = c("halfnorm", "hazard"), transect = c("line", "point"), cutpoints = NULL, K.max = NULL, mixture = c("poisson", "negbin") )ms_distance( key = c("halfnorm", "hazard"), transect = c("line", "point"), cutpoints = NULL, K.max = NULL, mixture = c("poisson", "negbin") )
key |
Detection function: |
transect |
Transect geometry: |
cutpoints |
Distance-bin edges, length |
K.max |
Truncation for the latent abundance sum. Defaults to
|
mixture |
Abundance mixing distribution. |
Per species s, site i, distance bin b:
N_{s,i} ~ Poisson(lambda_{s,i})
y_{s,i,.} | N ~ Multinomial(N_{s,i}; pi_{s,i,1..B}, 1 - p_det)
log lambda_{s,i} = X_i . (mu_lambda + b_lambda_s)
log sigma_{s,i} = X_sig_i . (mu_sigma + b_sigma_s)
b_lambda_s ~ N(0, Sigma_lambda), b_sigma_s ~ N(0, Sigma_sigma)
with pi_b the integral of the detection function over bin b. The latent
N_{s,i} integrates out per species-site in closed form, exactly as for
distance(); the per-species coefficient deviations are the random effects,
integrated by the shared community Laplace-EM.
Adding latent(n) to the abundance formula gives residual species
co-occurrence through n per-site latent factors with per-species loadings;
adding an areal term (icar() / car_proper() / bym2()) or spde()
alongside gives a shared spatial field. Both route through the same
block-coordinate engine as the other community families.
A tobs_family object.
method = "laplace" – the shared community Laplace-EM over the existing
distance kernel, plain or with latent() factors alone (lfMsDS).
method = "nested_laplace" – a shared field (icar() / car_proper() /
bym2() / spde()) on the abundance formula, with or without factors
(sfMsDS), by block coordinate ascent; the field is required on this route.
Poisson only, and no sampler route.
y is a 3D array [n_sites x n_bins x n_species] or a named list of
n_sites x n_bins per-bin count matrices. formula is the abundance
(log lambda) predictor, detection the detection-scale (log sigma)
predictor, and species the species labels.
distance() (single species), ms_abun() (community N-mixture).
f <- ms_distance(key = "halfnorm", transect = "line", cutpoints = c(0, 25, 50, 75, 100)) ff <- ms_distance(key = "halfnorm", transect = "line", cutpoints = c(0, 25, 50, 75, 100)) f
Per-species dynamic (HMM) occupancy with Gaussian community hyperpriors on the per-species first-season occupancy and detection coefficients, and shared community-wide colonisation / extinction transition coefficients.
ms_dyn_occu()ms_dyn_occu()
A tobs_family object.
method = "laplace" – the shared community Laplace-EM, the default: the
shared colonisation / extinction dynamics and the per-species first-season
occupancy / detection components recover across seeds (see
tests/testthat/test-ms-dyn-occu.R, community-mean 95% CI coverage
measured ~0.98).
method = "pg_gibbs" – the community Polya-Gamma machinery of msPGOcc
with a two-state HMM forward-filtering backward-sampling latent step
(spOccupancy tMsPGOcc), giving a calibrated community-variance
posterior; constant transitions, site-level detection, no structured term.
method = "nested_laplace" – a shared areal field on the first-season
occupancy formula (~ 1 + icar(graph = adj)) fits the spOccupancy
stMsPGOcc model: the field is shared across species, and because the
first-season occupancy psi1 only sets the initial mixing weight of each
species' HMM, the block-coordinate driver alternates the community EM
(field as a psi1 offset) with an areal field Newton, the field recovering
cleanly (cor ~0.94). A spatially-varying-coefficient bar
(~ spatial(~ 1 + w || cell, graph = adj)) adds a shared
covariate-weighted field alongside the intercept field, fitting the
svcTMsPGOcc model through the same K-field weighted-ICAR solve as the
community count SVC (both fields recover, cor ~0.90 / ~0.89). icar()
only; bym2() / car_proper() fields are a follow-up. The field is
required on this route.
method = "nuts" – the non-spatial community sampler over the exact
per-species HMM-forward marginal (in-tree C++), sampling the community
means, per-species first-season / detection deviations, both per-arm
community covariances and the shared colonisation / extinction
coefficients jointly, warm-started at the Laplace-EM mode; an areal term
errors with a pointer to "nested_laplace".
f <- ms_dyn_occu() ff <- ms_dyn_occu() f
Multiple detection data sources share one latent occupancy state per species, with Gaussian community hyperpriors on the per-species coefficients of the occupancy arm and of every per-source detection arm. Fit by a shared community Laplace-EM.
ms_int_occu()ms_int_occu()
A tobs_family object.
method = "laplace" – the shared community Laplace-EM, the default: the
shared occupancy mean and the per-source detection community components
recover near nominal across seeds and more than one source (see
tests/testthat/test-ms-int-occu.R). The binary community-mean intervals
carry the mild Laplace under-dispersion typical of occupancy data
(measured 95% CI coverage ~0.89 at small per-species n).
method = "pg_gibbs" – the community integrated Polya-Gamma Gibbs
sampler (one latent state per species-site observed by every source),
giving a calibrated community-variance posterior; site-level per-source
detection.
method = "nuts" – the community integrated sampler over the exact
multi-source two-state per-(species, site) marginal (in-tree C++),
sampling the community means, per-species deviations and the D + 1
per-arm community covariances jointly, warm-started at the Laplace-EM mode.
No route takes a structured term: a field or random effect on either formula errors with a pointer, and there is no nested-Laplace path for this family.
f <- ms_int_occu() ff <- ms_int_occu() f
Per-species occupancy and detection with Gaussian community hyperpriors on the
per-species coefficients of each arm (the spOccupancy msPGOcc model). The
occupancy and detection per-species deviations are independent, each with its
own community covariance, fit by a community Laplace-EM.
ms_occu()ms_occu()
A tobs_family object.
method = "laplace" – the shared community Laplace-EM, the default;
latent() factors alone (lfMsPGOcc) run here through the block-coordinate
driver.
method = "nuts" – the non-spatial community sampler over the closed-form
per-(species, site) two-state marginal, sampling the community means,
per-species deviations and both per-arm community covariances jointly,
warm-started at the Laplace-EM mode; an areal term errors with a pointer to
"nested_laplace".
method = "pg_gibbs" – the hierarchical Polya-Gamma Gibbs sampler
(spOccupancy msPGOcc), giving a calibrated community-variance posterior;
non-spatial.
method = "nested_laplace" – a shared areal field (icar() / bym2() /
car_proper()) on the occupancy arm: a plain intercept field runs the
in-tree C++ community-spatial nested Laplace-EM, a varying-coefficient bar
(svcMsPGOcc) or latent() factors beside the field (sfMsPGOcc) run the
block-coordinate fitter; the field is required on this route.
occu(), ms_dyn_occu(), ms_int_occu()
sim <- simulate_ms_occu(N = 80, J = 4, n.species = 8, seed = 1) fit <- tobs(~ x, data = sim$data, family = ms_occu(), detection = ~ 1, y = sim$y, species = paste0("sp", seq_len(8)), method = "laplace") summary(fit)sim <- simulate_ms_occu(N = 80, J = 4, n.species = 8, seed = 1) fit <- tobs(~ x, data = sim$data, family = ms_occu(), detection = ~ 1, y = sim$y, species = paste0("sp", seq_len(8)), method = "laplace") summary(fit)
The community version of occu_cover(): a per-species joint occupancy-cover
model with Gaussian community hyperpriors on the per-species coefficients of
all three arms (occupancy psi, detection p, and positive cover). Rare
species borrow strength from common ones through the shared community means
and covariances, the same pooling that stabilises ms_occu() and
ms_abun() but on the joint occupancy + vegetation-cover response.
ms_occu_cover(response = c("beta", "lognormal", "gaussian"))ms_occu_cover(response = c("beta", "lognormal", "gaussian"))
response |
likelihood for the positive cover arm. |
Per species s, cell i, visit j:
z_{s,i} ~ Bernoulli(psi_{s,i})
y_{s,i,j} | z ~ Bernoulli(p_{s,i,j})
c_{s,i,j} | y ~ f_pos(eta_pos_{s,i,j}, disp)
logit psi_{s,i} = X_occ_i . (mu_occ + b_occ_s)
logit p_{s,i,j} = X_p_{ij} . (mu_p + b_p_s)
g(cover) = X_pos_{ij} . (mu_pos + b_pos_s)
b_occ_s ~ N(0, Sigma_occ), b_p_s ~ N(0, Sigma_p),
b_pos_s ~ N(0, Sigma_pos)
The latent presence z integrates out per species-cell in closed form (the
same two-state mixture as occu_cover()); the per-species coefficient
deviations are the random effects, integrated by a Laplace-EM. The
positive-arm dispersion is a shared community parameter.
A tobs_family object.
y and y.pos are 3D arrays [n_sites x max_visits x n_species] (or named
lists of n_sites x max_visits matrices, one per species); species = is
required. The occupancy formula, the detection formula, and the cover
positive formula carry community covariates shared across species. coef()
returns the community means; ranef() the per-species coefficient deviations.
method = "laplace" – the community Laplace-EM (per-species deviations
integrated, closed-form community covariance M-step), non-spatial or with
the reduced-rank spatial factors below.
method = "nuts" – the community sampler over the exact per-(species,
cell) two-state marginal, warm-started at the Laplace-EM mode: non-spatial
it samples the community means, per-species deviations, the three per-arm
community covariances and the shared log-dispersion jointly; with a shared
areal field it samples the spatial-factor model below.
A single areal field term (icar(graph = adj), car_proper(graph = adj), or
bym2(graph = adj)) on the occupancy formula fits the reduced-rank (HMSC /
spatial-gllvm) community model: K shared latent fields with per-species
loadings on the occupancy predictor,
logit psi_sc = X_c (mu_occ + b_occ_s) + sum_k L_sk w_kc. A single shared
field with only a species intercept can shift each map's level but not its
shape; the loadings give each species its own spatial shape as a combination
of the shared factors, borrowing strength so rare taxa get a calibrated map.
The number of factors is set by control = list(n.factors = K), or chosen
automatically by control = list(n.factors = "auto", n.factors.max = M),
which fits the identified (lower-triangular loading) ladder and picks the rank
that maximises the empirical-Bayes Laplace marginal likelihood (the field is
integrated out, so its prior supplies the Occam penalty – latent-level
criteria such as WAIC track the field's effective dimension, not the rank, and
under-select). The per-rank evidence is returned in fit$spatial$K_selection.
sd.load sets the loading prior scale.
Adding the SAME icar(graph = adj) term to the cover positive formula
shares the latent fields across the two processes: the fields then also load on
the cover predictor through a free loading matrix,
g(cover)_sc = X_c (mu_pos + b_pos_s) + sum_k Lpos_sk w_kc, so a species'
spatial occupancy pattern and its spatial cover pattern are linked through one
set of factors. The cover loadings are returned in fit$spatial$loadings_cover.
The field is shared, so the cover-arm term must match the occupancy arm (same
type and graph), and a cover-arm field without a matching occupancy field errors.
The field structure is set by the term: icar() (improper intrinsic CAR, the
default), car_proper(graph = adj) (a proper CAR whose per-factor correlation
rho is returned in fit$spatial$rho_w), or bym2(graph = adj) (the
Riebler 2016 convolution whose per-factor spatial-variance fraction phi is
returned in fit$spatial$phi_w); the field hyperparameter is estimated by EM.
All three share one engine; fit$spatial$field_type records the choice.
Structured terms on the detection arm, or an unsupported field term, error from
the dispatcher.
The shared fields imply a residual species-association matrix (the
spatial-JSDM / HMSC output): tobs_associations() returns the occupancy
association corr(L L') and, with a cover-arm factor, the cover association
and the cross-arm occupancy-vs-cover association, each marginalised over the
loading posterior so an interval accompanies the estimate. predict() returns
the other JSDM output – the per-species per-cell occupancy maps (psi with
psi_lower / psi_upper), marginalised over the loading + field posterior so
a rare species borrows strength across the shared factors for a calibrated map.
The non-spatial fit is Laplace-EM. A community spatial occu_cover – a shared
latent field coupled across the occupancy and cover arms with per-species RE on
all three arms – is the reduced-rank spatial-factor fit: an icar() /
car_proper() / bym2() term on the occupancy formula (and, with a matching
term on the cover formula, a cover-arm factor) fits per-species loadings on the
shared field via Laplace-EM and NUTS, with the per-species community covariances
Sigma_occ / Sigma_p / Sigma_pos on top. The free
per-species loading form generalises a single common field amplitude, so it
subsumes the common-amplitude coupling of occu_cover()'s joint-coupled engine.
A community model whose per-arm variance components are integrated on an outer
grid (the engine route of occu_cover()'s joint-coupled path) is not used: the
joint nested-Laplace engine integrates every variance component on its outer
grid, so per-arm community RE variances plus the field hyperparameters exceed
any tractable grid size – the closed-form covariance M-step of the Laplace-EM is
the scaling route for community variance components.
Structured terms beyond the shared field error from the dispatcher rather than
being silently dropped.
The community-MEAN estimates (coef(), vcov(), confint()) are unbiased.
The community-VARIANCE components – the per-arm covariance matrices in
fit$ms_community$Sigma_occ / Sigma_p / Sigma_pos and their sd_*, which
set the spread of the per-species deviations – pick up Laplace small-cluster
attenuation at small per-species n under the EM M-step. They are debiased by
default with an adaptive Gauss-Hermite quadrature of the exact per-species RE
posterior (control$re.aghq = TRUE, the same correction the single-arm AGHQ
path applies; disable with re.aghq = FALSE, set nodes with control$n.quad).
The cover hurdle ties a species' psi / p / cover coefficients through the data,
so the per-species RE posterior is not separable across arms and the AGHQ is a
tensor product over the joint RE vector – n.quad^P nodes per species in the
total RE dimension P. That cost is exponential in P, so the debias is a
hard scope limit: it runs only up to a small total RE dimension
(control$re.aghq.maxdim, default 4, e.g. intercept-only on all three arms),
and larger RE designs keep the EM variance. The EM (Laplace) variance is a
documented lower bound on the true component – attenuated toward zero at small
per-species n, monotonically less attenuated as n grows – not a bias of
unknown sign. fit$ms_community$var_attenuation$debias records "aghq" or
"none", $affects names the lower-bounded components, and print() flags
the EM case.
occu_cover() (single species), ms_occu() (community occupancy,
no cover), ms_abun() (community N-mixture).
f <- ms_occu_cover(response = "lognormal") ff <- ms_occu_cover(response = "lognormal") f
The count of observed response entries the fit's likelihood reads, which is
the sample size stats::AIC() and stats::BIC() form an information
criterion at. What one entry is follows the family's own response layout: a
surveyed (site, visit) cell for the occupancy families, a (site, band) count
for distance sampling, a (site, visit, source) cell for the integrated ones.
## S3 method for class 'tobs_fit' nobs(object, ...)## S3 method for class 'tobs_fit' nobs(object, ...)
object |
A |
... |
Ignored. |
Integer count of the fit's observed response entries. A model type with no registered count is an error naming that type.
sim <- simulate_occu(N = 100, J = 3, n.occ.covs = 1, n.det.covs = 1, seed = 1) fit <- tobs(~ occ_cov1, data = sim$data, family = occu(), detection = ~ det_cov1, y = sim$y, method = "laplace", control = list(verbose = FALSE, progress = FALSE)) nobs(fit)sim <- simulate_occu(N = 100, J = 3, n.occ.covs = 1, n.det.covs = 1, seed = 1) fit <- tobs(~ occ_cov1, data = sim$data, family = occu(), detection = ~ det_cov1, y = sim$y, method = "laplace", control = list(verbose = FALSE, progress = FALSE)) nobs(fit)
Latent Bernoulli occupancy state with binomial detection per visit. The MacKenzie et al. (2002) single-season model.
occu()occu()
A tobs_family object.
method = "laplace" – penalized EM + Laplace with Gaussian marginals, the
default.
method = "laplace_sla" – the Laplace fit with simplified-Laplace
skew-corrected marginals.
method = "laplace_gibbs" / method = "laplace_mi" – the Laplace fit
with a post-EM Gibbs / multiple-imputation variance correction
(Rubin-pooled refits, not an MCMC chain).
method = "pg_gibbs" – a Polya-Gamma Gibbs sampler over the exact
posterior (the spOccupancy PGOcc engine): site-level detection, an areal
icar() field is accepted; a temporal() / re() / latent() / svc()
term errors with a pointer.
method = "nested_laplace" – multi-block nested Laplace: areal, temporal
and random-effect hyperparameters are integrated on an outer grid, and
predict() interpolates all-NA detection sites from the field.
method = "nested_laplace_sla" – the nested route with skew-corrected
marginals.
method = "nuts" – the unified C++ NUTS sampler over the structured
terms (spatial, temporal, re(), svc(), latent()), reporting Rhat /
ESS.
f <- occu() ff <- occu() f
Scans candidate spatial cell sizes and yearly clusterings of long-format,
typically single-visit plot data and scores each by how well a single-season
occupancy model separates occupancy (psi) from detection (p). Replication
is manufactured by pooling plots into a spatial cell (assumes plots in a cell
share psi) and years into a contiguous block (assumes occupancy is closed
across the block); an occasion is any record in a (cell, block) bucket, so
single-visit plots, repeat-visited plots, and the no-pooling limit all flow
through one code path.
occu_aggregation_scan( data, response, coords, year, plot = NULL, cell.sizes = NULL, block.lengths = NULL, score = c("info", "count"), family = c("occupancy"), control = list() )occu_aggregation_scan( data, response, coords, year, plot = NULL, cell.sizes = NULL, block.lengths = NULL, score = c("info", "count"), family = c("occupancy"), control = list() )
data |
Long data.frame, one row per plot-year record (or plot-year-visit). |
response |
Name of the 0/1 detection column. |
coords |
Length-2 character vector naming the x and y coordinate columns. |
year |
Name of the integer year column. |
plot |
Optional name of the plot-id column; enables the within-cell homogeneity proxy. |
cell.sizes |
Numeric vector of candidate cell edge lengths. |
block.lengths |
Integer vector of candidate contiguous block lengths (in
years). |
score |
|
family |
Observation family. Only |
control |
List of tuning knobs: |
Two scoring modes:
"info" fits the constant (intercept-only) occupancy model per candidate
and reports the posterior SEs of psi/p and the smallest eigenvalue and
condition number of the 2x2 (logit psi, logit p) observed information. A
confounding ridge shows up directly as a near-zero eigenvalue.
"count" checks only the structural necessary conditions (units with two
or more occasions, detected units, a non-degenerate naive detection rate).
The recommended candidate is the least-pooling identifiable one (smallest mean occasions per unit, ties broken by largest information eigenvalue): pool only as much as identifiability requires. When no candidate is identifiable the data cannot support a standard occupancy model and a Royle-Nichols or count model is the alternative.
A tobs_aggregation_scan object: candidates (scored data.frame),
recommended (chosen row or NULL), segmentation (auto changepoint
blocks, if used), and scan metadata.
set.seed(1) d <- data.frame(east = runif(200), north = runif(200), year = sample(2001:2006, 200, replace = TRUE), detected = rbinom(200, 1, 0.3)) scan <- occu_aggregation_scan(d, response = "detected", coords = c("east", "north"), year = "year", cell.sizes = c(0.2, 0.4), block.lengths = c(2, 3)) scanset.seed(1) d <- data.frame(east = runif(200), north = runif(200), year = sample(2001:2006, 200, replace = TRUE), detected = rbinom(200, 1, 0.3)) scan <- occu_aggregation_scan(d, response = "detected", coords = c("east", "north"), year = "year", cell.sizes = c(0.2, 0.4), block.lengths = c(2, 3)) scan
A hurdle for a response that is either absent or, when present, one of K
nominal (unordered) classes – a colour morph, microhabitat / substrate
use, or a cryptic-species / classifier label observed given the organism is
present. The presence/absence part is a Bernoulli arm; the class given present
is a baseline-category multinomial logit (the last class is the baseline). The
two arms factorise the likelihood exactly:
with .
occu_categorical(classes = NULL)occu_categorical(classes = NULL)
classes |
optional character vector of class labels (length |
This is the categorical counterpart of cover() (presence + a magnitude):
here the positive part is an unordered class, so it uses a multinomial logit
rather than beta / lognormal. For an ordered class response (Braun-Blanquet
cover bands) use cover(response = "ordinal"), which exploits the ordering;
this family is for classes with no ordering.
A tobs_family object.
y is a length-N integer vector in 0..K: 0 is absent, k is class k.
It may sit on the formula left-hand side (y ~ ...), so y = can be dropped.
The formula predictors are shared by both arms.
Non-spatial Laplace (method = "laplace"), the only route. The multinomial math is the
FD-validated tulpa kernel (multinomial_logit.h); the non-spatial fit is the
vectorised R Newton over the same closed forms. Spatial fields / NUTS (the
native multi-process likelihood) and the latent-class misclassification
variant (the K-class generalisation of fp_occu(), a confusion matrix on the
observed label) are documented follow-ups.
cover() (presence + magnitude), fp_occu() (two-state
false-positive detection).
f <- occu_categorical(classes = c("red", "green", "blue")) ff <- occu_categorical(classes = c("red", "green", "blue")) f
Cell-level latent presence psi, per-visit binomial detection p, and
per-visit positive cover f_pos (beta or lognormal) on a third linear
predictor. The N-mixture analogue for vegetation cover: where abun()
couples occupancy to counts via Royle's binomial-N marginal, occu_cover()
couples it to a positive-cover observation at each detected visit. The
latent presence z marginalises out in closed form (two states), so the
marginal log-likelihood is exact.
occu_cover( response = c("beta", "lognormal", "gaussian"), cover.aggregate = NULL )occu_cover( response = c("beta", "lognormal", "gaussian"), cover.aggregate = NULL )
response |
likelihood for the positive cover arm. |
cover.aggregate |
how the cover arm collapses per occupancy unit on the
shared-field spatial path: |
Per-cell likelihood:
any_det_i : L_i = psi_i * prod_j h_ij
no_det_i : L_i = psi_i * prod_j (1 - p_ij) + (1 - psi_i)
h_ij = (1 - p_ij) * 1{y_ij = 0}
+ p_ij * f_pos(y_pos_ij; eta_pos_ij, ...) * 1{y_ij = 1}
A detected visit (y_ij = 1) with a missing cover (y_pos_ij = NA) keeps its
detection term but drops the f_pos factor: cover is taken missing-at-random,
so the cover likelihood runs over the detected visits with an observed cover.
Reduces to occu() when the cover arm is degenerate, and to the
plot-level cover hurdle (cover()) when J = 1 and detection is perfect.
A tobs_family object.
The non-spatial fit is a direct Laplace approximation (method = "laplace")
or a NUTS sampler over the same exact two-state marginal (method = "nuts",
beta or lognormal cover), giving calibrated intervals and a per-draw
pointwise likelihood for WAIC / LOO. A shared areal field across the
occupancy and cover arms is the method = "nested_laplace" path (a structured
icar() / bym2() term on the psi formula); a structured term on a
non-spatial route errors from the dispatcher with a pointer to it.
method = "nuts" also samples the coupled areal field(s)
(icar() / bym2() / car_proper()), written either as areal terms or as
the bar spatial(~ 1 || cell, graph = adj), TOGETHER WITH their
hyperparameters: the field SD, the mixing (bym2) or spatial-correlation
(car_proper) parameter, and - where the formula asks for one - the cover-arm
copy amplitude. Each is bounded to the span the nested_laplace path's
outer quadrature integrates that axis over, in that grid's own coordinate,
and the same control$sigma.grid / alpha.grid / rho.car.grid knobs set
it on both routes - so the sampler is an independent reference for the
hyperparameter layer rather than a fit conditioned on that layer's point
estimate. The measure over that span is the one the outer grid declares for
the axis: flat in the grid's coordinate for the field SD and the mixing
parameter, and for the copy amplitude the penalized-complexity Exponential
slab, with control$copy.slab = "flat" asking for the flat alternative the
grid also accepts. The grid weighs that slab against a point mass at
alpha = 0 ("no coupling"), which a gradient sampler cannot visit, so the
sampled copy amplitude is the posterior conditional on a coupled field; the
point mass's share of the prior is read on the nested_laplace route. fit$nuts$sampled_hyper and
fit$nuts$fixed_hyper name which is which per fit (icar pins rho at 1:
the intrinsic precision has no mixing parameter; a field with no share() pins
the copy amplitude at 0), fit$hyper_draws carries
their posterior alongside field_sd, the geometric-mean marginal SD the
field implies. control$fixed.hyper = TRUE conditions on the warm
nested-Laplace estimate instead.
The grid-integrated nested_laplace path reports field_sd too
(fit$spatial$field_sd_mean / field_sd_sd, alongside sigma_mean), in
the SAME geo-mean-marginal-SD convention as the NUTS path's field_sd.
It is a fixed multiple of sigma (not an independent grid axis), so it is
NOT in fit$means / fit$vcov – folding it in there would make the
joint parameter vcov exactly singular. Do not compare fits, or a fit
against simulate_occu_cover()'s sigma, by reading the raw sigma on
this path: it is the field's amplitude against the unscaled intrinsic ICAR
precision Q = D - W, which differs from field_sd
by sqrt(scale_q), a graph-size-dependent factor (about 2.1 for a 30-node
chain graph). field_sd is the number comparable across fits and to a
simulation truth; sigma is the raw amplitude the engine's grid axis is
spelled in.
A spatially-varying coefficient - a weighted areal term beside the
unweighted intercept field, or a bar column
spatial(~ 1 + year || cell, graph = adj) - is sampled as a SECOND field
block, with its own whitened surface and its own (SD, mixing, copy amplitude)
coordinates: two fields share no hyperparameter. The surfaces are
fit$spatial_field and fit$trend_field / fit$trend_fields (named by the
weight column), and each block's hypers carry that block's suffix in
fit$hyper_draws and in fit$nuts$sampled_hyper (sigma_trend,
alpha_trend, indexed when there are several). Each field's amplitude is
addressed on its own through share(spatial(), terms = list(...)) – with
stated nodes, or grid(n = ) for the engine's axis read more finely – or
through control$alpha.grid[.trend] / control$alpha.n[.trend], the same
knobs the grid-integrated route reads. A second field
multiplies the warm fit's outer grid, so a defaulted axis is thinned to three
nodes over the same span - the sampled prior is unchanged, the warm fit stays
affordable. A correlated bar (|, one free-Sigma MCAR block across the
fields) stays on the grid-integrated nested_laplace path; the community
spatial-factor route is ms_occu_cover().
On the shared-field nested_laplace path the field-coupled occupancy slope
Wald interval is mildly anti-conservative at small N (the grid-integrated
Laplace under-disperses that coefficient; the pooled coverage across the three
arms stays near nominal). The non-spatial nuts path gives fully calibrated
occupancy intervals.
A term's process is the formula it sits in. An areal term written in the
occurrence formula therefore loads on the OCCURRENCE arm alone: the cover arm
sees no field, and the copy amplitude alpha is 0. This holds under
method = "nested_laplace" and method = "nuts" alike, so the same input
fits the same model on either engine.
To carry the occurrence field onto the cover arm as well, write the copy in
the positive formula:
positive = ~ pos_cov + share(spatial()) # amplitude estimated
positive = ~ pos_cov + share(spatial(), alpha = grid(c(...))) # over given nodes
positive = ~ pos_cov + share(spatial(), alpha = grid(n = 9)) # engine axis, finer
positive = ~ pos_cov + share(spatial(), alpha = 0.5) # fixed
positive = ~ pos_cov + share(spatial(), prior = list("pc.prec", c(4, 0.01)))
The amplitude is integrated over that axis on the nested_laplace path and
sampled over its span on the nuts path; a scalar alpha = pins it on both.
prior = regularizes the coefficient itself, in the joint driver's
list(<family>, <params>) shape (see prior_alpha in
tulpa::tulpa_nested_laplace_joint()); one reaches the engine per fit, so a
fit copying both an intercept and a trend block is refused it rather than
given it on the first.
control$alpha.grid[.trend] and control$alpha.n[.trend] are the WIRE
FORMAT these compile into, not user surface: a fit that sets one is refused
and told which formula form to write.
control$prior.alpha is still the lower-level spelling of prior =; set it
in one place, not both.
Without a share() the occurrence field is
NOT carried onto the cover arm – there is no implicit coupling – so the
amplitude is not a parameter of the model, and neither engine reports one.
Under nested_laplace alpha is absent from coef(), vcov() and the
n_params count; under nuts it is absent from fit$nuts$sampled_hyper,
fit$nuts$fixed_hyper and the fit$hyper_draws columns. predict() returns
a cover arm the occurrence field does not enter. Add share(spatial()) to
couple the two arms.
The axis the amplitude rides carries prior structure – a point mass at
alpha = 0 ("no coupling") and a log-spaced slab above it – so it is set
in one of two ways. share(alpha = grid(...)) STATES its nodes, and with them
that structure. share(alpha = grid(n = 9)) states a RESOLUTION: the engine
re-reads its own axis with that many slab nodes, point mass and slab bounds
unchanged, so sharpening the axis never restates its structure. terms =
gives either, per block. The resolution is the only way to raise this axis,
because it does not densify when the donor control$sigma.grid does: on an
informative data set the outer grid's quadrature effective sample size
saturates on the copy amplitude while every other axis tracks the request
(measured engine-side, NOTES_measurements.md). One block takes one of the
two; giving both is an error. control$alpha.grid[.trend] and
control$alpha.n[.trend] are the wire format these compile into and are
refused as user input.
Set it when the fit's hyperparameter intervals are reported. On the coupled
SBC fixture the declared resolution leaves the field SD miscalibrated once
the data are informative – at 10 visits per site its uniformity p-value is
9.1e-05, against 0.17 at 3 visits – and control$alpha.n = 21 returns every
scored quantity to nominal at both. Thirteen slab nodes do not (the field SD
still reads 4.5e-03), and the price is a 2.3-3x longer fit, since the outer
grid is a tensor. Point estimates are not affected the way the intervals are;
the measurement is in NOTES_measurements.md.
The spatial engines (method = "nested_laplace", default
control$engine = "joint", and method = "nuts") share one areal field (the
cell intercept, an unweighted icar() / bym2() term) across the occupancy
and cover arms, when the positive formula copies it. ADDITIONAL coupled
fields - spatially-varying coefficients,
e.g. a temporal trend - are added as weighted areal terms in the formula:
~ elev + icar(graph = adj) + icar(graph = adj, weight = year)
Each weighted term icar(graph, weight = col) is a second field whose
contribution to a predictor row is weight_i * z[cell_i]. All such fields
share the same graph; a share() carries one onto the cover arm with its own
scale (alpha for the intercept field, alpha_trend for a trend field),
integrated over the outer grid, and a field the positive formula does not
copy stays on occurrence. share(spatial(), terms = list(...)) gives a
per-block amplitude and must address every block. The intercept field is
reported in fit$spatial_field, trend fields in fit$trend_field /
fit$trend_fields. The trend coupling grid defaults to
the intercept block's amplitude; give the trend its own with
share(spatial(), terms = list(intercept = ..., trend = ...)). Under
method = "nuts" each field is its own sampled block, with the scales
sampled over those same spans rather than integrated over the grid.
A single trend field may also be requested out-of-band with
control = list(trend = list(weight = "<col>")), naming a numeric per-cell
covariate in the cell data; this is the equivalent of one weighted formula
term. Specify the trend field one way or the other, not both.
group.var)By default each site (one row of y / data, one latent occupancy state) is
its own field node, so the graph must have one node per site. Passing
group.var = "<col>" to the icar() / bym2() term maps each site to a
field node named by that integer column, so several sites can share one node.
The field then stays length n_cells (the graph) while occupancy, detection,
and cover run over n_sites. The motivating layout is a site = cell x
time-period: plots in a cell-period are the detection replicates, occupancy is
per cell-period, and a per-site time weight on a coupled trend field
(icar(graph, weight = time, group.var = "cell")) gives a detection-corrected
occupancy trend on a shared cell field.
re() / (1 | g))On the spatial nested_laplace path a single random INTERCEPT term on the psi
formula – re(g) or the (1 | g) bar – layers a per-group occupancy offset
on top of the shared field. It joins the joint fit as one iid prior block
whose latent rides the occupancy arm only; its variance integrates on the
outer grid alongside the field sigma / alpha and is reported as the sigma_re
hyperparameter, with the per-group BLUPs in fit$re and via ranef(). A
default re.sigma.grid (log-spaced) sets the grid; pass control$re.sigma.grid
to override. Scope: one random-intercept term (a scalar per group maps onto the
one iid block); a random slope, a correlated multi-coefficient block, or an RE
without a shared field is rejected (the joint engine integrates every variance
component on its grid, so multiple variance components do not scale – richer
RE is the non-spatial cover-hurdle EM's route). This is the single-species
analogue of the community spatial occu_cover (ms_occu_cover())
and the tulpaObs consumer of tulpa's field + per-group RE engine composition.
A bar is therefore always a random effect, never a spatial field, even when the
grouping factor names the areal graph nodes. The engine's inline-MCAR call
tulpa::spatial(graph, ~ 1 + x | cell) reads | cell as a separable spatial
field, but the same spelling in an occu_cover() formula is an IID random
effect on cell. For a spatial field on the cells use an areal term –
icar(graph = adj, group.var = "cell") (plus
icar(graph = adj, weight = x, group.var = "cell") for a spatially-varying
trend) – not a bar. When a formula carries a bar whose grouping factor is also
an areal term's group.var, occu_cover() emits a one-time message noting the
bar is fitted as a random effect; suppress it with base::suppressMessages().
A share(spatial()) in the positive formula shares the occupancy field: the
cover arm sees it as alpha * (occupancy field), the coregionalization copy
on the outer (sigma, alpha) grid. When the cover trend is spatially
structured but is not
a scalar multiple of the occupancy field, alpha collapses toward 0 and the
cover arm inherits no field, so per-cell conditional cover (and its change over
time, delta_cover_cond) comes out flat. A spatial() bar placed in the
positive formula adds an INDEPENDENT, non-copied areal field on the cover arm
alone:
tobs(occurrence = ~ x + icar(graph = adj, group.var = "cell"),
detection = ~ 1,
positive = ~ time + spatial(~ 1 + time || cell, graph = adj),
data = cell_dat, y = y, y.pos = y_pos,
family = occu_cover("lognormal"), method = "nested_laplace")
The occupancy field still drives psi and, via the alpha copy, delta_cover_exp;
the independent cover field carries a cover-specific structure the alpha copy
cannot express. Its intercept + coefficient columns become separate ICAR blocks
on the cover arm, each with its own precision integrated on the outer grid and
reported as sigma_pos_field (intercept) / sigma_pos_field_<col> (a covariate
column). The field SD grid defaults to control$sigma.grid; set
control$sigma.grid.pos.field to integrate it over its own (usually coarser)
grid, which keeps the added axis from multiplying the outer-grid cost.
The per-cell field posterior is in fit$pos_field / fit$pos_field_table
(and fit$pos_field_tables per column). Scope: it composes with the shared
occupancy field but uses per-visit cover (cover.aggregate = "none") and does
not combine with the correlated | MCAR field, the latent cover RE, or the
batched fused path; like the shared field it is fitted as ICAR (bym2/car read as
ICAR). A static intercept cover field is only weakly identified against the
occupancy field's alpha copy (both are per-cell cover offsets); a time-weighted
trend cover field is identified separately and is what makes delta_cover_cond
spatially varying.
The same placement works on the detection arm: a spatial-field term in the
detection formula (detection = ~ 1 + spatial(~ 0 + time || cell, graph = adj)) fits an independent field on the detection predictor, for a
spatially-structured detection probability. Its SD is reported as
sigma_p_field / sigma_p_field_<col>.
A full-field occu_cover() fit integrates over a large outer hyperparameter
grid and can run for hours, so a reboot or OOM kill otherwise loses the whole
run. control$checkpoint = list(path = "fit.ckpt", resume = TRUE) makes the
joint engine append each completed grid cell to path; a resume = TRUE run
loads the finished cells and solves only the remaining ones, reproducing the
from-scratch fit. resume = FALSE starts a fresh file (any stale checkpoint at
path is removed first). A torn tail from a killed write is discarded and
re-solved, and a checkpoint written for different data or settings is rejected
rather than resumed onto. Forwarded to tulpa::tulpa_nested_laplace_joint().
cover.aggregate)On the shared-field spatial path the cover arm has one observation per
detected visit, so a cell with many detected plots informs the shared field
far more than the single occupancy observation for that cell; the field is
then driven almost entirely by the cover arm and the detection-corrected
occupancy surface flattens. cover.aggregate collapses the cover arm to a
single response per occupancy unit (cell-period) so the two arms inform the
field with comparable weight:
"mean" – model the mean cover over the unit's detected visits;
"median" – model the median cover;
"latent" – model a per-unit cover random effect
shared across the unit's detected visits
and integrated out per unit, so the cover arm contributes one marginal
observation per unit while keeping every detected visit (the principled
counterpart of mean / median aggregation). The lognormal arm integrates
in closed form (compound-symmetry); the beta arm uses adaptive
Gauss-Hermite quadrature (control$n.quad, default 15). The within-unit
dispersion is pre-fit from the within-unit spread and held fixed;
is integrated on the outer grid (control$sigma.u.grid).
"none" – the per-visit cover arm (one cover observation per
detected visit).
NULL (the default) selects "mean" on the shared-field spatial
nested_laplace path and "none" on the non-spatial laplace path (which
has no shared field to over-weight). Aggregation needs a cell-level positive
design: a visit-level covariate in the positive formula cannot be collapsed
to one value per cell and errors. Aggregation is currently wired on the
spatial path only; requesting "mean" / "median" on the non-spatial
laplace fit errors rather than silently using per-visit cover.
Under method = "nested_laplace" (shared-field spatial) and method = "nuts"
(non-spatial, which samples the group SD), random
intercepts may be written on the detection or positive-cover formula
with the usual lme4 bar or re() spelling, e.g.
detection = ~ effort + (1 | habitat). The grouping is per visit – one code
per (site, visit) entry, distinct from the per-site occupancy-arm
grouping – so a many-level categorical visit covariate (an EUNIS
habitat class, an observer) enters as a partially pooled random intercept.
Crossed ((1 | habitat) + (1 | observer)) and nested
((1 | region/site), which expands to re(region) + re(region:site))
groupings are supported: each term joins the joint fit as its own iid latent
block. Each block's variance integrates on the outer grid (sigma_re_p /
sigma_re_pos for a lone term on the detection / cover arm, suffixed by the
grouping variable – sigma_re_p_habitat – when several terms share an arm;
tune with control$re.sigma.grid.p / re.sigma.grid.pos). The per-group BLUPs
are reported in fit$re (one entry per term, keyed by arm or "<arm>:<var>")
and via ranef(). predict() sums every term's BLUP offset on the predicted
arm when newdata carries the grouping columns and type = "detection" (or
"occurrence" / "cover_cond"); an unseen / held-out level shrinks that term
to the population mean. Several crossed terms multiply the outer grid, so set
control$integration = "ccd" (and/or coarsen the RE grids) at scale.
Random slopes are also supported (tulpa engine >= 0.0.39): an uncorrelated
slope ((x || g), (0 + x | g)) is one per-row weighted iid block per
coefficient, and a correlated slope ((1 + x | g)) one multivariate
free-Sigma miid block. A slope term's fit$re entry carries an
[n_groups x n_coefs] BLUP matrix, a per-coefficient sigma, and (correlated)
a cor matrix; the hyperparameters are sigma_re_p_<coef> and
cor_re_p_<ci>_<cj>, marginalized over the grid. predict() weights each
coefficient by its covariate column in newdata (intercept = 1). Each slope
covariate is standardized to unit SD before fitting (mirroring the
fixed-effect design), so the variance grid is scale-invariant – the reported
slope BLUPs and sigma are back-transformed to the covariate's natural units
(correlation is scale-free). A correlated slope adds p(p+1)/2 Sigma axes to
the outer grid, so its free-Sigma grid uses a compact principled default
(symmetric correlation nodes including 0); widen it with
control$re.logchol.grid.p / re.logchol.grid.pos.
The positive-cover RE needs per-visit cover (cover.aggregate = "none"). As
with the occupancy-arm RE, each grid-integrated variance carries the binary /
small-cluster inner-Laplace attenuation and is a lower bound on the truth; the
BLUPs recover the per-group structure.
method = "nuts" samples an observation-arm random intercept instead of
integrating it on a grid: each grouping factor is a non-centered block
(b_g = sigma_re z_g, z_g ~ N(0, 1)) whose group SD is a coordinate of the
sampled vector under a N(0, 1.5^2) prior on log sigma_re – the same
weakly informative width the other observation-family samplers use, and the
reason the sampled SD carries no small-cluster attenuation to correct for.
Crossed and nested groupings are simply several blocks, on either or both
observation arms. fit$re keys and ranef() match the nested_laplace
path; each term additionally reports sigma_median and the per-draw
sigma_draws, the summaries to quote for a right-skewed variance component,
with per-SD convergence in fit$nuts$re_sigma_rhat / re_sigma_ess. The
sampled coefficient surface (coef(), vcov(), fit$draws) is unchanged.
Still on nested_laplace only: a random slope, and an observation-arm RE
composed with the coupled areal field. waic() / cpo() on a
sampled fit read the coefficient draws, so they score the RE at zero – the
same limitation the sampled coupled field has.
occu() (no cover), cover() (plot-level hurdle, no detection),
abun() (counts not cover).
N <- 120; J <- 4 sim <- simulate_occu_cover(N = N, J = J, n.occ.covs = 1, n.det.covs = 1, n.pos.covs = 1, positive = "beta", seed = 1) long <- data.frame(site_id = rep(seq_len(N), each = J), visit = rep(seq_len(J), times = N), y = as.vector(t(sim$y)), det_cov1 = sim$visit_data$det_cov1, pos_cov1 = sim$visit_data$pos_cov1) od <- tobs_data(long, y = "y", site = "site_id", visit = "visit", det.covs = c("det_cov1", "pos_cov1")) cell_dat <- cbind(data.frame(site_id = seq_len(N)), sim$data) y_pos <- sim$y_pos; y_pos[is.na(y_pos)] <- 0 fit <- tobs(~ occ_cov1, data = cell_dat, family = occu_cover("beta"), detection = ~ det_cov1, positive = ~ pos_cov1, y = od$y, y.pos = y_pos, visits = od$det.covs, method = "laplace") summary(fit)N <- 120; J <- 4 sim <- simulate_occu_cover(N = N, J = J, n.occ.covs = 1, n.det.covs = 1, n.pos.covs = 1, positive = "beta", seed = 1) long <- data.frame(site_id = rep(seq_len(N), each = J), visit = rep(seq_len(J), times = N), y = as.vector(t(sim$y)), det_cov1 = sim$visit_data$det_cov1, pos_cov1 = sim$visit_data$pos_cov1) od <- tobs_data(long, y = "y", site = "site_id", visit = "visit", det.covs = c("det_cov1", "pos_cov1")) cell_dat <- cbind(data.frame(site_id = seq_len(N)), sim$data) y_pos <- sim$y_pos; y_pos[is.na(y_pos)] <- 0 fit <- tobs(~ occ_cov1, data = cell_dat, family = occu_cover("beta"), detection = ~ det_cov1, positive = ~ pos_cov1, y = od$y, y.pos = y_pos, visits = od$det.covs, method = "laplace") summary(fit)
Assemble the paired occurrence / cover response arms, the visit-level
covariate grid, and the site-level design frame that an occu_cover() fit
consumes, from one long (one row per site-visit / plot) data frame. This is
the same construction tobs() runs internally when a single occu_cover()
fit is handed a long frame; call it directly when you want to inspect the
arms before fitting, or pass them on as y = , y.pos = , visits = ,
data = to tobs().
The occurrence and cover arms are pivoted onto one shared (sites, visits)
grid, so they align row-for-row, and the site-level design is the first row
per site. The cover arm is positive-only (meaningful where the species is
detected): in the dense grid absent / unsampled cover cells are set to 0,
and in the compact (ragged) carrier they are NA in the values and never
read by the joint engine.
occu_cover_inputs( data, site, visit, response, y.pos, occ.covs = NULL, det.covs = NULL, coords = NULL, compact = TRUE, positive = "beta" )occu_cover_inputs( data, site, visit, response, y.pos, occ.covs = NULL, det.covs = NULL, coords = NULL, compact = TRUE, positive = "beta" )
data |
A long / plot-level data frame: one row per site-visit. |
site, visit, response, y.pos
|
Column names: the site identifier, the
visit / replicate, the 0/1 detection response, and the continuous cover
response (used only where |
occ.covs |
Optional character vector of site-level covariate columns.
The default ( |
det.covs |
Optional character vector of visit-level covariate columns. |
coords |
Optional length-2 character vector of coordinate columns. |
compact |
Logical (default |
positive |
The cover-arm distribution, matching |
A list with y, y_pos, visits, site_data, coords,
n_sites, max_visits, n_visits, and compact.
sim <- simulate_occu_cover(N = 20, J = 3, seed = 1) long <- data.frame(site = rep(1:20, each = 3), visit = rep(1:3, 20), occur = as.vector(t(sim$y)), cover = as.vector(t(ifelse(is.na(sim$y_pos), 0, sim$y_pos))), det_cov1 = sim$visit_data$det_cov1, occ_cov1 = rep(sim$data$occ_cov1, each = 3)) inp <- occu_cover_inputs(long, site = "site", visit = "visit", response = "occur", y.pos = "cover", det.covs = "det_cov1", compact = FALSE, positive = "lognormal") dim(inp$y)sim <- simulate_occu_cover(N = 20, J = 3, seed = 1) long <- data.frame(site = rep(1:20, each = 3), visit = rep(1:3, 20), occur = as.vector(t(sim$y)), cover = as.vector(t(ifelse(is.na(sim$y_pos), 0, sim$y_pos))), det_cov1 = sim$visit_data$det_cov1, occ_cov1 = rep(sim$data$occ_cov1, each = 3)) inp <- occu_cover_inputs(long, site = "site", visit = "visit", response = "occur", y.pos = "cover", det.covs = "det_cov1", compact = FALSE, positive = "lognormal") dim(inp$y)
Joint occupancy of S species where the occupancy state z in {0,1}^S
follows a log-linear model with first-order (per species) and second-order
(per species pair) natural parameters (Rota et al. 2016; unmarked
occuMulti). The second-order parameters are the species interactions
(positive = co-occur more than independent; negative = avoid). The state
formula is the shared occupancy covariate design (each natural parameter
carries its own coefficients); detection the shared site-level per-species
detection design. The latent state integrates out by enumerating the 2^S
states, so the exact marginal is maximised with an observed-information vcov.
occu_multi()occu_multi()
y is a length-S list of N x J 0/1/NA detection matrices (or a 3D
[sites x visits x species] array); species names the arms.
A tobs_family object for tobs().
method = "laplace" – the enumerated 2^S-state marginal maximised
(BFGS) with an observed-information covariance; the only route (no field,
no sampler).
sim <- simulate_occu_multi(S = 2, N = 300, seed = 1) fit <- tobs(~ scov1, data = sim$data, family = occu_multi(), detection = ~ 1, y = sim$y, species = sim$species, control = list(verbose = FALSE)) coef(fit)sim <- simulate_occu_multi(S = 2, N = 300, seed = 1) fit <- tobs(~ scov1, data = sim$data, family = occu_multi(), detection = ~ 1, y = sim$y, species = sim$species, control = list(verbose = FALSE)) coef(fit)
A cell-level occupancy gate, a plot-level availability gate, per-visit
detection, and the cover hurdle, for data where a site's "visits" are
spatially distinct plots aggregated into a (cell, period) rather than
temporal revisits (the EVA / MOTIVATE vegetation layout; Nichols et al.
2008; Mordecai et al. 2011). occu_cover() treats plots as detection
replicates of one occupancy state, which on spatial subunits conflates
within-cell prevalence into the detection arm (Kendall & White 2009); this
family separates them with an explicit middle level:
occu_multiscale_cover(response = c("beta", "lognormal", "gaussian"))occu_multiscale_cover(response = c("beta", "lognormal", "gaussian"))
response |
likelihood for the positive cover arm. |
z_c ~ Bernoulli(psi_c) # cell / range occupancy a_cj | z=1 ~ Bernoulli(theta_cj) # plot availability / use y_cjv|a=1 ~ Bernoulli(p_cjv) # detection cover|y=1 ~ f_pos(.; eta_pos, disp) # cover hurdle (beta / lognormal)
Both z (over cells) and a (over plots) marginalize in closed form (two
states each), so the joint marginal log-likelihood is exact and reuses the
same nested-Laplace cell-coupling machinery as occu_cover().
A tobs_family object.
y / y.pos are [n_plots x max_visits] matrices (plots are the rows, the
availability units; visits the columns). The state-process formula is the
cell-level occupancy predictor and MUST carry an areal field naming the
per-plot cell column, icar(graph = adj, group.var = "cell"). availability = ~ ... is the plot-level theta predictor (default ~ 1); detection the
per-visit p predictor; positive = ~ ... the cover predictor (default the
detection formula). y.pos is read only where y == 1.
A detected visit (y_cjv = 1) with a missing cover (y_pos_cjv = NA) keeps
its detection term but drops the f_pos factor: cover is taken
missing-at-random, so the cover likelihood runs over the detected visits with
an observed cover. This is the rule occu_cover() applies, and it holds on
every engine below.
The availability (theta) and detection (p) levels separate only with
replication WITHIN a plot. Single releves supply none, so a plain fit
identifies psi (cell) and the product theta * p (plot) – it reduces to
occu_cover() with p := theta * p. Within-plot temporal replication (e.g.
a resurvey of the same plot in a later period) makes the third level
estimable.
Three engines. method = "nested_laplace" carries a single shared areal
field coupled across the occupancy (sigma) and cover (alpha * sigma)
arms, integrated over the outer (sigma, alpha) grid. method = "laplace"
and method = "nuts" are the non-spatial path (iid cells, no field): the
exact three-level marginal (z over cells, a over plots both summed in closed
form) optimised directly ("laplace", a Gaussian observed-Fisher posterior)
or sampled ("nuts", the exact coefficient posterior with calibrated
intervals and WAIC / LOO). Cells are declared the same way on every path, via
an icar(graph = adj, group.var = "<cell>") term (the graph drives the field
under "nested_laplace" and supplies only the plot -> cell map under
"laplace" / "nuts"). On the "nested_laplace" path additional weighted
areal terms in the psi formula
(icar(graph = adj, group.var = "<cell>", weight = <cell covariate>)) add
spatially-varying-coefficient trend fields, each coupled onto the cover arm
with its own alpha_trend; the fitted fields are in fit$trend_field /
fit$trend_fields. The coupled / trend field is not sampled, so
method = "nuts" takes a single cell-declaring areal term only.
The coupling is written as share(spatial()) in the positive formula, the
same spelling occu_cover() takes; it needs a field to copy, so it is
accepted under method = "nested_laplace" and refused on the two
non-spatial engines, which fix the field at 0. Omitting it decouples the
arms: the block is pinned at alpha = 0 and the field rides occupancy
alone, the same meaning the absence has on occu_cover(). There is no
implicit default coupling.
The copy amplitude's axis carries prior structure – a point mass at
alpha = 0 and a log-spaced slab above it – so share(alpha = grid(...))
STATES its nodes, while share(alpha = grid(n = )) states a RESOLUTION: the
engine re-reads its own axis with that many slab nodes, point mass and bounds
unchanged. terms = gives either, per block; one block takes one of the
two. control$alpha.grid[.trend] / control$alpha.n[.trend] are the wire
format these compile into and are refused as user input.
occu_cover() (two-level), cover() (plot hurdle, no detection).
f <- occu_multiscale_cover(response = "beta") ff <- occu_multiscale_cover(response = "beta") f
Constructs a prior specification consumed by tobs() when a Laplace
method (method = "laplace" etc.) is used for the occupancy family. The
penalty is applied as
a quadratic term + sum((beta_j - mu_j)^2 / (2 * sd_j^2)) added to the
negative-log-posterior in each M-step submodel block by
tulpa::tulpa_laplace() (via the per-block beta_prior).
occu_priors( p.intercept = list(mean = 0, sd = 1.5), p.slope = list(mean = 0, sd = 2.5), beta.occ.intercept = list(mean = 0, sd = 2), beta.occ.slope = list(mean = 0, sd = 5) )occu_priors( p.intercept = list(mean = 0, sd = 1.5), p.slope = list(mean = 0, sd = 2.5), beta.occ.intercept = list(mean = 0, sd = 2), beta.occ.slope = list(mean = 0, sd = 5) )
p.intercept |
Prior on the detection intercept (logit scale).
A list |
p.slope |
Prior on detection slopes (every non-intercept detection
coefficient). Default |
beta.occ.intercept |
Prior on the occupancy (psi / psi1) intercept.
Default |
beta.occ.slope |
Prior on occupancy slopes. Default
|
The defaults are weakly informative: they pull the detection intercept
toward p = 0.5 strongly enough to break the psi-p identifiability
ridge at small J, but stay wide enough not to bias estimates at
N >= 600 with informative covariates. Setting any sd to Inf disables
that component of the prior (same penalised objective, just 1/Inf^2 = 0).
An occu_priors object, ready to pass to tobs(..., priors = ...).
# disable the detection-slope penalty priors <- occu_priors(p.slope = list(mean = 0, sd = Inf)) priors sim <- simulate_occu(N = 100, J = 3, n.occ.covs = 1, n.det.covs = 1, seed = 1) fit <- tobs(~ occ_cov1, data = sim$data, family = occu(), detection = ~ det_cov1, y = sim$y, method = "laplace", priors = occu_priors(), control = list(verbose = FALSE, progress = FALSE)) coef(fit)# disable the detection-slope penalty priors <- occu_priors(p.slope = list(mean = 0, sd = Inf)) priors sim <- simulate_occu(N = 100, J = 3, n.occ.covs = 1, n.det.covs = 1, seed = 1) fit <- tobs(~ occ_cov1, data = sim$data, family = occu(), detection = ~ det_cov1, y = sim$y, method = "laplace", priors = occu_priors(), control = list(verbose = FALSE, progress = FALSE)) coef(fit)
Occupancy where a survey records the TIME to first detection rather than a
0/1 outcome (Garrard et al. 2008; unmarked occuTTD). At an occupied
site the time-to-detection is exponential with rate lambda (constant
hazard); a survey of length survey.length that reaches its end without a
detection is censored. An unoccupied site never detects. The state formula
models logit psi (occupancy); detection models log lambda (the
site-level detection rate). The latent occupancy state integrates out in
closed form (two states), so the fit maximises the exact marginal with an
observed-information vcov.
occu_ttd(survey.length = 1)occu_ttd(survey.length = 1)
survey.length |
Survey length |
y is an N x J matrix of detection times: a value in (0, survey.length)
is a detection; a value >= survey.length is a non-detection (censored);
NA is a survey not conducted.
A tobs_family object for tobs().
method = "laplace" – the exact marginal maximised (BFGS) with an
observed-information covariance; the only route (no field, no sampler).
sim <- simulate_occu_ttd(N = 200, J = 4, Tmax = 3, seed = 1) fit <- tobs(~ psi_cov1, data = sim$data, family = occu_ttd(survey.length = 3), detection = ~ rate_cov1, y = sim$y, control = list(verbose = FALSE)) coef(fit)sim <- simulate_occu_ttd(N = 200, J = 4, Tmax = 3, seed = 1) fit <- tobs(~ psi_cov1, data = sim$data, family = occu_ttd(survey.length = 3), detection = ~ rate_cov1, y = sim$y, control = list(verbose = FALSE)) coef(fit)
A single-season occupancy detection-history dataset for 120 wetland sites
surveyed on 4 visits, drawn from a known generative model (MacKenzie et al.
2002). Occupancy increases with site wetness and decreases with
elevation; per-visit detection increases with survey effort. Twelve
detection cells are set to NA to mimic incomplete histories. Synthetic, not
field data; the generative coefficients are in truth.
peatland_occupeatland_occu
A list with components:
120 x 4 integer matrix of detections (0/1/NA).
Data frame of site covariates: elevation, wetness
(both standardised).
Named list with one 120 x 4 visit covariate matrix,
effort (standardised).
120 x 2 matrix of x / y coordinates in the unit square.
List of generative quantities: beta_psi, beta_p, the
realised latent occupancy z, and per-site psi.
occu(), tobs(), simulate_occu()
data(peatland_occu) str(peatland_occu$y) fit <- tobs(~ elevation + wetness, data = peatland_occu$occ.covs, family = occu(), detection = ~ effort, y = peatland_occu$y, visits = peatland_occu$det.covs) coef(fit)data(peatland_occu) str(peatland_occu$y) fit <- tobs(~ elevation + wetness, data = peatland_occu$occ.covs, family = occu(), detection = ~ effort, y = peatland_occu$y, visits = peatland_occu$det.covs) coef(fit)
The tulpa::pit_residuals() method for tobs_fit: the family's own
predictive CDF, with the randomisation step a discrete response needs.
## S3 method for class 'tobs_fit' pit_residuals( object, n.samples = 250, nsim = NULL, seed = NULL, observed = NULL, ... )## S3 method for class 'tobs_fit' pit_residuals( object, n.samples = 250, nsim = NULL, seed = NULL, observed = NULL, ... )
object |
A |
n.samples |
Number of posterior samples (default 250). |
nsim |
Alias for |
seed |
Optional RNG seed for the posterior-draw selection, forwarded by
|
observed |
Must be |
... |
Must be empty; an unrecognised argument is an error. |
Numeric vector of PIT residuals.
tulpa::test_uniformity() for the uniformity test on the result.
sim <- simulate_occu(N = 100, J = 3, n.occ.covs = 1, n.det.covs = 1, seed = 1) fit <- tobs(~ occ_cov1, data = sim$data, family = occu(), detection = ~ det_cov1, y = sim$y, method = "laplace", control = list(verbose = FALSE, progress = FALSE)) pit <- pit_residuals(fit, seed = 1) hist(pit, breaks = 10)sim <- simulate_occu(N = 100, J = 3, n.occ.covs = 1, n.det.covs = 1, seed = 1) fit <- tobs(~ occ_cov1, data = sim$data, family = occu(), detection = ~ det_cov1, y = sim$y, method = "laplace", control = list(verbose = FALSE, progress = FALSE)) pit <- pit_residuals(fit, seed = 1) hist(pit, breaks = 10)
Heatmap of the identifiability score over the scanned (cell size x temporal
block) grid: information smallest-eigenvalue for score = "info", fraction
of replicated units for score = "count". Identifiable cells are outlined.
## S3 method for class 'tobs_aggregation_scan' plot(x, ...)## S3 method for class 'tobs_aggregation_scan' plot(x, ...)
x |
A |
... |
Ignored. |
Invisible NULL.
set.seed(1) d <- data.frame(east = runif(200), north = runif(200), year = sample(2001:2006, 200, replace = TRUE), detected = rbinom(200, 1, 0.3)) scan <- occu_aggregation_scan(d, response = "detected", coords = c("east", "north"), year = "year", cell.sizes = c(0.2, 0.4), block.lengths = c(2, 3)) plot(scan)set.seed(1) d <- data.frame(east = runif(200), north = runif(200), year = sample(2001:2006, 200, replace = TRUE), detected = rbinom(200, 1, 0.3)) scan <- occu_aggregation_scan(d, response = "detected", coords = c("east", "north"), year = "year", cell.sizes = c(0.2, 0.4), block.lengths = c(2, 3)) plot(scan)
2x2 panel: detection frequency histogram, per-visit detection rates, visit completeness, and spatial detection map (if coordinates available).
## S3 method for class 'tobs_data' plot(x, ...)## S3 method for class 'tobs_data' plot(x, ...)
x |
An |
... |
Ignored. |
Invisible NULL.
sim <- simulate_occu(N = 30, J = 3, seed = 1) long <- data.frame(site = rep(seq_len(30), times = 3), visit = rep(1:3, each = 30), det = as.vector(sim$y), occ_cov1 = sim$data$occ_cov1, effort = runif(90)) dat <- tobs_data(long, y = "det", site = "site", visit = "visit", occ.covs = "occ_cov1", det.covs = "effort") plot(dat)sim <- simulate_occu(N = 30, J = 3, seed = 1) long <- data.frame(site = rep(seq_len(30), times = 3), visit = rep(1:3, each = 30), det = as.vector(sim$y), occ_cov1 = sim$data$occ_cov1, effort = runif(90)) dat <- tobs_data(long, y = "det", site = "site", visit = "visit", occ.covs = "occ_cov1", det.covs = "effort") plot(dat)
The discrepancy-statistic check (the spOccupancy ppcOcc construction):
latent occupancy is drawn from its full conditional, a detection replicate
from the fitted model, and the two are compared through a discrepancy
statistic to give a Bayesian p-value. This is a test, not a plot, so it is
its own generic rather than a method on tulpa::pp_check(), which draws the
graphical check.
ppc(object, ...) ## S3 method for class 'tobs_fit' ppc(object, fit.stat = c("freeman-tukey", "chi-squared"), n.samples = 500, ...)ppc(object, ...) ## S3 method for class 'tobs_fit' ppc(object, fit.stat = c("freeman-tukey", "chi-squared"), n.samples = 500, ...)
object |
A |
... |
Passed to methods. |
fit.stat |
|
n.samples |
Number of posterior samples (default 500). |
A list with fit.y, fit.y.rep, and bayesian.p.
sim <- simulate_occu(N = 60, J = 3, seed = 1) fit <- tobs(~ occ_cov1, data = sim$data, family = occu(), detection = ~ det_cov1, y = sim$y, method = "laplace", control = list(verbose = FALSE)) ppc(fit, n.samples = 100)$bayesian.psim <- simulate_occu(N = 60, J = 3, seed = 1) fit <- tobs(~ occ_cov1, data = sim$data, family = occu(), detection = ~ det_cov1, y = sim$y, method = "laplace", control = list(verbose = FALSE)) ppc(fit, n.samples = 100)$bayesian.p
Occurrence probability is always p = plogis(X * beta_occ). The
conditional positive cover mu depends on the positive-part family:
## S3 method for class 'cover_fit' predict( object, newdata = NULL, type = NULL, include.RE = FALSE, times = NULL, time.col = NULL, level = 0.95, nsim = 1000L, draws = TRUE, weights = NULL, aggregate = FALSE, mc.tol = 0.05, nsim.max = 10000L, mc.floor = 0.001, ... )## S3 method for class 'cover_fit' predict( object, newdata = NULL, type = NULL, include.RE = FALSE, times = NULL, time.col = NULL, level = 0.95, nsim = 1000L, draws = TRUE, weights = NULL, aggregate = FALSE, mc.tol = 0.05, nsim.max = 10000L, mc.floor = 0.001, ... )
object |
A |
newdata |
A data frame of covariates matching the original formula. For
the nested-Laplace fit, one row per spatial unit (or a |
type |
Separate-Laplace fit: one of |
include.RE |
Ignored for the separate-Laplace fit (no spatial projection); the nested-Laplace fit always projects the shared field. |
times, time.col, level, nsim, draws
|
Nested-Laplace fit only: |
weights |
Nested-Laplace fit only: pool several |
aggregate |
Nested-Laplace fit, |
mc.tol, nsim.max, mc.floor
|
Nested-Laplace fit, trajectory only: the
Monte Carlo precision target, the draw cap and the SD floor the error is
measured against, as in |
... |
Unused. |
positive = "lognormal": mu = exp(eta_pos + sigma_pos^2 / 2)
(lognormal back-transform on log-cover).
positive = "beta": mu = plogis(eta_pos) (mean of the beta on
the natural cover scale with logit link).
Expected cover is E[y] = p * mu under both positive parts.
The separate-Laplace fit (method = "laplace") returns a fixed-effects-only
numeric vector. The nested-Laplace shared-field fit
(method = "nested_laplace") instead projects the shared occupancy-cover
field and returns a tobs_prediction of posterior draws – the same tidy /
change contract as predict.tobs_fit() for occu_cover(): pass
type = "change" with times = c(t1, t2) and time.col for a per-cell
delta map. Each prediction unit is a row of newdata (or a cell column
indexing the field cells), and every quantity is marginalized per draw over
the grid-integrated joint posterior.
Separate-Laplace fit: a numeric vector. Nested-Laplace fit: a
tobs_prediction.
fit <- tobs(cover ~ moisture, data = meadow_cover, family = cover(response = "beta")) nd <- data.frame(moisture = c(-1, 0, 1)) predict(fit, newdata = nd, type = "occupancy") predict(fit, newdata = nd, type = "conditional") predict(fit, newdata = nd, type = "expected")fit <- tobs(cover ~ moisture, data = meadow_cover, family = cover(response = "beta")) nd <- data.frame(moisture = c(-1, 0, 1)) predict(fit, newdata = nd, type = "occupancy") predict(fit, newdata = nd, type = "conditional") predict(fit, newdata = nd, type = "expected")
Returns, per row of newdata, the presence probability psi, the
conditional class probabilities p_1..p_K (given present), and the
unconditional class probabilities psi * p_k plus the absent probability
1 - psi.
## S3 method for class 'occu_categorical_fit' predict(object, newdata, ...)## S3 method for class 'occu_categorical_fit' predict(object, newdata, ...)
object |
an |
newdata |
a data frame of predictors for the shared formula. |
... |
ignored. |
A list with psi (length n), cond (n x K conditional class
probabilities), and joint (n x (K+1): absent, then class 1..K).
sim <- simulate_occu_categorical(N = 300, seed = 1) fit <- tobs(~ x, data = sim$data, family = occu_categorical(), y = sim$y) pr <- predict(fit, newdata = data.frame(x = c(-1, 0, 1))) round(pr$joint, 3)sim <- simulate_occu_categorical(N = 300, seed = 1) fit <- tobs(~ x, data = sim$data, family = occu_categorical(), y = sim$y) pr <- predict(fit, newdata = data.frame(x = c(-1, 0, 1))) round(pr$joint, 3)
Four modes:
In-sample: predict(fit) returns fitted values.
State posterior / NA-response: predict(fit, type = "state") returns
the marginalised per-site occupancy posterior of a "nested_laplace" fit:
the per-row eta posterior is a Gaussian mixture over the hyperparameter
grid (per-cell fitted linear predictor and predictive variance), so psi
is its Gauss-Hermite mean and psi_lower / psi_upper are the mixture-CDF
quantiles – a calibrated 95% credible interval, exact for every latent
prior. Rows with heldout = TRUE are the INLA-style NA-response
prediction targets (single-season sites whose detection history was
all-missing), where occupancy is interpolated from the latent field rather
than informed by detections.
Design-matrix: predict(fit, X.0 = ...) predicts at new covariate values.
Terms-based: predict(fit, terms = "elev") varies one covariate, others at mean.
Joint occu_cover: for a occu_cover joint-coupled fit,
predict(fit, newdata, type = "occurrence" | "cover_cond" | "cover_exp" | "change") samples the joint latent from the grid-integrated posterior
(the outer-grid mixture via tulpa::tulpa_posterior_draws()) and
marginalises every derived quantity per draw. type = "change" with
times = c(t1, ..., tK) (K >= 2) returns a per-cell change table. Each
quantity – occupancy psi, conditional cover cover_cond, expected
cover cover_exp – has a level column per step (psi_T1..psi_TK, ...)
carrying the posterior mean with .sd / .lwr / .upr at level, and a
delta per step against t1 (delta_psi, delta_cover_cond,
delta_cover_exp) with .lwr / .upr and .prob_pos, the posterior
probability P(delta > 0) per cell. The expected-cover change is split
exactly into its occupancy- and abundance-driven parts
(delta_cover_from_occ, delta_cover_from_ab), reported the same way.
With two times the delta columns are unsuffixed; with K > 2 each carries
_T<k>. Every step is evaluated on ONE draw set, so levels and deltas
share a posterior and are jointly valid. A standalone occu() joint fit
returns the same layout with occupancy only. The result is a
tobs_prediction table (one row per cell) carrying per-unit [cell x nsim]
draw matrices in attr(, "draws"); map it yourself, e.g.
left_join(cents, pr, by = "cell") then
geom_tile(aes(x, y, fill = delta_psi)) (or geom_sf() on polygon cells).
type = "trajectory" with times = c(t1, ..., tK) predicts occupancy
psi, conditional cover cover_cond and expected cover cover_exp at
each time, for any times the user chooses, as a long table with the
posterior mean, median and level interval of each (one row per cell and
time); aggregate = TRUE averages over the cells per draw and gives the
overall trajectory, one row per time. All times share one draw set.
Community families: ms_occu(), ms_dyn_occu(), ms_int_occu(),
ms_abun(), ms_distance() and ms_occu_cover() predict per-species
matrices [rows x species], in-sample from fitted() and at newdata
from the per-species coefficients. Each family answers to its own response
types: "occupancy" / "detection" (ms_occu, ms_dyn_occu,
ms_int_occu), "abundance" / "detection" (ms_abun),
"lambda" / "sigma" (ms_distance), and "occupancy" /
"detection" / "cover_cond" / "cover_exp" (ms_occu_cover). A
fit carrying an areal field or a latent-factor loading is in-sample only,
the field having no value at an unseen site.
Spatial-factor community: for a reduced-rank spatial-factor
ms_occu_cover() fit, predict(fit, type = "occupancy" | "cover_cond" | "cover_exp") returns the per-species per-cell posterior as a long table
(one row per cell x species): "occupancy" gives psi, "cover_cond" the
conditional cover mean E[cover | present], "cover_exp" the unconditional
expected cover psi * E[cover | present], each with a _median and a
_lower / _upper interval. The maps are marginalised over the loading +
field posterior, so a rare species borrows strength across the shared
factors for a calibrated map. The latent fields are tied to the cell graph,
so X.0 / newdata prediction is not supported.
## S3 method for class 'tobs_fit' predict( object, X.0 = NULL, type = c("occupancy", "detection", "both", "state"), quantiles = c(0.025, 0.5, 0.975), terms = NULL, n.points = 50L, newdata = NULL, times = NULL, level = 0.95, nsim = 1000L, draws = TRUE, time.col = NULL, X.det.0 = NULL, weights = NULL, aggregate = FALSE, mc.tol = 0.05, nsim.max = 10000L, mc.floor = 0.001, ... )## S3 method for class 'tobs_fit' predict( object, X.0 = NULL, type = c("occupancy", "detection", "both", "state"), quantiles = c(0.025, 0.5, 0.975), terms = NULL, n.points = 50L, newdata = NULL, times = NULL, level = 0.95, nsim = 1000L, draws = TRUE, time.col = NULL, X.det.0 = NULL, weights = NULL, aggregate = FALSE, mc.tol = 0.05, nsim.max = 10000L, mc.floor = 0.001, ... )
object |
A |
X.0 |
Optional design matrix for occupancy prediction. |
type |
|
quantiles |
Quantile levels for credible intervals: a strictly
increasing length-3 vector (lower, middle, upper), each in (0, 1). The
levels name the columns they fill, so |
terms |
Name of a single term to vary: one column of the predicted
process's design matrix, every other column held at its column mean. A
vector of more than one term is an error, since a second term would be
held at its mean rather than grouped over its levels. For a grid over
more than one covariate, build the design matrix and pass it as |
n.points |
Number of prediction points per continuous term. |
newdata |
data.frame of prediction units. For |
times |
|
level |
|
nsim |
|
draws |
|
time.col |
|
X.det.0 |
Optional detection design for out-of-sample |
weights |
|
aggregate |
|
mc.tol |
|
nsim.max |
|
mc.floor |
Trajectory only: the smallest posterior SD a Monte Carlo
error is measured against, on the quantity's own scale (default 0.001).
Where occupancy saturates near 0 or 1 its posterior SD goes to about 0,
and an error relative to it would be unstable; the error is measured
against |
... |
Ignored. |
Depends on mode. In-sample: fitted() result. "state": a
data.frame with row, psi (marginalised posterior mean), psi_lower /
psi_upper (equal-tailed 95% credible interval; NA when the engine did
not return per-cell predictive variance), and heldout. Design-matrix/
terms: data.frame with estimate and CIs; type = "detection" on an
int_occu() fit returns a named list of such data.frames (one per
source), and type = "both" returns list(occupancy = , detection = ).
occu_cover: a tobs_prediction table (one row per cell) with per-unit
draw matrices in attr(, "draws"); type = "trajectory" a long table
(cell, time, then <q>_mean / _median / _lwr / _upr for psi,
cover_cond and cover_exp), one row per cell and time, or per time
with aggregate = TRUE. A trajectory states its Monte Carlo precision in
attr(, "mc_se_max") (the largest MC standard error of any reported
median or bound, in posterior SDs) and attr(, "nsim_used"); under
nsim = "auto" also attr(, "mc_tol") and attr(, "nsim_capped")
(TRUE when nsim.max stopped the draws before mc.tol was met).
attr(, "mc_binding") names the bound that sets mc_se_max: a one-row
data frame with its cell (NA with aggregate = TRUE), time,
quantity, bound ("median", "lwr" or "upr") and mc_se.
sim <- simulate_occu(N = 100, J = 3, n.occ.covs = 1, n.det.covs = 1, seed = 1) fit <- tobs(~ occ_cov1, data = sim$data, family = occu(), detection = ~ det_cov1, y = sim$y, method = "laplace", control = list(verbose = FALSE, progress = FALSE)) pr <- predict(fit, type = "occupancy") summary(pr$psi)sim <- simulate_occu(N = 100, J = 3, n.occ.covs = 1, n.det.covs = 1, seed = 1) fit <- tobs(~ occ_cov1, data = sim$data, family = occu(), detection = ~ det_cov1, y = sim$y, method = "laplace", control = list(verbose = FALSE, progress = FALSE)) pr <- predict(fit, type = "occupancy") summary(pr$psi)
Print a batched multi-response occu_cover fit.
## S3 method for class 'tobs_batch' print(x, ...)## S3 method for class 'tobs_batch' print(x, ...)
x |
A |
... |
Ignored. |
x, invisibly.
sims <- lapply(1:2, function(s) simulate_occu_cover(N = 40, J = 3, seed = s)) y <- lapply(sims, function(s) s$y) y_pos <- lapply(sims, function(s) { p <- s$y_pos; p[is.na(p)] <- 0; p }) fit <- tobs(~ occ_cov1, data = sims[[1]]$data, family = occu_cover("lognormal"), detection = ~ 1, y = y, y.pos = y_pos, species = c("a", "b"), method = "laplace") print(fit)sims <- lapply(1:2, function(s) simulate_occu_cover(N = 40, J = 3, seed = s)) y <- lapply(sims, function(s) s$y) y_pos <- lapply(sims, function(s) { p <- s$y_pos; p[is.na(p)] <- 0; p }) fit <- tobs(~ occ_cov1, data = sims[[1]]$data, family = occu_cover("lognormal"), detection = ~ 1, y = y, y.pos = y_pos, species = c("a", "b"), method = "laplace") print(fit)
Print method for tobs_family
## S3 method for class 'tobs_family' print(x, ...)## S3 method for class 'tobs_family' print(x, ...)
x |
a |
... |
ignored. |
x, invisibly.
occu() abun(mixture = "negbin")occu() abun(mixture = "negbin")
Print method for tobs_fit
## S3 method for class 'tobs_fit' print(x, ...)## S3 method for class 'tobs_fit' print(x, ...)
x |
a |
... |
forwarded to underlying print methods. |
x, invisibly.
sim <- simulate_occu(N = 100, J = 3, n.occ.covs = 1, n.det.covs = 1, seed = 1) fit <- tobs(~ occ_cov1, data = sim$data, family = occu(), detection = ~ det_cov1, y = sim$y, method = "laplace", control = list(verbose = FALSE, progress = FALSE)) fitsim <- simulate_occu(N = 100, J = 3, n.occ.covs = 1, n.det.covs = 1, seed = 1) fit <- tobs(~ occ_cov1, data = sim$data, family = occu(), detection = ~ det_cov1, y = sim$y, method = "laplace", control = list(verbose = FALSE, progress = FALSE)) fit
Returns the per-group random-effect posterior summaries. Under NUTS the
non-centred draws are reconstructed to the natural BLUP scale
(b_{g,c} = sigma_c * (L z_g)_c) and summarised; the deterministic Laplace
path returns the variance-component EM modes and their Schur-complement
standard errors. A community fit reports its per-species deviations from
the community mean. Every family and engine returns the same columns.
## S3 method for class 'tobs_fit' ranef(object, ...)## S3 method for class 'tobs_fit' ranef(object, ...)
object |
A |
... |
Ignored. |
A data frame with one row per random-effect level and coefficient
and the columns arm (the linear predictor the effect enters: psi, p,
lambda, ...; psi+p for an effect shared across arms; NA when the fit
does not distinguish arms), group (the grouping variable; "species"
for the per-species deviations of a community fit), level (the group
level, or the species), term (the coefficient the effect shifts,
"(Intercept)" for a random intercept), estimate and std.error
(NA where the fitter reports no posterior SD). Zero rows when the fit
carries no random effects.
On a community fit, std.error is the per-species posterior SD of the
deviation, the square root of the matching diagonal entry of the
species' posterior covariance fit$ms_community$Cinv[[s]] (the analogue
of lme4's conditional variance). On the Laplace routes that covariance
is conditional on the community means; on a spatial fit integrated over
an outer hyperparameter grid it combines the per-node covariances and
the spread of the per-node BLUPs (law of total covariance); on the NUTS
and Polya-Gamma Gibbs routes it is the covariance of the sampled
deviations. The ms_abun() log_r (negative binomial) and
logit_omega (zero-inflated) deviations carry an SD as well: every route
that fits them (joint AGHQ, and NUTS for log_r) keeps them in the
covariance. std.error is NA for a species whose per-species
posterior solve failed.
sim <- simulate_occu(N = 150, J = 4, n.occ.covs = 1, n.det.covs = 1, n.visit.groups = 8, sigma.visit = 1, seed = 1) fit <- tobs(~ occ_cov1, data = sim$data, family = occu(), detection = ~ (1 | visit_group), visits = sim$visits, y = sim$y, method = "laplace", control = list(verbose = FALSE, progress = FALSE)) ranef(fit)sim <- simulate_occu(N = 150, J = 4, n.occ.covs = 1, n.det.covs = 1, n.visit.groups = 8, sigma.visit = 1, seed = 1) fit <- tobs(~ occ_cov1, data = sim$data, family = occu(), detection = ~ (1 | visit_group), visits = sim$visits, y = sim$y, method = "laplace", control = list(verbose = FALSE, progress = FALSE)) ranef(fit)
Sequential-depletion removal sampling: latent abundance
N_i ~ Poisson(lambda_i) (or negative binomial) observed through K ordered
removal passes, where pass k removes
Binomial(N_i - sum_{l<k} y_{il}, p_{ik}) of the individuals still present.
The declining catch sequence identifies detection p and abundance N; the
latent N is summed out in closed form (truncation K.max), so the fit is a
direct Laplace approximation (no EM), with a NUTS path over the same marginal.
removal(K.max = NULL, mixture = c("poisson", "negbin"))removal(K.max = NULL, mixture = c("poisson", "negbin"))
K.max |
upper bound for the latent-abundance marginal sum (the exact
integration over |
mixture |
latent-abundance distribution, both fitted via tulpa's
closed-form marginal Laplace engine. |
The tobs() formula is the abundance (log lambda) model; detection is
the per-pass detection (logit p) model. The response y is an
n_sites x K integer matrix of per-pass removals with the passes in column
order; complete pass sequences are required (no NA).
A tobs_family object.
method = "laplace" – a direct Laplace approximation over the closed-form
marginal (Poisson or negative binomial); a grouped random effect is
integrated by adaptive Gauss-Hermite quadrature.
method = "nested_laplace" – an areal icar() / car_proper() field on
the abundance arm, optionally composed with a temporal() field, through
the shared areal-BFGS driver.
method = "nuts" – the in-tree C++ NUTS sampler over the same marginal,
with a fixed-hyper areal or temporal field block on the abundance arm (one
of the two, not both).
Royle, J. A. (2004). N-mixture models for estimating population size from spatially replicated counts. Biometrics 60, 108-115. Dorazio, R. M., Jelks, H. L., Jordan, F. (2005). Improving removal-based estimates of abundance. Biometrics 61, 1093-1101.
f <- removal(K.max = 100) ff <- removal(K.max = 100) f
One contract for every family: a list with occ, the STATE-level residual
series (per site, or per site and season for a multi-season family), and
det, the OBSERVATION-level series (per visit / pass / distance bin). A
family carrying only one of the two levels fills that one and leaves the
other NULL; nothing returns a bare matrix, so a caller reading $occ gets
the state residual or NULL, never an error.
## S3 method for class 'tobs_fit' residuals(object, type = c("deviance", "pearson", "response"), ...)## S3 method for class 'tobs_fit' residuals(object, type = c("deviance", "pearson", "response"), ...)
object |
A |
type |
One of |
... |
Ignored. |
A list with occ (state-level) and det (observation-level)
residuals, either of which may be NULL for a family that has no such
level. A model type with no registered handler is an error naming it.
sim <- simulate_occu(N = 100, J = 3, n.occ.covs = 1, n.det.covs = 1, seed = 1) fit <- tobs(~ occ_cov1, data = sim$data, family = occu(), detection = ~ det_cov1, y = sim$y, method = "laplace", control = list(verbose = FALSE, progress = FALSE)) res <- residuals(fit, type = "pearson") str(res)sim <- simulate_occu(N = 100, J = 3, n.occ.covs = 1, n.det.covs = 1, seed = 1) fit <- tobs(~ occ_cov1, data = sim$data, family = occu(), detection = ~ det_cov1, y = sim$y, method = "laplace", control = list(verbose = FALSE, progress = FALSE)) res <- residuals(fit, type = "pearson") str(res)
Occupancy with abundance-induced detection heterogeneity (Royle & Nichols
2003; unmarked occuRN). Latent abundance N_i ~ Poisson(lambda_i)
drives the per-visit detection probability 1 - (1 - r_ij)^{N_i}, where
r_ij is the per-individual detection probability. The state formula models
log lambda (abundance); detection models logit r. Detection is site-level
by default; passing visits makes it visit-varying (logit r_ij gains the
visit-level covariates), exactly as for the occupancy / N-mixture front doors.
The latent N marginalises in closed form (a Poisson sum to K.max), so the
fit maximises the exact marginal with an observed-information vcov.
royle_nichols(K.max = NULL)royle_nichols(K.max = NULL)
K.max |
Upper summation bound for the latent abundance (default: a data-driven Poisson-tail guess). |
A tobs_family object for tobs().
method = "laplace" – the exact marginal maximised (BFGS) with an
observed-information covariance; the only route (no field, no sampler).
sim <- simulate_royle_nichols(N = 150, J = 5, seed = 1) fit <- tobs(~ x, data = sim$data, family = royle_nichols(), detection = ~ 1, y = sim$y, control = list(verbose = FALSE)) coef(fit) # Visit-varying detection via `visits`: sv <- simulate_royle_nichols(N = 150, J = 5, beta.r.visit = 0.8, seed = 1) fv <- tobs(~ x, data = sv$data, family = royle_nichols(), detection = ~ w, y = sv$y, visits = sv$visits, control = list(verbose = FALSE)) coef(fv)sim <- simulate_royle_nichols(N = 150, J = 5, seed = 1) fit <- tobs(~ x, data = sim$data, family = royle_nichols(), detection = ~ 1, y = sim$y, control = list(verbose = FALSE)) coef(fit) # Visit-varying detection via `visits`: sv <- simulate_royle_nichols(N = 150, J = 5, beta.r.visit = 0.8, seed = 1) fv <- tobs(~ x, data = sv$data, family = royle_nichols(), detection = ~ w, y = sv$y, visits = sv$visits, control = list(verbose = FALSE)) coef(fv)
Runs the POSTERIOR simulation-based calibration experiment (Talts et al.
2018; Sailynoja, Schmitt, Buerkner and Vehtari 2026, Algorithm 2) on a
fitted tobs_fit, through the engine's tulpa::sbc() front door. The truth
is drawn from the fit's own posterior at the observed data, a replicate is
simulated at that truth on FRESH cells, the model is refitted on the two data
sets together, and the truth's rank under that augmented posterior is
recorded. Under exact inference those ranks are Uniform(0, 1); the departure
from uniformity is a verdict on the approximation.
## S3 method for class 'tobs_fit' sbc( object, n.sim = 100L, n.draws = 1000L, n.ref = 200L, quantities = NULL, controls = character(), bad.factor = 1.25, level = 0.95, seed = 0L, model.only = FALSE, fit.control = list(), control = list(), ... )## S3 method for class 'tobs_fit' sbc( object, n.sim = 100L, n.draws = 1000L, n.ref = 200L, quantities = NULL, controls = character(), bad.factor = 1.25, level = 0.95, seed = 0L, model.only = FALSE, fit.control = list(), control = list(), ... )
object |
A fitted |
n.sim |
Number of simulations. Each costs one refit on the pooled data. |
n.draws |
Posterior draws per fit backing the reported predictives. |
n.ref |
Reference draws behind the |
quantities |
Optional character vector restricting what is scored. |
controls |
Mis-scaled control arms to add: any of |
bad.factor |
The control arms' SD multiplier. |
level |
Simultaneous band level. |
seed |
Seed offset. Simulation |
model.only |
Return the callback list instead of running the
experiment, for inspection or for passing to |
fit.control |
Merged into the |
control |
Passed to |
... |
Unused. |
Registered families: occu_cover (the coupled occupancy + cover hurdle on
the joint nested-Laplace engine), and occu, abun, count, removal,
distance, fp_occu, royle_nichols, occu_ttd and double_observer,
whose site marginals multiply. For those the replicate is the family's own
simulate() kernel at the drawn theta, the rank arm is the family's exact marginal
log-likelihood, and two kinds of fit are refused rather than approximated: a
structured term (its field is a latent quantity shared across sites that
theta does not hold, which is what the coupled occu_cover route handles by
drawing on fresh cells) and a visit-level observation design (the replicate
kernel assembles the observation arm from the site-level design, so the two
sides would carry different models).
dyn_occu (constant-rate only) pools on the SITE axis and leaves the
season axis alone, since its response is 3D
[n_sites x max_visits x n_seasons]: a bespoke replicate generator
forward-simulates the colonization/extinction HMM, but the posterior
draws and the rank statistic are the same shared machinery every other
registered family uses.
int_occu (full source-site overlap only) pools on the SITE axis and
leaves the per-source axis alone, since its response is a list of one
detection matrix per source; the rank statistic is the shared machinery
unchanged. Registering it surfaced a real bug (since fixed): the detection
intercept was silently left in standardized-covariate
units on every fit with an autoscaled detection covariate, independent of
SBC.
gdistremoval pools two response matrices (yDist band counts, yRem
period counts) together on the SITE axis, reusing the same
list-of-matrices pool int_occu uses; its replicate generator is its
own simulate() handler.
dyn_abun (constant-rate only) shares dyn_occu's 3D
[n_sites x max_visits x n_seasons] response and site-axis pooling, but
unlike dyn_occu it already has a working simulate() handler (the
Dail-Madsen open N-mixture forward is not a two-state HMM that needs a
bespoke generator), so its replicate is the same family-simulate() route
as occu/abun/etc.
occu_categorical is a tobs_multiarm_fit with two independent
Laplace-Gaussian blocks (presence, class) rather than a joint MVN draw
matrix; draws() samples the two blocks independently and loglik_many()
scores the two-arm likelihood directly, since this family has no
.tobs_pointwise_loglik dispatch to reuse.
distsamp_open (constant-dynamics, Poisson only) shares dyn_abun's 3D
response and site-axis pooling, and its fit$means/fit$draws are the
standard .tobs_bfgs_marginal_fit() shape, so draws/simulate/
loglik_many are all the shared generic ones.
occu_multi's response is a list of S per-species matrices, the same
shape int_occu() pools; its simulate() is custom, since species share
one joint multi-species state rather than being independently observed.
dyn_int_occu is the product of the multi-season and multi-source shapes:
a named list of S per-source 3D arrays, pooled by a new
.tobs_sbc_pool_named_3d that composes both existing pooling rules.
t_occu is a pg_gibbs family whose fit$draws is already the real
pooled posterior sample; it has no simulate() handler and no
.tobs_pointwise_loglik dispatch, so both are custom – simulate draws
a fresh AR1 year effect at the theta's own (sigma, rho), and
loglik_many is a Laplace approximation to that year effect's marginal
(FD-validated, brute-force cross-checked at T = 2).
cover is a tobs_multiarm_fit with the same two-independent-block shape
as occu_categorical; positive = "lognormal" only for v1, with
dispersion held fixed (no SE is reported for it anywhere in the package).
occu_multiscale_cover is a standard single-block fit (unlike cover's
two-block one); the exchangeable unit is the CELL rather than the plot, so
pooling and site labeling track cell indices.
A family is registered by adding one entry to the internal registry – a replicate generator, a refit call and optionally a joint statistic; the pooling, the grouping labels, the arm construction and the controls are shared.
An object of class sbc from tulpa, carrying the per-quantity
rank ECDFs, the exact simultaneous band, and the uniformity tests. When
model.only = TRUE, the callback list instead.
The arm fixed effects, the areal field SD on each arm, the copy scale
alpha, and the dispersion when the fit estimates one. The per-cell field
itself is not scored – it is integrated out by the fit and carries no truth
theta could hold. The cover-arm field SD and alpha are read per draw off
the outer hyperparameter grid, whose cells are sampled by their own
normalized weight, so what is ranked is the grid-marginalized posterior of
each rather than a function of component modes.
A quantity whose posterior has no spread – a dispersion the joint engine
holds fixed in its cell-coupling spec, for instance – is dropped rather than
scored, and named in attr(x, "fixed").
An extra log_lik quantity ranks a joint statistic of the whole parameter
vector, which catches a posterior getting each marginal right while getting
the dependence between them wrong.
The replicate is drawn on new cells whose areal graph is block-diagonal
against the observed one. occu_cover couples its occurrence and cover arms
through one shared field, and theta carries no per-cell field value, so a
replicate re-observing the observed cells would depend on the observed data
given theta and break the factorization posterior SBC rests on. group_ids
is supplied to tulpa::sbc(), which verifies the observable half of that
(disjoint group labels) rather than assuming it.
controls = c("wide", "narrow") adds arms reporting the same draws rescaled
about their mean by bad.factor and its reciprocal. They are deliberately
mis-scaled posteriors and are expected to leave the band: a calibration read
that nothing can fail is not evidence.
Talts, S., Betancourt, M., Simpson, D., Vehtari, A. and Gelman, A. (2018). Validating Bayesian inference algorithms with simulation-based calibration. arXiv:1804.06788.
Sailynoja, T., Schmitt, M., Buerkner, P.-C. and Vehtari, A. (2026). Posterior SBC: simulation-based calibration checking conditional on data. Statistics and Computing 36:78.
N <- 20L; J <- 3L adj <- matrix(0L, N, N) for (s in seq_len(N)) { if (s > 1L) adj[s, s - 1L] <- 1L if (s < N) adj[s, s + 1L] <- 1L } sim <- simulate_occu_cover(N = N, J = J, positive = "lognormal", adj = adj, sigma = 0.7, alpha = 1, seed = 1L) long <- data.frame(site_id = rep(seq_len(N), each = J), visit = rep(seq_len(J), times = N), y = as.vector(t(sim$y)), det_cov1 = sim$visit_data$det_cov1, pos_cov1 = sim$visit_data$pos_cov1) od <- tobs_data(long, y = "y", site = "site_id", visit = "visit", det.covs = c("det_cov1", "pos_cov1")) y_pos <- sim$y_pos; y_pos[is.na(y_pos)] <- 0 # The dispersion goes on the outer grid so it is estimated and scored, and # the field-SD grid is pinned so both stages integrate the same support. ctl <- list(engine = "joint", verbose = FALSE, sigma.grid = exp(seq(log(0.15), log(2.0), length.out = 9)), phi.grid.pos = exp(seq(log(0.20), log(0.90), length.out = 7))) fit <- tobs(~ occ_cov1 + icar(graph = adj), data = cbind(data.frame(site_id = seq_len(N)), sim$data), family = occu_cover("lognormal"), detection = ~ det_cov1, positive = ~ pos_cov1 + share(spatial()), y = od$y, y.pos = y_pos, visits = od$det.covs, method = "nested_laplace", control = ctl) sbc(fit, n.sim = 20L, controls = "narrow", fit.control = ctl)N <- 20L; J <- 3L adj <- matrix(0L, N, N) for (s in seq_len(N)) { if (s > 1L) adj[s, s - 1L] <- 1L if (s < N) adj[s, s + 1L] <- 1L } sim <- simulate_occu_cover(N = N, J = J, positive = "lognormal", adj = adj, sigma = 0.7, alpha = 1, seed = 1L) long <- data.frame(site_id = rep(seq_len(N), each = J), visit = rep(seq_len(J), times = N), y = as.vector(t(sim$y)), det_cov1 = sim$visit_data$det_cov1, pos_cov1 = sim$visit_data$pos_cov1) od <- tobs_data(long, y = "y", site = "site_id", visit = "visit", det.covs = c("det_cov1", "pos_cov1")) y_pos <- sim$y_pos; y_pos[is.na(y_pos)] <- 0 # The dispersion goes on the outer grid so it is estimated and scored, and # the field-SD grid is pinned so both stages integrate the same support. ctl <- list(engine = "joint", verbose = FALSE, sigma.grid = exp(seq(log(0.15), log(2.0), length.out = 9)), phi.grid.pos = exp(seq(log(0.20), log(0.90), length.out = 7))) fit <- tobs(~ occ_cov1 + icar(graph = adj), data = cbind(data.frame(site_id = seq_len(N)), sim$data), family = occu_cover("lognormal"), detection = ~ det_cov1, positive = ~ pos_cov1 + share(spatial()), y = od$y, y.pos = y_pos, visits = od$det.covs, method = "nested_laplace", control = ctl) sbc(fit, n.sim = 20L, controls = "narrow", fit.control = ctl)
Latent abundance N_i ~ Poisson(lambda_i) (or, under
mixture = "negbin", N_i ~ NegBin(mean = lambda_i, size = size) with
variance lambda + lambda^2 / size) with log lambda_i = X_lambda beta_lambda, and replicate counts y_ij ~ Binomial(N_i, p_i) with
logit p_i = X_p beta_p (site-level detection). The returned y is an
N x J integer count matrix suitable for tobs() with abun().
simulate_abun( N = 100, J = 4, n.abund.covs = 2, n.det.covs = 1, beta.lambda = NULL, beta.p = NULL, mixture = c("poisson", "negbin", "zip", "zinb"), size = 2, omega = 0.3, seed = NULL )simulate_abun( N = 100, J = 4, n.abund.covs = 2, n.det.covs = 1, beta.lambda = NULL, beta.p = NULL, mixture = c("poisson", "negbin", "zip", "zinb"), size = 2, omega = 0.3, seed = NULL )
N |
Number of sites (default 100). |
J |
Number of replicate visits (default 4). |
n.abund.covs |
Number of abundance covariates (default 2). |
n.det.covs |
Number of detection covariates (default 1). |
beta.lambda |
Abundance coefficients |
beta.p |
Detection coefficients |
mixture |
Abundance mixing distribution: |
size |
Negative-binomial size |
omega |
Structural-zero probability for |
seed |
Optional random seed. |
A list with y (N x J count matrix), data (covariate data frame),
and truth (the coefficients, per-site lambda, p, latent N, and the
mixture / size used).
sim <- simulate_abun(N = 50, J = 3, seed = 1) dim(sim$y)sim <- simulate_abun(N = 50, J = 3, seed = 1) dim(sim$y)
One observed value per site from a GLMM with no detection process, matching
the count() family: log-link Poisson / negative-binomial counts, an
identity-link Gaussian response, or a logit-link binomial response (k
successes out of n trials per site). The mean predictor is X beta with
beta the fixed-effect coefficients (intercept first).
simulate_count( N = 200, beta = c(1, 0.5), response = c("poisson", "negbin", "gaussian", "binomial"), size = 2, sd = 1, trials = 10, seed = NULL )simulate_count( N = 200, beta = c(1, 0.5), response = c("poisson", "negbin", "gaussian", "binomial"), size = 2, sd = 1, trials = 10, seed = NULL )
N |
Number of sites (default 200). |
beta |
Fixed-effect coefficients, length |
response |
The response distribution: |
size |
Negative-binomial size (dispersion); larger is closer to Poisson.
Used only for |
sd |
Gaussian residual SD. Used only for |
trials |
Per-site trial count for |
seed |
Random seed. |
A list with y (a length-N numeric response vector; success counts
for the binomial response), data (the site covariates), and truth (the
coefficients, response, dispersion, and the binomial trials).
sim <- simulate_count(N = 100, beta = c(1, 0.5), seed = 1) length(sim$y)sim <- simulate_count(N = 100, beta = c(1, 0.5), seed = 1) length(sim$y)
Generates synthetic data matching the cover(response = "lognormal")
generative model:
simulate_cover( N = 200L, beta.occ = c(-0.5, 0.8), beta.pos = c(-1, 0.3), sigma.pos = 0.4, response = c("lognormal", "gaussian"), spatial.range = NULL, spatial.var = 1, seed = NULL )simulate_cover( N = 200L, beta.occ = c(-0.5, 0.8), beta.pos = c(-1, 0.3), sigma.pos = 0.4, response = c("lognormal", "gaussian"), spatial.range = NULL, spatial.var = 1, seed = NULL )
N |
Number of sites (default 200). |
beta.occ |
Length-2 occurrence coefficients (intercept, slope on x). |
beta.pos |
Length-2 log-cover coefficients (intercept, slope on x). |
sigma.pos |
Lognormal residual standard deviation (default 0.4). |
response |
Positive-arm likelihood: |
spatial.range |
Length scale of the exponential spatial field
(in unit-square distance). |
spatial.var |
Marginal variance of the spatial field (default 1). |
seed |
Optional integer seed. |
= X %*% beta_occ + spatial_occ
= X %*% beta_pos + spatial_pos
= plogis(eta_occ)
~ Bernoulli(p)
~ Normal(eta_pos, sigma_pos^2) | occur = 1
= exp(log_cover) when occur = 1, else 0
Both layers share the same design matrix by default (one continuous
covariate plus an intercept). When spatial.range is supplied, a simple
exponential-kernel Gaussian field on a random unit-square coordinate set
is added to both linear predictors; the two layers see independently
simulated draws of that field, which matches the two-independent-arm fit.
The joint engine's shared field is a different generative model.
A list with:
A data frame with cover, covariate x, and lon, lat.
data$cover (length-N numeric vector, for passing to tobs()).
An N x 2 matrix of coordinates.
A list with beta_occ, beta_pos, sigma_pos,
p, mu, occur, spatial_occ, spatial_pos.
sim <- simulate_cover(N = 200, seed = 1) head(sim$data)sim <- simulate_cover(N = 200, seed = 1) head(sim$data)
Generates synthetic data matching the joint nested-Laplace cover-hurdle
parameterisation (sigma_occ * z on the occurrence arm, alpha * sigma * z on the cover arm) where z is a BYM2 latent field on the
supplied region adjacency. The cover-arm field amplitude
sigma_pos = alpha * sigma is the derived ratio surfaced as the
"alpha" column of fit$joint$theta_grid.
simulate_cover_joint( N = 300L, adj, beta.occ = c(-0.3, 0.7), beta.pos = c(0.4, -0.5), sigma = 0.6, rho = 0.7, alpha = 1, positive = c("beta", "lognormal", "gaussian"), phi = 30, sigma.pos.resid = 0.4, seed = NULL )simulate_cover_joint( N = 300L, adj, beta.occ = c(-0.3, 0.7), beta.pos = c(0.4, -0.5), sigma = 0.6, rho = 0.7, alpha = 1, positive = c("beta", "lognormal", "gaussian"), phi = 30, sigma.pos.resid = 0.4, seed = NULL )
N |
Number of sites (default 300). |
adj |
|
beta.occ |
Length-2 occurrence coefficients (intercept + slope). |
beta.pos |
Length-2 cover-arm coefficients (intercept + slope). |
sigma |
Marginal spatial-field amplitude (default 0.6); the
occurrence-arm linear predictor adds |
rho |
BYM2 mixing parameter, in |
alpha |
Cover-arm scaling: cover-arm linear predictor adds
|
positive |
Likelihood for the positive arm: |
phi |
Beta precision when |
sigma.pos.resid |
Lognormal residual SD when |
seed |
Optional integer seed. |
Both BYM2 sub-blocks (phi, theta) are drawn IID Normal and
demeaned to mean zero before scaling, matching the sum-to-zero
constraint applied to both sub-blocks by tulpa::spatial_bym2() inside
the nested-Laplace engine (see .joint_inner_var()). Without the
demean each seed carries mean(w_s) ~ N(0, sigma^2 / n_s) and the
constrained intercept identified by the engine targets
beta_pos_0_truth + alpha * mean(w_s) rather than beta_pos_0_truth;
coverage of the population truth then collapses with alpha while
the engine-reported posterior is correctly calibrated for the
constrained parameter. See example/validation/SUMMARY.md in
INLAabun (Demo 3) for the diagnosis.
A list with:
Data frame with x and region (factor).
Length-N numeric vector of cover values (zeros where
occur = 0).
The adjacency matrix passed in (for downstream
tulpa::spatial_bym2()).
Named list with beta_occ, beta_pos, sigma,
rho, alpha, sigma_pos = alpha * sigma, positive, plus the
simulated phi_f, theta_f, w_s, region.
adj <- matrix(0L, 10, 10) for (s in 1:10) for (j in setdiff(c(s - 1L, s + 1L), c(0L, 11L))) adj[s, j] <- 1L sim <- simulate_cover_joint(N = 100, adj = adj, alpha = 1.0, seed = 1) head(sim$data)adj <- matrix(0L, 10, 10) for (s in 1:10) for (j in setdiff(c(s - 1L, s + 1L), c(0L, 11L))) adj[s, j] <- 1L sim <- simulate_cover_joint(N = 100, adj = adj, alpha = 1.0, seed = 1) head(sim$data)
Latent abundance N_i ~ Poisson(lambda_i) (or NegBin(lambda_i, size)) with
log lambda_i = X_lambda beta_lambda, observed through a half-normal or
hazard-rate detection function with log sigma_i = X_sigma beta_sigma. Each
individual lands in a distance bin (or goes undetected) by the multinomial
cell probabilities. Returns an N x n_bins integer matrix of per-bin counts
suitable for tobs() with distance().
simulate_distance( N = 200, cutpoints = seq(0, 1, length.out = 6), key = c("halfnorm", "hazard"), transect = c("line", "point"), n.abund.covs = 1, n.sigma.covs = 1, beta.lambda = NULL, beta.sigma = NULL, shape = 3, mixture = c("poisson", "negbin"), size = 5, seed = NULL )simulate_distance( N = 200, cutpoints = seq(0, 1, length.out = 6), key = c("halfnorm", "hazard"), transect = c("line", "point"), n.abund.covs = 1, n.sigma.covs = 1, beta.lambda = NULL, beta.sigma = NULL, shape = 3, mixture = c("poisson", "negbin"), size = 5, seed = NULL )
N |
Number of sites (default 200). |
cutpoints |
Distance-bin edges (length |
key |
|
transect |
|
n.abund.covs, n.sigma.covs
|
Number of abundance / detection covariates. |
beta.lambda |
Abundance coefficients (log scale). Default
|
beta.sigma |
Detection-scale coefficients (log scale). Default
|
shape |
Hazard-rate shape |
mixture |
|
size |
Negative-binomial size |
seed |
Optional random seed. |
A list with y (N x n_bins count matrix), data (covariates),
cutpoints, and truth (coefficients, per-site lambda / sigma, latent
N, key/transect/mixture/shape/size).
sim <- simulate_distance(N = 50, seed = 1) head(sim$y)sim <- simulate_distance(N = 50, seed = 1) head(sim$y)
Draws from the distsamp_open() model: an open metapopulation
(N_1 ~ Poisson / NB(lambda), N_t = Binomial(N_{t-1}, omega) + Poisson(gamma)) observed by distance sampling (half-normal key, scale
sigma) at each primary period.
simulate_distsamp_open( N = 200, cutpoints = c(0, 10, 20, 30, 40), n.seasons = 4L, transect = "line", n.abund.covs = 1, n.det.covs = 1, beta.lambda = NULL, beta.sigma = NULL, omega = 0.7, gamma = 2.5, mixture = c("poisson", "negbin", "zip", "zinb"), size = 3, zi = 0.3, dynamics = c("constant", "notrend", "trend", "autoreg", "ricker", "gompertz"), K = NULL, r = 0.3, seed = NULL )simulate_distsamp_open( N = 200, cutpoints = c(0, 10, 20, 30, 40), n.seasons = 4L, transect = "line", n.abund.covs = 1, n.det.covs = 1, beta.lambda = NULL, beta.sigma = NULL, omega = 0.7, gamma = 2.5, mixture = c("poisson", "negbin", "zip", "zinb"), size = 3, zi = 0.3, dynamics = c("constant", "notrend", "trend", "autoreg", "ricker", "gompertz"), K = NULL, r = 0.3, seed = NULL )
N |
Number of sites (default 200). |
cutpoints |
Distance-bin edges |
n.seasons |
Number of primary periods (default 4). |
transect |
|
n.abund.covs, n.det.covs
|
Number of abundance / distance covariates. |
beta.lambda, beta.sigma
|
Coefficients on the log-abundance and log-scale arms. Defaults give moderate abundance / detection. |
omega, gamma
|
Apparent survival probability and recruitment rate (intercept-only defaults 0.7 / 2.5). |
mixture |
Initial-abundance distribution: |
size |
Negative-binomial size (overdispersion |
zi |
Structural-zero probability when |
dynamics |
Population-dynamics form (see |
K |
Carrying capacity for |
r |
Intrinsic growth rate for |
seed |
Optional random seed. |
A list with y ([n_sites x n_bins x n_seasons] distance-band
counts), data, and truth.
sim <- simulate_distsamp_open(N = 40, n.seasons = 3, seed = 1) dim(sim$y)sim <- simulate_distsamp_open(N = 40, n.seasons = 3, seed = 1) dim(sim$y)
Draws from the double_observer() model: site abundance
N ~ Poisson(lambda) observed by two observers with detection p1 / p2. For
type = "independent" each individual is recorded by observer 1 only, observer
2 only, or both (three cell counts). For type = "dependent" a primary observer
records what it detects and a secondary observer records only the primary's
misses (two cell counts, primary-detected and secondary-only); the primary
observer alternates across sites (role-swapping) so p1 and p2 are
identifiable, and the per-site primary indicator is returned as primary.
simulate_double_observer( N = 200, type = c("independent", "dependent"), n.abund.covs = 1, n.det.covs = 1, beta.lambda = NULL, beta.p1 = NULL, beta.p2 = NULL, seed = NULL )simulate_double_observer( N = 200, type = c("independent", "dependent"), n.abund.covs = 1, n.det.covs = 1, beta.lambda = NULL, beta.p1 = NULL, beta.p2 = NULL, seed = NULL )
N |
Number of sites (default 200). |
type |
|
n.abund.covs, n.det.covs
|
Number of abundance / detection covariates. |
beta.lambda |
Log-abundance coefficients |
beta.p1, beta.p2
|
Per-observer detection coefficients (logit). Default moderate detection. |
seed |
Optional random seed. |
A list with y (N x 3 cell counts for "independent", N x 2 for
"dependent"), data, primary (the per-site primary observer, for
"dependent"), and truth.
sim <- simulate_double_observer(N = 50, seed = 1) head(sim$y)sim <- simulate_double_observer(N = 50, seed = 1) head(sim$y)
Latent N_1 ~ Poisson(lambda); for t >= 2, N_t = Binomial(N_(t-1), omega_{t-1}) + Poisson(gamma_{t-1}); observed via Binomial(N_t, p) over J
secondary visits in each of T primary seasons. Returns a 3D array N x J x T
suitable for tobs() with dyn_abun().
simulate_dyn_abun( N = 150, T = 4, J = 3, n.abund.covs = 1, beta.lambda = NULL, p = 0.5, omega = 0.6, gamma = 1, beta.omega = NULL, beta.gamma = NULL, mixture = c("poisson", "negbin"), r = 2, zi = 0, seed = NULL )simulate_dyn_abun( N = 150, T = 4, J = 3, n.abund.covs = 1, beta.lambda = NULL, p = 0.5, omega = 0.6, gamma = 1, beta.omega = NULL, beta.gamma = NULL, mixture = c("poisson", "negbin"), r = 2, zi = 0, seed = NULL )
N |
Number of sites (default 150). |
T |
Number of primary seasons (default 4). |
J |
Number of secondary visits per season (default 3). |
n.abund.covs |
Number of initial-abundance covariates (default 1). |
beta.lambda |
Initial-abundance coefficients (log). Default
|
p, omega, gamma
|
Detection, apparent-survival, and recruitment parameters
(scalars; defaults 0.5, 0.6, 1.0). |
beta.omega, beta.gamma
|
Optional length-2 coefficients
|
mixture |
Initial-abundance distribution: |
r |
Negative-binomial size for |
zi |
Structural-zero share for a zero-inflated model (default 0). With
probability |
seed |
Optional random seed. |
Survival and recruitment are constant across seasons by default. Supplying
beta.omega (logit-scale) or beta.gamma (log-scale) instead makes the rate
depend on a per-(site, interval) season covariate z: omega_{i,t} = plogis(beta_omega[1] + beta_omega[2] * z_{i,t}), gamma_{i,t} = exp(beta_gamma[1] + beta_gamma[2] * z_{i,t}). The covariate is returned as a
[N x (T-1)] matrix column season_cov of data, ready for
omega = ~ season_cov.
A list with y (N x J x T count array), data (covariates, including
the [N x (T-1)] season_cov matrix column when a season-varying rate is
used), and truth (coefficients, per-site lambda, the realised
omega / gamma rates, and r under "negbin").
sim <- simulate_dyn_abun(N = 40, T = 3, J = 3, seed = 1) dim(sim$y)sim <- simulate_dyn_abun(N = 40, T = 3, J = 3, seed = 1) dim(sim$y)
Draws from the dyn_int_occu() model: a dynamic (multi-season) occupancy
process (psi1, colonization gamma, extinction eps) observed by S
detection sources with per-source detection probability.
simulate_dyn_int_occu( N = 200, T.seasons = 4, S = 2, J = 3, psi1 = 0.5, gamma = 0.3, eps = 0.2, p = c(0.4, 0.6), field = NULL, trend = NULL, source.seasons = NULL, seed = NULL )simulate_dyn_int_occu( N = 200, T.seasons = 4, S = 2, J = 3, psi1 = 0.5, gamma = 0.3, eps = 0.2, p = c(0.4, 0.6), field = NULL, trend = NULL, source.seasons = NULL, seed = NULL )
N |
Number of sites (default 200). |
T.seasons |
Number of seasons (default 4). |
S |
Number of detection sources (default 2). |
J |
Visits per source (scalar or length- |
psi1, gamma, eps
|
Season-1 occupancy, colonization, extinction (defaults 0.5 / 0.3 / 0.2). |
p |
Per-source detection probabilities (length- |
field |
Optional per-site shared areal field (length |
trend |
Optional per-site varying-coefficient (SVC) areal field (length
|
source.seasons |
Optional length- |
seed |
Optional random seed. |
A list with y (a length-S list of [N x J x T] arrays), data,
sources, and truth.
sim <- simulate_dyn_int_occu(N = 40, T.seasons = 3, seed = 1) lapply(sim$y, dim)sim <- simulate_dyn_int_occu(N = 40, T.seasons = 3, seed = 1) lapply(sim$y, dim)
Colonization and extinction are constant across the n_seasons - 1 transition
intervals by default. Supplying beta.gamma and/or beta.epsilon makes the
corresponding rate SEASON-VARYING: a per-(site, interval) covariate is drawn
into an [N x (n_seasons - 1)] matrix column (gamma_cov / eps_cov) of the
returned data, and the rate is plogis(beta[1] + beta[2] * cov). Fit these
with colonization = ~ gamma_cov / extinction = ~ eps_cov (the matrix
column drives the interval-indexed rate).
simulate_dyn_occu( N = 100, J = 4, n.seasons = 5, beta.occ = c(0.5), beta.det = c(0), gamma = 0.2, epsilon = 0.1, beta.gamma = NULL, beta.epsilon = NULL, beta.det.season = NULL, seed = NULL )simulate_dyn_occu( N = 100, J = 4, n.seasons = 5, beta.occ = c(0.5), beta.det = c(0), gamma = 0.2, epsilon = 0.1, beta.gamma = NULL, beta.epsilon = NULL, beta.det.season = NULL, seed = NULL )
N |
Number of sites (default 100). |
J |
Number of visits per season (default 4). |
n.seasons |
Number of seasons (default 5). |
beta.occ |
Initial occupancy coefficients. |
beta.det |
Detection coefficients. |
gamma |
Colonization probability (default 0.2); ignored when
|
epsilon |
Extinction probability (default 0.1); ignored when
|
beta.gamma |
Optional |
beta.epsilon |
Optional |
beta.det.season |
Optional |
seed |
Random seed. |
A list with y (3D array), data, and truth.
sim <- simulate_dyn_occu(N = 40, J = 3, n.seasons = 4, seed = 1) dim(sim$y)sim <- simulate_dyn_occu(N = 40, J = 3, n.seasons = 4, seed = 1) dim(sim$y)
Latent occupancy z_i ~ Bernoulli(psi_i) with logit psi = X_psi beta_psi,
observed through J replicate visits emitting a detection state y in {0,1,2}:
at an occupied site a visit detects with probability p11 and the detection is
certain (state 2) with probability b else ambiguous (state 1); at an
unoccupied site a visit yields a false-positive ambiguous detection (state 1)
with probability p10. Returns an N x J integer matrix in {0, 1, 2}
suitable for tobs() with fp_occu().
simulate_fp_occu( N = 300, J = 5, n.occ.covs = 1, beta.psi = NULL, p11 = 0.6, p10 = 0.05, b = 0.5, seed = NULL )simulate_fp_occu( N = 300, J = 5, n.occ.covs = 1, beta.psi = NULL, p11 = 0.6, p10 = 0.05, b = 0.5, seed = NULL )
N |
Number of sites (default 300). |
J |
Number of visits (default 5). |
n.occ.covs |
Number of occupancy covariates (default 1). |
beta.psi |
Occupancy coefficients (logit). Default
|
p11, p10, b
|
True detection, false-positive, and certain-classification probabilities (scalars; defaults 0.6, 0.05, 0.5). |
seed |
Optional random seed. |
A list with y (N x J state matrix), data (occupancy covariates),
and truth (coefficients, per-site psi, scalar p11/p10/b, latent z).
sim <- simulate_fp_occu(N = 50, J = 4, seed = 1) table(sim$y)sim <- simulate_fp_occu(N = 50, J = 4, seed = 1) table(sim$y)
Draws from the gdistremoval() model: site abundance N ~ Poisson(lambda),
the detected birds cross-classified by a distance band (half-normal key, scale
sigma) and by a removal period (per-period capture r).
simulate_gdistremoval( N = 200, cutpoints = c(0, 10, 20, 30, 40), n.periods = 4L, transect = "line", n.abund.covs = 1, n.det.covs = 1, n.rem.covs = 1, beta.lambda = NULL, beta.sigma = NULL, beta.r = NULL, seed = NULL )simulate_gdistremoval( N = 200, cutpoints = c(0, 10, 20, 30, 40), n.periods = 4L, transect = "line", n.abund.covs = 1, n.det.covs = 1, n.rem.covs = 1, beta.lambda = NULL, beta.sigma = NULL, beta.r = NULL, seed = NULL )
N |
Number of sites (default 200). |
cutpoints |
Distance-bin edges |
n.periods |
Number of removal periods. |
transect |
|
n.abund.covs, n.det.covs, n.rem.covs
|
Number of abundance / distance / removal covariates. |
beta.lambda, beta.sigma, beta.r
|
Coefficients on the log-abundance,
log-scale, and logit-removal arms ( |
seed |
Optional random seed. |
A list with y (distance-band counts), y_rem (removal-period
counts), data, and truth.
sim <- simulate_gdistremoval(N = 50, seed = 1) dim(sim$y) dim(sim$y_rem)sim <- simulate_gdistremoval(N = 50, seed = 1) dim(sim$y) dim(sim$y_rem)
Simulate integrated (multi-source) occupancy data
simulate_int_occu( N.total = 150, n.data = 2, J = c(4, 3), n.shared = 20, beta.occ = c(0.5, 0.3), beta.det = list(c(0.2, -0.4), c(-0.1, 0.3)), seed = NULL )simulate_int_occu( N.total = 150, n.data = 2, J = c(4, 3), n.shared = 20, beta.occ = c(0.5, 0.3), beta.det = list(c(0.2, -0.4), c(-0.1, 0.3)), seed = NULL )
N.total |
Total number of unique sites (default 150). |
n.data |
Number of data sources (default 2). |
J |
Vector of visits per source (default c(4, 3)). |
n.shared |
Number of sites shared across sources (default 20). |
beta.occ |
Occupancy coefficients (default c(0.5, 0.3)). |
beta.det |
List of detection coefficient vectors per source. |
seed |
Random seed. |
A list with y (list of matrices, each row named by the site of
data it measures), data, site_maps, and truth.
sim <- simulate_int_occu(N.total = 60, n.shared = 10, seed = 1) lapply(sim$y, dim)sim <- simulate_int_occu(N.total = 60, n.shared = 10, seed = 1) lapply(sim$y, dim)
Presence y_{i,s} ~ Bernoulli(psi_{i,s}) with logit psi = X beta_s, the
per-species occupancy coefficients drawn from Gaussian community hyperpriors.
Matches the jsdm() family, which observes presence directly (no detection
process), so the response is an N x n_species presence matrix.
simulate_jsdm( N = 100, n.species = 10, beta.comm.mean = c(0, 0.5), beta.comm.sd = c(0.5, 0.3), seed = NULL )simulate_jsdm( N = 100, n.species = 10, beta.comm.mean = c(0, 0.5), beta.comm.sd = c(0.5, 0.3), seed = NULL )
N |
Number of sites (default 100). |
n.species |
Number of species (default 10). |
beta.comm.mean |
Community-mean occupancy coefficients, length
|
beta.comm.sd |
Between-species SD of each occupancy coefficient (same
length as |
seed |
Random seed. |
A list with y (an N x n_species 0/1 presence matrix), data, and
truth (per-species coefficients and the community hyperparameters).
sim <- simulate_jsdm(N = 60, n.species = 5, seed = 1) dim(sim$y) # 60 sites x 5 species presence matrixsim <- simulate_jsdm(N = 60, n.species = 5, seed = 1) dim(sim$y) # 60 sites x 5 species presence matrix
Per-species Royle (2004) N-mixture with Gaussian community hyperpriors:
beta_lambda_s ~ N(mu_lambda, Sigma_lambda),
beta_p_s ~ N(mu_p, Sigma_p), then N_{s,i} ~ Poisson(lambda_{s,i})
(or NegBin(mu = lambda, size = r_s) with a per-species size
r_s = exp(mu_log_r + b_logr_s)) and
y_{s,i,j} ~ Binomial(N_{s,i}, p_{s,i,j}). The returned y is a 3D array
[n_sites x J x n_species] suitable for tobs() with ms_abun().
simulate_ms_abun( n.species = 12, N = 80, J = 4, n.abund.covs = 1, n.det.covs = 1, mu.lambda = NULL, mu.p = NULL, sd.lambda = 0.5, sd.p = 0.4, mixture = c("poisson", "negbin", "zip", "zinb"), size = 3, sigma.logr = 0.4, omega = 0.3, sigma.omega = 0.4, graph = NULL, sigma.field = 0.6, seed = NULL )simulate_ms_abun( n.species = 12, N = 80, J = 4, n.abund.covs = 1, n.det.covs = 1, mu.lambda = NULL, mu.p = NULL, sd.lambda = 0.5, sd.p = 0.4, mixture = c("poisson", "negbin", "zip", "zinb"), size = 3, sigma.logr = 0.4, omega = 0.3, sigma.omega = 0.4, graph = NULL, sigma.field = 0.6, seed = NULL )
n.species |
Number of species (default 12). |
N |
Number of sites (default 80). |
J |
Number of replicate visits (default 4). |
n.abund.covs |
Number of abundance covariates (default 1). |
n.det.covs |
Number of detection covariates (default 1). |
mu.lambda |
Community-mean abundance coefficients |
mu.p |
Community-mean detection coefficients on the logit scale.
Default |
sd.lambda |
Per-coefficient community SD for the abundance arm (the
sqrt-diagonal of |
sd.p |
Per-coefficient community SD for the detection arm. Default 0.4. |
mixture |
Abundance mixing distribution: |
size |
Community-mean negative-binomial size, equal to
|
sigma.logr |
Standard deviation of the per-species log-dispersion random
effect |
omega |
Community-mean structural-zero probability, equal to
|
sigma.omega |
Standard deviation of the per-species structural-zero-logit
random effect |
graph |
Optional |
sigma.field |
Standard deviation of the shared spatial field (used only
when |
seed |
Optional random seed. |
A list with y (3D count array), data (site covariate frame),
species (species names), and truth (community means / SDs, the
per-species coefficients, lambda, p, latent N, the shared field when
graph is given, and – under negbin – mu_log_r, sigma_log_r, the
per-species b_logr / r_s).
truth carries each community mean twice. mu_lambda, mu_p,
mu_log_r and mu_omega are the POPULATION constants the per-species
values were drawn around. mu_lambda_real, mu_p_real, mu_log_r_real
and mu_omega_real are the mean of the values this seed actually drew,
which sits sd / sqrt(n_species) from the constant – 0.12 on
mu_log_r at sigma_logr = 0.5 and 18 species.
Which one to score against follows from the question. An interval for a
community mean targets the POPULATION constant and its width includes that
sd^2 / n_species term, so a coverage measurement scores against
mu_log_r. A point-recovery or interval-SCALE measurement scores against
mu_log_r_real: against the constant, the draw carries about two thirds
of the spread across seeds, and a seed block that happens to draw wide
reads as a miscalibrated interval. That is what the 18-species fixture
measured – its seed block drew the species means 18-21% wider than
sigma_logr / sqrt(18), and both arms calibrate once the draw is put at
its expectation. The
_real entries are supplied so the split does not have to be rebuilt at
each call site; tests/testthat/helper-community-mean.R is the assertion
that consumes them.
sim <- simulate_ms_abun(n.species = 4, N = 30, J = 3, seed = 1) dim(sim$y)sim <- simulate_ms_abun(n.species = 4, N = 30, J = 3, seed = 1) dim(sim$y)
Per-species GLMM with Gaussian community hyperpriors on the coefficients (the
ms_count() / spAbundance msAbund model): g(mu_{s,i}) = X_i beta_s,
beta_s ~ N(beta_comm_mean, diag(beta_comm_sd^2)), y_{s,i} drawn Poisson /
negative-binomial (log link) or Gaussian (identity). No detection.
simulate_ms_count( N = 120, n.species = 10, beta.comm.mean = c(1, 0.5), beta.comm.sd = c(0.4, 0.3), response = c("poisson", "negbin", "gaussian", "binomial"), size = 2, size.log.sd = 0.3, sd = 1, trials = 10, seed = NULL )simulate_ms_count( N = 120, n.species = 10, beta.comm.mean = c(1, 0.5), beta.comm.sd = c(0.4, 0.3), response = c("poisson", "negbin", "gaussian", "binomial"), size = 2, size.log.sd = 0.3, sd = 1, trials = 10, seed = NULL )
N |
Number of sites. |
n.species |
Number of species. |
beta.comm.mean, beta.comm.sd
|
Community mean and SD of the per-species coefficients (intercept first). Length sets the number of covariates. |
response |
One of |
size |
Negative-binomial community size (mean of the per-species
|
size.log.sd |
Across-species SD of |
sd |
Gaussian residual SD. |
trials |
Binomial trial count ( |
seed |
Optional RNG seed. |
A list with y (an N x n_species matrix), data, and truth.
sim <- simulate_ms_count(N = 50, n.species = 4, seed = 1) head(sim$y)sim <- simulate_ms_count(N = 50, n.species = 4, seed = 1) head(sim$y)
Per-species binned distance sampling with Gaussian community hyperpriors:
beta_lambda_s ~ N(mu_lambda, diag(sd_lambda^2)),
beta_sigma_s ~ N(mu_sigma, diag(sd_sigma^2)), then
N_{s,i} ~ Poisson(lambda_{s,i}) and the detected individuals allocated to
distance bins by the half-normal detection function. The returned y is a 3D
array [n_sites x n_bins x n_species] suitable for tobs() with
ms_distance().
simulate_ms_distance( n.species = 10, N = 100, cutpoints = c(0, 25, 50, 75, 100), transect = c("line", "point"), key = c("halfnorm", "hazard"), shape = 0, n.abund.covs = 1, n.det.covs = 0, mu.lambda = NULL, mu.sigma = NULL, sd.lambda = 0.4, sd.sigma = 0.2, n.factors = 0, load.sd = 0.5, field = NULL, quad.order = 64L, seed = NULL )simulate_ms_distance( n.species = 10, N = 100, cutpoints = c(0, 25, 50, 75, 100), transect = c("line", "point"), key = c("halfnorm", "hazard"), shape = 0, n.abund.covs = 1, n.det.covs = 0, mu.lambda = NULL, mu.sigma = NULL, sd.lambda = 0.4, sd.sigma = 0.2, n.factors = 0, load.sd = 0.5, field = NULL, quad.order = 64L, seed = NULL )
n.species |
Number of species (default 10). |
N |
Number of sites (default 100). |
cutpoints |
Distance-bin edges (default |
transect |
Transect geometry: |
key |
Detection function: |
shape |
Hazard-rate log-shape, shared across species (ignored under the half-normal key). Default 0. |
n.abund.covs, n.det.covs
|
Number of abundance / detection-scale covariates (default 1 and 0). |
mu.lambda |
Community-mean abundance coefficients on the log scale.
Default |
mu.sigma |
Community-mean detection-scale coefficients on the log scale.
Default |
sd.lambda, sd.sigma
|
Per-coefficient community SDs. Default 0.4 and 0.2. |
n.factors |
If |
load.sd |
SD of the factor loadings (default 0.5). |
field |
Optional length- |
quad.order |
Gauss-Legendre nodes per bin used to integrate the per-bin
detection probabilities (default 64, matching |
seed |
Optional random seed. |
A list with y, data, species, cutpoints, and truth
(community means / SDs, per-species coefficients, lambda, sigma, and –
with factors – the loadings, factors and the implied residual
correlation cor_res). The latent N is drawn inside the shared C++
simulator and is not returned.
sim <- simulate_ms_distance(n.species = 4, N = 30, seed = 1) dim(sim$y)sim <- simulate_ms_distance(n.species = 4, N = 30, seed = 1) dim(sim$y)
Simulate temporal multi-species occupancy data
simulate_ms_dyn_occu( N = 50, J = 3, n.species = 5, n.seasons = 4, beta.comm.mean = c(0), beta.comm.sd = c(0.5), gamma = 0.15, epsilon = 0.1, field = NULL, trend = NULL, seed = NULL )simulate_ms_dyn_occu( N = 50, J = 3, n.species = 5, n.seasons = 4, beta.comm.mean = c(0), beta.comm.sd = c(0.5), gamma = 0.15, epsilon = 0.1, field = NULL, trend = NULL, seed = NULL )
N |
Number of sites (default 50). |
J |
Visits per season (default 3). |
n.species |
Number of species (default 5). |
n.seasons |
Number of seasons (default 4). |
beta.comm.mean |
Community mean for occupancy (default c(0)). |
beta.comm.sd |
Community SD for occupancy (default c(0.5)). |
gamma |
Colonization probability (default 0.15). |
epsilon |
Extinction probability (default 0.1). |
field |
Optional per-site shared areal field (length |
trend |
Optional per-site shared varying-coefficient field (length |
seed |
Random seed. |
A list with y (4D array), data, and truth.
ms_dyn_occu(), the family this simulates for.
sim <- simulate_ms_dyn_occu(N = 30, J = 2, n.species = 3, n.seasons = 3, seed = 1) dim(sim$y)sim <- simulate_ms_dyn_occu(N = 30, J = 2, n.species = 3, n.seasons = 3, seed = 1) dim(sim$y)
Simulate integrated multi-species occupancy data
simulate_ms_int_occu( N = 100, J = c(3, 4), n.species = 5, n.data = 2, seed = NULL )simulate_ms_int_occu( N = 100, J = c(3, 4), n.species = 5, n.data = 2, seed = NULL )
N |
Number of sites (default 100). |
J |
Vector of visits per source (default c(3, 4)). |
n.species |
Number of species (default 5). |
n.data |
Number of data sources (default 2). |
seed |
Random seed. |
A list with y (list of 3D arrays), data, and truth.
ms_int_occu(), the family this simulates for.
sim <- simulate_ms_int_occu(N = 40, n.species = 3, seed = 1) lapply(sim$y, dim)sim <- simulate_ms_int_occu(N = 40, n.species = 3, seed = 1) lapply(sim$y, dim)
Simulate multi-species occupancy data
simulate_ms_occu( N = 100, J = 4, n.species = 10, beta.comm.mean = c(0, 0.5), beta.comm.sd = c(0.5, 0.3), alpha.comm.mean = c(0), alpha.comm.sd = c(0.5), seed = NULL )simulate_ms_occu( N = 100, J = 4, n.species = 10, beta.comm.mean = c(0, 0.5), beta.comm.sd = c(0.5, 0.3), alpha.comm.mean = c(0), alpha.comm.sd = c(0.5), seed = NULL )
N |
Number of sites (default 100). |
J |
Number of visits (default 4). |
n.species |
Number of species (default 10). |
beta.comm.mean |
Community mean for occupancy (default c(0, 0.5)). |
beta.comm.sd |
Community SD for occupancy (default c(0.5, 0.3)). |
alpha.comm.mean |
Community mean for detection (default c(0)). |
alpha.comm.sd |
Community SD for detection (default c(0.5)). |
seed |
Random seed. |
A list with y (3D array), data, and truth.
sim <- simulate_ms_occu(N = 40, J = 3, n.species = 4, seed = 1) dim(sim$y)sim <- simulate_ms_occu(N = 40, J = 3, n.species = 4, seed = 1) dim(sim$y)
Per-species joint occupancy-detection + cover model with Gaussian community
hyperpriors on the per-species coefficients of all three arms:
beta_occ_s ~ N(mu_occ, diag(sd_occ^2)),
beta_p_s ~ N(mu_p, diag(sd_p^2)),
beta_pos_s ~ N(mu_pos, diag(sd_pos^2)), then per cell
z_{s,i} ~ Bernoulli(psi_{s,i}), per visit
y_{s,i,j} | z = 1 ~ Bernoulli(p_{s,i,j}), and per detected visit
c_{s,i,j} ~ f_pos(eta_pos_{s,i,j}, disp). The site and visit covariates are
shared across species (community covariates). The returned y / y_pos are
3D arrays [n_sites x J x n_species] suitable for tobs() with
ms_occu_cover() (y_pos is NA where not detected).
simulate_ms_occu_cover( n.species = 12, N = 120, J = 5, n.occ.covs = 1, n.det.covs = 1, n.pos.covs = 1, mu.occ = NULL, mu.p = NULL, mu.pos = NULL, sd.occ = 0.5, sd.p = 0.4, sd.pos = 0.4, positive = c("lognormal", "beta", "gaussian"), phi = 30, sigma.pos = 0.4, seed = NULL )simulate_ms_occu_cover( n.species = 12, N = 120, J = 5, n.occ.covs = 1, n.det.covs = 1, n.pos.covs = 1, mu.occ = NULL, mu.p = NULL, mu.pos = NULL, sd.occ = 0.5, sd.p = 0.4, sd.pos = 0.4, positive = c("lognormal", "beta", "gaussian"), phi = 30, sigma.pos = 0.4, seed = NULL )
n.species |
Number of species (default 12). |
N |
Number of sites / cells (default 120). |
J |
Number of replicate visits (default 5). |
n.occ.covs, n.det.covs, n.pos.covs
|
Number of covariates on each arm (drawn IID standard normal, shared across species). |
mu.occ, mu.p, mu.pos
|
Community-mean coefficient vectors
|
sd.occ, sd.p, sd.pos
|
Per-coefficient community SD on each arm (length 1, recycled, or one per coefficient). Default 0.5 / 0.4 / 0.4. |
positive |
|
phi |
Beta precision when |
sigma.pos |
Lognormal residual SD when |
seed |
Optional integer seed. |
A list with y (3D detection array), y_pos (3D cover array, NA
where not detected), data (per-cell covariate frame), visit_data
(per-visit covariate frame, N*J rows in site-major order), species
(species names), and truth (community means / SDs, per-species
coefficients, the dispersion, and the latent state).
sim <- simulate_ms_occu_cover(n.species = 4, N = 30, J = 3, seed = 1) dim(sim$y)sim <- simulate_ms_occu_cover(n.species = 4, N = 30, J = 3, seed = 1) dim(sim$y)
Generates occupancy / detection / cover data for n.species species sharing
one latent ICAR spatial factor w on the cell graph, with per-species
loadings L_s on the occupancy state predictor and Gaussian community priors
on the per-species arm coefficients. This is the ground-truth generator for
the Stage-1 reduced-rank spatial JSDM.
simulate_ms_occu_cover_spatial( adj, n.species = 8L, J = 4L, K = 1L, n.occ.covs = 1L, n.det.covs = 1L, n.pos.covs = 1L, mu.occ = NULL, mu.p = NULL, mu.pos = NULL, sd.occ = 0.4, sd.p = 0.4, sd.pos = 0.3, mean.load = 0, sd.load = 1, cover.factor = FALSE, mean.load.pos = 0, sd.load.pos = 1, field = c("icar", "car_proper", "bym2"), rho = 0.9, phi = 0.7, sigma.pos = 0.4, positive = c("lognormal", "beta"), seed = NULL )simulate_ms_occu_cover_spatial( adj, n.species = 8L, J = 4L, K = 1L, n.occ.covs = 1L, n.det.covs = 1L, n.pos.covs = 1L, mu.occ = NULL, mu.p = NULL, mu.pos = NULL, sd.occ = 0.4, sd.p = 0.4, sd.pos = 0.3, mean.load = 0, sd.load = 1, cover.factor = FALSE, mean.load.pos = 0, sd.load.pos = 1, field = c("icar", "car_proper", "bym2"), rho = 0.9, phi = 0.7, sigma.pos = 0.4, positive = c("lognormal", "beta"), seed = NULL )
adj |
N x N 0/1 adjacency matrix of the cell graph (required); |
n.species |
Number of species. |
J |
Number of detection visits per cell. |
K |
Number of shared latent spatial factors ( |
n.occ.covs, n.det.covs, n.pos.covs
|
Number of (Gaussian) covariates on the occupancy, detection, and cover arms; each arm also has an intercept. |
mu.occ, mu.p, mu.pos
|
Community mean coefficient vectors (intercept first).
|
sd.occ, sd.p, sd.pos
|
Community RE SDs (diagonal |
mean.load, sd.load
|
Mean and SD of the per-species loadings |
cover.factor |
Logical; when |
mean.load.pos, sd.load.pos
|
Mean and SD of the cover-arm loadings |
field |
Areal structure of the shared latent factors: |
rho |
Proper-CAR correlation in |
phi |
BYM2 spatial-variance fraction in |
sigma.pos |
Lognormal cover residual SD (on the log scale). |
positive |
Cover family; only |
seed |
Optional RNG seed. |
A list with y (N x J x n_species 0/1 detections), y_pos
(N x J x n_species cover, NA off detected visits), data (cell-level
covariate frame), species, and truth (the canonical-form generating
parameters: mu_*, sd_*, per-species b_*, loadings L, field w,
psi, z).
adj <- matrix(0, 10, 10) adj[cbind(1:9, 2:10)] <- 1 adj <- adj + t(adj) sim <- simulate_ms_occu_cover_spatial(adj, n.species = 4, J = 3, seed = 1) dim(sim$y)adj <- matrix(0, 10, 10) adj[cbind(1:9, 2:10)] <- 1 adj <- adj + t(adj) sim <- simulate_ms_occu_cover_spatial(adj, n.species = 4, J = 3, seed = 1) dim(sim$y)
Simulate single-species occupancy data
simulate_occu( N = 100, J = 4, n.occ.covs = 2, n.det.covs = 1, beta.occ = NULL, beta.det = NULL, n.visit.groups = 0L, sigma.visit = 1, seed = NULL )simulate_occu( N = 100, J = 4, n.occ.covs = 2, n.det.covs = 1, beta.occ = NULL, beta.det = NULL, n.visit.groups = 0L, sigma.visit = 1, seed = NULL )
N |
Number of sites (default 100). |
J |
Number of visits (default 4). |
n.occ.covs |
Number of occupancy covariates (default 2). |
n.det.covs |
Number of detection covariates (default 1). |
beta.occ |
Occupancy coefficients (auto-generated if NULL). |
beta.det |
Detection coefficients (auto-generated if NULL). |
n.visit.groups |
Number of groups a visit can belong to (an observer,
say), each adding an effect to the detection logit of the visits it made.
Visits are assigned to groups uniformly at random. |
sigma.visit |
Standard deviation of the per-group detection effects,
drawn from |
seed |
Random seed. |
A list with y, data, and truth. With n_visit_groups > 0 it
also carries visits, a data frame with one row per site-visit in
site-major order and the factor column visit_group, and truth gains
b_visit (the group effects) and sigma_visit.
sim <- simulate_occu(N = 50, J = 3, seed = 1) dim(sim$y) obs <- simulate_occu(N = 50, J = 3, n.visit.groups = 5, seed = 1) table(obs$visits$visit_group)sim <- simulate_occu(N = 50, J = 3, seed = 1) dim(sim$y) obs <- simulate_occu(N = 50, J = 3, n.visit.groups = 5, seed = 1) table(obs$visits$visit_group)
Simulate from an occu_categorical model
simulate_occu_categorical( N = 500L, beta.occ = c(0.2, 0.8), beta.class = NULL, seed = NULL )simulate_occu_categorical( N = 500L, beta.occ = c(0.2, 0.8), beta.class = NULL, seed = NULL )
N |
number of units. |
beta.occ |
length-2 presence coefficients (intercept, slope on |
beta.class |
a |
seed |
optional RNG seed. |
A list with data (a data frame with x), y (length-N integer in
0..K), and truth.
sim <- simulate_occu_categorical(N = 100, seed = 1) table(sim$y)sim <- simulate_occu_categorical(N = 100, seed = 1) table(sim$y)
Per-cell mixture: latent z_i ~ Bernoulli(psi_i), per-visit detection
y_ij | z_i = 1 ~ Bernoulli(p_ij), per-visit cover y_pos_ij | y_ij = 1
drawn from positive ("beta" or "lognormal") on the cover-arm linear
predictor. Used by the recovery test and as the generator for the
joint occupancy + cover hurdle family (see occu_cover()).
simulate_occu_cover( N = 200L, J = 4L, n.occ.covs = 1L, n.det.covs = 1L, n.pos.covs = 1L, beta.occ = NULL, beta.p = NULL, beta.pos = NULL, positive = c("lognormal", "beta", "gaussian"), phi = 30, sigma.pos = 0.4, adj = NULL, sigma = 0.6, alpha = 1, trend = FALSE, sigma.trend = 0.6, alpha.trend = 1, pos.field = FALSE, sigma.pos.int = 0.5, sigma.pos.trend = 0.6, det.field = FALSE, sigma.p.int = 0.5, sigma.p.trend = 0.6, re.det.groups = NULL, sigma.re.p = 0.7, re.pos.groups = NULL, sigma.re.pos = 0.7, re.det = NULL, seed = NULL )simulate_occu_cover( N = 200L, J = 4L, n.occ.covs = 1L, n.det.covs = 1L, n.pos.covs = 1L, beta.occ = NULL, beta.p = NULL, beta.pos = NULL, positive = c("lognormal", "beta", "gaussian"), phi = 30, sigma.pos = 0.4, adj = NULL, sigma = 0.6, alpha = 1, trend = FALSE, sigma.trend = 0.6, alpha.trend = 1, pos.field = FALSE, sigma.pos.int = 0.5, sigma.pos.trend = 0.6, det.field = FALSE, sigma.p.int = 0.5, sigma.p.trend = 0.6, re.det.groups = NULL, sigma.re.p = 0.7, re.pos.groups = NULL, sigma.re.pos = 0.7, re.det = NULL, seed = NULL )
N |
Number of sites (cells). |
J |
Number of visits per site. |
n.occ.covs, n.det.covs, n.pos.covs
|
Number of covariates on each arm (drawn IID standard normal). |
beta.occ, beta.p, beta.pos
|
Coefficient vectors c(intercept, slopes). Defaults pick weakly-informative values: psi intercept at logit(0.4), p intercept at logit(0.5), cover intercept on the appropriate link. |
positive |
|
phi |
Beta precision when |
sigma.pos |
Lognormal residual SD when |
adj |
Optional N x N adjacency matrix. When supplied, generates the shared ICAR field; when NULL, the simulator is non-spatial (matches v1). |
sigma |
Spatial field amplitude (used only when |
alpha |
Cover-arm scaling on the shared field (used only when |
trend |
Logical; when |
sigma.trend |
Trend-field amplitude (used only when |
alpha.trend |
Cover-arm scaling on the trend field (used only when
|
pos.field |
Logical; when |
sigma.pos.int |
Cover-arm intercept-field SD (used only when
|
sigma.pos.trend |
Cover-arm trend-field SD (used only when
|
det.field |
Logical; when |
sigma.p.int |
Detection-arm intercept-field SD (used only when
|
sigma.p.trend |
Detection-arm trend-field SD (used only when
|
re.det.groups |
Optional integer |
sigma.re.p |
SD of the |
re.pos.groups |
Optional integer |
sigma.re.pos |
SD of the |
re.det |
Optional named list of FURTHER per-visit detection random
effects, for crossed / nested / slope designs. Each element
|
seed |
Optional integer seed. |
When adj is supplied (a square adjacency matrix), an ICAR field
f[1..N] is drawn from MVN(0, Q^-) (with sum-to-zero constraint),
and the linear predictors become
eta_psi_i = X_psi[i, ] %*% beta_occ + sigma * f[i] eta_pos_ij = X_pos[i, ] %*% beta_pos + alpha * sigma * f[i]
This is the shared-field parameterisation (field SD sigma, cover-arm copy
amplitude alpha) that the nested-Laplace occu_cover() fit estimates.
A list with y (N x J detection matrix), y_pos (N x J cover
matrix, NA where not detected), data (per-site covariate frame, gaining
a time column when trend = TRUE), visit_data (per-visit covariate
frame, N*J rows in site-major order), and truth (the coefficients,
dispersion, and field(s) if generated; f2, sigma_trend, alpha_trend,
and time when trend = TRUE).
sim <- simulate_occu_cover(N = 50, J = 3, seed = 1) dim(sim$y)sim <- simulate_occu_cover(N = 50, J = 3, seed = 1) dim(sim$y)
Draws from the occu_multi() model: a joint occupancy state for S species
from the log-linear (first + second order) occupancy model, then per-species
site-level detections at the observed visit pattern.
simulate_occu_multi( S = 2, N = 300, J = 4, n.state.covs = 1, beta.first = NULL, beta.second = NULL, beta.p = NULL, seed = NULL )simulate_occu_multi( S = 2, N = 300, J = 4, n.state.covs = 1, beta.first = NULL, beta.second = NULL, beta.p = NULL, seed = NULL )
S |
Number of species (default 2). |
N |
Number of sites (default 300). |
J |
Number of replicate visits (default 4). |
n.state.covs |
Number of shared occupancy covariates (default 1). |
beta.first |
Length- |
beta.second |
Length- |
beta.p |
Length- |
seed |
Optional random seed. |
A list with y (a length-S list of N x J matrices), data,
species, and truth.
sim <- simulate_occu_multi(S = 2, N = 50, J = 3, seed = 1) length(sim$y)sim <- simulate_occu_multi(S = 2, N = 50, J = 3, seed = 1) length(sim$y)
Draws cell occupancy z_c ~ Bernoulli(psi_c), plot availability
a_cj | z_c=1 ~ Bernoulli(theta_cj), visit detection
y_cjv | a_cj=1 ~ Bernoulli(p_cjv), and the cover hurdle
cover_cjv | y_cjv=1 ~ f_pos, with a shared areal ICAR field on the
occupancy (sigma) and cover (alpha * sigma) arms. The data-generating
process for occu_multiscale_cover() recovery tests.
simulate_occu_multiscale_cover( n.cells = 60L, plots.per.cell = 4L, visits.per.plot = 2L, beta.psi = c(0.4, 0.6), beta.theta = c(0.2, 0.5), beta.p = c(0, 0.5), beta.pos = c(log(0.1), -0.4), positive = c("lognormal", "beta", "gaussian"), phi = 0.35, adj = NULL, sigma = 0.7, alpha = 1, trend = FALSE, sigma.trend = 0.7, alpha.trend = 1, seed = NULL )simulate_occu_multiscale_cover( n.cells = 60L, plots.per.cell = 4L, visits.per.plot = 2L, beta.psi = c(0.4, 0.6), beta.theta = c(0.2, 0.5), beta.p = c(0, 0.5), beta.pos = c(log(0.1), -0.4), positive = c("lognormal", "beta", "gaussian"), phi = 0.35, adj = NULL, sigma = 0.7, alpha = 1, trend = FALSE, sigma.trend = 0.7, alpha.trend = 1, seed = NULL )
n.cells |
number of areal cells (graph nodes). |
plots.per.cell |
plots (the availability units) per cell. |
visits.per.plot |
detection replicate visits per plot. |
beta.psi, beta.theta, beta.p, beta.pos
|
length-2 |
positive |
|
phi |
cover dispersion (lognormal log-scale SD, or beta precision). |
adj |
|
sigma |
areal-field marginal SD on the occupancy arm. |
alpha |
cover-arm field scaling ( |
trend |
add a second, spatially-varying-coefficient (SVC) areal field
weighted per cell by a cell-level covariate |
sigma.trend, alpha.trend
|
trend-field marginal SD and its cover-arm
scaling (used only when |
seed |
optional RNG seed. |
A list with y, y_pos ([n_plots x visits_per_plot]), the
plot-level data (cell id cell, covariates), adj, and truth.
sim <- simulate_occu_multiscale_cover(n.cells = 10, plots.per.cell = 2, visits.per.plot = 2, seed = 1) dim(sim$y)sim <- simulate_occu_multiscale_cover(n.cells = 10, plots.per.cell = 2, visits.per.plot = 2, seed = 1) dim(sim$y)
Draws from the occu_ttd() model: site occupancy z ~ Bernoulli(psi) and,
at occupied sites, exponential time-to-detection at rate lambda over a
survey of length Tmax (a non-detection is censored at Tmax).
simulate_occu_ttd( N = 200, J = 4, n.psi.covs = 1, n.rate.covs = 1, beta.psi = NULL, beta.rate = NULL, Tmax = 3, seed = NULL )simulate_occu_ttd( N = 200, J = 4, n.psi.covs = 1, n.rate.covs = 1, beta.psi = NULL, beta.rate = NULL, Tmax = 3, seed = NULL )
N |
Number of sites (default 200). |
J |
Number of replicate surveys per site (default 4). |
n.psi.covs, n.rate.covs
|
Number of occupancy / rate covariates. |
beta.psi |
Occupancy coefficients |
beta.rate |
Log detection-rate coefficients |
Tmax |
Survey length (scalar). Default 3. |
seed |
Optional random seed. |
A list with y (N x J TTD matrix), data, Tmax, and truth.
sim <- simulate_occu_ttd(N = 50, J = 3, seed = 1) dim(sim$y)sim <- simulate_occu_ttd(N = 50, J = 3, seed = 1) dim(sim$y)
Latent abundance N_i ~ Poisson(lambda_i) (or NegBin(lambda_i, size)) with
log lambda_i = X_lambda beta_lambda, observed through K ordered removal
passes: pass k removes Binomial(N_i - sum_{l<k} y_{il}, p_{ik}) of the
individuals still present, with logit p_{ik} = X_p beta_p (site-level
detection here). The returned y is an N x K integer matrix of per-pass
removals suitable for tobs() with removal().
simulate_removal( N = 100, K = 4, n.abund.covs = 2, n.det.covs = 1, beta.lambda = NULL, beta.p = NULL, mixture = c("poisson", "negbin"), size = 3, seed = NULL )simulate_removal( N = 100, K = 4, n.abund.covs = 2, n.det.covs = 1, beta.lambda = NULL, beta.p = NULL, mixture = c("poisson", "negbin"), size = 3, seed = NULL )
N |
Number of sites (default 100). |
K |
Number of removal passes (default 4). |
n.abund.covs |
Number of abundance covariates (default 2). |
n.det.covs |
Number of detection covariates (default 1). |
beta.lambda |
Abundance coefficients on the log scale. Default
|
beta.p |
Detection coefficients on the logit scale. Default
|
mixture |
|
size |
Negative-binomial size |
seed |
Optional random seed. |
A list with y (N x K removal matrix), data (covariate data frame),
and truth (coefficients, per-site lambda, p, latent N, mixture/size).
sim <- simulate_removal(N = 50, K = 3, seed = 1) dim(sim$y)sim <- simulate_removal(N = 50, K = 3, seed = 1) dim(sim$y)
Latent abundance N_i ~ Poisson(lambda_i), log lambda = X beta_lambda, and
per-visit detection y_ij ~ Bernoulli(1 - (1 - r_ij)^{N_i}) with per-individual
detection logit r_ij = beta_r. Detection is site-level by default; supplying
beta.r.visit draws a per-visit covariate w and makes logit r_ij = beta_r + beta_r_visit * w_ij visit-varying. Matches the royle_nichols()
family; the response is an N x J 0/1 detection-history matrix.
simulate_royle_nichols( N = 200, J = 5, beta.lambda = c(0.3, 0.5), beta.r = -0.8, beta.r.visit = NULL, seed = NULL )simulate_royle_nichols( N = 200, J = 5, beta.lambda = c(0.3, 0.5), beta.r = -0.8, beta.r.visit = NULL, seed = NULL )
N |
Number of sites (default 200). |
J |
Number of visits per site (default 5). |
beta.lambda |
Log-abundance coefficients |
beta.r |
Per-individual detection logit (a scalar intercept). |
beta.r.visit |
Optional scalar slope on a drawn per-visit detection
covariate |
seed |
Random seed. |
A list with y (N x J 0/1 matrix), data, truth
(beta_lambda, beta_r, beta_r_visit, realised abundance N, lambda,
per-site or per-visit r), and, when beta_r_visit is supplied, visits.
sim <- simulate_royle_nichols(N = 100, J = 4, seed = 1) dim(sim$y) # Visit-varying detection: sv <- simulate_royle_nichols(N = 100, J = 4, beta.r.visit = 0.8, seed = 1) names(sv$visits)sim <- simulate_royle_nichols(N = 100, J = 4, seed = 1) dim(sim$y) # Visit-varying detection: sv <- simulate_royle_nichols(N = 100, J = 4, beta.r.visit = 0.8, seed = 1) names(sv$visits)
Simulate multi-season occupancy with an AR1 year effect (tPGOcc)
simulate_t_occu( N = 150, T.seasons = 8, J = 3, beta.occ = c(0.2), p = 0.4, rho = 0.6, sigma = 0.7, seed = NULL )simulate_t_occu( N = 150, T.seasons = 8, J = 3, beta.occ = c(0.2), p = 0.4, rho = 0.6, sigma = 0.7, seed = NULL )
N |
Number of sites (default 150). |
T.seasons |
Number of seasons / years (default 8). |
J |
Visits per season (default 3). |
beta.occ |
Occupancy coefficients (intercept first; default |
p |
Detection probability (default 0.4). |
rho |
AR1 correlation of the year effect (default 0.6). |
sigma |
AR1 innovation SD of the year effect (default 0.7). |
seed |
Optional random seed. |
A list with y (3D array [N x T x J]), data, and truth.
t_occu(), the family this simulates for.
sim <- simulate_t_occu(N = 30, T.seasons = 4, J = 2, seed = 1) dim(sim$y)sim <- simulate_t_occu(N = 30, T.seasons = 4, J = 2, seed = 1) dim(sim$y)
Each replicate draws a parameter vector from the fit's posterior draws and simulates the latent state and the response from it, so the replicates carry parameter uncertainty.
## S3 method for class 'tobs_fit' simulate(object, nsim = 1, seed = NULL, ...)## S3 method for class 'tobs_fit' simulate(object, nsim = 1, seed = NULL, ...)
object |
A |
nsim |
Number of simulated datasets (default 1). |
seed |
Optional random seed. As for |
... |
Must be empty. |
A list of length nsim, one simulated response per element (named
sim_1, sim_2, ...), each in the layout the family's y takes: a
detection matrix, a [sites x visits x seasons] array, a list of source
or species matrices, and so on. The list carries the "seed" attribute
stats::simulate() specifies: the .Random.seed in force when seed is
NULL, otherwise seed with its RNGkind() as the "kind" attribute.
Every family returns this layout, whatever nsim is.
sim <- simulate_occu(N = 100, J = 3, n.occ.covs = 1, n.det.covs = 1, seed = 1) fit <- tobs(~ occ_cov1, data = sim$data, family = occu(), detection = ~ det_cov1, y = sim$y, method = "laplace", control = list(verbose = FALSE, progress = FALSE)) y_rep <- simulate(fit, nsim = 2, seed = 1) lapply(y_rep, colMeans)sim <- simulate_occu(N = 100, J = 3, n.occ.covs = 1, n.det.covs = 1, seed = 1) fit <- tobs(~ occ_cov1, data = sim$data, family = occu(), detection = ~ det_cov1, y = sim$y, method = "laplace", control = list(verbose = FALSE, progress = FALSE)) y_rep <- simulate(fit, nsim = 2, seed = 1) lapply(y_rep, colMeans)
Returns detection statistics including naive occupancy, naive detection, per-visit rates, and detection frequency table.
## S3 method for class 'tobs_data' summary(object, ...)## S3 method for class 'tobs_data' summary(object, ...)
object |
An |
... |
Ignored. |
An tobs_data_summary object (printed automatically).
sim <- simulate_occu(N = 30, J = 3, seed = 1) long <- data.frame(site = rep(seq_len(30), times = 3), visit = rep(1:3, each = 30), det = as.vector(sim$y), occ_cov1 = sim$data$occ_cov1, effort = runif(90)) dat <- tobs_data(long, y = "det", site = "site", visit = "visit", occ.covs = "occ_cov1", det.covs = "effort") summary(dat)sim <- simulate_occu(N = 30, J = 3, seed = 1) long <- data.frame(site = rep(seq_len(30), times = 3), visit = rep(1:3, each = 30), det = as.vector(sim$y), occ_cov1 = sim$data$occ_cov1, effort = runif(90)) dat <- tobs_data(long, y = "det", site = "site", visit = "visit", occ.covs = "occ_cov1", det.covs = "effort") summary(dat)
Extends summary.tulpa_fit with an extra skew column populated from
object$skew when the fit was produced with an SLA method
(method = "laplace_sla" / "nested_laplace_sla").
All quantile / mean / sd columns come from object$draws, which under
simplified Laplace are skew-normal samples – so 2.5%/97.5% quantiles are
already SLA-corrected.
## S3 method for class 'tobs_fit' summary(object, ...)## S3 method for class 'tobs_fit' summary(object, ...)
object |
A |
... |
Forwarded to |
A summary.tulpa_fit object as tulpa::summary.tulpa_fit() returns
it. Its coefficient table, coef(summary(fit)), has one row per
coefficient named <arm>_<term> as coef.tobs_fit() names it, with
columns estimate, std.error and the two interval bounds, plus a skew
column when $skew is present and rhat, ess_bulk, ess_tail on a
sampled fit. Every family, and every method within a family, returns this
layout.
sim <- simulate_occu(N = 100, J = 3, n.occ.covs = 1, n.det.covs = 1, seed = 1) fit <- tobs(~ occ_cov1, data = sim$data, family = occu(), detection = ~ det_cov1, y = sim$y, method = "laplace", control = list(verbose = FALSE, progress = FALSE)) summary(fit)sim <- simulate_occu(N = 100, J = 3, n.occ.covs = 1, n.det.covs = 1, seed = 1) fit <- tobs(~ occ_cov1, data = sim$data, family = occu(), detection = ~ det_cov1, y = sim$y, method = "laplace", control = list(verbose = FALSE, progress = FALSE)) summary(fit)
The spOccupancy tPGOcc model: per-(site, season) Bernoulli occupancy with a
shared AR1 year random effect on the occupancy logit and NO colonization /
extinction dynamics (that is dyn_occu()). Use it for an occupancy trend over
years with a temporal random effect. Fit with method = "pg_gibbs" (the
Polya-Gamma Gibbs sampler spOccupancy uses); y is a 3D array
[n_sites x n_seasons x max_visits] (or a list of per-season
[n_sites x max_visits] matrices), the occupancy formula and detection
formula are site-level.
t_occu()t_occu()
A tobs_family object.
method = "pg_gibbs" – the Polya-Gamma Gibbs sampler: conditional on the
year effects the seasons factorise, so the joint (coefficients, AR1 year
effect) update is one exact Gaussian Markov random field draw; the only
route.
f <- t_occu() ff <- t_occu() f
Coefficient table for a tobs_fit
## S3 method for class 'tobs_fit' tidy(x, conf.level = 0.95, ...)## S3 method for class 'tobs_fit' tidy(x, conf.level = 0.95, ...)
x |
A fitted |
conf.level |
Interval level (default 0.95). |
... |
Ignored. |
A data frame with one row per coefficient: arm (the linear
predictor it enters, NA for a coordinate that enters none), term (its
design column, without the arm prefix), estimate, std.error,
conf.low and conf.high. Every family returns this layout.
sim <- simulate_occu(N = 100, J = 3, n.occ.covs = 1, n.det.covs = 1, seed = 1) fit <- tobs(~ occ_cov1, data = sim$data, family = occu(), detection = ~ det_cov1, y = sim$y, method = "laplace", control = list(verbose = FALSE, progress = FALSE)) tidy(fit)sim <- simulate_occu(N = 100, J = 3, n.occ.covs = 1, n.det.covs = 1, seed = 1) fit <- tobs(~ occ_cov1, data = sim$data, family = occu(), detection = ~ det_cov1, y = sim$y, method = "laplace", control = list(verbose = FALSE, progress = FALSE)) tidy(fit)
Unified entry point for occupancy, abundance, distance, removal, and related
models that share the latent-state-plus-imperfect-detection generative
template. The specific model is chosen via the family argument; the engine
(Laplace / nested Laplace / NUTS) via engine.
tobs( formula, data, family, occurrence = NULL, detection = NULL, positive = NULL, y = NULL, visits = NULL, method = c("auto", "laplace", "laplace_sla", "laplace_gibbs", "laplace_mi", "pg_gibbs", "nested_laplace", "nested_laplace_sla", "nuts"), priors = NULL, control = list(), by = NULL, ... )tobs( formula, data, family, occurrence = NULL, detection = NULL, positive = NULL, y = NULL, visits = NULL, method = c("auto", "laplace", "laplace_sla", "laplace_gibbs", "laplace_mi", "pg_gibbs", "nested_laplace", "nested_laplace_sla", "nuts"), priors = NULL, control = list(), by = NULL, ... )
formula |
state-process formula, e.g. Single-vector-response families (the cover hurdle, Structured effects are written as terms inside the formula, the way
The spatial fields also have a single-verb umbrella The continuous fields ( Random effects also accept |
data |
data frame of site-level covariates with |
family |
a |
occurrence |
state-process (occupancy / latent-presence) formula for the
cover hurdle ( |
detection |
detection-process formula, e.g. |
positive |
positive-arm (cover) formula for
Defaults to |
y |
response. Shape depends on family:
For a single-vector-response family ( |
visits |
optional visit-level detection covariates. Accepts either:
When For |
method |
inference route, naming a fully-specified path rather than a pair of orthogonal knobs. Each family accepts a subset of the routes below (listed per route, from the package's support table); an unsupported method errors with the list of methods that family supports, and the structured terms a route carries for a given family are on that family's help page.
|
priors |
optional prior specification. For occupancy families fit
with a Laplace method ( |
control |
list of low-level engine controls. Names follow the
dotted-separator convention. Every default below is resolved from one table
( Sampler controls (
Sampler controls (
Laplace controls (
Progress controls (every method): Control names are validated against the chosen |
by |
optional name of a species column for a per-species batched fit
( The same long-frame contract drives a SINGLE The long-frame |
... |
family-specific named arguments forwarded to the underlying engine builder. |
An object of class c("tobs_fit", "<family>_fit", "tulpa_fit").
When control$n.seeds > 1, a tobs_stack ensemble of the seed-offset
refits is returned instead (see tobs_stack()). When by is supplied, a
tobs_batch of per-species fits is returned (see tobs_get()).
# Single-season occupancy sim <- simulate_occu(N = 100, J = 3, n.occ.covs = 1, n.det.covs = 1, seed = 1) fit <- tobs( formula = ~ occ_cov1, data = sim$data, family = occu(), detection = ~ det_cov1, y = sim$y, method = "laplace", control = list(verbose = FALSE, progress = FALSE) ) summary(fit)# Single-season occupancy sim <- simulate_occu(N = 100, J = 3, n.occ.covs = 1, n.det.covs = 1, seed = 1) fit <- tobs( formula = ~ occ_cov1, data = sim$data, family = occu(), detection = ~ det_cov1, y = sim$y, method = "laplace", control = list(verbose = FALSE, progress = FALSE) ) summary(fit)
For a reduced-rank spatial-factor community occupancy-cover fit (a
ms_occu_cover() model carrying an icar(), car_proper(), or bym2()
field on the occupancy arm), return the residual species associations the
shared latent fields imply – the spatial-JSDM / HMSC output. The K unit-scale
fields induce a residual occupancy covariance L L' across species; the
reported matrix is its correlation form, identified (invariant to the factor
rotation, sign, and column scale). When the fit also carries a cover-arm
factor, the cover association corr(L_pos L_pos') and the joint cross-arm
association (standardized L_occ L_pos', species occupancy vs species cover)
are available too.
tobs_associations( object, type = c("occupancy", "cover", "cross"), summary = NULL )tobs_associations( object, type = c("occupancy", "cover", "cross"), summary = NULL )
object |
A fitted |
type |
Which association to return: |
summary |
One of |
Each matrix is summarised from draws of the loading posterior (not the plug-in mode), so a central interval accompanies the point estimate.
An S x S correlation matrix (when summary is given) or a list of
the point estimate, posterior median, and interval bounds.
adj <- matrix(0, 16, 16) adj[cbind(1:15, 2:16)] <- 1 adj <- adj + t(adj) sim <- simulate_ms_occu_cover_spatial(adj, n.species = 6, K = 2, J = 4, seed = 1) fit <- tobs(~ occ_cov1 + icar(graph = adj), data = sim$data, family = ms_occu_cover("lognormal"), detection = ~ det_cov1, positive = ~ pos_cov1, y = sim$y, y.pos = sim$y_pos, species = sim$species, method = "laplace", control = list(n.factors = 2, max.iter = 30, verbose = FALSE)) round(tobs_associations(fit, summary = "median"), 2)adj <- matrix(0, 16, 16) adj[cbind(1:15, 2:16)] <- 1 adj <- adj + t(adj) sim <- simulate_ms_occu_cover_spatial(adj, n.species = 6, K = 2, J = 4, seed = 1) fit <- tobs(~ occ_cov1 + icar(graph = adj), data = sim$data, family = ms_occu_cover("lognormal"), detection = ~ det_cov1, positive = ~ pos_cov1, y = sim$y, y.pos = sim$y_pos, species = sim$species, method = "laplace", control = list(n.factors = 2, max.iter = 30, verbose = FALSE)) round(tobs_associations(fit, summary = "median"), 2)
Diagnostics for potential identifiability issues. Checks for confounded covariates, low detection or detection-free sites, and sparse replication.
tobs_check_id(model, fit = NULL)tobs_check_id(model, fit = NULL)
model |
A |
fit |
Optional |
Applies to the families whose response is a site-by-visit grid:
occu(), abun(), removal() and fp_occu(). On any other family the
pre-fit checks are reported as not applicable rather than silently skipped
– an empty issues on a family the checks never ran on is not a clean bill
of health. The post-fit sampler checks apply to any NUTS fit.
A list with identifiable, the issues messages, and
prefit_checked (FALSE where the family carries no site-by-visit grid).
sim <- simulate_occu(N = 100, J = 3, n.occ.covs = 1, n.det.covs = 1, seed = 1) fit <- tobs(~ occ_cov1, data = sim$data, family = occu(), detection = ~ det_cov1, y = sim$y, method = "laplace", control = list(verbose = FALSE, progress = FALSE)) chk <- tobs_check_id(fit$model, fit) chk$identifiablesim <- simulate_occu(N = 100, J = 3, n.occ.covs = 1, n.det.covs = 1, seed = 1) fit <- tobs(~ occ_cov1, data = sim$data, family = occu(), detection = ~ det_cov1, y = sim$y, method = "laplace", control = list(verbose = FALSE, progress = FALSE)) chk <- tobs_check_id(fit$model, fit) chk$identifiable
waic(), loo(), dic(), and cpo() on a tobs_fit build the family
pointwise log-likelihood matrix once and hand it to the engine's single
criteria layer tulpa::tulpa_criteria(), which derives WAIC / DIC / CPO /
LPML / PSIS-LOO. DIC additionally evaluates the deviance at the posterior
mean of the parameters, supplied by the family-specific
.tobs_loglik_at_mean().
## S3 method for class 'tobs_fit' waic(x, n.draws = 1000L, loo.unit = c("obs", "cell"), n.threads = NULL, ...) ## S3 method for class 'tobs_fit' loo(x, n.draws = 1000L, loo.unit = c("obs", "cell"), n.threads = NULL, ...) ## S3 method for class 'tobs_fit' dic(object, n.draws = 1000L, n.threads = NULL, ...) ## S3 method for class 'tobs_fit' cpo( object, n.draws = 1000L, loo.unit = c("obs", "cell"), n.threads = NULL, ... ) ## S3 method for class 'tobs_fit' pointwise_loglik(object, ndraws = NULL, ...)## S3 method for class 'tobs_fit' waic(x, n.draws = 1000L, loo.unit = c("obs", "cell"), n.threads = NULL, ...) ## S3 method for class 'tobs_fit' loo(x, n.draws = 1000L, loo.unit = c("obs", "cell"), n.threads = NULL, ...) ## S3 method for class 'tobs_fit' dic(object, n.draws = 1000L, n.threads = NULL, ...) ## S3 method for class 'tobs_fit' cpo( object, n.draws = 1000L, loo.unit = c("obs", "cell"), n.threads = NULL, ... ) ## S3 method for class 'tobs_fit' pointwise_loglik(object, ndraws = NULL, ...)
x, object
|
A |
n.draws |
Posterior draws used to build the pointwise log-likelihood
(the cover / occu_cover paths sample this many; the draw-matrix families use
the first |
loo.unit |
The cross-validation unit for |
n.threads |
Threads for the parallel |
... |
For |
ndraws |
For |
waic() and loo() are the loo package's generics and build their
objects through loo::waic() and loo::loo() on the same pointwise matrix
(loo() via PSIS with relative effective sample sizes), so
loo::loo_compare() and the rest of that ecosystem read them directly.
dic() and cpo() are tulpa's, and return a tulpa_criteria object.
dic() and cpo() return a tulpa_criteria object. waic()
returns a waic object and loo() a psis_loo object from the loo
package; read the estimates from $estimates (e.g.
waic(fit)$estimates["waic", "Estimate"]). Both carry the
cross-validation unit they scored in their "loo_unit" attribute.
tulpa::tulpa_criteria(), loo::loo_compare()
sim <- simulate_occu(N = 100, J = 3, n.occ.covs = 1, n.det.covs = 1, seed = 1) fit <- tobs(~ occ_cov1, data = sim$data, family = occu(), detection = ~ det_cov1, y = sim$y, method = "laplace", control = list(verbose = FALSE, progress = FALSE)) waic(fit) loo(fit) dic(fit)sim <- simulate_occu(N = 100, J = 3, n.occ.covs = 1, n.det.covs = 1, seed = 1) fit <- tobs(~ occ_cov1, data = sim$data, family = occu(), detection = ~ det_cov1, y = sim$y, method = "laplace", control = list(verbose = FALSE, progress = FALSE)) waic(fit) loo(fit) dic(fit)
Convert long-format data to a site x visit observation object
tobs_data( df, y, site, visit, type = c("occurrence", "abundance", "cover", "positive"), occ.covs = NULL, det.covs = NULL, coords = NULL, sites = NULL, visits = NULL, cover.floor = 0, compact = FALSE )tobs_data( df, y, site, visit, type = c("occurrence", "abundance", "cover", "positive"), occ.covs = NULL, det.covs = NULL, coords = NULL, sites = NULL, visits = NULL, cover.floor = 0, compact = FALSE )
df |
Data.frame in long format (one row per site-visit). |
y |
Character, name of the response column. Its meaning is set by
|
site |
Character, name of site identifier column. |
visit |
Character, name of visit/replicate column. |
type |
Response kind. |
occ.covs |
Character vector of site-level covariate names. |
det.covs |
Character vector of visit-level covariate names. A named column that is a factor or character is preserved as categorical: the downstream detection / positive design expands it to k - 1 dummies for a k-level factor, with the factor's first level (for a character column, the first level after sorting the unique values) as the reference. Numeric columns are kept as a continuous covariate. |
coords |
Character vector of length 2 for coordinate columns. |
sites |
Optional site level set. When supplied, the site x visit grid
uses these site identifiers in this order (rather than |
visits |
Optional visit level set, analogous to |
cover.floor |
For |
compact |
Logical (default |
An tobs_data object. With compact = TRUE its $y is a
tobs_ragged carrier and its $det.covs are length-V vectors.
sim <- simulate_occu(N = 30, J = 3, seed = 1) long <- data.frame(site = rep(seq_len(30), times = 3), visit = rep(1:3, each = 30), det = as.vector(sim$y), occ_cov1 = sim$data$occ_cov1, effort = runif(90)) dat <- tobs_data(long, y = "det", site = "site", visit = "visit", occ.covs = "occ_cov1", det.covs = "effort") datsim <- simulate_occu(N = 30, J = 3, seed = 1) long <- data.frame(site = rep(seq_len(30), times = 3), visit = rep(1:3, each = 30), det = as.vector(sim$y), occ_cov1 = sim$data$occ_cov1, effort = runif(90)) dat <- tobs_data(long, y = "det", site = "site", visit = "visit", occ.covs = "occ_cov1", det.covs = "effort") dat
On a nested-Laplace fit that keeps the per-cell pieces of its outer grid
(for example occu_cover() or abun() with an areal field), the
covariance is the grid-marginalized one and the bounds invert the Gaussian
mixture the grid defines, rather than being read off the Monte Carlo draws
the fit also carries; coef(), summary() and tidy() read the same
mixture. Every other fit reads as
tulpa::vcov.tulpa_fit() / tulpa::confint.tulpa_fit() describe.
## S3 method for class 'tobs_fit' vcov(object, ...) ## S3 method for class 'tobs_fit' confint(object, parm = NULL, level = 0.95, ...)## S3 method for class 'tobs_fit' vcov(object, ...) ## S3 method for class 'tobs_fit' confint(object, parm = NULL, level = 0.95, ...)
object |
A |
... |
Ignored. |
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 posterior draws when they carry a column of that name. |
level |
Interval level (default 0.95). |
vcov(): the fixed-effect covariance matrix. confint(): a
two-column matrix of lower and upper bounds, one row per requested
parameter, carrying the interval_source, interval_declined,
retained_mass and skew_applied attributes tulpa::confint.tulpa_fit()
documents.
sim <- simulate_occu(N = 100, J = 3, n.occ.covs = 1, n.det.covs = 1, seed = 1) fit <- tobs(~ occ_cov1, data = sim$data, family = occu(), detection = ~ det_cov1, y = sim$y, method = "laplace", control = list(verbose = FALSE, progress = FALSE)) vcov(fit) confint(fit, level = 0.9)sim <- simulate_occu(N = 100, J = 3, n.occ.covs = 1, n.det.covs = 1, seed = 1) fit <- tobs(~ occ_cov1, data = sim$data, family = occu(), detection = ~ det_cov1, y = sim$y, method = "laplace", control = list(verbose = FALSE, progress = FALSE)) vcov(fit) confint(fit, level = 0.9)
Format occupancy data from matrices/lists
tobs_format(y, occ.covs = NULL, det.covs = NULL, coords = NULL, species = NULL)tobs_format(y, occ.covs = NULL, det.covs = NULL, coords = NULL, species = NULL)
y |
Detection history matrix (n_sites x max_visits) or 3D array. |
occ.covs |
Data.frame of site-level covariates, or named list. |
det.covs |
Named list of detection covariates. Each element is a vector (length n_sites, constant across visits) or matrix (n_sites x max_visits). |
coords |
Optional n_sites x 2 coordinate matrix. |
species |
Optional character or integer species identifier. |
An tobs_data object.
sim <- simulate_occu(N = 50, J = 3, seed = 1) dat <- tobs_format(sim$y, occ.covs = sim$data[, c("occ_cov1", "occ_cov2")], det.covs = list(det_cov1 = sim$data$det_cov1)) datsim <- simulate_occu(N = 50, J = 3, seed = 1) dat <- tobs_format(sim$y, occ.covs = sim$data[, c("occ_cov1", "occ_cov2")], det.covs = list(det_cov1 = sim$data$det_cov1)) dat
Format multi-species occupancy data
tobs_format_ms( y, occ.covs = NULL, det.covs = NULL, coords = NULL, species.names = NULL )tobs_format_ms( y, occ.covs = NULL, det.covs = NULL, coords = NULL, species.names = NULL )
y |
3D array (n_sites x max_visits x n_species) or named list of matrices. |
occ.covs |
Data.frame of site-level covariates. |
det.covs |
Named list of detection covariates. |
coords |
Optional n_sites x 2 coordinate matrix. |
species.names |
Optional character vector of species names. |
An tobs_data object with multi-species structure.
sim <- simulate_ms_occu(N = 40, J = 3, n.species = 4, seed = 1) dat <- tobs_format_ms(sim$y, occ.covs = sim$data, species.names = paste0("sp", 1:4)) dat$n_speciessim <- simulate_ms_occu(N = 40, J = 3, n.species = 4, seed = 1) dat <- tobs_format_ms(sim$y, occ.covs = sim$data, species.names = paste0("sp", 1:4)) dat$n_species
Extract one species' fit from a batched occu_cover fit.
tobs_get(x, species)tobs_get(x, species)
x |
A |
species |
Species label (character) or index (integer). |
The single-species tobs_fit for that species.
sim_a <- simulate_occu_cover(N = 30, J = 3, seed = 1) sim_b <- simulate_occu_cover(N = 30, J = 3, seed = 2) long <- rbind( data.frame(site = rep(1:30, each = 3), visit = rep(1:3, 30), sp = "a", occur = as.vector(t(sim_a$y)), cover = as.vector(t(ifelse(is.na(sim_a$y_pos), 0, sim_a$y_pos)))), data.frame(site = rep(1:30, each = 3), visit = rep(1:3, 30), sp = "b", occur = as.vector(t(sim_b$y)), cover = as.vector(t(ifelse(is.na(sim_b$y_pos), 0, sim_b$y_pos))))) long$det_cov1 <- rep(sim_a$visit_data$det_cov1, 2) long$occ_cov1 <- rep(rep(sim_a$data$occ_cov1, each = 3), 2) fit <- tobs(~ occ_cov1, data = long, family = occu_cover("lognormal"), detection = ~ det_cov1, positive = ~ 1, method = "laplace", by = "sp", site = "site", visit = "visit", response = "occur", y.pos = "cover", det.covs = "det_cov1", control = list(verbose = FALSE)) fit_a <- tobs_get(fit, "a") coef(fit_a)sim_a <- simulate_occu_cover(N = 30, J = 3, seed = 1) sim_b <- simulate_occu_cover(N = 30, J = 3, seed = 2) long <- rbind( data.frame(site = rep(1:30, each = 3), visit = rep(1:3, 30), sp = "a", occur = as.vector(t(sim_a$y)), cover = as.vector(t(ifelse(is.na(sim_a$y_pos), 0, sim_a$y_pos)))), data.frame(site = rep(1:30, each = 3), visit = rep(1:3, 30), sp = "b", occur = as.vector(t(sim_b$y)), cover = as.vector(t(ifelse(is.na(sim_b$y_pos), 0, sim_b$y_pos))))) long$det_cov1 <- rep(sim_a$visit_data$det_cov1, 2) long$occ_cov1 <- rep(rep(sim_a$data$occ_cov1, each = 3), 2) fit <- tobs(~ occ_cov1, data = long, family = occu_cover("lognormal"), detection = ~ det_cov1, positive = ~ 1, method = "laplace", by = "sp", site = "site", visit = "visit", response = "occur", y.pos = "cover", det.covs = "det_cov1", control = list(verbose = FALSE)) fit_a <- tobs_get(fit, "a") coef(fit_a)
The tulpa goodness-of-fit generics for tobs_fit: dispersion,
zero-inflation, and outlier counts compared against the fitted model's
simulated replicates. For the Kolmogorov-Smirnov uniformity test, hand
pit_residuals() to tulpa::test_uniformity(), which takes a PIT vector
directly.
## S3 method for class 'tobs_fit' test_dispersion( object, n.samples = 250, nsim = NULL, seed = NULL, observed = NULL, alternative = c("greater", "two.sided", "less"), ... ) ## S3 method for class 'tobs_fit' test_zero_inflation( object, n.samples = 250, nsim = NULL, seed = NULL, observed = NULL, ... ) ## S3 method for class 'tobs_fit' test_outliers( object, n.samples = 250, nsim = NULL, seed = NULL, observed = NULL, ... )## S3 method for class 'tobs_fit' test_dispersion( object, n.samples = 250, nsim = NULL, seed = NULL, observed = NULL, alternative = c("greater", "two.sided", "less"), ... ) ## S3 method for class 'tobs_fit' test_zero_inflation( object, n.samples = 250, nsim = NULL, seed = NULL, observed = NULL, ... ) ## S3 method for class 'tobs_fit' test_outliers( object, n.samples = 250, nsim = NULL, seed = NULL, observed = NULL, ... )
object |
A fitted |
n.samples |
Number of posterior-predictive replicates to simulate. |
nsim |
The same budget under the name the tulpa default methods
use; overrides |
seed |
Optional random seed for the replicates; the caller's RNG stream is restored afterwards. |
observed |
Must be |
alternative |
Tail of the dispersion p-value: |
... |
Must be empty; an unrecognised argument is an error. |
A list with the observed statistic (observed), its
posterior-predictive expectation (expected), their ratio (ratio)
and a tail p-value (p.value). The names are the same on every
family, so a caller can read the statistic without branching on the
model type.
sim <- simulate_occu(N = 100, J = 3, n.occ.covs = 1, n.det.covs = 1, seed = 1) fit <- tobs(~ occ_cov1, data = sim$data, family = occu(), detection = ~ det_cov1, y = sim$y, method = "laplace", control = list(verbose = FALSE, progress = FALSE)) test_dispersion(fit, seed = 1) test_zero_inflation(fit, seed = 1)sim <- simulate_occu(N = 100, J = 3, n.occ.covs = 1, n.det.covs = 1, seed = 1) fit <- tobs(~ occ_cov1, data = sim$data, family = occu(), detection = ~ det_cov1, y = sim$y, method = "laplace", control = list(verbose = FALSE, progress = FALSE)) test_dispersion(fit, seed = 1) test_zero_inflation(fit, seed = 1)
Returns the fitted response over the range of one covariate, holding the others at their means, on the process's response scale (occupancy / detection probability, or abundance intensity).
tobs_marginal_effect( object, covariate, process = c("occupancy", "detection", "abundance"), n.points = 100L )tobs_marginal_effect( object, covariate, process = c("occupancy", "detection", "abundance"), n.points = 100L )
object |
A |
covariate |
Name of covariate. |
process |
|
n.points |
Number of prediction points (default 100). |
A data.frame with covariate values and the predicted response.
sim <- simulate_abun(N = 100, J = 4, n.abund.covs = 2, n.det.covs = 1, seed = 1) fit <- tobs(~ abund_cov1 + abund_cov2, data = sim$data, family = abun(K.max = 50), detection = ~ det_cov1, y = sim$y, control = list(verbose = FALSE)) me <- tobs_marginal_effect(fit, "abund_cov1", process = "abundance") head(me) # estimate is on the abundance (lambda) scale, not a probabilitysim <- simulate_abun(N = 100, J = 4, n.abund.covs = 2, n.det.covs = 1, seed = 1) fit <- tobs(~ abund_cov1 + abund_cov2, data = sim$data, family = abun(K.max = 50), detection = ~ det_cov1, y = sim$y, control = list(verbose = FALSE)) me <- tobs_marginal_effect(fit, "abund_cov1", process = "abundance") head(me) # estimate is on the abundance (lambda) scale, not a probability
Coefficient, covariance, and summary accessors for fits that store
independent per-arm coefficient blocks (cover_fit, occu_categorical_fit)
rather than a flat posterior. The arms are fit independently, so the joint
covariance is block-diagonal. When a full posterior is present (the NUTS
cover path) the inference methods defer to the draw-based tulpa_fit
methods.
## S3 method for class 'tobs_multiarm_fit' nobs(object, ...) ## S3 method for class 'tobs_multiarm_fit' coef(object, arm = NULL, ...) ## S3 method for class 'tobs_multiarm_fit' vcov(object, ...) ## S3 method for class 'tobs_multiarm_fit' confint(object, parm, level = 0.95, ...) ## S3 method for class 'tobs_multiarm_fit' logLik(object, ...) ## S3 method for class 'tobs_multiarm_fit' glance(x, ...) ## S3 method for class 'tobs_multiarm_fit' tidy(x, conf.level = 0.95, ...) ## S3 method for class 'tobs_multiarm_fit' summary(object, level = 0.95, ...) ## S3 method for class 'tobs_multiarm_fit' plot(x, ...) ## S3 method for class 'tobs_multiarm_fit' fitted(object, ...) ## S3 method for class 'tobs_multiarm_fit' residuals(object, ...)## S3 method for class 'tobs_multiarm_fit' nobs(object, ...) ## S3 method for class 'tobs_multiarm_fit' coef(object, arm = NULL, ...) ## S3 method for class 'tobs_multiarm_fit' vcov(object, ...) ## S3 method for class 'tobs_multiarm_fit' confint(object, parm, level = 0.95, ...) ## S3 method for class 'tobs_multiarm_fit' logLik(object, ...) ## S3 method for class 'tobs_multiarm_fit' glance(x, ...) ## S3 method for class 'tobs_multiarm_fit' tidy(x, conf.level = 0.95, ...) ## S3 method for class 'tobs_multiarm_fit' summary(object, level = 0.95, ...) ## S3 method for class 'tobs_multiarm_fit' plot(x, ...) ## S3 method for class 'tobs_multiarm_fit' fitted(object, ...) ## S3 method for class 'tobs_multiarm_fit' residuals(object, ...)
object, x
|
A |
... |
Ignored, or forwarded to |
arm |
Optional arm name ( |
parm |
Ignored (present for |
level |
Confidence level for |
conf.level |
Interval level for |
The layout every tobs_fit returns: nobs() an integer; coef() a
named vector (presence_(Intercept), positive_x, class_2:x, ...);
vcov() the block-diagonal covariance and confint() a two-column matrix,
both named as coef(); logLik() a logLik object; tidy() the
arm / term / estimate / std.error / conf.low / conf.high table;
summary() a summary.tulpa_fit object whose coef() is the estimate /
std.error / interval table, rows named as coef();
glance() the one-row column set of glance.tobs_fit(); plot() one
Gaussian-density panel per coefficient (up to 4), invisibly.
fit <- tobs(cover ~ moisture, data = meadow_cover, family = cover(response = "beta")) coef(fit, arm = "positive") confint(fit) tidy(fit) summary(fit)fit <- tobs(cover ~ moisture, data = meadow_cover, family = cover(response = "beta")) coef(fit, arm = "positive") confint(fit) tidy(fit) summary(fit)
Generates state-process predictions at new coordinates, including the fitted spatial field interpolated to those locations by inverse-distance weighting over its five nearest nodes. The returned scale follows the family's state-process link: occupancy probability for occupancy families, abundance intensity (lambda) for the count families.
tobs_predict_spatial( object, newcoords, newocc.covs = NULL, quantiles = c(0.025, 0.5, 0.975), node.coords = NULL )tobs_predict_spatial( object, newcoords, newocc.covs = NULL, quantiles = c(0.025, 0.5, 0.975), node.coords = NULL )
object |
A |
newcoords |
Matrix of new coordinates (n_new x 2). |
newocc.covs |
Optional data.frame of state-process covariates at the new
locations, one row per row of |
quantiles |
Quantile levels for credible intervals (default 0.025, 0.5,
0.975). Strictly increasing, each in (0, 1); one column is reported per
level, named from that level ( |
node.coords |
Optional matrix of coordinates for the fitted field's nodes (n_nodes x 2), required for an areal field and ignored when the term already carries its own coordinates. |
The field is taken from the sampled field columns of object$draws when the
fit has them (the NUTS paths), so the reported sd and quantiles carry the
field's own posterior variance. On the deterministic backends
(laplace / nested_laplace) the draws hold only the coefficients, so the
posterior-mean surface in object$spatial_field is used instead: it enters
every draw as the same offset, and the reported spread is then the
coefficients' alone.
A continuous term (gp(), spde(), svc()) carries the coordinates of its
own nodes, so nothing extra is needed. An areal term (icar(), bym2(),
car()) has graph nodes and no geometry, so interpolating it to a new point
is only defined once the nodes are placed: pass node.coords, one row per
element of object$spatial_field. Without it the call is an error rather
than a prediction that quietly drops the field.
newocc.covs is expanded through the fit's own state-process formula and its
columns are then keyed to the fitted coefficient names, so the frame may hold
its covariates in any order and may carry factors, interactions and
transforms. It needs one row per row of newcoords.
newocc.covs = NULL predicts at the fitted covariate MEANS, which the
intercept-plus-zeros design expresses only where the state covariates were
centred. A fit whose state design is not centred is an error rather than a
prediction at covariate values the caller did not choose.
A data.frame with mean, sd, and one column per requested quantile
level (on the response scale: occupancy probability or abundance
intensity), carrying the levels used in attr(, "quantiles").
sim <- simulate_occu(N = 30, J = 3, n.occ.covs = 1, seed = 1) d <- transform(sim$data, cell = seq_len(30)) adj <- matrix(0L, 30, 30) adj[cbind(1:29, 2:30)] <- 1L adj[cbind(2:30, 1:29)] <- 1L fit <- tobs(~ occ_cov1 + icar(graph = adj, group.var = "cell"), data = d, family = occu(), detection = ~ 1, y = sim$y, method = "nested_laplace", control = list(verbose = FALSE, progress = FALSE)) # the chain graph's nodes placed at 1, ..., 30 on a line tobs_predict_spatial(fit, newcoords = cbind(c(5.5, 20.5), 0), newocc.covs = data.frame(occ_cov1 = c(0, 1)), node.coords = cbind(1:30, 0))sim <- simulate_occu(N = 30, J = 3, n.occ.covs = 1, seed = 1) d <- transform(sim$data, cell = seq_len(30)) adj <- matrix(0L, 30, 30) adj[cbind(1:29, 2:30)] <- 1L adj[cbind(2:30, 1:29)] <- 1L fit <- tobs(~ occ_cov1 + icar(graph = adj, group.var = "cell"), data = d, family = occu(), detection = ~ 1, y = sim$y, method = "nested_laplace", control = list(verbose = FALSE, progress = FALSE)) # the chain graph's nodes placed at 1, ..., 30 on a line tobs_predict_spatial(fit, newcoords = cbind(c(5.5, 20.5), 0), newocc.covs = data.frame(occ_cov1 = c(0, 1)), node.coords = cbind(1:30, 0))
Per-site expected species richness sum_s psi_{s,i} with a posterior
credible interval propagated from the community-mean occupancy draws. For the
dynamic community family this is the first-season richness (from psi1).
tobs_richness(object)tobs_richness(object)
object |
A |
A data.frame with site-level richness estimates.
sim <- simulate_ms_occu(N = 40, J = 3, n.species = 5, seed = 1) fit <- tobs(~ x, data = sim$data, family = ms_occu(), detection = ~ 1, y = sim$y, species = paste0("sp", 1:5), control = list(verbose = FALSE)) head(tobs_richness(fit))sim <- simulate_ms_occu(N = 40, J = 3, n.species = 5, seed = 1) fit <- tobs(~ x, data = sim$data, family = ms_occu(), detection = ~ 1, y = sim$y, species = paste0("sp", 1:5), control = list(verbose = FALSE)) head(tobs_richness(fit))
Combines two or more fitted models into one predictive distribution with
leave-one-out predictive stacking. Each member contributes its
Pareto-smoothed importance-sampling LOO predictive density; the weights
maximize the stacked leave-one-out predictive density and so favour members
that predict held-out observations best. Works across every family
(single-season / community / dynamic / integrated occupancy, JSDM, and the
cover hurdle), each scored by the marginal likelihood in
.tobs_pointwise_loglik().
tobs_stack(..., method = c("stacking", "pseudobma")) ## S3 method for class 'tobs_stack' print(x, ...) ## S3 method for class 'tobs_stack' fitted(object, ...) ## S3 method for class 'tobs_stack' predict( object, X.0 = NULL, quantiles = c(0.025, 0.5, 0.975), n.draws = 4000L, ... )tobs_stack(..., method = c("stacking", "pseudobma")) ## S3 method for class 'tobs_stack' print(x, ...) ## S3 method for class 'tobs_stack' fitted(object, ...) ## S3 method for class 'tobs_stack' predict( object, X.0 = NULL, quantiles = c(0.025, 0.5, 0.975), n.draws = 4000L, ... )
... |
ignored. |
method |
weighting scheme passed to |
x |
a |
object |
a |
X.0 |
optional occupancy design matrix for out-of-sample prediction. When supplied, all members must share the same occupancy design. |
quantiles |
credible-interval quantile levels. |
n.draws |
size of the pooled stacked-predictive sample (design-matrix mode); each member contributes a share proportional to its weight. |
The members must be fit to the same observation set. They may otherwise
differ freely – different covariates, priors, or inference methods. (An
ensemble of pure seed-variants, e.g. from tobs(..., control = list(n.seeds = K)), is a valid but degenerate case: the members are statistically
identical, so the weights come out roughly uniform.)
An object of class tobs_stack: a list with weights (named
numeric, summing to 1), fits (the members), loo (per-member
loo::loo() objects), comparison (a data frame of elpd_loo, weight,
and worst Pareto-k per member), and method. stats::predict() / stats::fitted() on
the result return the weight-combined predictive.
sim <- simulate_occu(N = 100, J = 3, n.occ.covs = 2, n.det.covs = 1, seed = 1) ctrl <- list(verbose = FALSE, progress = FALSE) f1 <- tobs(~ occ_cov1, data = sim$data, family = occu(), detection = ~ det_cov1, y = sim$y, method = "laplace", control = ctrl) f2 <- tobs(~ occ_cov1 + occ_cov2, data = sim$data, family = occu(), detection = ~ det_cov1, y = sim$y, method = "laplace", control = ctrl) ens <- tobs_stack(simple = f1, full = f2) ens$weights # weight-combined in-sample psi / p / z str(predict(ens))sim <- simulate_occu(N = 100, J = 3, n.occ.covs = 2, n.det.covs = 1, seed = 1) ctrl <- list(verbose = FALSE, progress = FALSE) f1 <- tobs(~ occ_cov1, data = sim$data, family = occu(), detection = ~ det_cov1, y = sim$y, method = "laplace", control = ctrl) f2 <- tobs(~ occ_cov1 + occ_cov2, data = sim$data, family = occu(), detection = ~ det_cov1, y = sim$y, method = "laplace", control = ctrl) ens <- tobs_stack(simple = f1, full = f2) ens$weights # weight-combined in-sample psi / p / z str(predict(ens))
Spatial fields, random effects, temporal structure, varying coefficients and
latent factors are written inside the model formula, in the process they
belong to, rather than passed as tobs() arguments. A term entered in
the state formula acts on occupancy or abundance; the same term written in
detection = ~ ... acts on detection.
These are formula specials, not exported functions: they are resolved
against an internal registry while the formula is parsed, and calling them
outside a tobs() formula is an error. This page documents the call
shapes and their arguments.
icar(graph), bym2(graph, scale.factor), car(graph) and
car_proper(graph) put a field over the nodes of a symmetric adjacency
matrix graph. bym2() computes its Riebler scale.factor from the
graph when it is not supplied. All four accept:
group.varColumn naming each observation's graph node, when the graph is over regions rather than rows.
weightPer-node numeric column turning the field into a
spatially varying coefficient (weight_i * z[node_i]) instead of an
intercept field. car() does not take it.
idName for the realization, so share("id") can share it
with another process.
The umbrella form spatial(~ 1 + w || cell, graph = adj, model = "icar")
forwards to the same terms and additionally reads a bar, which is how areal
varying coefficients are usually written (see below).
An intrinsic areal field (icar, bym2, car) has one
constant null direction per CONNECTED COMPONENT of its graph, and each of
them is identified separately: the engine augments the precision with one
sum-to-zero per component (INLA's adjust.for.con.comp = TRUE), and the
components share one field variance. A graph with islands or with separate
survey regions is therefore fitted as intended. Because the component count
is also what an adjacency looks like when a spatial join has quietly dropped
edges, and because it is not visible in the fitted object, a graph with more
than one component reports its component count and sizes at fit time.
A component of a single node has no neighbours to borrow from. The areal
cover paths reject one outright, with an error naming the node; where such a
node is accepted it keeps a proper N(0, 1/tau) effect, because the
identification augments the precision rather than imposing a hard constraint,
which on a lone node would pin it to exactly zero.
gp(lon, lat, ...) is an NNGP-approximated Gaussian process over
coordinates, multiscale_gp(lon, lat, ...) a two-scale (local plus
regional) version, and spde(lon, lat, ...) a Matern field over a
triangular mesh. Coordinates come either from bare column names or from
coords = , a two-column matrix.
covCovariance function: "exponential" (default),
"matern", "gaussian", "spherical".
nuMatern smoothness.
nnNumber of nearest neighbours in the Vecchia
approximation, capped at n - 1. multiscale_gp() splits this
into nn.local and nn.regional.
prior.rangeLength-2 c(r0, alpha) PC prior on the
range, read as P(range < r0) = alpha. Required by gp() and
svc(), which ship no default; spde() defaults to
c(0.5, 0.5) alongside prior.sigma.
mesh, max.edge, cutoff
spde() mesh
controls, passed to tulpa::spatial_spde().
A coefficient that varies over space is a spatial field like any other, so it
is written under spatial() and the model = choice says whether
space is a graph or a set of coordinates. In both forms you name the
coefficients that vary; the difference is only what they vary over.
Areal, over graph nodes, written as a bar:
~ 1 + w + spatial(~ 1 + w || cell, graph = adj)
with method = "nested_laplace". The bar's left-hand side names the
varying coefficients (one field per column, the intercept included), its
right-hand side the graph node. Broad family support (single-season, dynamic,
integrated and the community routes) and recovery-tested throughout. This is
what most spatially-varying-coefficient questions want. model =
selects the areal structure ("icar" by default, or "bym2" /
"car" / "car_proper").
Continuous, over coordinates, as NNGP surfaces:
~ elevation + spatial(lon, lat, model = "svc",
coefficients = "elevation", prior.range = c(50, 0.05))
There is no node index to group on, so the coefficients are named directly instead of through a bar.
coefficientsNames of the design columns whose coefficients
vary, matched against the design of the arm the surfaces load on. One
surface per entry; "(Intercept)" gives a plain intercept surface.
indicesThe same selection by column position, for a design
column with no usable name. Give coefficients or indices,
not both.
coordsTwo-column coordinate matrix, as an alternative to
bare lon, lat column names. One coordinate pair per site.
cov"exponential" (default), "matern" or
"gaussian".
nnNearest neighbours in the Vecchia approximation, default
15, capped at n - 1.
prior.rangeRequired, no default: c(r0, alpha) with
P(range < r0) = alpha.
sigma2.prior.scaleScale of the prior on the surface's marginal variance.
svc(lon, lat, coefficients = ) is the direct constructor and stays
available, the way icar() does alongside
spatial(model = "icar").
The continuous form fits on single-season occu() under
"laplace", "nested_laplace" and "nuts", and on
removal(), distance(), fp_occu()
and dyn_abun() under "laplace" and
"nested_laplace". The surfaces load on the state arm (the occupancy
logit, or log lambda for the count families) and come back as
fit$svc_field, with the integrated hyperparameters in
fit$svc_hyper and the resolved design columns in
fit$svc_indices. On any other family, method or arm the term errors and
points at the areal bar rather than being silently dropped.
re(group, ...) is a grouped random effect, and lme4 bars are
sugar over it: (1 | g), (x | g), (x || g),
(0 + x | g), (1 | g:h) and (1 | g/h) are rewritten to
re() calls before the formula is evaluated.
type"intercept" (default), "slope" or
"iid".
covariateSlope column(s): a column name, several names, a
cbind() matrix or a one-sided formula. Required for
type = "slope".
modelStructure over the group levels: "iid"
(default), "ar1", "rw1", "rw2".
correlatedWhether slopes share a correlated covariance with the group intercept.
interceptFALSE drops the implicit group intercept,
giving a slope-only block.
temporal(time, ...) puts a field over the levels of time.
type"ar1" (default), "rw1", "rw2" or
"iid".
groupOptional grouping, for one series per group.
cyclicWraps the last level onto the first.
tau.shape, tau.rate
Gamma prior on the field precision.
latent(n.factors) adds per-site latent factors with per-species
loadings to a community model, giving residual co-occurrence.
constraint and sigma.prior.rate control the identification
anchor and the loading prior.
share("id") shares one realization of the term named id across
processes, optionally scaled: alpha = 0.5 pins the coupling
amplitude, alpha = grid(c(0.25, 0.5, 1)) marginalizes over it on the
outer integration.
residual = carries an arm-specific deviation beside the copy, so the
sharing arm's surface is alpha * w + delta rather than
alpha * w alone. Use it where the two arms respond to related but not
identical surfaces: a plain copy has no way to bend, so a pattern the shared
field does not have is not fitted at all. residual = "full" gives the
deviation one latent per field node, spanning the same surfaces as a field
placed on the arm itself. residual = r gives it r basis
functions on a basis orthogonalized against the shared field, so the latent
problem is K + r rather than 2K, and the deviation cannot
reproduce the copied component. Because the two halves are orthogonal, the
reported amplitude is identified: it is the projection of the fitted arm
surface onto the occurrence surface, taken on the linear-predictor scale. A
rank-r deviation is pinned at the shared field's own SD, which costs
one warm fit of the model without it. Available on occu_cover() under
method = "nested_laplace", one per fit, and it needs a shared field to
deviate from.
grid() states the axis's NODES, and the span the fit works over is
wider than the nodes themselves, for two separate reasons.
First, each node represents the cell around it, so the axis runs half a node
step past the outermost node at each end, in the axis's own log coordinate.
That is what makes the quadrature weights sum over a whole axis rather than
stopping on the last node, and under method = "nuts" the sampled
amplitude takes the same span as the support of its flat prior, so one stated
grid moves both backends over one region. For k equally log-spaced
nodes this is exact: k - 1 steps between the nodes plus a half step at
each end, so the span is k / (k - 1) times the stated range in the log
coordinate. It is therefore largest for the shortest grid – 2x at two nodes,
1.125x at nine. With refinement off, c(0.2, 0.5) gives
[0.127, 0.791].
Second, refinement. control$adaptive.grid is TRUE by default,
and what it does depends on who placed the axis. An axis YOU stated is
DENSIFIED: further nodes are placed inside the range you wrote, never past
its ends, so the nodes you give are a bound and the half step above is the
whole of the widening. An axis the fit placed for you – the default copy
axis, a field SD axis – is EXTENDED when a boundary cell still holds weight
(adaptive.grid.edge.thresh, default 0.02), which is what lets a fit
whose default grid misses the mode go and find it.
Refining an axis changes how densely its nodes sit, not the region they
cover: the reported span stays the declared span unless a refinement point
lands past it. A stated c(0.2, 0.5) is only densified, so it spans
[0.127, 0.791] with refinement on as with it off. Holding an axis at
exactly the nodes stated takes BOTH control$adaptive.grid = FALSE and
control$var.of.means.consistency = FALSE: refinement reaches an axis
from two independent passes, and the consistency pass runs whatever the
adaptive grid is set to, so a fit that turns off only the first still
integrates nodes placed inside the range it wrote.
Under method = "nuts" a fit reports the span each sampled
hyperparameter was worked over in fit$nuts$hyper_support, which is
the region that hyperparameter's flat prior is supported on.
tobs for the model front door.
# `occu()` takes the detection history as `y` and a ONE-sided state formula: # a two-sided formula would be resolved against a response the family does # not carry. `data` is site-level, `visits` visit-level. sim <- simulate_occu(N = 30, J = 3, seed = 1) d <- transform(sim$data, cell = seq_len(30), lon = runif(30), lat = runif(30)) # A chain graph over the 30 cells: neighbours are consecutive indices. adj <- matrix(0L, 30, 30) adj[cbind(1:29, 2:30)] <- 1L adj[cbind(2:30, 1:29)] <- 1L ctrl <- list(verbose = FALSE, progress = FALSE) # Areal field on occupancy fit <- tobs(~ occ_cov1 + icar(graph = adj, group.var = "cell"), data = d, family = occu(), detection = ~ 1, y = sim$y, method = "nested_laplace", control = ctrl) fit$spatial_field[1:5] # Detection random effect by observer, who changes between visits obs <- simulate_occu(N = 60, J = 3, n.visit.groups = 4, seed = 1) fit_re <- tobs(~ occ_cov1, data = obs$data, family = occu(), detection = ~ (1 | visit_group), y = obs$y, visits = obs$visits, method = "laplace", control = ctrl) coef(fit_re) # Areal spatially varying coefficient on `occ_cov2` fit_svc <- tobs(~ occ_cov2 + spatial(~ 1 + occ_cov2 || cell, graph = adj), data = d, family = occu(), detection = ~ 1, y = sim$y, method = "nested_laplace", control = ctrl) coef(fit_svc) # Continuous NNGP varying coefficient on the `occ_cov2` slope fit_nngp <- tobs(~ occ_cov2 + spatial(lon, lat, model = "svc", coefficients = "occ_cov2", prior.range = c(0.5, 0.05)), data = d, family = occu(), detection = ~ 1, y = sim$y, method = "laplace", control = ctrl) coef(fit_nngp)# `occu()` takes the detection history as `y` and a ONE-sided state formula: # a two-sided formula would be resolved against a response the family does # not carry. `data` is site-level, `visits` visit-level. sim <- simulate_occu(N = 30, J = 3, seed = 1) d <- transform(sim$data, cell = seq_len(30), lon = runif(30), lat = runif(30)) # A chain graph over the 30 cells: neighbours are consecutive indices. adj <- matrix(0L, 30, 30) adj[cbind(1:29, 2:30)] <- 1L adj[cbind(2:30, 1:29)] <- 1L ctrl <- list(verbose = FALSE, progress = FALSE) # Areal field on occupancy fit <- tobs(~ occ_cov1 + icar(graph = adj, group.var = "cell"), data = d, family = occu(), detection = ~ 1, y = sim$y, method = "nested_laplace", control = ctrl) fit$spatial_field[1:5] # Detection random effect by observer, who changes between visits obs <- simulate_occu(N = 60, J = 3, n.visit.groups = 4, seed = 1) fit_re <- tobs(~ occ_cov1, data = obs$data, family = occu(), detection = ~ (1 | visit_group), y = obs$y, visits = obs$visits, method = "laplace", control = ctrl) coef(fit_re) # Areal spatially varying coefficient on `occ_cov2` fit_svc <- tobs(~ occ_cov2 + spatial(~ 1 + occ_cov2 || cell, graph = adj), data = d, family = occu(), detection = ~ 1, y = sim$y, method = "nested_laplace", control = ctrl) coef(fit_svc) # Continuous NNGP varying coefficient on the `occ_cov2` slope fit_nngp <- tobs(~ occ_cov2 + spatial(lon, lat, model = "svc", coefficients = "occ_cov2", prior.range = c(0.5, 0.05)), data = d, family = occu(), detection = ~ 1, y = sim$y, method = "laplace", control = ctrl) coef(fit_nngp)
Re-enters tobs() with the arguments the fit was originally built from
($tobs_call, recorded by every family), so the refit keeps the fit's
family dispatcher, method and control instead of dropping to a bare
Laplace refit of the occupancy state alone. A name in ... that is one of
tobs()'s own arguments (formula, data, family, occurrence,
detection, positive, y, visits, method, priors, control)
replaces that argument outright; any other name (n.iter, seed,
verbose, ...) is merged into control, matching where the recorded fit's
own value for it lives.
## S3 method for class 'tobs_fit' update(object, ..., evaluate = TRUE)## S3 method for class 'tobs_fit' update(object, ..., evaluate = TRUE)
object |
A |
... |
Named arguments to override; see Details on where each lands. |
evaluate |
If TRUE (default), refit the model. If FALSE, return the
unevaluated |
Updated tobs_fit object (or an unevaluated call if evaluate = FALSE).
sim <- simulate_occu(N = 100, J = 3, n.occ.covs = 1, n.det.covs = 1, seed = 1) fit <- tobs(~ occ_cov1, data = sim$data, family = occu(), detection = ~ det_cov1, y = sim$y, method = "laplace", control = list(verbose = FALSE, progress = FALSE)) fit0 <- update(fit, formula = ~ 1) coef(fit0)sim <- simulate_occu(N = 100, J = 3, n.occ.covs = 1, n.det.covs = 1, seed = 1) fit <- tobs(~ occ_cov1, data = sim$data, family = occu(), detection = ~ det_cov1, y = sim$y, method = "laplace", control = list(verbose = FALSE, progress = FALSE)) fit0 <- update(fit, formula = ~ 1) coef(fit0)
For each variable in vars, adds two new columns to data:
within_between(data, group, vars, suffix = c("_btw", "_wtn"), na.rm = TRUE)within_between(data, group, vars, suffix = c("_btw", "_wtn"), na.rm = TRUE)
data |
A data frame. |
group |
Character name (or character vector of names) identifying the grouping column(s). With multiple names, grouping is by the interaction. |
vars |
Character vector of numeric columns to decompose. Must all
exist in |
suffix |
Length-2 character vector giving the suffixes for the
between- and within-group columns. Default |
na.rm |
Logical; passed to |
<var><suffix[1]> - the per-group mean of var (the between-group
component, constant within each group),
<var><suffix[2]> - var - <var><suffix[1]> (the within-group
deviation, summing to zero within each group).
The decomposition is exact: data[[var]] == data[[btw]] + data[[wtn]].
Including both columns in a regression (~ x_btw + x_wtn) separates the
cross-group association from the within-group association without imposing
that they be the same coefficient (the random-effects assumption).
Typical use in the MOTIVATE cover-resurvey workflow:
plots <- within_between(plots, group = "plot_id", vars = "year")
fit <- tobs(~ year_btw + year_wtn + bym2(graph = adj), data = plots,
family = cover("beta"), y = plots$cover,
method = "nested_laplace")
year_wtn then carries the within-plot temporal trend; year_btw absorbs
across-plot timing heterogeneity (older versus newer baseline plots).
The input data with 2 * length(vars) new columns appended.
Mundlak, Y. (1978). On the pooling of time series and cross section data. Econometrica, 46(1), 69-85.
Bell, A. & Jones, K. (2015). Explaining fixed effects: random effects modeling of time-series cross-sectional and panel data. Political Science Research and Methods, 3(1), 133-153.
set.seed(1) plots <- data.frame( plot_id = rep(1:5, each = 4), year = rep(2000:2003, times = 5) + rep(c(0, 5, 10, 15, 20), each = 4), cover = stats::runif(20) ) decomposed <- within_between(plots, group = "plot_id", vars = "year") head(decomposed)set.seed(1) plots <- data.frame( plot_id = rep(1:5, each = 4), year = rep(2000:2003, times = 5) + rep(c(0, 5, 10, 15, 20), each = 4), cover = stats::runif(20) ) decomposed <- within_between(plots, group = "plot_id", vars = "year") head(decomposed)