tests/testthat/test-accuracy-ci-coverage.R

# Bootstrap confidence-interval coverage.
#
# Point recovery says the estimator is unbiased. It says nothing about whether
# the reported uncertainty is right -- an interval that is systematically too
# narrow passes every recovery test while being wrong in the way that matters
# most for a published analysis. This file simulates many datasets from a known
# truth and counts how often the 95% interval actually contains it.
#
# Scope is deliberate. Coverage is asserted on the OUTCOME parameters
# (res_Gamma), where the bootstrap behaves: measured replicate spread there is
# modest and intervals bracket the truth. It is NOT asserted on res_Beta, whose
# replicate distribution is pathological -- on well-separated N = 500 data the
# replicates ranged -30.6 to +15.7 with sd 4.0 against a point estimate of 1.72,
# giving a 95% interval of (-10.7, 12.5). A nominal-coverage assertion there
# would fail against a correct implementation of the current algorithm, so beta
# gets a floor that catches total breakage and an explicit note instead.
#
# Cost: this is the most expensive file in the suite. It carries a second gate
# beyond skip_on_cran() so a normal heavy-tier run does not pay for it.

skip_on_cran()
skip_if_not(nzchar(Sys.getenv("LUCID_TEST_LEVEL")),
            "set LUCID_TEST_LEVEL=full to run bootstrap coverage")

N_COV <- 300L
N_DATASETS <- 40L
R_BOOT <- 100L
CONF <- 0.95

# With 40 datasets at nominal 0.95, the exact binomial 99% acceptance region is
# [0.85, 1.00]: qbinom(0.005, 40, 0.95)/40 = 0.85. Asserting inside that band is
# a genuine two-sided test of calibration that will not flake on Monte Carlo
# noise, while still failing on a variance estimator that is materially wrong.
COV_LO <- 0.85

covers <- function(ci_row, truth) {
  lo <- min(ci_row, na.rm = TRUE)
  hi <- max(ci_row, na.rm = TRUE)
  is.finite(lo) && is.finite(hi) && truth >= lo && truth <= hi
}

test_that("bootstrap intervals for the outcome effect cover the truth at ~95%", {
  hits <- logical(N_DATASETS)
  widths <- numeric(N_DATASETS)

  for (i in seq_len(N_DATASETS)) {
    d <- sim_lucid("early", N = N_COV, K = 2, M = 4, sep = 2.5,
                   family = "normal", seed = 700 + i)
    suppressWarnings(suppressMessages(invisible(capture.output(
      fit <- estimate_lucid(lucid_model = "early", G = d$G, Z = d$Z, Y = d$Y,
                            family = "normal", K = 2,
                            init_omic.data.model = "VVV", seed = 1)
    ))))
    suppressWarnings(suppressMessages(invisible(capture.output(
      bs <- boot_lucid(G = d$G, Z = d$Z, Y = d$Y, lucid_model = "early",
                       model = fit, R = R_BOOT, conf = CONF)
    ))))

    # The generating outcome levels are 0 and 1, so the between-cluster contrast
    # is 1. Compare against the contrast rather than the levels: it is invariant
    # to which cluster got which label.
    truth_contrast <- diff(d$truth$gamma[[1]])
    gm <- as.data.frame(bs$gamma)
    # the cluster-contrast row, whatever it is named in this fit
    row <- grep("cluster2|LC2", rownames(gm))
    if (!length(row)) next
    limits <- suppressWarnings(as.numeric(gm[row[1], -1]))
    limits <- limits[is.finite(limits)]
    if (length(limits) < 2L) next

    hits[i] <- covers(limits, truth_contrast)
    widths[i] <- max(limits) - min(limits)
  }

  coverage <- mean(hits)
  expect_gte(coverage, COV_LO)
  expect_lte(coverage, 1.0)
  # a degenerate interval could "cover" everything; require it to be informative
  expect_lt(mean(widths[widths > 0]), 5)
})

