tests/testthat/test-bma.R

# tests/testthat/test-bma.R

test_that("bma returns a well-formed object on a tiny model space (K=2)", {
  set.seed(123)

  # --- tiny data: K=2 regressors, M=2 => MS=2^2=4 models
  n <- 12
  x1 <- rnorm(n)
  x2 <- rnorm(n)
  y  <- 1 + 0.5 * x1 - 0.25 * x2 + rnorm(n, sd = 0.2)

  data <- cbind(y = y, x1 = x1, x2 = x2)

  ms <- model_space(data, M = 2, g = "None", HC = FALSE)

  out <- bma(ms, EMS = 1, dilution = 0, Narrative = 0, round = 10)

  # --- list shape
  expect_type(out, "list")
  expect_length(out, 15)

  # --- extracted scalars
  expect_equal(out[[6]], 2)          # K
  expect_equal(out[[7]], 4)          # MS
  expect_equal(out[[8]], 1)          # EMS we passed
  expect_equal(out[[9]], 0)          # dilution flag

  # --- names of regressors
  expect_equal(out[[5]], c("x1", "x2"))

  # --- main tables dimensions: (K+1) rows, 7 columns
  uniform_table <- out[[1]]
  random_table  <- out[[2]]

  expect_true(is.matrix(uniform_table))
  expect_true(is.matrix(random_table))
  expect_equal(dim(uniform_table), c(3, 7))
  expect_equal(dim(random_table),  c(3, 7))

  # row/col names present and correct
  expect_equal(rownames(uniform_table), c("CONST", "x1", "x2"))
  expect_equal(colnames(uniform_table), c("PIP","PM","PSD","PMcon","PSDcon","P(+)","PSC"))

  # intercept PIP is NA by construction
  expect_true(is.na(uniform_table["CONST","PIP"]))
  expect_true(is.na(random_table["CONST","PIP"]))

  # PIPs for slopes in [0,1]
  expect_true(all(uniform_table[c("x1","x2"),"PIP"] >= 0))
  expect_true(all(uniform_table[c("x1","x2"),"PIP"] <= 1))
  expect_true(all(random_table[c("x1","x2"),"PIP"] >= 0))
  expect_true(all(random_table[c("x1","x2"),"PIP"] <= 1))

  # PSC and P(+) are probabilities in [0,1]
  expect_true(all(uniform_table[,"PSC"] >= 0 & uniform_table[,"PSC"] <= 1))
  expect_true(all(random_table[,"PSC"]  >= 0 & random_table[,"PSC"]  <= 1))
  expect_true(all(uniform_table[,"P(+)"] >= 0 & uniform_table[,"P(+)"] <= 1))
  expect_true(all(random_table[,"P(+)"]  >= 0 & random_table[,"P(+)"]  <= 1))

  # --- EBA object has K+1 rows and required columns
  eba_object <- out[[3]]
  expect_true(is.data.frame(eba_object))
  expect_equal(nrow(eba_object), 3)
  expect_true(all(c("Variable","Lower bound","Minimum","Mean","Maximum","Upper bound","EBA test","%(+)") %in% names(eba_object)))

  # --- pms_table has 2 rows (Binomial, Binomial-beta) and 2 cols
  pms_table <- out[[4]]
  expect_true(is.matrix(pms_table))
  expect_equal(dim(pms_table), c(2, 2))
  expect_equal(rownames(pms_table), c("Binomial","Binomial-beta"))
  expect_equal(colnames(pms_table), c("Prior model size","Posterior model size"))

  # --- for_model_pmp must have 4 columns: prior_u, prior_r, pmp_u, pmp_r
  for_model_pmp <- out[[12]]
  expect_equal(dim(for_model_pmp), c(4, 4))
  # posterior model probabilities sum to 1
  expect_equal(sum(for_model_pmp[, 3]), 1, tolerance = 1e-10)
  expect_equal(sum(for_model_pmp[, 4]), 1, tolerance = 1e-10)

  # --- for_model_sizes must have M+1 rows (=3) and 4 cols
  for_model_sizes <- out[[13]]
  expect_equal(dim(for_model_sizes), c(3, 4))
  # prior size probs sum to 1
  expect_equal(sum(for_model_sizes[, 1]), 1, tolerance = 1e-10)
  expect_equal(sum(for_model_sizes[, 2]), 1, tolerance = 1e-10)
  # posterior size probs sum to 1
  expect_equal(sum(for_model_sizes[, 3]), 1, tolerance = 1e-10)
  expect_equal(sum(for_model_sizes[, 4]), 1, tolerance = 1e-10)

  # --- for_jointness has MS rows and (K + 2) cols: Reg_ID + pmp_u + pmp_r
  for_jointness <- out[[10]]
  expect_equal(dim(for_jointness), c(4, 4))

  # --- for_best_models has MS rows and (K + (K+1) + (K+1) + 2 + 2) cols
  # = K Reg_ID + betas(K+1) + SE(K+1) + DF + R2 + pmp_u + pmp_r
  # = K + (K+1) + (K+1) + 4 = 2 + 3 + 3 + 4 = 12
  for_best_models <- out[[11]]
  expect_equal(nrow(for_best_models), 4)
  expect_equal(ncol(for_best_models), 12)

  # --- alphas length is MS (stored as MS x 1 matrix)
  alphas <- out[[14]]
  expect_true(is.matrix(alphas))
  expect_equal(dim(alphas), c(4, 1))
})

