% Generated by roxygen2: do not edit by hand
% Please edit documentation in R/method-loo.R
\name{loo}
\alias{loo}
\alias{loo.INLAvaan}
\alias{loo.inlavaan_internal}
\alias{add_loo}
\title{Approximate Leave-One-Out Cross-Validation for INLAvaan Models}
\usage{
loo(x, ...)

\method{loo}{INLAvaan}(
  x,
  type = c("auto", "loso", "loco"),
  units = NULL,
  second_order = TRUE,
  theta = NULL,
  Sigma = NULL,
  cores = NULL,
  verbose = FALSE,
  ...
)

\method{loo}{inlavaan_internal}(
  x,
  type = c("auto", "loso", "loco"),
  units = NULL,
  second_order = TRUE,
  theta = NULL,
  Sigma = NULL,
  cores = NULL,
  verbose = FALSE,
  ...
)

add_loo(object, cores = NULL, verbose = FALSE)
}
\arguments{
\item{x}{A fitted \link{INLAvaan} object (or its \code{inlavaan_internal} list).}

\item{...}{Not used.}

\item{type}{Unit type: \code{"auto"} (default) resolves to \code{"loso"}
(per-subject) for single-level models and \code{"loco"} (per-cluster,
marginal predictive) for two-level models. \code{"loco"} cannot be forced
on a model without clusters; \code{"loso"} on a two-level model scores the
conditional (leave-one-unit-out) predictive instead (with a warning;
see Details).}

\item{units}{Optional integer vector of unit indices to score; defaults
to all units. For LOSO these are case numbers (row numbers of the
analysed dataset, as recorded in the fit -- for multigroup fits the
full results are stacked by group, but a unit is always addressed by
its case number); for LOCO, cluster positions.}

\item{second_order}{Logical; compute the second-order correction
(default \code{TRUE}). \code{FALSE} skips the Hessian stage entirely and reports
first-order estimates.}

\item{theta, Sigma}{Optional posterior mean vector and covariance matrix
(in the unconstrained parameter space, as stored in \code{theta_star} and
\code{Sigma_theta}) at which to evaluate the LOO instead of the fit's own
Laplace summary. See Details.}

\item{cores}{Number of cores for the Hessian stage. The default \code{NULL}
runs serially; parallelism must be requested explicitly.}

\item{verbose}{Logical; print progress (default \code{FALSE}).}

\item{object}{A fitted \link{INLAvaan} object.}
}
\value{
An object of class \code{inlavaan_loo}: a list with elements
\describe{
\item{\code{per_unit}}{Data frame of pointwise results: \code{unit} (case
number for LOSO, cluster position for LOCO), \code{group} (multigroup
fits only), \code{nobs} (1 for LOSO, the cluster size for LOCO),
\code{l_star} (unit log-likelihood at the summary), \code{score_norm},
\code{lpd_1}/\code{lpd_2} (pointwise log predictive density),
\code{log_cpo_1}/\code{log_cpo_2} (pointwise LOO contributions), \code{det_term},
and \code{ok} (second-order success flag).}
\item{\code{estimates}}{Matrix with rows \code{elpd_loo}, \code{p_loo}, \code{looic} and
columns \code{Estimate}, \code{SE} (headline second-order values).}
\item{\code{elpd_1}, \code{elpd_2}, \code{se_1}, \code{se_2}, \code{p_loo_1}, \code{p_loo_2}}{
First- and second-order aggregates.}
\item{\code{type}, \code{flavour}, \code{n_units}, \code{n_groups}, \code{n_ok},
\code{second_order}, \code{theta_overridden}}{Metadata; \code{flavour} records
whether units were scored jointly with their covariates
(\code{"joint"}) or conditionally on them (\code{"conditional"}, for
\code{fixed.x} fits).}
}

\code{add_loo()} returns a copy of \code{object} with the LOO result
stored alongside the fit (the input object is unchanged); reassign it,
e.g. \code{fit <- add_loo(fit)}. Only the default LOO is stored, so the
stored result always matches \code{loo(fit)}.
}
\description{
Computes leave-one-out (LOO) cross-validation for a fitted \link{INLAvaan}
model from a single fit, with no refitting and no sampling, via a Taylor
approximation of the case-deletion posterior around the Laplace summary.
Single-level models are scored per subject (leave-one-subject-out, LOSO);
two-level models are scored per cluster (leave-one-cluster-out, LOCO).
}
\details{
For a unit \eqn{u} (a subject for LOSO, a cluster for LOCO) with
log-likelihood contribution \eqn{\ell_u(\theta)}, score \eqn{s_u} and
Hessian \eqn{H_u} evaluated at the posterior summary
\eqn{(\theta^*, \Sigma)}, the log conditional predictive ordinate is
approximated to first and second order by
\deqn{\log \mathrm{CPO}_u^{(1)} = \ell_u - \tfrac{1}{2} s_u' \Sigma s_u,}
\deqn{\log \mathrm{CPO}_u^{(2)} = \ell_u
  - \tfrac{1}{2} s_u' (\Sigma^{-1} + H_u)^{-1} s_u
  + \tfrac{1}{2} \log |I + \Sigma H_u|.}
The reported \code{elpd_loo} is the sum of the second-order terms (first-order
when \code{second_order = FALSE}), with standard error
\eqn{\sqrt{n \, \mathrm{var}(\log \mathrm{CPO}_u)}} and
\code{looic} \eqn{= -2 \, \mathrm{elpd}}. The effective number of parameters is
\eqn{p_{\mathrm{loo}} = \sum_u (\mathrm{lpd}_u - \log \mathrm{CPO}_u)},
where \eqn{\mathrm{lpd}_u} is the analogous Taylor approximation of the
full-posterior pointwise log predictive density. Units whose second-order
curvature matrix is not positive definite fall back to first order for
that unit only (flagged in \code{per_unit$ok}).

The type is resolved automatically: per-cluster (\code{"loco"}) when the model
was fitted with a \code{cluster} argument, per-subject (\code{"loso"}) otherwise.
For a two-level model these are the two estimands of Merkle, Furr &
Rabe-Hesketh (2019): the default per-cluster \code{"loco"} is the \emph{marginal}
predictive (leave-one-cluster-out -- prediction for a \emph{new} cluster),
while \code{type = "loso"} forces the \emph{conditional} predictive
(leave-one-unit-out -- prediction for a new observation within an
\emph{observed} cluster), where row \eqn{i} of cluster \eqn{j} contributes
\eqn{\ell_i = \ell_j(\mathrm{full}) - \ell_j(\mathrm{minus\ row\ } i)},
the conditional density of the row given the rest of its cluster. The two
answer different questions and are easily conflated, so the per-cluster
marginal is the default and \code{type = "loso"} warns. It works with and
without missing data, costs one cluster evaluation per row per Hessian
direction, and is best subset with \code{units}.

\strong{Multigroup models.} Groups are independent, so each unit is scored
against its own group's implied moments; without a mean structure the
exchangeability transformation applies per group, and cross-group
equality constraints (\code{group.equal}) flow through the packed parameter
space automatically. The per-unit results are stacked by group (a
\code{group} column records the membership), and units are identified by
\emph{case number} -- the row number of the analysed dataset -- so a unit
keeps its identity across fits that assign or order groups differently
(e.g. a pooled fit versus a grouped fit of the same data, which
\code{\link[=compare]{compare()}} pairs unit by unit). This makes
\code{compare(..., loo = TRUE)} the instrument of choice for the
measurement-invariance ladder: configural, metric, and scalar fits are
compared on a proper predictive scale with paired standard errors.

Supplying \code{theta} and/or \code{Sigma} scores the model at an \emph{arbitrary}
Gaussian posterior summary instead of the fit's own, without refitting.
This is the building block for refit-free model exploration: for example,
conditioning the encompassing model's summary on a parameter being zero
(a rank-one update of \code{theta} and \code{Sigma}) and scoring the result gives
the LOO of that submodel from a single fit. INLAvaan provides only this
evaluation API; search strategies are left to the user. A conditioned
\code{Sigma} may be singular; the computation automatically restricts to the
non-degenerate block, which is exact.

Parallelism is strictly opt-in: the default \code{cores = NULL} runs serially,
and \code{cores > 1} parallelises the Hessian stage via forking (not available
on Windows).

Calling \code{loo()} never modifies the fitted object. Under the default
\code{test = "standard"}, \code{\link[=inlavaan]{inlavaan()}} already computes and stores the full
LOO at fit time whenever the model is supported, has a mean structure,
and the predicted serial cost is within a 10-second budget (measured by
timing one score evaluation); \code{test = "loo"} forces the computation
regardless of the budget, and \code{fit <- add_loo(fit)} stores it post hoc.
A stored result is returned directly by \code{loo(fit)} when called with
default arguments, and is reused by \code{\link[=fitmeasures]{fitmeasures()}} and \code{\link[=compare]{compare()}}
without recomputation.

\strong{Exogenous covariates.} The flavour of the score follows the fitted
likelihood. Under \code{fixed.x = FALSE} the covariates are modelled jointly
and each unit is scored by the joint predictive density of its outcomes
\emph{and} covariates (\code{flavour = "joint"}). Under \code{fixed.x = TRUE} (the
lavaan default) the fitted likelihood is the conditional one, and each
unit is scored by the predictive density of its outcomes \emph{given} its
covariates (\code{flavour = "conditional"}); since the conditional likelihood
is exactly invariant to the frozen covariate moments, this involves no
additional approximation. The two flavours estimate different quantities
whose scales differ by the covariate predictive density, so a joint and a
conditional elpd must never be compared (\code{\link[=compare]{compare()}} refuses
mixed-flavour comparisons). Conditional scores of models conditioning on
\emph{different} covariate sets are comparable provided the outcome variables
match -- the natural setting for covariate selection. Both flavours
support any covariate placement: single-level covariates, and
cluster-level (between) and/or within-level covariates in two-level
models.

\strong{Missing data.} Fits estimated by full-information maximum likelihood
(\code{missing = "ml"}) are scored on the \emph{observed-data} predictive: each
unit contributes the density of the entries it actually has, with its
full row (single-level) or whole cluster (two-level) removed from the
conditioning set. For single-level fits the casewise kernels operate on
each unit's observed subset, grouping rows by missing pattern, so a unit
with fewer observed entries contributes a smaller log-likelihood term
\emph{and} a smaller score and thus self-weights in the elpd. Two-level fits
are scored per cluster (\code{"loco"}): each cluster contributes its
observed-data marginal likelihood, evaluated by lavaan's raw-data
cluster kernels (no per-cluster sufficient statistics are needed, since
leave-one-cluster-out deletes the whole cluster). All carry the same
missing-at-random assumption as the FIML fit itself. Because the score
is the observed-entry predictive, a \code{\link[=compare]{compare()}} of two missing-data fits
is meaningful only when they share the same observed entries (the same
data \emph{and} the same holes). The two-level conditional predictive
(\code{type = "loso"}) is available under missing data too, on the same
kernels.

Supported models: continuous-indicator models fitted with the \code{ML}
estimator (including FIML, \code{missing = "ml"}, single- and two-level),
single-group or multigroup (multigroup two-level models are not
supported yet). If the \code{loo} package is attached it masks this generic,
but \code{loo(fit)} continues to dispatch correctly because the method is
registered by generic name.
}
\examples{
\donttest{
HS.model <- "
  visual  =~ x1 + x2 + x3
  textual =~ x4 + x5 + x6
  speed   =~ x7 + x8 + x9
"
utils::data("HolzingerSwineford1939", package = "lavaan")
fit <- acfa(HS.model, HolzingerSwineford1939, meanstructure = TRUE)

# Leave-one-subject-out (LOSO) from the single fit -- no refitting
res <- loo(fit)
res
head(res$per_unit)

# Score a submodel without refitting: condition the Laplace summary on the
# visual ~~ speed covariance being zero, then evaluate at that summary
int <- get_inlavaan_internal(fit)
theta <- int$theta_star
Sigma <- int$Sigma_theta
p <- which(names(coef(fit)) == "visual~~speed")
theta_c <- theta - Sigma[, p] * (theta[p] / Sigma[p, p])
Sigma_c <- Sigma - tcrossprod(Sigma[, p]) / Sigma[p, p]
loo(fit, theta = theta_c, Sigma = Sigma_c)

# Two-level models are scored per cluster (LOCO) automatically
utils::data("Demo.twolevel", package = "lavaan")
model2l <- "
  level: 1
    fw =~ y1 + y2 + y3
    fw ~ x1 + x2 + x3
  level: 2
    fb =~ y1 + y2 + y3
    fb ~ w1 + w2
"
fit2l <- asem(model2l, Demo.twolevel, cluster = "cluster",
              meanstructure = TRUE, fixed.x = FALSE)
loo(fit2l)
}
}
\seealso{
\code{\link[=fitmeasures]{fitmeasures()}}, \code{\link[=compare]{compare()}}, \code{\link[=inlavaan]{inlavaan()}}
}
