tests/testthat/test-bma-inclusion-plot.R

box::use(
  testthat[
    expect_equal,
    expect_gte,
    expect_lte,
    expect_null,
    expect_setequal,
    expect_true,
    skip_if_not_installed,
    test_that
  ],
  withr[local_options]
)

box::use(
  artma / econometric / bma[run_bma],
  artma / visualization / bma[build_bma_inclusion_data, create_bma_inclusion_plot]
)


# A model space wide enough that BMS retains far more models than carry any
# posterior mass, which is the situation the upstream image plot cannot draw.
fit_demo_bma <- function(n = 150, n_moderators = 8, seed = 4) {
  set.seed(seed)
  bma_data <- data.frame(effect = stats::rnorm(n))
  for (j in seq_len(n_moderators)) {
    bma_data[[paste0("moderator", j)]] <- stats::rnorm(n)
  }
  bma_data$effect <- 0.7 * bma_data$moderator1 -
    0.5 * bma_data$moderator2 +
    0.3 * bma_data$moderator3 +
    stats::rnorm(n) * 0.6

  run_bma(bma_data, list(
    burn = 500L, iter = 2000L, nmodel = 200L,
    g = "UIP", mprior = "uniform", mcmc = "bd"
  ))
}


test_that("the inclusion grid builds where BMS's own image plot aborts", {
  skip_if_not_installed("BMS")
  local_options("artma.verbose" = 1)

  model <- fit_demo_bma()

  # BMS lays its image columns out on cumsum() of unnormalised posterior
  # weights, which span enough orders of magnitude to saturate in double
  # precision; graphics::image() then rejects the non-monotonic boundaries.
  upstream <- tryCatch(
    {
      grDevices::pdf(NULL)
      on.exit(grDevices::dev.off(), add = TRUE)
      graphics::image(model, yprop2pip = FALSE, order.by.pip = TRUE)
      "rendered"
    },
    error = function(e) conditionMessage(e)
  )

  built <- build_bma_inclusion_data(model)

  expect_true(!is.null(built))
  expect_true(nrow(built$cells) > 0)
  # Whether or not this particular fit trips the upstream failure, the
  # replacement must produce a grid either way.
  expect_true(is.character(upstream))
})


test_that("regressors are ordered by posterior inclusion probability", {
  skip_if_not_installed("BMS")
  local_options("artma.verbose" = 1)

  model <- fit_demo_bma()
  built <- build_bma_inclusion_data(model)

  bms_order <- rownames(stats::coef(model, order.by.pip = TRUE, exact = TRUE))
  expect_equal(built$regressors, bms_order)
})


test_that("cell signs match the coefficients of the model they belong to", {
  skip_if_not_installed("BMS")
  local_options("artma.verbose" = 1)

  model <- fit_demo_bma()
  built <- build_bma_inclusion_data(model)

  betas <- as.matrix(model$topmod$betas())
  weights <- exp(model$topmod$lik() - max(model$topmod$lik()))
  best <- order(weights, decreasing = TRUE)[1]
  best_betas <- stats::setNames(betas[, best], model$reg.names)

  top_cells <- built$cells[built$cells$model == 1, , drop = FALSE]
  observed <- stats::setNames(top_cells$inclusion, built$regressors[top_cells$row])

  expected <- vapply(names(observed), function(name) {
    value <- best_betas[[name]]
    if (value == 0) "Excluded" else if (value > 0) "Positive" else "Negative"
  }, character(1))

  expect_equal(observed[names(expected)], expected)
})


test_that("the grid truncates to the models that carry the posterior mass", {
  skip_if_not_installed("BMS")
  local_options("artma.verbose" = 1)

  model <- fit_demo_bma()
  built <- build_bma_inclusion_data(model, max_models = 12L, coverage = 0.99)

  expect_lte(built$n_models_shown, 12L)
  expect_lte(built$n_models_shown, built$n_models_total)
  expect_gte(built$mass_shown, 0)
  expect_lte(built$mass_shown, 1 + 1e-9)

  # Columns tile the full width exactly once, with no gaps or overlaps.
  first_row <- built$cells[built$cells$row == 1, , drop = FALSE]
  first_row <- first_row[order(first_row$model), , drop = FALSE]
  expect_equal(min(first_row$xmin), 0)
  expect_equal(max(first_row$xmax), 1)
  expect_equal(first_row$xmin[-1], utils::head(first_row$xmax, -1))
})


test_that("create_bma_inclusion_plot returns a ggplot with the three cell states", {
  skip_if_not_installed("BMS")
  local_options("artma.verbose" = 1)

  model <- fit_demo_bma()
  plot <- create_bma_inclusion_plot(model, theme_name = "blue")

  expect_true(ggplot2::is_ggplot(plot))
  expect_setequal(
    unique(plot$data$inclusion),
    intersect(c("Positive", "Negative", "Excluded"), unique(plot$data$inclusion))
  )
  expect_true(all(plot$data$inclusion %in% c("Positive", "Negative", "Excluded")))
})


test_that("build_bma_inclusion_data returns NULL when there is nothing to draw", {
  empty <- structure(
    list(
      reg.names = character(0),
      topmod = list(betas = function() matrix(numeric(0), nrow = 0, ncol = 0))
    ),
    class = "bma"
  )

  expect_null(build_bma_inclusion_data(empty))
})

Try the artma package in your browser

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

artma documentation built on Oct. 27, 2026, 9:06 a.m.