test_that("bma Narrative prior downweights models with multiple substitutes (tiny K=2)", {
  set.seed(1)

  n <- 10
  x1 <- rnorm(n)
  x2 <- rnorm(n)
  y  <- 1 + x1 + rnorm(n, sd = 0.5)
  data <- cbind(y = y, x1 = x1, x2 = x2)

  ms <- model_space(data, M = 2, g = "None", HC = FALSE)

  # Nar_vec groups both regressors together => group 1 contains x1 and x2
  Nar_vec <- c(1L, 1L)

  # Without narrative (baseline)
  out0 <- bma(ms, EMS = 1, Narrative = 0, dilution = 0, round = 12)

  # With narrative, scalar p < 1 (penalize having both x1 and x2 together)
  out1 <- bma(ms, EMS = 1, Narrative = 1, p = 0.5, Nar_vec = Nar_vec, dilution = 0, round = 12)

  pmp0 <- out0[[12]][, 3]  # pmp_uniform
  pmp1 <- out1[[12]][, 3]

  # Model with both regressors included is the one with rowSums(Reg_ID)==2.
  Reg_ID <- ms[[2]][, 1:2, drop = FALSE]
  idx_both <- which(rowSums(Reg_ID) == 2)

  # Narrative prior should not increase probability of the "both included" model
  # (it should be penalized relative to other models)
  expect_true(pmp1[idx_both] <= pmp0[idx_both] + 1e-12)

  # Posterior probabilities still sum to 1
  expect_equal(sum(pmp1), 1, tolerance = 1e-10)
})

test_that("bma errors when both dilution and Narrative are enabled", {
  set.seed(2)

  n <- 8
  x1 <- rnorm(n)
  x2 <- rnorm(n)
  y  <- 1 + rnorm(n)
  data <- cbind(y = y, x1 = x1, x2 = x2)

  ms <- model_space(data, M = 2, g = "None", HC = FALSE)

  expect_error(
    bma(ms, dilution = 1, Narrative = 1, Nar_vec = c(1L,1L), p = 0.7),
    "CANNOT CHOOSE BOTH"
  )
})

test_that("bma errors when Narrative=1 but Nar_vec missing or wrong length", {
  set.seed(3)

  n <- 8
  x1 <- rnorm(n)
  x2 <- rnorm(n)
  y  <- 1 + rnorm(n)
  data <- cbind(y = y, x1 = x1, x2 = x2)

  ms <- model_space(data, M = 2, g = "None", HC = FALSE)

  expect_error(
    bma(ms, Narrative = 1, p = 0.7, Nar_vec = NULL),
    "Nar_vec"
  )

  expect_error(
    bma(ms, Narrative = 1, p = 0.7, Nar_vec = c(1L)),
    "Nar_vec should have K elements"
  )
})

test_that("bma errors when Narrative=1 and p is a vector with wrong length vs groups", {
  set.seed(4)

  n <- 10
  x1 <- rnorm(n)
  x2 <- rnorm(n)
  y  <- 1 + rnorm(n)
  data <- cbind(y = y, x1 = x1, x2 = x2)

  ms <- model_space(data, M = 2, g = "None", HC = FALSE)

  # Two groups implied (1 and 2), but p length mismatch
  Nar_vec <- c(1L, 2L)

  expect_error(
    bma(ms, Narrative = 1, Nar_vec = Nar_vec, p = c(0.7, 0.5, 0.2)),
    "p is misspecified"
  )
})


