Nothing
#' Batch-Means Covariance Estimator for SGD Trajectories
#'
#' Estimate the asymptotic covariance from a single SGD trajectory using the
#' increasing-batch construction from Xi et al. (2020).
#'
#' @param trajectory numeric matrix with rows = iterations and columns = parameters.
#' @param alpha stepsize decay exponent in
#' \eqn{\eta_i = \eta i^{-\alpha}}. Must satisfy \eqn{1/2 < \alpha < 1}.
#' @param M number of retained batches (excluding burn-in batch 0). If `NULL`,
#' use \eqn{\lfloor n^{(1-\alpha)/2} \rfloor}.
#' @param N decorrelation constant in
#' \eqn{e_k = \lfloor ((k+1)N)^{1/(1-\alpha)} \rfloor}. If `NULL`, use
#' \eqn{N = n^{1-\alpha}/(M+1)}.
#' @param drop_burnin logical; if `TRUE`, discard batch 0.
#' @param burnin_iter non-negative integer. Explicitly discard the first
#' `burnin_iter` iterations before building Xi-style batches. After trimming,
#' batch boundaries are rebuilt from iteration 1 of the retained trajectory.
#'
#' @return A list containing the covariance estimate, pooled mean, batch sizes,
#' batch boundaries, and chain-level metadata.
#' @export
batch_means_estimator <- function(
trajectory,
alpha = 0.501,
M = NULL,
N = NULL,
drop_burnin = TRUE,
burnin_iter = 0) {
stopifnot(
is.matrix(trajectory) || is.data.frame(trajectory),
is.numeric(alpha),
length(alpha) == 1,
alpha > 0.5,
alpha < 1,
is.logical(drop_burnin),
length(drop_burnin) == 1,
is.numeric(burnin_iter),
length(burnin_iter) == 1,
is.finite(burnin_iter),
burnin_iter >= 0,
abs(burnin_iter - round(burnin_iter)) < sqrt(.Machine$double.eps)
)
trajectory <- as.matrix(trajectory)
storage.mode(trajectory) <- "double"
burnin_iter <- as.integer(round(burnin_iter))
n_iter_total <- nrow(trajectory)
n_param <- ncol(trajectory)
stopifnot(
n_iter_total >= 2,
n_param >= 1
)
if (burnin_iter > 0) {
stopifnot(
"burnin_iter is too large; at least 2 iterations must remain after trimming." =
n_iter_total - burnin_iter >= 2
)
trajectory <- trajectory[seq.int(burnin_iter + 1L, n_iter_total), , drop = FALSE]
}
n_iter <- nrow(trajectory)
boundary <- .batch_means_boundaries(
n_iter = n_iter,
alpha = alpha,
M = M,
N = N
)
keep_idx <- if (drop_burnin) {
seq.int(2L, length(boundary$end))
} else {
seq_along(boundary$end)
}
stopifnot(
"No valid batch remains after burn-in removal; increase iterations or reduce M." =
length(keep_idx) > 0
)
if (length(keep_idx) < 2) {
warning(
"Only one batch is retained; covariance estimate may be unstable.",
call. = FALSE
)
}
batch_means <- t(vapply(keep_idx, function(k) {
idx <- boundary$start[k]:boundary$end[k]
colMeans(trajectory[idx, , drop = FALSE])
}, numeric(n_param)))
batch_sizes <- boundary$size[keep_idx]
global_mean <- colSums(batch_means * batch_sizes) / sum(batch_sizes)
centered <- sweep(batch_means, 2, global_mean, "-")
covariance <- matrix(0, n_param, n_param)
for (i in seq_len(nrow(centered))) {
covariance <- covariance + batch_sizes[i] * tcrossprod(centered[i, ])
}
covariance <- covariance / length(keep_idx)
param_names <- colnames(trajectory)
if (is.null(param_names)) {
param_names <- paste0("param_", seq_len(n_param))
}
names(global_mean) <- param_names
dimnames(covariance) <- list(param_names, param_names)
ret <- list(
mean = global_mean,
covariance = covariance,
n_eff = sum(batch_sizes),
batch_sizes = batch_sizes,
batch_start = boundary$start[keep_idx],
batch_end = boundary$end[keep_idx],
batch_means = batch_means,
alpha = alpha,
M = length(keep_idx),
N = boundary$N,
dropped_burnin = drop_burnin,
burnin_iter = burnin_iter,
n_iterations = n_iter,
n_iterations_total = n_iter_total
)
class(ret) <- "ngme_batch_means"
ret
}
#' Pooled Batch-Means Confidence Intervals from Multiple Chains
#'
#' Compute pooled point estimates, covariance, and Wald confidence intervals from
#' multiple chain trajectories.
#'
#' @param chain_trajectories list of numeric matrices, each with rows = iterations
#' and columns = parameters.
#' @param level confidence level.
#' @param alpha stepsize decay exponent in \eqn{(1/2, 1)}.
#' @param M number of retained batches per chain (excluding burn-in batch 0).
#' @param N decorrelation constant used in the batch boundary formula.
#' @param drop_burnin logical; if `TRUE`, discard batch 0.
#' @param burnin_iter non-negative integer. Explicitly discard the first
#' `burnin_iter` iterations of each chain before Xi-style batching.
#'
#' @return A list with pooled estimates, covariance, standard errors, and
#' confidence intervals.
#' @export
batch_means_ci <- function(
chain_trajectories,
level = 0.95,
alpha = 0.501,
M = NULL,
N = NULL,
drop_burnin = TRUE,
burnin_iter = 0) {
if (is.matrix(chain_trajectories) || is.data.frame(chain_trajectories)) {
chain_trajectories <- list(as.matrix(chain_trajectories))
}
stopifnot(
is.list(chain_trajectories),
length(chain_trajectories) >= 1,
is.numeric(level),
length(level) == 1,
level > 0,
level < 1
)
chain_trajectories <- lapply(chain_trajectories, function(x) {
stopifnot(is.matrix(x) || is.data.frame(x))
x <- as.matrix(x)
storage.mode(x) <- "double"
x
})
n_param <- unique(vapply(chain_trajectories, ncol, integer(1)))
stopifnot(
"All chains must have the same number of parameters." = length(n_param) == 1
)
n_iter_vec <- vapply(chain_trajectories, nrow, integer(1))
if (length(unique(n_iter_vec)) > 1) {
min_iter <- min(n_iter_vec)
warning(
"Chains have different lengths; truncating all chains to the minimum length.",
call. = FALSE
)
chain_trajectories <- lapply(chain_trajectories, function(x) {
x[seq_len(min_iter), , drop = FALSE]
})
}
param_names <- colnames(chain_trajectories[[1]])
if (is.null(param_names)) {
param_names <- paste0("param_", seq_len(n_param))
}
chain_results <- lapply(chain_trajectories, function(x) {
if (is.null(colnames(x))) {
colnames(x) <- param_names
}
batch_means_estimator(
trajectory = x,
alpha = alpha,
M = M,
N = N,
drop_burnin = drop_burnin,
burnin_iter = burnin_iter
)
})
n_chain <- length(chain_results)
chain_means <- do.call(rbind, lapply(chain_results, `[[`, "mean"))
pooled_mean <- colMeans(chain_means)
pooled_cov <- Reduce(`+`, lapply(chain_results, `[[`, "covariance")) / n_chain
dimnames(pooled_cov) <- list(param_names, param_names)
n_eff_vec <- vapply(chain_results, `[[`, numeric(1), "n_eff")
n_eff <- min(n_eff_vec)
if (length(unique(n_eff_vec)) > 1) {
warning(
"Effective sample sizes differ across chains; using the minimum for CI width.",
call. = FALSE
)
}
z_value <- stats::qnorm(1 - (1 - level) / 2)
std_error <- sqrt(pmax(diag(pooled_cov), 0) / (n_eff * n_chain))
conf_int <- cbind(
lower = pooled_mean - z_value * std_error,
upper = pooled_mean + z_value * std_error
)
pooled_mean <- stats::setNames(pooled_mean, param_names)
std_error <- stats::setNames(std_error, param_names)
rownames(conf_int) <- param_names
ret <- list(
estimates = pooled_mean,
std_error = std_error,
conf_int = conf_int,
covariance = pooled_cov,
level = level,
z_value = z_value,
alpha = alpha,
M = chain_results[[1]]$M,
N = chain_results[[1]]$N,
burnin_iter = burnin_iter,
n_eff = n_eff,
n_chains = n_chain,
n_iterations = chain_results[[1]]$n_iterations,
n_iterations_total = chain_results[[1]]$n_iterations_total,
chain_results = chain_results
)
class(ret) <- "ngme_batch_ci"
ret
}
#' Batch-Means Confidence Intervals from an ngme Fit
#'
#' Compute Xi-style batch-means confidence intervals directly from trajectories
#' stored in an `ngme` object.
#'
#' @param ngme fitted `ngme` object with `store_traj = TRUE` during optimization.
#' @param name either `"all"` (default) to use all parameters jointly, a latent
#' model name, or `"general"` for measurement-noise/fixed-effect block.
#' @param level confidence level.
#' @param alpha stepsize decay exponent in \eqn{(1/2, 1)}.
#' @param M number of retained batches (excluding burn-in batch 0).
#' @param N decorrelation constant in the batch boundary formula.
#' @param apply_transform logical; if `TRUE`, inference is first computed in the
#' raw optimization space and then mapped to user scale via post-hoc Delta
#' method (Post-delta).
#' @param drop_burnin logical; if `TRUE`, discard batch 0.
#' @param burnin_iter non-negative integer. Explicitly discard the first
#' `burnin_iter` optimization iterations before Xi-style batching.
#'
#' @return Same structure as [batch_means_ci()] with extra metadata fields
#' `name` and `apply_transform`.
#' @export
ngme_batch_ci <- function(
ngme,
name = "all",
level = 0.95,
alpha = 0.501,
M = NULL,
N = NULL,
apply_transform = TRUE,
drop_burnin = TRUE,
burnin_iter = 0) {
stopifnot(
inherits(ngme, "ngme"),
is.character(name),
length(name) == 1,
is.logical(apply_transform),
length(apply_transform) == 1
)
chain_data <- .ngme_chain_trajectories(
ngme = ngme,
name = name,
apply_transform = FALSE
)
raw_ret <- batch_means_ci(
chain_trajectories = chain_data$chains,
level = level,
alpha = alpha,
M = M,
N = N,
drop_burnin = drop_burnin,
burnin_iter = burnin_iter
)
ret <- if (apply_transform) {
.delta_transform_batch_ci(raw_ret, chain_data$transforms)
} else {
raw_ret
}
ret$name <- name
ret$apply_transform <- apply_transform
if (apply_transform) {
ret$raw_estimates <- raw_ret$estimates
ret$raw_covariance <- raw_ret$covariance
ret$raw_std_error <- raw_ret$std_error
ret$raw_conf_int <- raw_ret$conf_int
}
ret
}
#' Refit an Existing ngme Object with SGD and Compute Batch-Means CI
#'
#' Run one additional SGD stage (warm-started from an existing `ngme` fit),
#' then compute Xi-style batch-means confidence intervals from the newly stored
#' trajectories.
#'
#' @param fit existing fitted `ngme` object (can be obtained with any optimizer).
#' @param iterations optimization iterations for the SGD CI stage.
#' @param alpha Xi-style exponent used for both polynomial schedule and
#' batch-means inference.
#' @param optimizer optimizer for the CI stage; must be `sgd(...)` for Xi theory.
#' @param burnin burn-in iterations before SGD optimization.
#' @param n_batch number of optimization checkpoints.
#' @param n_parallel_chain number of parallel chains for SGD.
#' @param t0 non-negative schedule offset in
#' \eqn{\eta_t = \eta_0 (t + t0)^{-\alpha}}.
#' @param start_sd standard deviation for randomized chain initialization.
#' @param seed random seed for CI-stage optimization.
#' @param verbose logical; print optimization progress.
#' @param level confidence level for CI.
#' @param name parameter block for CI (`"all"`, latent name, or `"general"`).
#' @param M number of retained Xi batches (excluding batch 0).
#' @param N decorrelation constant in Xi batch boundaries.
#' @param apply_transform logical; if `TRUE`, apply Post-delta transform after
#' raw-space inference.
#' @param drop_burnin logical; if `TRUE`, discard Xi batch 0.
#' @param burnin_iter non-negative integer used both as optimizer schedule
#' warmup (`stepsize_schedule_burnin_iter`) and as explicit CI trajectory
#' trimming before Xi-style batching.
#' @param control_opt optional pre-built `control_opt` object for the CI stage.
#' If supplied, it is used directly (with `store_traj` forced to `TRUE`).
#' @param ... additional arguments forwarded to [control_opt_batch_ci()] when
#' `control_opt` is `NULL`.
#'
#' @return An object of class `ngme_batch_ci` with extra fields:
#' `refit` (the SGD-refit model), `refit_control_opt`, and `refit_source`.
#' @export
compute_ngme_ci <- function(
fit,
iterations = 4000,
alpha = 0.501,
optimizer = sgd(stepsize = 0.03),
burnin = 100,
n_batch = 20,
n_parallel_chain = 4,
t0 = 1,
start_sd = 0.2,
seed = Sys.time(),
verbose = FALSE,
level = 0.95,
name = "all",
M = NULL,
N = NULL,
apply_transform = TRUE,
drop_burnin = TRUE,
burnin_iter = 0,
control_opt = NULL,
...) {
stopifnot(inherits(fit, "ngme"))
fit_meta <- attr(fit, "fit")
stopifnot(
"Missing fit metadata in `fit`; please pass a fitted ngme object from ngme()." =
!is.null(fit_meta),
"Missing formula in fit metadata." = !is.null(fit_meta$formula),
"Missing data in fit metadata." = !is.null(fit_meta$data)
)
dots <- list(...)
if ("schedule_burnin_iter" %in% names(dots)) {
stop(
"In compute_ngme_ci(), `schedule_burnin_iter` is unified with `burnin_iter`. ",
"Please set only `burnin_iter`.",
call. = FALSE
)
}
if (is.null(control_opt)) {
control_opt <- do.call(
control_opt_batch_ci,
utils::modifyList(
list(
optimizer = optimizer,
burnin = burnin,
iterations = iterations,
n_batch = n_batch,
n_parallel_chain = n_parallel_chain,
alpha = alpha,
t0 = t0,
schedule_burnin_iter = burnin_iter,
start_sd = start_sd,
seed = seed,
verbose = verbose
),
dots
)
)
} else {
stopifnot(
inherits(control_opt, "control_opt"),
"When `control_opt` is supplied, extra `...` arguments are not used." =
length(dots) == 0
)
}
stopifnot(
"compute_ngme_ci currently requires optimizer = sgd(...) for Xi-style guarantees." =
identical(control_opt$sgd_method, "sgd")
)
if (isTRUE(control_opt$trend_std_conv_check) ||
isTRUE(control_opt$R_hat_conv_check) ||
isTRUE(control_opt$pflug_conv_check)) {
warning(
"Convergence diagnostics are enabled in `control_opt`; early stopping may reduce fixed-iteration Xi-style validity.",
call. = FALSE
)
}
control_opt$stepsize_schedule_burnin_iter <- as.integer(round(burnin_iter))
control_opt$store_traj <- TRUE
fit_env <- build_cv_rebuild_env(
formula = fit_meta$formula,
ngme_obj = fit,
data = fit_meta$data
)
control_ngme_refit <- fit$replicates[[1]]$control_ngme
if (is.null(control_ngme_refit)) {
control_ngme_refit <- control_ngme()
}
refit <- do.call(
"ngme",
list(
formula = fit_meta$formula,
data = fit_meta$data,
family = fit_meta$family,
control_opt = control_opt,
control_ngme = control_ngme_refit,
group = fit_meta$group,
replicate = fit_meta$replicate,
start = fit
),
envir = fit_env
)
ci <- ngme_batch_ci(
ngme = refit,
name = name,
level = level,
alpha = alpha,
M = M,
N = N,
apply_transform = apply_transform,
drop_burnin = drop_burnin,
burnin_iter = burnin_iter
)
ci$refit <- refit
ci$refit_control_opt <- control_opt
ci$refit_source <- fit
ci
}
#' @rdname compute_ngme_ci
#' @export
compute_ngme_CI <- compute_ngme_ci
#' Refit an Existing ngme Object with SGLD and Extract Samples
#'
#' Run one additional SGLD stage (warm-started from an existing `ngme` fit),
#' then extract posterior-like samples from stored trajectories.
#'
#' @param fit existing fitted `ngme` object (can be obtained with any optimizer).
#' @param iterations optimization iterations for the SGLD stage.
#' @param optimizer optimizer for the sampling stage; must be `sgld(...)`.
#' @param burnin burn-in iterations before optimization.
#' @param n_batch number of optimization checkpoints.
#' @param n_parallel_chain number of parallel chains.
#' @param alpha polynomial schedule exponent used by `poly_decay(alpha, t0)`.
#' @param t0 non-negative schedule offset.
#' @param start_sd standard deviation for randomized chain initialization.
#' @param seed random seed for SGLD stage.
#' @param verbose logical; print optimization progress.
#' @param name parameter block to extract: `"all"` (default), latent model name,
#' or `"general"`.
#' @param burnin_iter non-negative integer used both as optimizer schedule
#' warmup (`stepsize_schedule_burnin_iter`) and as explicit trajectory
#' trimming before sampling.
#' @param thinning positive integer thinning interval.
#' @param n_gibbs_samples optional positive integer. If supplied, override
#' `control_ngme$n_gibbs_samples` for the SGLD refit stage only; otherwise
#' inherit the value from `fit`.
#' @param apply_transform logical; apply parameter transforms to user scale.
#' @param combine_chains logical; if `TRUE`, return one combined data.frame,
#' otherwise return one data.frame per chain.
#' @param control_opt optional pre-built `control_opt` object for the SGLD
#' stage. If supplied, it is used directly (with `store_traj` forced to
#' `TRUE`).
#' @param ... additional arguments forwarded to [control_opt()] when
#' `control_opt` is `NULL`.
#'
#' @return A data.frame (or list of data.frames) from [ngme_sgld_samples()]
#' with extra attributes `refit`, `refit_control_opt`, and `refit_source`.
#' @export
compute_ngme_sgld_samples <- function(
fit,
iterations = 4000,
optimizer = sgld(stepsize = 0.01, temperature = 1),
burnin = 100,
n_batch = 20,
n_parallel_chain = 4,
alpha = 0.6,
t0 = 10,
start_sd = 0.2,
seed = Sys.time(),
verbose = FALSE,
name = "all",
burnin_iter = 0,
thinning = 1,
n_gibbs_samples = NULL,
apply_transform = TRUE,
combine_chains = TRUE,
control_opt = NULL,
...) {
stopifnot(inherits(fit, "ngme"))
stopifnot(
is.null(n_gibbs_samples) || (
is.numeric(n_gibbs_samples) &&
length(n_gibbs_samples) == 1 &&
is.finite(n_gibbs_samples) &&
n_gibbs_samples > 0 &&
abs(n_gibbs_samples - round(n_gibbs_samples)) < sqrt(.Machine$double.eps)
)
)
fit_meta <- attr(fit, "fit")
stopifnot(
"Missing fit metadata in `fit`; please pass a fitted ngme object from ngme()." =
!is.null(fit_meta),
"Missing formula in fit metadata." = !is.null(fit_meta$formula),
"Missing data in fit metadata." = !is.null(fit_meta$data)
)
dots <- list(...)
if ("schedule_burnin_iter" %in% names(dots)) {
stop(
"In compute_ngme_sgld_samples(), `schedule_burnin_iter` is unified with `burnin_iter`. ",
"Please set only `burnin_iter`.",
call. = FALSE
)
}
if (is.null(control_opt)) {
control_opt <- do.call(
"control_opt",
utils::modifyList(
list(
optimizer = optimizer,
burnin = burnin,
iterations = iterations,
n_batch = n_batch,
n_parallel_chain = n_parallel_chain,
store_traj = TRUE,
trend_std_conv_check = FALSE,
R_hat_conv_check = FALSE,
pflug_conv_check = FALSE,
stepsize_control = poly_decay(
alpha = alpha,
t0 = t0,
burnin_iter = burnin_iter
),
start_sd = start_sd,
seed = seed,
verbose = verbose
),
dots
)
)
} else {
stopifnot(
inherits(control_opt, "control_opt"),
"When `control_opt` is supplied, extra `...` arguments are not used." =
length(dots) == 0
)
}
stopifnot(
"compute_ngme_sgld_samples currently requires optimizer = sgld(...)." =
identical(control_opt$sgd_method, "sgld")
)
if (isTRUE(control_opt$trend_std_conv_check) ||
isTRUE(control_opt$R_hat_conv_check) ||
isTRUE(control_opt$pflug_conv_check)) {
warning(
"Convergence diagnostics are enabled in `control_opt`; early stopping may reduce fixed-iteration sampling consistency.",
call. = FALSE
)
}
control_opt$stepsize_schedule_burnin_iter <- as.integer(round(burnin_iter))
control_opt$store_traj <- TRUE
fit_env <- build_cv_rebuild_env(
formula = fit_meta$formula,
ngme_obj = fit,
data = fit_meta$data
)
control_ngme_refit <- fit$replicates[[1]]$control_ngme
if (is.null(control_ngme_refit)) {
control_ngme_refit <- control_ngme()
}
if (!is.null(n_gibbs_samples)) {
control_ngme_refit$n_gibbs_samples <- as.integer(round(n_gibbs_samples))
}
refit <- do.call(
"ngme",
list(
formula = fit_meta$formula,
data = fit_meta$data,
family = fit_meta$family,
control_opt = control_opt,
control_ngme = control_ngme_refit,
group = fit_meta$group,
replicate = fit_meta$replicate,
start = fit
),
envir = fit_env
)
ret <- ngme_sgld_samples(
ngme = refit,
name = name,
burnin_iter = burnin_iter,
thinning = thinning,
apply_transform = apply_transform,
combine_chains = combine_chains
)
attr(ret, "refit") <- refit
attr(ret, "refit_control_opt") <- control_opt
attr(ret, "refit_source") <- fit
ret
}
#' Extract Posterior-like Samples from Stored SGLD Trajectories
#'
#' Build posterior-like samples from optimizer trajectories by dropping an
#' initial burn-in segment and applying thinning.
#'
#' @param ngme fitted `ngme` object with `store_traj = TRUE`.
#' @param name parameter block to extract: `"all"` (default), latent model name,
#' or `"general"`.
#' @param burnin_iter non-negative integer. Number of initial iterations to
#' discard before sampling.
#' @param thinning positive integer thinning interval.
#' @param apply_transform logical; apply parameter transforms to user scale.
#' @param combine_chains logical; if `TRUE`, return one combined data.frame,
#' otherwise return one data.frame per chain.
#'
#' @return A data.frame (or list of data.frames when `combine_chains = FALSE`)
#' with columns `.chain`, `.draw`, `.iter`, and one column per parameter.
#' @export
ngme_sgld_samples <- function(
ngme,
name = "all",
burnin_iter = 0,
thinning = 1,
apply_transform = TRUE,
combine_chains = TRUE) {
stopifnot(
inherits(ngme, "ngme"),
is.character(name),
length(name) == 1,
is.numeric(burnin_iter),
length(burnin_iter) == 1,
is.finite(burnin_iter),
burnin_iter >= 0,
abs(burnin_iter - round(burnin_iter)) < sqrt(.Machine$double.eps),
is.numeric(thinning),
length(thinning) == 1,
is.finite(thinning),
thinning >= 1,
abs(thinning - round(thinning)) < sqrt(.Machine$double.eps),
is.logical(apply_transform),
length(apply_transform) == 1,
is.logical(combine_chains),
length(combine_chains) == 1
)
chain_data <- .ngme_chain_trajectories(
ngme = ngme,
name = name,
apply_transform = apply_transform
)
burnin_iter <- as.integer(round(burnin_iter))
thinning <- as.integer(round(thinning))
n_iter_vec <- vapply(chain_data$chains, nrow, integer(1))
n_iter <- min(n_iter_vec)
if (length(unique(n_iter_vec)) > 1) {
warning(
"Chains have different lengths; truncating to the minimum length.",
call. = FALSE
)
}
stopifnot(
"burnin_iter must be smaller than the available trajectory length." =
burnin_iter < n_iter
)
keep_iter <- seq.int(burnin_iter + 1L, n_iter, by = thinning)
stopifnot(length(keep_iter) >= 1)
per_chain <- lapply(seq_along(chain_data$chains), function(chain_idx) {
mat <- chain_data$chains[[chain_idx]][seq_len(n_iter), , drop = FALSE]
mat <- mat[keep_iter, , drop = FALSE]
df <- as.data.frame(mat, check.names = FALSE)
df$.chain <- chain_idx
df$.draw <- seq_len(nrow(df))
df$.iter <- keep_iter
df <- df[, c(".chain", ".draw", ".iter", colnames(mat)), drop = FALSE]
rownames(df) <- NULL
df
})
if (isTRUE(combine_chains)) {
ret <- do.call(rbind, per_chain)
rownames(ret) <- NULL
} else {
ret <- per_chain
}
attr(ret, "name") <- name
attr(ret, "burnin_iter") <- burnin_iter
attr(ret, "thinning") <- thinning
attr(ret, "apply_transform") <- apply_transform
attr(ret, "n_chains") <- length(per_chain)
attr(ret, "n_samples_per_chain") <- length(keep_iter)
ret
}
#' Quantile Confidence Intervals from SGLD Samples
#'
#' Compute parameter-wise quantile confidence intervals from posterior-like
#' SGLD samples returned by [ngme_sgld_samples()].
#'
#' @param samples data.frame (or list of data.frames) returned by
#' [ngme_sgld_samples()].
#' @param lower lower quantile probability (e.g. 0.025).
#' @param upper upper quantile probability (e.g. 0.975).
#'
#' @return A list with posterior-like point estimates (`estimates`), quantile
#' intervals (`ci`), and sample covariance matrix (`covariance`).
#' Class: `ngme_sgld_ci`.
#' @export
ngme_sgld_ci <- function(samples, lower = 0.025, upper = 0.975) {
stopifnot(
is.data.frame(samples) ||
(is.list(samples) &&
length(samples) >= 1 &&
all(vapply(samples, is.data.frame, logical(1)))),
is.numeric(lower),
length(lower) == 1,
is.finite(lower),
lower > 0,
lower < 1,
is.numeric(upper),
length(upper) == 1,
is.finite(upper),
upper > 0,
upper < 1,
lower < upper
)
if (is.list(samples) && !is.data.frame(samples)) {
samples <- do.call(rbind, samples)
rownames(samples) <- NULL
}
burnin_iter <- attr(samples, "burnin_iter")
thinning <- attr(samples, "thinning")
apply_transform <- attr(samples, "apply_transform")
name <- attr(samples, "name")
param_cols <- setdiff(colnames(samples), c(".chain", ".draw", ".iter"))
stopifnot(length(param_cols) >= 1)
stopifnot(
"All parameter columns in `samples` must be numeric." =
all(vapply(samples[, param_cols, drop = FALSE], is.numeric, logical(1)))
)
sample_mat <- as.matrix(samples[, param_cols, drop = FALSE])
storage.mode(sample_mat) <- "double"
colnames(sample_mat) <- param_cols
estimates <- colMeans(sample_mat)
std_error <- if (nrow(sample_mat) > 1) {
apply(sample_mat, 2, stats::sd) / sqrt(nrow(sample_mat))
} else {
setNames(rep(0, ncol(sample_mat)), colnames(sample_mat))
}
qmat <- t(vapply(seq_len(ncol(sample_mat)), function(j) {
stats::quantile(
sample_mat[, j],
probs = c(lower, upper),
names = FALSE,
na.rm = TRUE
)
}, numeric(2)))
colnames(qmat) <- c("lower", "upper")
rownames(qmat) <- colnames(sample_mat)
cov_mat <- if (nrow(sample_mat) > 1) {
stats::cov(sample_mat)
} else {
matrix(0, ncol(sample_mat), ncol(sample_mat))
}
dimnames(cov_mat) <- list(colnames(sample_mat), colnames(sample_mat))
n_chains <- attr(samples, "n_chains")
n_samples_per_chain <- attr(samples, "n_samples_per_chain")
if (is.null(n_chains)) {
n_chains <- if (".chain" %in% colnames(samples)) {
length(unique(samples$.chain))
} else {
1L
}
}
if (is.null(n_samples_per_chain)) {
n_samples_per_chain <- if (".chain" %in% colnames(samples)) {
as.integer(min(table(samples$.chain)))
} else {
nrow(samples)
}
}
ret <- list(
estimates = stats::setNames(estimates, colnames(sample_mat)),
std_error = stats::setNames(as.numeric(std_error), colnames(sample_mat)),
ci = qmat,
covariance = cov_mat,
lower = lower,
upper = upper,
burnin_iter = burnin_iter,
thinning = thinning,
n_chains = n_chains,
n_samples_per_chain = n_samples_per_chain,
n_samples = nrow(sample_mat),
name = name,
apply_transform = apply_transform,
samples = samples
)
class(ret) <- "ngme_sgld_ci"
ret
}
#' Summary for Batch-Means CI Results
#'
#' Summarize an object of class `ngme_batch_ci`, including parameter means,
#' standard errors, confidence intervals, and covariance matrix.
#'
#' @param object an object of class `ngme_batch_ci`.
#' @param digits number of digits used for printing.
#' @param ... currently unused.
#'
#' @return A list with summary table and covariance matrix.
#' @export
summary.ngme_batch_ci <- function(object, digits = 4, ...) {
stopifnot(inherits(object, "ngme_batch_ci"))
conf_int <- as.matrix(object$conf_int)
est <- as.numeric(object$estimates)
se <- as.numeric(object$std_error)
pnames <- names(object$estimates)
if (is.null(pnames)) {
pnames <- rownames(conf_int)
}
if (is.null(pnames)) {
pnames <- paste0("param_", seq_along(est))
}
table <- data.frame(
parameter = pnames,
mean = est,
std_error = se,
lower = conf_int[, "lower"],
upper = conf_int[, "upper"],
check.names = FALSE,
row.names = NULL
)
ret <- list(
table = table,
covariance = object$covariance,
level = object$level,
alpha = object$alpha,
M = object$M,
N = object$N,
burnin_iter = object$burnin_iter,
n_eff = object$n_eff,
n_chains = object$n_chains,
n_iterations = object$n_iterations,
n_iterations_total = object$n_iterations_total,
name = object$name,
apply_transform = object$apply_transform,
digits = digits
)
if (!is.null(object$raw_estimates)) {
raw_conf <- as.matrix(object$raw_conf_int)
raw_names <- names(object$raw_estimates)
if (is.null(raw_names)) {
raw_names <- rownames(raw_conf)
}
if (is.null(raw_names)) {
raw_names <- paste0("param_", seq_along(object$raw_estimates))
}
ret$raw_table <- data.frame(
parameter = raw_names,
mean = as.numeric(object$raw_estimates),
std_error = as.numeric(object$raw_std_error),
lower = raw_conf[, "lower"],
upper = raw_conf[, "upper"],
check.names = FALSE,
row.names = NULL
)
ret$raw_covariance <- object$raw_covariance
}
class(ret) <- "summary.ngme_batch_ci"
ret
}
#' @export
print.summary.ngme_batch_ci <- function(x, ...) {
.format_table <- function(tbl, digits) {
out <- tbl
is_num <- vapply(out, is.numeric, logical(1))
out[is_num] <- lapply(out[is_num], signif, digits = digits)
out
}
cat("Batch-means CI summary\n")
cat(
" level =", x$level,
"| alpha =", x$alpha,
"| M =", x$M,
"| n_eff =", x$n_eff,
"| n_chains =", x$n_chains, "\n"
)
if (!is.null(x$name)) {
cat(" block =", x$name, "\n")
}
if (!is.null(x$apply_transform)) {
cat(" apply_transform =", x$apply_transform, "\n")
}
cat("\nParameter summary:\n")
print(.format_table(x$table, x$digits), row.names = FALSE)
cat("\nCovariance matrix:\n")
print(signif(x$covariance, x$digits))
if (!is.null(x$raw_table)) {
cat("\nRaw-space parameter summary (pre-transform):\n")
print(.format_table(x$raw_table, x$digits), row.names = FALSE)
cat("\nRaw-space covariance matrix:\n")
print(signif(x$raw_covariance, x$digits))
}
invisible(x)
}
.batch_means_boundaries <- function(n_iter, alpha, M = NULL, N = NULL) {
stopifnot(
is.numeric(n_iter),
length(n_iter) == 1,
n_iter >= 2,
is.numeric(alpha),
length(alpha) == 1,
alpha > 0.5,
alpha < 1
)
if (is.null(M)) {
M <- floor(n_iter^((1 - alpha) / 2))
}
M <- as.integer(M)
stopifnot(
is.numeric(M),
length(M) == 1,
M >= 1
)
if (is.null(N)) {
N <- (n_iter^(1 - alpha)) / (M + 1)
}
stopifnot(
is.numeric(N),
length(N) == 1,
N > 0
)
end <- floor(((seq_len(M + 1L)) * N)^(1 / (1 - alpha)))
end <- pmin(pmax(as.integer(end), 1L), as.integer(n_iter))
end <- cummax(end)
end[length(end)] <- as.integer(n_iter)
# Remove zero-size batches induced by integer rounding
keep <- c(TRUE, diff(end) > 0)
end <- end[keep]
start <- c(1L, head(end, -1) + 1L)
size <- end - start + 1L
list(
start = start,
end = end,
size = size,
N = N
)
}
.ngme_chain_trajectories <- function(ngme, name = "all", apply_transform = TRUE) {
ngme_rep <- ngme$replicates[[1]]
if (!identical(name, "all")) {
tr <- get_trace_trajectories(
ngme = ngme,
name = name,
apply_transform = apply_transform
)
tr_list <- unname(tr$trajectories)
parameter_names <- make.unique(tr$parameter_names)
chains <- lapply(seq_len(tr$n_chains), function(chain_idx) {
mat <- do.call(cbind, lapply(tr_list, function(x) {
x[, chain_idx, drop = FALSE]
}))
mat <- as.matrix(mat)
colnames(mat) <- parameter_names
mat
})
transforms <- tr$transformations
stopifnot(length(transforms) == length(parameter_names))
return(list(chains = chains, parameter_names = parameter_names, transforms = transforms))
}
latent_names <- names(ngme_rep$models)
parts <- lapply(latent_names, function(lat_name) {
latent <- ngme_rep$models[[lat_name]]
traj <- attr(latent, "lat_traj")
stopifnot(
"Please run ngme() with store_traj=TRUE before using ngme_batch_ci()." =
!is.null(traj)
)
ts <- get_latent_info(latent)
ts$name <- paste0(ts$name, " (", lat_name, ")")
list(traj = traj, ts = ts)
})
block_traj <- attr(ngme_rep, "block_traj")
stopifnot(
"Please run ngme() with store_traj=TRUE before using ngme_batch_ci()." =
!is.null(block_traj)
)
ts_block <- get_noise_info(ngme_rep$noise)
fe_names <- if (length(ngme_rep$feff) == 0) {
NULL
} else if (!is.null(names(ngme_rep$feff)) &&
all(!is.na(names(ngme_rep$feff))) &&
all(nzchar(names(ngme_rep$feff)))) {
names(ngme_rep$feff)
} else {
paste("fixed effect", seq_len(length(ngme_rep$feff)))
}
fe_trans <- rep(list(identity), length(ngme_rep$feff))
ts_block$name <- c(ts_block$name, fe_names)
ts_block$trans <- c(ts_block$trans, fe_trans)
parts <- c(parts, list(list(traj = block_traj, ts = ts_block)))
n_chain <- length(parts[[1]]$traj)
stopifnot(
"All trajectory components must have the same number of chains." =
all(vapply(parts, function(x) length(x$traj) == n_chain, logical(1)))
)
parameter_names <- make.unique(unlist(lapply(parts, function(x) x$ts$name), use.names = FALSE))
parameter_transforms <- unlist(lapply(parts, function(x) x$ts$trans), recursive = FALSE, use.names = FALSE)
stopifnot(length(parameter_transforms) == length(parameter_names))
chains <- lapply(seq_len(n_chain), function(chain_idx) {
mats <- lapply(parts, function(part) {
mat <- as.matrix(part$traj[[chain_idx]])
if (apply_transform) {
mat <- .apply_parameter_transforms(mat, part$ts$trans)
}
mat
})
min_iter <- min(vapply(mats, ncol, integer(1)))
mats <- lapply(mats, function(x) x[, seq_len(min_iter), drop = FALSE])
chain_mat <- t(do.call(rbind, mats))
colnames(chain_mat) <- parameter_names
chain_mat
})
list(chains = chains, parameter_names = parameter_names, transforms = parameter_transforms)
}
.apply_parameter_transforms <- function(mat, transforms) {
stopifnot(length(transforms) == nrow(mat))
for (i in seq_len(nrow(mat))) {
ff <- transforms[[i]]
if (is.null(ff)) {
next
}
vals <- ff(as.numeric(mat[i, ]))
stopifnot(length(vals) == ncol(mat))
mat[i, ] <- vals
}
mat
}
.delta_transform_batch_ci <- function(ci, transforms) {
stopifnot(
is.list(ci),
is.numeric(ci$estimates),
is.matrix(ci$covariance),
is.list(transforms),
length(transforms) == length(ci$estimates)
)
param_names <- names(ci$estimates)
mean_raw <- as.numeric(ci$estimates)
mean_trans <- numeric(length(mean_raw))
deriv_trans <- numeric(length(mean_raw))
for (i in seq_along(mean_raw)) {
ff <- transforms[[i]]
if (is.null(ff)) {
ff <- identity
}
mean_trans[i] <- ff(mean_raw[i])
deriv_trans[i] <- .delta_derivative(ff, mean_raw[i])
}
jacobian <- diag(deriv_trans, nrow = length(deriv_trans), ncol = length(deriv_trans))
cov_trans <- jacobian %*% ci$covariance %*% jacobian
std_error <- sqrt(pmax(diag(cov_trans), 0) / (ci$n_eff * ci$n_chains))
conf_int <- cbind(
lower = mean_trans - ci$z_value * std_error,
upper = mean_trans + ci$z_value * std_error
)
names(mean_trans) <- param_names
names(std_error) <- param_names
dimnames(cov_trans) <- list(param_names, param_names)
rownames(conf_int) <- param_names
ci$estimates <- mean_trans
ci$covariance <- cov_trans
ci$std_error <- std_error
ci$conf_int <- conf_int
if (!is.null(ci$chain_results)) {
ci$chain_results <- lapply(ci$chain_results, function(cr) {
cr_mean <- as.numeric(cr$mean)
cr_mean_trans <- numeric(length(cr_mean))
cr_deriv <- numeric(length(cr_mean))
for (j in seq_along(cr_mean)) {
ff <- transforms[[j]]
if (is.null(ff)) {
ff <- identity
}
cr_mean_trans[j] <- ff(cr_mean[j])
cr_deriv[j] <- .delta_derivative(ff, cr_mean[j])
}
cr_jacobian <- diag(cr_deriv, nrow = length(cr_deriv), ncol = length(cr_deriv))
cr$mean <- stats::setNames(cr_mean_trans, param_names)
cr$covariance <- cr_jacobian %*% cr$covariance %*% cr_jacobian
dimnames(cr$covariance) <- list(param_names, param_names)
if (!is.null(cr$batch_means) && is.matrix(cr$batch_means)) {
for (j in seq_along(transforms)) {
ff <- transforms[[j]]
if (is.null(ff)) {
ff <- identity
}
cr$batch_means[, j] <- ff(as.numeric(cr$batch_means[, j]))
}
}
cr
})
}
ci
}
.delta_derivative <- function(ff, x) {
h <- 1e-6 * max(1, abs(x))
if (!is.finite(h) || h <= 0) {
h <- 1e-6
}
x_plus <- x + h
x_minus <- x - h
y_plus <- ff(x_plus)
y_minus <- ff(x_minus)
if (is.finite(y_plus) && is.finite(y_minus)) {
return((y_plus - y_minus) / (2 * h))
}
# fallback to one-sided differences for domain-constrained transforms
y0 <- ff(x)
if (is.finite(y_plus) && is.finite(y0)) {
return((y_plus - y0) / h)
}
if (is.finite(y0) && is.finite(y_minus)) {
return((y0 - y_minus) / h)
}
stop("Failed to compute Delta-method derivative for parameter transform.")
}
Any scripts or data that you put into this service are public.
Add the following code to your website.
For more information on customizing the embed code, read Embedding Snippets.