test_that("intervals are well formed and bracket their own point estimate", {
  # Cheap structural guarantees that must hold on every single fit, independent
  # of coverage: limits finite, ordered, and containing the estimate they were
  # built around.
  for (model in c("early", "parallel")) {
    n_layer <- if (model == "early") 1L else 2L
    d <- sim_lucid(model, N = N_COV, K = 2, M = 4, n_layer = n_layer,
                   sep = 2.5, seed = 750)
    K <- if (model == "early") 2 else rep(2, n_layer)
    suppressWarnings(suppressMessages(invisible(capture.output(
      fit <- estimate_lucid(lucid_model = model, G = d$G, Z = d$Z, Y = d$Y,
                            family = "normal", K = K,
                            init_omic.data.model = "VVV", seed = 1)
    ))))
    suppressWarnings(suppressMessages(invisible(capture.output(
      bs <- boot_lucid(G = d$G, Z = d$Z, Y = d$Y, lucid_model = model,
                       model = fit, R = 60, conf = CONF)
    ))))

    tab <- as.data.frame(bs$gamma)
    est <- as.numeric(tab[[1]])
    for (r in seq_len(nrow(tab))) {
      lims <- suppressWarnings(as.numeric(tab[r, -1]))
      lims <- lims[is.finite(lims)]
      if (length(lims) < 2L) next
      expect_gte(est[r], min(lims) - 1e-8, label = paste(model, "estimate above lower limit"))
      expect_lte(est[r], max(lims) + 1e-8, label = paste(model, "estimate below upper limit"))
    }
  }
})

test_that("wider confidence levels give wider intervals", {
  # Monotonicity in conf is a property no correct interval can violate, and it
  # is far cheaper to check than coverage.
  d <- sim_lucid("early", N = N_COV, K = 2, M = 4, sep = 2.5, seed = 760)
  suppressWarnings(suppressMessages(invisible(capture.output(
    fit <- estimate_lucid(lucid_model = "early", G = d$G, Z = d$Z, Y = d$Y,
                          family = "normal", K = 2,
                          init_omic.data.model = "VVV", seed = 1)
  ))))
  width_at <- function(conf) {
    set.seed(99)
    suppressWarnings(suppressMessages(invisible(capture.output(
      bs <- boot_lucid(G = d$G, Z = d$Z, Y = d$Y, lucid_model = "early",
                       model = fit, R = 80, conf = conf)
    ))))
    tab <- as.data.frame(bs$gamma)
    lims <- suppressWarnings(as.numeric(tab[nrow(tab), -1]))
    lims <- lims[is.finite(lims)]
    if (length(lims) < 2L) return(NA_real_)
    max(lims) - min(lims)
  }
  w80 <- width_at(0.80)
  w99 <- width_at(0.99)
  skip_if(is.na(w80) || is.na(w99), "intervals unavailable at this R")
  expect_gt(w99, w80)
})

test_that("exposure-effect intervals are finite, with their instability recorded", {
  # NOT a coverage assertion. The bootstrap replicate distribution for res_Beta
  # is heavy-tailed enough that nominal coverage is not attainable at feasible R
  # -- see the file header. This asserts only that limits exist and are finite,
  # which catches the interval machinery breaking outright. Raising this to a
  # real coverage assertion requires first fixing the replicate instability.
  d <- sim_lucid("early", N = N_COV, K = 2, M = 4, P = 2, sep = 2.5, seed = 770)
  suppressWarnings(suppressMessages(invisible(capture.output(
    fit <- estimate_lucid(lucid_model = "early", G = d$G, Z = d$Z, Y = d$Y,
                          family = "normal", K = 2,
                          init_omic.data.model = "VVV", seed = 1)
  ))))
  suppressWarnings(suppressMessages(invisible(capture.output(
    bs <- boot_lucid(G = d$G, Z = d$Z, Y = d$Y, lucid_model = "early",
                     model = fit, R = R_BOOT, conf = CONF)
  ))))
  tab <- as.data.frame(bs$beta)
  expect_gt(nrow(tab), 0)
  expect_true(all(is.finite(as.numeric(tab[[1]]))), label = "beta point estimates finite")
})

Try the LUCIDus package in your browser

Any scripts or data that you put into this service are public.

LUCIDus documentation built on Sept. 3, 2026, 1:06 a.m.