test_that("bma works and is internally consistent on MS=16 (K=4, full model space)", {
  set.seed(202)

  n <- 25
  x1 <- rnorm(n)
  x2 <- rnorm(n)
  x3 <- rnorm(n)
  x4 <- rnorm(n)

  # build y with some signal
  y <- 1 + 0.7*x1 - 0.5*x3 + rnorm(n, sd = 0.4)

  data <- cbind(y = y, x1 = x1, x2 = x2, x3 = x3, x4 = x4)

  # full model space: K=4, M=4 => MS=16
  ms <- model_space(data, M = 4, g = "None", HC = FALSE)

  out <- bma(ms, EMS = 2, dilution = 0, Narrative = 0, round = 12)

  # --- basic extracted scalars
  expect_equal(out[[6]], 4)    # K
  expect_equal(out[[7]], 16)   # MS
  expect_equal(out[[8]], 2)    # EMS

  # --- Tables are (K+1) x 7
  uniform_table <- out[[1]]
  random_table  <- out[[2]]

  expect_equal(dim(uniform_table), c(5, 7))
  expect_equal(dim(random_table),  c(5, 7))
  expect_equal(rownames(uniform_table), c("CONST","x1","x2","x3","x4"))
  expect_equal(colnames(uniform_table), c("PIP","PM","PSD","PMcon","PSDcon","P(+)","PSC"))

  # --- Posterior model probabilities sum to 1
  for_model_pmp <- out[[12]]
  expect_equal(dim(for_model_pmp), c(16, 4))
  expect_equal(sum(for_model_pmp[, 3]), 1, tolerance = 1e-10) # pmp_uniform
  expect_equal(sum(for_model_pmp[, 4]), 1, tolerance = 1e-10) # pmp_random

  # --- Posterior model size probabilities sum to 1 and have length M+1 = 5
  for_model_sizes <- out[[13]]
  expect_equal(dim(for_model_sizes), c(5, 4))
  expect_equal(sum(for_model_sizes[, 1]), 1, tolerance = 1e-10) # prior uniform sizes
  expect_equal(sum(for_model_sizes[, 2]), 1, tolerance = 1e-10) # prior random sizes
  expect_equal(sum(for_model_sizes[, 3]), 1, tolerance = 1e-10) # post uniform sizes
  expect_equal(sum(for_model_sizes[, 4]), 1, tolerance = 1e-10) # post random sizes

  # --- PIP range checks (slopes only)
  expect_true(all(uniform_table[c("x1","x2","x3","x4"), "PIP"] >= 0))
  expect_true(all(uniform_table[c("x1","x2","x3","x4"), "PIP"] <= 1))
  expect_true(all(random_table[c("x1","x2","x3","x4"), "PIP"] >= 0))
  expect_true(all(random_table[c("x1","x2","x3","x4"), "PIP"] <= 1))

  # --- PSC and P(+) are probabilities
  expect_true(all(uniform_table[, "PSC"] >= 0 & uniform_table[, "PSC"] <= 1))
  expect_true(all(random_table[, "PSC"]  >= 0 & random_table[, "PSC"]  <= 1))
  expect_true(all(uniform_table[, "P(+)"] >= 0 & uniform_table[, "P(+)"] <= 1))
  expect_true(all(random_table[, "P(+)"]  >= 0 & random_table[, "P(+)"]  <= 1))

  # --- Jointness table dimensions: MS rows, K + 2 cols
  for_jointness <- out[[10]]
  expect_equal(dim(for_jointness), c(16, 6))

  # --- for_best_models dimensions: MS rows, K + (K+1) + (K+1) + 4 cols = 4 + 5 + 5 + 4 = 18
  for_best_models <- out[[11]]
  expect_equal(nrow(for_best_models), 16)
  expect_equal(ncol(for_best_models), 18)

  # --- EBA object has K+1 rows and required columns
  eba_object <- out[[3]]
  expect_true(is.data.frame(eba_object))
  expect_equal(nrow(eba_object), 5)
  expect_true(all(c("Variable","Lower bound","Minimum","Mean","Maximum","Upper bound","EBA test","%(+)") %in% names(eba_object)))

  # --- Consistency identity check (slopes only):
  # PM ≈ PIP * PMcon  (in your code: con_PM = PM / PIP)
  # Use tolerance because of rounding (round=12 reduces this problem).
  for (v in c("x1","x2","x3","x4")) {
    pip <- uniform_table[v, "PIP"]
    pm  <- uniform_table[v, "PM"]
    pmc <- uniform_table[v, "PMcon"]

    # skip if pip is extremely small (avoid 0 * Inf style numerical artefacts)
    if (is.finite(pip) && pip > 1e-12) {
      expect_equal(pm, pip * pmc, tolerance = 1e-7)
    }
  }
})

test_that("bma Narrative prior penalizes multi-substitute models in MS=16 space", {
  set.seed(303)

  n <- 25
  X <- replicate(4, rnorm(n))
  colnames(X) <- paste0("x", 1:4)
  y <- 1 + 0.8*X[,1] + rnorm(n, sd = 0.5)
  data <- cbind(y = y, X)

  ms <- model_space(data, M = 4, g = "None", HC = FALSE)

  # Two groups: {x1,x2} are substitutes (group 1), {x3,x4} substitutes (group 2)
  Nar_vec <- c(1L, 1L, 2L, 2L)

  out0 <- bma(ms, EMS = 2, Narrative = 0, dilution = 0, round = 12)
  out1 <- bma(ms, EMS = 2, Narrative = 1, p = c(0.6, 0.5), Nar_vec = Nar_vec, dilution = 0, round = 12)

  pmp0 <- out0[[12]][, 3]  # pmp_uniform
  pmp1 <- out1[[12]][, 3]

  Reg_ID <- ms[[2]][, 1:4, drop = FALSE]

  # Models that include BOTH x1 and x2 (two substitutes in group 1) should be penalized
  idx_two_in_g1 <- which(Reg_ID[,1] == 1L & Reg_ID[,2] == 1L)

  # At least one of these models should have (weakly) lower posterior after narrative penalty
  expect_true(any(pmp1[idx_two_in_g1] <= pmp0[idx_two_in_g1] + 1e-12))

  # Still a valid posterior distribution
  expect_equal(sum(pmp1), 1, tolerance = 1e-10)
})

Try the rmsBMA package in your browser

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

rmsBMA documentation built on March 14, 2026, 5:06 p.m.