This vignette demonstrates publication-bias modeling with publication_bias(method = "beta_selection") and publication_bias(method = "beta_binomial") and publication_bias(method = "logistic_selection") after fitting mars models.

Current implementation note: - Univariate publication-bias fits estimate a scalar tau2 inside the selection model. - Multilevel and multivariate publication-bias fits use the fitted MARS covariance blocks directly, rather than collapsing to diagonal sampling variances. - Reported standard errors use a conservative rule: when both Hessian-based and OPG-based SEs are finite, the larger value is reported.

library(mars)
extract_full_data_coefficients <- function(fit) {
  est_raw <- if(!is.null(fit$beta_r)) {
    fit$beta_r
  } else if(!is.null(fit$beta_fe)) {
    fit$beta_fe
  } else {
    fit$est_values$mu
  }

  est_vec <- as.numeric(est_raw)
  est_names <- names(est_raw)
  if(is.matrix(est_raw) || is.data.frame(est_raw)) {
    est_vec <- as.numeric(est_raw[, 1, drop = TRUE])
    est_names <- rownames(est_raw)
  }
  if(length(est_names) != length(est_vec)) {
    est_names <- colnames(fit$design_matrix)
  }

  se_vec <- rep(NA_real_, length(est_vec))
  vc <- fit$varcov_beta
  if(is.matrix(vc) && nrow(vc) == length(est_vec)) {
    se_vec <- sqrt(pmax(diag(vc), 0))
    if(!is.null(colnames(vc)) && length(colnames(vc)) == length(est_vec)) {
      est_names <- colnames(vc)
    }
  }

  data.frame(
    term = est_names,
    estimate = est_vec,
    std_error = se_vec,
    stringsAsFactors = FALSE
  )
}

compare_pub_bias_3 <- function(x, y, z,
                               label_x = "beta_selection",
                               label_y = "beta_binomial",
                               label_z = "logistic_selection",
                               ref = NULL,
                               label_ref = "all_data") {
  a <- x$coefficients
  b <- y$coefficients
  c <- z$coefficients
  names(a)[names(a) == "estimate"] <- paste0("estimate_", label_x)
  names(a)[names(a) == "std_error"] <- paste0("se_", label_x)
  names(b)[names(b) == "estimate"] <- paste0("estimate_", label_y)
  names(b)[names(b) == "std_error"] <- paste0("se_", label_y)
  names(c)[names(c) == "estimate"] <- paste0("estimate_", label_z)
  names(c)[names(c) == "std_error"] <- paste0("se_", label_z)

  out <- merge(
    a[, c("term", paste0("estimate_", label_x), paste0("se_", label_x))],
    b[, c("term", paste0("estimate_", label_y), paste0("se_", label_y))],
    by = "term",
    all = TRUE
  )
  out <- merge(
    out,
    c[, c("term", paste0("estimate_", label_z), paste0("se_", label_z))],
    by = "term",
    all = TRUE
  )
  if(!is.null(ref)) {
    r <- ref
    names(r)[names(r) == "estimate"] <- paste0("estimate_", label_ref)
    names(r)[names(r) == "std_error"] <- paste0("se_", label_ref)
    out <- merge(
      out,
      r[, c("term", paste0("estimate_", label_ref), paste0("se_", label_ref))],
      by = "term",
      all = TRUE
    )
  }
  out[order(out$term), ]
}

plot_pub_bias_compare_3 <- function(tbl,
                                    label_x = "beta_selection",
                                    label_y = "beta_binomial",
                                    label_z = "logistic_selection",
                                    label_ref = "all_data",
                                    main = "Method Comparison") {
  ref_est_name <- paste0("estimate_", label_ref)
  ref_se_name <- paste0("se_", label_ref)
  est_x <- tbl[[paste0("estimate_", label_x)]]
  se_x <- tbl[[paste0("se_", label_x)]]
  est_y <- tbl[[paste0("estimate_", label_y)]]
  se_y <- tbl[[paste0("se_", label_y)]]
  est_z <- tbl[[paste0("estimate_", label_z)]]
  se_z <- tbl[[paste0("se_", label_z)]]
  has_ref <- ref_est_name %in% names(tbl)
  est_ref <- if(has_ref) tbl[[ref_est_name]] else rep(NA_real_, nrow(tbl))
  se_ref <- if(has_ref && ref_se_name %in% names(tbl)) tbl[[ref_se_name]] else rep(NA_real_, nrow(tbl))
  k <- nrow(tbl)
  xpos <- seq_len(k)
  offsets <- if(has_ref) c(-0.27, -0.09, 0.09, 0.27) else c(-0.18, 0, 0.18)
  all_vals <- c(
    est_x, est_y, est_z, est_ref,
    est_x - 1.96 * se_x, est_x + 1.96 * se_x,
    est_y - 1.96 * se_y, est_y + 1.96 * se_y,
    est_z - 1.96 * se_z, est_z + 1.96 * se_z,
    est_ref - 1.96 * se_ref, est_ref + 1.96 * se_ref
  )
  ylim <- range(all_vals[is.finite(all_vals)])
  old_par <- par(no.readonly = TRUE)
  on.exit(par(old_par), add = TRUE)
  x_axis_cex <- max(0.55, min(0.8, 8 / max(nchar(tbl$term), 1L)))
  mar <- old_par$mar
  mar[1] <- max(mar[1], 8 + 3 * x_axis_cex)
  par(mar = mar)

  plot(xpos, est_x, type = "n", xaxt = "n", xlab = "", ylab = "Estimate", main = main, ylim = ylim)
  abline(h = 0, lty = 2, col = "gray60")
  axis(1, at = xpos, labels = tbl$term, las = 2, cex.axis = x_axis_cex)
  title(xlab = "Coefficient Term", line = 6)

  keep_x <- is.finite(est_x) & is.finite(se_x)
  keep_y <- is.finite(est_y) & is.finite(se_y)
  keep_z <- is.finite(est_z) & is.finite(se_z)
  keep_ref <- has_ref & is.finite(est_ref) & is.finite(se_ref)
  pt_x <- is.finite(est_x)
  pt_y <- is.finite(est_y)
  pt_z <- is.finite(est_z)
  pt_ref <- has_ref & is.finite(est_ref)
  miss_se_x <- is.finite(est_x) & !is.finite(se_x)
  miss_se_y <- is.finite(est_y) & !is.finite(se_y)
  miss_se_z <- is.finite(est_z) & !is.finite(se_z)
  miss_se_ref <- has_ref & is.finite(est_ref) & !is.finite(se_ref)
  x_x <- xpos + offsets[1]
  x_y <- xpos + offsets[2]
  x_z <- xpos + offsets[3]
  x_ref <- if(has_ref) xpos + offsets[4] else xpos
  if(any(keep_x)) {
    segments(x_x[keep_x], est_x[keep_x] - 1.96 * se_x[keep_x],
             x_x[keep_x], est_x[keep_x] + 1.96 * se_x[keep_x], col = "#1F78B4", lwd = 2)
  }
  if(any(keep_y)) {
    segments(x_y[keep_y], est_y[keep_y] - 1.96 * se_y[keep_y],
             x_y[keep_y], est_y[keep_y] + 1.96 * se_y[keep_y], col = "#E31A1C", lwd = 2)
  }
  if(any(keep_z)) {
    segments(x_z[keep_z], est_z[keep_z] - 1.96 * se_z[keep_z],
             x_z[keep_z], est_z[keep_z] + 1.96 * se_z[keep_z], col = "#33A02C", lwd = 2)
  }
  if(any(keep_ref)) {
    segments(x_ref[keep_ref], est_ref[keep_ref] - 1.96 * se_ref[keep_ref],
             x_ref[keep_ref], est_ref[keep_ref] + 1.96 * se_ref[keep_ref], col = "#4D4D4D", lwd = 2)
  }
  if(any(pt_x)) {
    points(x_x[pt_x], est_x[pt_x], pch = 19, col = "#1F78B4")
  }
  if(any(pt_y)) {
    points(x_y[pt_y], est_y[pt_y], pch = 17, col = "#E31A1C")
  }
  if(any(pt_z)) {
    points(x_z[pt_z], est_z[pt_z], pch = 15, col = "#33A02C")
  }
  if(any(pt_ref)) {
    points(x_ref[pt_ref], est_ref[pt_ref], pch = 18, col = "#4D4D4D")
  }
  if(any(miss_se_x)) {
    points(x_x[miss_se_x], est_x[miss_se_x], pch = 1, col = "#1F78B4", cex = 1.2, lwd = 1.5)
  }
  if(any(miss_se_y)) {
    points(x_y[miss_se_y], est_y[miss_se_y], pch = 2, col = "#E31A1C", cex = 1.2, lwd = 1.5)
  }
  if(any(miss_se_z)) {
    points(x_z[miss_se_z], est_z[miss_se_z], pch = 0, col = "#33A02C", cex = 1.2, lwd = 1.5)
  }
  if(any(miss_se_ref)) {
    points(x_ref[miss_se_ref], est_ref[miss_se_ref], pch = 5, col = "#4D4D4D", cex = 1.2, lwd = 1.5)
  }

  legend_labels <- c(label_x, label_y, label_z)
  legend_cols <- c("#1F78B4", "#E31A1C", "#33A02C")
  legend_pch <- c(19, 17, 15)
  if(has_ref) {
    legend_labels <- c(legend_labels, label_ref)
    legend_cols <- c(legend_cols, "#4D4D4D")
    legend_pch <- c(legend_pch, 18)
  }
  legend("topright", legend = legend_labels, col = legend_cols, pch = legend_pch, bty = "n")
  if(any(miss_se_x) || any(miss_se_y) || any(miss_se_z) || any(miss_se_ref)) {
    mtext("Open symbols indicate estimates with unavailable SE.", side = 3, line = 0.2, cex = 0.8)
  }
}

extract_alt_interval <- function(x, term = "(Intercept)") {
  if(!is.null(x$summary) &&
     all(c("point_estimate", "ci_lower", "ci_upper") %in% colnames(x$summary)) &&
     term %in% rownames(x$summary)) {
    out <- x$summary[term, c("point_estimate", "ci_lower", "ci_upper"), drop = FALSE]
    return(out)
  }

  if(!is.null(x$summary) &&
     all(c("estimate", "ci_lower", "ci_upper") %in% colnames(x$summary)) &&
     term %in% rownames(x$summary)) {
    out <- x$summary[term, c("estimate", "ci_lower", "ci_upper"), drop = FALSE]
    names(out) <- c("point_estimate", "ci_lower", "ci_upper")
    return(out)
  }

  stop("Could not extract a point estimate + interval for term: ", term)
}

Univariate Example

fit_uni <- mars(
  formula = yi ~ 1,
  data = bcg,
  studyID = "trial",
  variance = "vi",
  varcov_type = "univariate",
  structure = "univariate",
  estimation_method = "MLE"
)

summary(fit_uni)
#> Results generated with MARS:v 0.5.1.1 
#> Thursday, April  2, 2026
#> 
#> Model Type: 
#> univariate
#> 
#> Estimation Method: 
#> Maximum Likelihood
#> 
#> Model Formula: 
#> yi ~ 1
#> 
#> Data Summary: 
#> Number of Effect Sizes: 13
#> Number of Fixed Effects: 1
#> Number of Random Effects: 1
#> 
#> Random Components: 
#>       term  var     SD
#>  intercept 0.28 0.5292
#> 
#> Fixed Effects Estimates: 
#>    attribute estimate     SE z_test   p_value  lower   upper
#>  (Intercept)  -0.7112 0.1719 -4.137 3.513e-05 -1.048 -0.3743
#> 
#> Model Fit Statistics: 
#>  logLik   Dev   AIC   BIC AICc
#>  -12.67 31.33 29.33 30.46 37.9
#> 
#> Q Error: 152.233 (12), p < 0.0001
#> 
#> I2 (General): 92.03957
#> 
#> Residual Diagnostics: 
#>   n n_finite_raw mean_raw sd_raw   rmse    mae q_pearson mean_abs_studentized
#>  13           13 -0.02945 0.6938 0.6672 0.5958     13.16               0.9527
#>  max_abs_studentized prop_abs_studentized_gt2 prop_abs_studentized_gt3
#>                1.434                        0                        0
#> 
#> Normality (whitened residuals):                   test n_tested statistic p_value
#>  shapiro_wilk_whitened       13     0.886 0.08592
#> 
#> Heteroscedasticity trend (|raw residual| ~ fitted):   n corr_abs_raw_fitted slope p_value
#>  13                  NA    NA      NA
pb_uni <- publication_bias(
  fit_uni,
  method = "beta_selection",
  p_value_tail = "two.sided"
)

summary(pb_uni)
#> Publication Bias Model (MARS)
#> Method: beta_selection 
#> Structure: univariate 
#> Effects: 13  | Studies: 13 
#> P-value tail: two.sided 
#> Convergence: 0 - Converged 
#> Objective: 11.24 
#> 
#> Selection Parameters:
#>  parameter estimate
#>      alpha   1.0330
#>       beta   0.2139
#>       tau2   0.5664
#> 
#> Selection Weight Profile (p-value grid):
#>           model p_value weight
#>  beta_selection  0.0100 0.1870
#>  beta_selection  0.1189 0.2224
#>  beta_selection  0.2278 0.2520
#>  beta_selection  0.3367 0.2877
#>  beta_selection  0.4456 0.3343
#>  beta_selection  0.5544 0.3999
#>  beta_selection  0.6633 0.5014
#>  beta_selection  0.7722 0.6852
#>  beta_selection  0.8811 1.1473
#>  beta_selection  0.9900 8.0641
#> 
#> Bias-Adjusted Coefficients:
#>         term estimate std_error z_value p_value
#>  (Intercept)  -1.0912    0.2881 -3.7873   2e-04
#> 
#> Adjusted vs Unadjusted Summary:
#>         term unadjusted_estimate unadjusted_se unadjusted_ci_lower
#>  (Intercept)             -0.7112        0.1719             -1.0481
#>  unadjusted_ci_upper estimate std_error adjusted_ci_lower adjusted_ci_upper
#>              -0.3743  -1.0912    0.2881           -1.6559           -0.5265
#>  estimate_diff percent_change direction_changed ci_overlap
#>          -0.38       -53.4245             FALSE       TRUE
#> 
#> Small-Study Effects Check (Egger-type):
#> Model: SND ~ precision 
#> Method: Egger-type small-study-effects regression with study-cluster robust SE (CR1) 
#> Intercept: -7.8519 SE: 3.2091 t: -2.4468 df: 12 p: 0.0308 
#> 
#> SE note:
#> SE rule: reported SEs use the larger of the Hessian-based and OPG-based values when both are finite, otherwise the finite one.
pb_uni_bb <- publication_bias(
  fit_uni,
  method = "beta_binomial",
  n_bins = 8,
  p_value_tail = "two.sided"
)

summary(pb_uni_bb)
#> Publication Bias Model (MARS)
#> Method: beta_binomial 
#> Structure: univariate 
#> Effects: 13  | Studies: 13 
#> P-value tail: two.sided 
#> Convergence: 0 - Converged 
#> Objective: 12.4 
#> 
#> Selection Parameters:
#>  parameter estimate
#>      alpha   0.5407
#>       beta   0.2341
#>       tau2   0.3591
#>     n_bins   8.0000
#> 
#> Selection Weight Profile (bin probabilities):
#>  bin p_lower p_upper weight
#>    1   0.000   0.125 0.1072
#>    2   0.125   0.250 0.0651
#>    3   0.250   0.375 0.0575
#>    4   0.375   0.500 0.0575
#>    5   0.500   0.625 0.0629
#>    6   0.625   0.750 0.0767
#>    7   0.750   0.875 0.1148
#>    8   0.875   1.000 0.4583
#> 
#> Bias-Adjusted Coefficients:
#>         term estimate std_error z_value p_value
#>  (Intercept)  -0.6824    0.1927 -3.5419   4e-04
#> 
#> Adjusted vs Unadjusted Summary:
#>         term unadjusted_estimate unadjusted_se unadjusted_ci_lower
#>  (Intercept)             -0.7112        0.1719             -1.0481
#>  unadjusted_ci_upper estimate std_error adjusted_ci_lower adjusted_ci_upper
#>              -0.3743  -0.6824    0.1927           -1.0601           -0.3048
#>  estimate_diff percent_change direction_changed ci_overlap
#>         0.0288         4.0426             FALSE       TRUE
#> 
#> Small-Study Effects Check (Egger-type):
#> Model: SND ~ precision 
#> Method: Egger-type small-study-effects regression with study-cluster robust SE (CR1) 
#> Intercept: -7.8519 SE: 3.2091 t: -2.4468 df: 12 p: 0.0308 
#> 
#> SE note:
#> SE rule: reported SEs use the larger of the Hessian-based and OPG-based values when both are finite, otherwise the finite one. beta_binomial uses discretized p-value bins; coefficient magnitudes can differ materially from beta_selection.
pb_uni_logit <- publication_bias(
  fit_uni,
  method = "logistic_selection",
  p_value_tail = "two.sided"
)

summary(pb_uni_logit)
#> Publication Bias Model (MARS)
#> Method: logistic_selection 
#> Structure: univariate 
#> Effects: 13  | Studies: 13 
#> P-value tail: two.sided 
#> Convergence: 0 - Converged 
#> Objective: 13.92 
#> 
#> Selection Parameters:
#>        parameter estimate
#>  logit_intercept   9.0929
#>      logit_slope   0.4472
#>             tau2   0.4256
#> 
#> Selection Weight Profile (p-value grid):
#>               model p_value weight
#>  logistic_selection  0.0100 1.0000
#>  logistic_selection  0.1189 1.0000
#>  logistic_selection  0.2278 0.9999
#>  logistic_selection  0.3367 0.9999
#>  logistic_selection  0.4456 0.9999
#>  logistic_selection  0.5544 0.9999
#>  logistic_selection  0.6633 0.9999
#>  logistic_selection  0.7722 0.9999
#>  logistic_selection  0.8811 0.9999
#>  logistic_selection  0.9900 0.9999
#> 
#> Bias-Adjusted Coefficients:
#>         term estimate std_error z_value p_value
#>  (Intercept)  -0.7724    0.3054 -2.5287  0.0114
#> 
#> Adjusted vs Unadjusted Summary:
#>         term unadjusted_estimate unadjusted_se unadjusted_ci_lower
#>  (Intercept)             -0.7112        0.1719             -1.0481
#>  unadjusted_ci_upper estimate std_error adjusted_ci_lower adjusted_ci_upper
#>              -0.3743  -0.7724    0.3054            -1.371           -0.1737
#>  estimate_diff percent_change direction_changed ci_overlap
#>        -0.0612        -8.5986             FALSE       TRUE
#> 
#> Small-Study Effects Check (Egger-type):
#> Model: SND ~ precision 
#> Method: Egger-type small-study-effects regression with study-cluster robust SE (CR1) 
#> Intercept: -7.8519 SE: 3.2091 t: -2.4468 df: 12 p: 0.0308 
#> 
#> SE note:
#> SE rule: reported SEs use the larger of the Hessian-based and OPG-based values when both are finite, otherwise the finite one.

Selection and adjusted fixed effects:

pb_uni$selection
#>     alpha      beta      tau2 
#> 1.0330169 0.2138633 0.5663630
pb_uni$covariance_mode
#> [1] "publication_bias_tau2"
pb_uni$coefficients
#>          term  estimate std_error   z_value      p_value
#> 1 (Intercept) -1.091154 0.2881115 -3.787261 0.0001523172
pb_uni_bb$selection_weights
#>   bin p_lower p_upper     weight
#> 1   1   0.000   0.125 0.10719722
#> 2   2   0.125   0.250 0.06507810
#> 3   3   0.250   0.375 0.05746743
#> 4   4   0.375   0.500 0.05747199
#> 5   5   0.500   0.625 0.06291976
#> 6   6   0.625   0.750 0.07672810
#> 7   7   0.750   0.875 0.11482686
#> 8   8   0.875   1.000 0.45831054
pb_uni_logit$selection
#> logit_intercept     logit_slope            tau2 
#>       9.0929409       0.4471942       0.4255670
pb_uni_logit$coefficients
#>          term   estimate std_error   z_value    p_value
#> 1 (Intercept) -0.7723525 0.3054325 -2.528718 0.01144801

For the univariate case, the publication-bias likelihood still estimates a scalar tau2, so the selection parameter vector includes tau2 along with the selection-function parameters.

ref_uni <- extract_full_data_coefficients(fit_uni)
cmp_uni <- compare_pub_bias_3(pb_uni, pb_uni_bb, pb_uni_logit, ref = ref_uni)
cmp_uni
#>          term estimate_beta_selection se_beta_selection estimate_beta_binomial
#> 1 (Intercept)               -1.091154         0.2881115              -0.682448
#>   se_beta_binomial estimate_logistic_selection se_logistic_selection
#> 1        0.1926782                  -0.7723525             0.3054325
#>   estimate_all_data se_all_data
#> 1        -0.7111991   0.1718968
plot_pub_bias_compare_3(cmp_uni, main = "Univariate: publication-bias methods with all-data reference")

Funnel plot from the publication-bias fit:

funnel_plot(pb_uni)

Jackknife version of the complete-data fit and each publication-bias model:

set.seed(2026)

# Keep the vignette jackknife examples intentionally small/lightweight.
bcg_jack <- bcg

jack_complete <- mars_alt_estimation(
  type = "jackknife",
  data = bcg_jack,
  formula = yi ~ 1,
  studyID = "trial",
  effectID = NULL,
  sample_size = NULL,
  variance = "vi",
  varcov_type = "univariate",
  structure = "univariate",
  estimation_method = "MLE",
  jackknife_target = "complete_data"
)

jack_beta_selection <- mars_alt_estimation(
  type = "jackknife",
  data = bcg_jack,
  formula = yi ~ 1,
  studyID = "trial",
  effectID = NULL,
  sample_size = NULL,
  variance = "vi",
  varcov_type = "univariate",
  structure = "univariate",
  estimation_method = "MLE",
  jackknife_target = "beta_selection",
  publication_bias_args = list(
    p_value_tail = "two.sided",
    maxit = 100,
    integration_points = 31
  )
)

jack_beta_binomial <- mars_alt_estimation(
  type = "jackknife",
  data = bcg_jack,
  formula = yi ~ 1,
  studyID = "trial",
  effectID = NULL,
  sample_size = NULL,
  variance = "vi",
  varcov_type = "univariate",
  structure = "univariate",
  estimation_method = "MLE",
  jackknife_target = "beta_binomial",
  publication_bias_args = list(
    p_value_tail = "two.sided",
    n_bins = 8,
    maxit = 100,
    integration_points = 31
  )
)

jack_logistic <- mars_alt_estimation(
  type = "jackknife",
  data = bcg_jack,
  formula = yi ~ 1,
  studyID = "trial",
  effectID = NULL,
  sample_size = NULL,
  variance = "vi",
  varcov_type = "univariate",
  structure = "univariate",
  estimation_method = "MLE",
  jackknife_target = "logistic_selection",
  publication_bias_args = list(
    p_value_tail = "two.sided",
    maxit = 100,
    integration_points = 31
  )
)

jack_complete_stats <- compute_alt_stats(jack_complete, type = "jackknife")
jack_beta_selection_stats <- compute_alt_stats(jack_beta_selection, type = "jackknife")
jack_beta_binomial_stats <- compute_alt_stats(jack_beta_binomial, type = "jackknife")
jack_logistic_stats <- compute_alt_stats(jack_logistic, type = "jackknife")

jackknife_compare <- rbind(
  complete_data = extract_alt_interval(jack_complete_stats),
  beta_selection = extract_alt_interval(jack_beta_selection_stats),
  beta_binomial = extract_alt_interval(jack_beta_binomial_stats),
  logistic_selection = extract_alt_interval(jack_logistic_stats)
)

jackknife_compare
#>                    point_estimate   ci_lower   ci_upper
#> complete_data          -0.7071685 -0.7867671 -0.6292833
#> beta_selection         -1.1713117 -4.1793912 -0.7214840
#> beta_binomial          -0.6908430 -1.7810121  0.6252479
#> logistic_selection     -0.7525089 -0.8203594 -0.6654832
jack_est <- jackknife_compare[, "point_estimate"]
jack_lo <- jackknife_compare[, "ci_lower"]
jack_hi <- jackknife_compare[, "ci_upper"]
xpos <- seq_len(nrow(jackknife_compare))

plot(
  xpos, jack_est,
  ylim = range(c(jack_lo, jack_hi)),
  xaxt = "n", xlab = "", ylab = "Jackknife Estimate",
  pch = 19, col = "#1F78B4",
  main = "Univariate Jackknife: complete data and publication-bias targets"
)
segments(xpos, jack_lo, xpos, jack_hi, lwd = 2, col = "#1F78B4")
abline(h = 0, lty = 2, col = "gray60")
axis(1, at = xpos, labels = rownames(jackknife_compare), las = 2)
title(xlab = "Jackknife Target", line = 6)

Multilevel Example

fit_ml <- mars(
  formula = effect ~ 1 + (1 | district/study),
  data = school,
  studyID = "district",
  variance = "var",
  varcov_type = "multilevel",
  structure = "multilevel",
  estimation_method = "MLE"
)

summary(fit_ml)
#> Results generated with MARS:v 0.5.1.1 
#> Thursday, April  2, 2026
#> 
#> Model Type: 
#> multilevel
#> 
#> Estimation Method: 
#> Maximum Likelihood
#> 
#> Model Formula: 
#> effect ~ 1 + (1 | district/study)
#> 
#> Data Summary: 
#> Number of Effect Sizes: 56
#> Number of Fixed Effects: 1
#> Number of Random Effects: 2
#> 
#> Random Data Summary: 
#> Number unique district: 11 
#> Number unique study: 56
#> 
#> Random Components: 
#>      term     var     SD
#>  district 0.05774 0.2403
#>     study 0.03286 0.1813
#> 
#> Fixed Effects Estimates: 
#>    attribute estimate      SE z_test p_value   lower  upper
#>  (Intercept)   0.1845 0.08048  2.292 0.02191 0.02671 0.3422
#> 
#> Model Fit Statistics: 
#>  logLik   Dev   AIC   BIC AICc
#>  -8.395 24.79 22.79 28.87 24.5
#> 
#> Q Error: 578.864 (55), p < 0.0001
#> 
#> I2 (General): 
#>     names values
#>  district  92.38
#>     study  87.34
#> 
#> I2 (Jackson): 98.69621
#> I2 (Between): 86.3727
#> 
#> Residual Diagnostics: 
#>   n n_finite_raw mean_raw sd_raw   rmse    mae q_pearson mean_abs_studentized
#>  56           56 -0.06446 0.3258 0.3293 0.2632      96.8                1.065
#>  max_abs_studentized prop_abs_studentized_gt2 prop_abs_studentized_gt3
#>                4.196                   0.1071                  0.03571
#> 
#> Normality (whitened residuals):                   test n_tested statistic p_value
#>  shapiro_wilk_whitened       56    0.9555 0.03766
#> 
#> Heteroscedasticity trend (|raw residual| ~ fitted):   n corr_abs_raw_fitted slope p_value
#>  56                  NA    NA      NA
pb_ml <- publication_bias(
  fit_ml,
  method = "beta_selection",
  p_value_tail = "two.sided"
)

summary(pb_ml)
#> Publication Bias Model (MARS)
#> Method: beta_selection 
#> Structure: multilevel 
#> Effects: 56  | Studies: 11 
#> P-value tail: two.sided 
#> Convergence: 0 - Converged 
#> Objective: -111 
#> 
#> Selection Parameters:
#>  parameter estimate
#>      alpha   1.0047
#>       beta   0.0034
#> 
#> Selection Weight Profile (p-value grid):
#>           model p_value weight
#>  beta_selection  0.0100 0.0034
#>  beta_selection  0.1189 0.0039
#>  beta_selection  0.2278 0.0044
#>  beta_selection  0.3367 0.0051
#>  beta_selection  0.4456 0.0061
#>  beta_selection  0.5544 0.0077
#>  beta_selection  0.6633 0.0101
#>  beta_selection  0.7722 0.0150
#>  beta_selection  0.8811 0.0286
#>  beta_selection  0.9900 0.3375
#> 
#> Bias-Adjusted Coefficients:
#>         term estimate std_error  z_value p_value
#>  (Intercept)   0.3259     9e-04 359.3879       0
#> 
#> Adjusted vs Unadjusted Summary:
#>         term unadjusted_estimate unadjusted_se unadjusted_ci_lower
#>  (Intercept)              0.1845        0.0805              0.0267
#>  unadjusted_ci_upper estimate std_error adjusted_ci_lower adjusted_ci_upper
#>               0.3422   0.3259     9e-04            0.3241            0.3277
#>  estimate_diff percent_change direction_changed ci_overlap
#>         0.1415        76.6906             FALSE       TRUE
#> 
#> Small-Study Effects Check (Egger-type):
#> Model: SND ~ precision 
#> Method: Egger-type small-study-effects regression with study-cluster robust SE (CR1) 
#> Intercept: 3.69 SE: 1.8503 t: 1.9943 df: 10 p: 0.0741 
#> 
#> SE note:
#> SE rule: reported SEs use the larger of the Hessian-based and OPG-based values when both are finite, otherwise the finite one. Covariance structure: used the fitted MARS within/between-study covariance blocks directly for the publication-bias likelihood.
pb_ml_bb <- publication_bias(
  fit_ml,
  method = "beta_binomial",
  n_bins = 8,
  p_value_tail = "two.sided"
)

summary(pb_ml_bb)
#> Publication Bias Model (MARS)
#> Method: beta_binomial 
#> Structure: multilevel 
#> Effects: 56  | Studies: 11 
#> P-value tail: two.sided 
#> Convergence: 0 - Converged 
#> Objective: -302.1 
#> 
#> Selection Parameters:
#>  parameter estimate
#>      alpha 122.3738
#>       beta   0.3659
#>     n_bins   8.0000
#> 
#> Selection Weight Profile (bin probabilities):
#>  bin p_lower p_upper weight
#>    1   0.000   0.125 0.0000
#>    2   0.125   0.250 0.0000
#>    3   0.250   0.375 0.0000
#>    4   0.375   0.500 0.0000
#>    5   0.500   0.625 0.0000
#>    6   0.625   0.750 0.0006
#>    7   0.750   0.875 0.0195
#>    8   0.875   1.000 0.9798
#> 
#> Bias-Adjusted Coefficients:
#>         term estimate std_error  z_value p_value
#>  (Intercept)  -1.3124    0.0201 -65.3197       0
#> 
#> Adjusted vs Unadjusted Summary:
#>         term unadjusted_estimate unadjusted_se unadjusted_ci_lower
#>  (Intercept)              0.1845        0.0805              0.0267
#>  unadjusted_ci_upper estimate std_error adjusted_ci_lower adjusted_ci_upper
#>               0.3422  -1.3124    0.0201           -1.3518            -1.273
#>  estimate_diff percent_change direction_changed ci_overlap
#>        -1.4968       -811.496              TRUE      FALSE
#> 
#> Small-Study Effects Check (Egger-type):
#> Model: SND ~ precision 
#> Method: Egger-type small-study-effects regression with study-cluster robust SE (CR1) 
#> Intercept: 3.69 SE: 1.8503 t: 1.9943 df: 10 p: 0.0741 
#> 
#> SE note:
#> SE rule: reported SEs use the larger of the Hessian-based and OPG-based values when both are finite, otherwise the finite one. Covariance structure: used the fitted MARS within/between-study covariance blocks directly for the publication-bias likelihood. beta_binomial uses discretized p-value bins; coefficient magnitudes can differ materially from beta_selection.
pb_ml_logit <- publication_bias(
  fit_ml,
  method = "logistic_selection",
  p_value_tail = "two.sided"
)

summary(pb_ml_logit)
#> Publication Bias Model (MARS)
#> Method: logistic_selection 
#> Structure: multilevel 
#> Effects: 56  | Studies: 11 
#> P-value tail: two.sided 
#> Convergence: 0 - Converged 
#> Objective: 8.395 
#> 
#> Selection Parameters:
#>        parameter estimate
#>  logit_intercept  11.7447
#>      logit_slope  12.3280
#> 
#> Selection Weight Profile (p-value grid):
#>               model p_value weight
#>  logistic_selection  0.0100      1
#>  logistic_selection  0.1189      1
#>  logistic_selection  0.2278      1
#>  logistic_selection  0.3367      1
#>  logistic_selection  0.4456      1
#>  logistic_selection  0.5544      1
#>  logistic_selection  0.6633      1
#>  logistic_selection  0.7722      1
#>  logistic_selection  0.8811      1
#>  logistic_selection  0.9900      1
#> 
#> Bias-Adjusted Coefficients:
#>         term estimate std_error z_value p_value
#>  (Intercept)   0.1845    0.0871  2.1186  0.0341
#> 
#> Adjusted vs Unadjusted Summary:
#>         term unadjusted_estimate unadjusted_se unadjusted_ci_lower
#>  (Intercept)              0.1845        0.0805              0.0267
#>  unadjusted_ci_upper estimate std_error adjusted_ci_lower adjusted_ci_upper
#>               0.3422   0.1845    0.0871            0.0138            0.3551
#>  estimate_diff percent_change direction_changed ci_overlap
#>              0         -2e-04             FALSE       TRUE
#> 
#> Small-Study Effects Check (Egger-type):
#> Model: SND ~ precision 
#> Method: Egger-type small-study-effects regression with study-cluster robust SE (CR1) 
#> Intercept: 3.69 SE: 1.8503 t: 1.9943 df: 10 p: 0.0741 
#> 
#> SE note:
#> SE rule: reported SEs use the larger of the Hessian-based and OPG-based values when both are finite, otherwise the finite one. Covariance structure: used the fitted MARS within/between-study covariance blocks directly for the publication-bias likelihood.

Multilevel coefficients with standard errors:

pb_ml$covariance_mode
#> [1] "fixed_mars_structure"
pb_ml$covariance_structure
#> $structure
#> [1] "multilevel"
#> 
#> $Tau
#>            [,1]       [,2]
#> [1,] 0.05773805 0.00000000
#> [2,] 0.00000000 0.03286473
pb_ml$coefficients
#>          term  estimate   std_error  z_value p_value
#> 1 (Intercept) 0.3259153 0.000906862 359.3879       0
pb_ml_bb$coefficients
#>          term  estimate  std_error   z_value p_value
#> 1 (Intercept) -1.312393 0.02009184 -65.31968       0
pb_ml_logit$coefficients
#>          term  estimate std_error z_value    p_value
#> 1 (Intercept) 0.1844551 0.0870634 2.11863 0.03412178

For multilevel fits, publication_bias() now uses the fitted MARS block covariance structure directly. In that case the output stores covariance_mode = "fixed_mars_structure" and the publication-bias selection parameters no longer include a separate scalar tau2.

ref_ml <- extract_full_data_coefficients(fit_ml)
cmp_ml <- compare_pub_bias_3(pb_ml, pb_ml_bb, pb_ml_logit, ref = ref_ml)
cmp_ml
#>          term estimate_beta_selection se_beta_selection estimate_beta_binomial
#> 1 (Intercept)               0.3259153       0.000906862              -1.312393
#>   se_beta_binomial estimate_logistic_selection se_logistic_selection
#> 1       0.02009184                   0.1844551             0.0870634
#>   estimate_all_data se_all_data
#> 1         0.1844554   0.0804815
plot_pub_bias_compare_3(cmp_ml, main = "Multilevel: publication-bias methods with all-data reference")

Jackknife version of the multilevel complete-data fit and each publication-bias model:

set.seed(2026)

school_jack <- subset(school, district %in% sort(unique(district))[1:4])

jack_ml_complete <- mars_alt_estimation(
  type = "jackknife",
  data = school_jack,
  formula = effect ~ 1 + (1 | district/study),
  studyID = "district",
  effectID = NULL,
  sample_size = NULL,
  variance = "var",
  varcov_type = "multilevel",
  structure = "multilevel",
  estimation_method = "MLE",
  jackknife_target = "complete_data"
)

jack_ml_beta_selection <- mars_alt_estimation(
  type = "jackknife",
  data = school_jack,
  formula = effect ~ 1 + (1 | district/study),
  studyID = "district",
  effectID = NULL,
  sample_size = NULL,
  variance = "var",
  varcov_type = "multilevel",
  structure = "multilevel",
  estimation_method = "MLE",
  jackknife_target = "beta_selection",
  publication_bias_args = list(
    p_value_tail = "two.sided",
    maxit = 80,
    integration_points = 31
  )
)

jack_ml_beta_binomial <- mars_alt_estimation(
  type = "jackknife",
  data = school_jack,
  formula = effect ~ 1 + (1 | district/study),
  studyID = "district",
  effectID = NULL,
  sample_size = NULL,
  variance = "var",
  varcov_type = "multilevel",
  structure = "multilevel",
  estimation_method = "MLE",
  jackknife_target = "beta_binomial",
  publication_bias_args = list(
    p_value_tail = "two.sided",
    n_bins = 8,
    maxit = 80,
    integration_points = 31
  )
)

jack_ml_logistic <- mars_alt_estimation(
  type = "jackknife",
  data = school_jack,
  formula = effect ~ 1 + (1 | district/study),
  studyID = "district",
  effectID = NULL,
  sample_size = NULL,
  variance = "var",
  varcov_type = "multilevel",
  structure = "multilevel",
  estimation_method = "MLE",
  jackknife_target = "logistic_selection",
  publication_bias_args = list(
    p_value_tail = "two.sided",
    maxit = 80,
    integration_points = 31
  )
)

jack_ml_complete_stats <- compute_alt_stats(jack_ml_complete, type = "jackknife")
jack_ml_beta_selection_stats <- compute_alt_stats(jack_ml_beta_selection, type = "jackknife")
jack_ml_beta_binomial_stats <- compute_alt_stats(jack_ml_beta_binomial, type = "jackknife")
jack_ml_logistic_stats <- compute_alt_stats(jack_ml_logistic, type = "jackknife")

jackknife_compare_ml <- rbind(
  complete_data = extract_alt_interval(jack_ml_complete_stats),
  beta_selection = extract_alt_interval(jack_ml_beta_selection_stats),
  beta_binomial = extract_alt_interval(jack_ml_beta_binomial_stats),
  logistic_selection = extract_alt_interval(jack_ml_logistic_stats)
)

jackknife_compare_ml
#>                    point_estimate   ci_lower  ci_upper
#> complete_data          0.23973526 0.14570705 0.3043585
#> beta_selection         0.04208093 0.01522397 0.1019976
#> beta_binomial          0.20373586 0.08254856 0.3298935
#> logistic_selection     0.16972049 0.08100785 0.1962584

The multilevel jackknife illustration uses only the first four top-level clusters so the vignette stays quick to run while still showing the leave-one- cluster-out workflow.

jack_ml_est <- jackknife_compare_ml[, "point_estimate"]
jack_ml_lo <- jackknife_compare_ml[, "ci_lower"]
jack_ml_hi <- jackknife_compare_ml[, "ci_upper"]
xpos_ml <- seq_len(nrow(jackknife_compare_ml))

plot(
  xpos_ml, jack_ml_est,
  ylim = range(c(jack_ml_lo, jack_ml_hi)),
  xaxt = "n", xlab = "", ylab = "Jackknife Estimate",
  pch = 19, col = "#1F78B4",
  main = "Multilevel Jackknife: complete data and publication-bias targets"
)
segments(xpos_ml, jack_ml_lo, xpos_ml, jack_ml_hi, lwd = 2, col = "#1F78B4")
abline(h = 0, lty = 2, col = "gray60")
axis(1, at = xpos_ml, labels = rownames(jackknife_compare_ml), las = 2)
title(xlab = "Jackknife Target", line = 6)

Multivariate Example

becker09 <- na.omit(becker09)

fit_mv <- mars(
  data = becker09,
  studyID = "ID",
  effectID = "numID",
  sample_size = "N",
  effectsize_type = "cor",
  varcov_type = "weighted",
  variable_names = c(
    "Cognitive_Performance",
    "Somatic_Performance",
    "Selfconfidence_Performance",
    "Somatic_Cognitive",
    "Selfconfidence_Cognitive",
    "Selfconfidence_Somatic"
  )
)

summary(fit_mv)
#> Results generated with MARS:v 0.5.1.1 
#> Thursday, April  2, 2026
#> 
#> Model Type: 
#> multivariate
#> 
#> Estimation Method: 
#> Restricted Maximum Likelihood
#> 
#> Model Formula: 
#> NULL
#> 
#> Data Summary: 
#> Number of Effect Sizes: 48
#> Number of Fixed Effects: 6
#> Number of Random Effects: 6
#> 
#> Random Components: 
#>          ri_1     ri_2     ri_3      ri_4      ri_5       ri_6
#> ri_1  0.13204  0.07630 -0.03144  0.003058 -0.018150  0.0022236
#> ri_2  0.99878  0.04420 -0.01698  0.001532 -0.009861  0.0008355
#> ri_3 -0.42074 -0.39284  0.04229 -0.007313  0.009630 -0.0122546
#> ri_4  0.23176  0.20062 -0.97927  0.001319 -0.001498  0.0022776
#> ri_5 -0.62514 -0.58701  0.58603 -0.516444  0.006384 -0.0024641
#> ri_6  0.09632  0.06256 -0.93797  0.987249 -0.485426  0.0040360
#> 
#> Fixed Effects Estimates: 
#>  attribute estimate      SE  z_test   p_value   lower     upper
#>          1 -0.09773 0.13777 -0.7093 4.781e-01 -0.3677  0.172299
#>          2 -0.17556 0.08620 -2.0367 4.168e-02 -0.3445 -0.006613
#>          3  0.31868 0.08406  3.7911 1.500e-04  0.1539  0.483438
#>          4  0.52720 0.03335 15.8070 2.785e-56  0.4618  0.592564
#>          5 -0.41756 0.04590 -9.0968 9.305e-20 -0.5075 -0.327591
#>          6 -0.40071 0.04182 -9.5821 9.510e-22 -0.4827 -0.318750
#> 
#> Model Fit Statistics: 
#>  logLik    Dev   AIC   BIC  AICc
#>   18.63 -37.25 16.75 63.67 29.48
#> 
#> Q Error: 230.642 (42), p < 0.0001
#> 
#> I2 (General): 
#>  names values
#>   ri_1  94.53
#>   ri_2  85.26
#>   ri_3  84.69
#>   ri_4  14.71
#>   ri_5  45.51
#>   ri_6  34.56
#> 
#> I2 (Jackson): 
#>  names values
#>   ri_1  90.98
#>   ri_2  77.93
#>   ri_3  81.33
#>   ri_4  16.14
#>   ri_5  43.25
#>   ri_6  31.42
#> 
#> I2 (Between): 83.393
#> 
#> Residual Diagnostics: 
#>   n n_finite_raw mean_raw sd_raw   rmse    mae q_pearson mean_abs_studentized
#>  48           48 -0.02155 0.2127 0.2116 0.1646     133.5                1.419
#>  max_abs_studentized prop_abs_studentized_gt2 prop_abs_studentized_gt3
#>                5.006                   0.2708                   0.1042
#> 
#> Normality (whitened residuals):                   test n_tested statistic p_value
#>  shapiro_wilk_whitened       48    0.9789  0.5351
#> 
#> Heteroscedasticity trend (|raw residual| ~ fitted):   n corr_abs_raw_fitted     slope p_value
#>  48           -0.002893 -0.001091  0.9844
pb_mv <- publication_bias(
  fit_mv,
  method = "beta_selection",
  p_value_tail = "two.sided"
)

summary(pb_mv)
#> Publication Bias Model (MARS)
#> Method: beta_selection 
#> Structure: UN 
#> Effects: 48  | Studies: 8 
#> P-value tail: two.sided 
#> Convergence: 0 - Converged 
#> Objective: -17.08 
#> 
#> Selection Parameters:
#>  parameter estimate
#>      alpha   0.9761
#>       beta   0.8709
#> 
#> Selection Weight Profile (p-value grid):
#>           model p_value weight
#>  beta_selection  0.0100 0.9522
#>  beta_selection  0.1189 0.9111
#>  beta_selection  0.2278 0.9125
#>  beta_selection  0.3367 0.9219
#>  beta_selection  0.4456 0.9372
#>  beta_selection  0.5544 0.9590
#>  beta_selection  0.6633 0.9901
#>  beta_selection  0.7722 1.0375
#>  beta_selection  0.8811 1.1248
#>  beta_selection  0.9900 1.5442
#> 
#> Bias-Adjusted Coefficients:
#>                       term estimate std_error z_value p_value
#>  factor(data[[effectID]])1  -0.0573    0.1721 -0.3330  0.7391
#>  factor(data[[effectID]])2  -0.2066    0.1568 -1.3172  0.1878
#>  factor(data[[effectID]])3   0.2559    0.1346  1.9009  0.0573
#>  factor(data[[effectID]])4   0.5083    0.2685  1.8933  0.0583
#>  factor(data[[effectID]])5  -0.4056    0.2949 -1.3752  0.1691
#>  factor(data[[effectID]])6  -0.3925    0.0697 -5.6313  0.0000
#> 
#> Adjusted vs Unadjusted Summary:
#>                       term unadjusted_estimate unadjusted_se
#>  factor(data[[effectID]])1             -0.0977        0.1378
#>  factor(data[[effectID]])2             -0.1756        0.0862
#>  factor(data[[effectID]])3              0.3187        0.0841
#>  factor(data[[effectID]])4              0.5272        0.0334
#>  factor(data[[effectID]])5             -0.4176        0.0459
#>  factor(data[[effectID]])6             -0.4007        0.0418
#>  unadjusted_ci_lower unadjusted_ci_upper estimate std_error adjusted_ci_lower
#>              -0.3678              0.1723  -0.0573    0.1721           -0.3946
#>              -0.3445             -0.0066  -0.2066    0.1568           -0.5139
#>               0.1539              0.4834   0.2559    0.1346           -0.0080
#>               0.4618              0.5926   0.5083    0.2685           -0.0179
#>              -0.5075             -0.3276  -0.4056    0.2949           -0.9837
#>              -0.4827             -0.3187  -0.3925    0.0697           -0.5291
#>  adjusted_ci_upper estimate_diff percent_change direction_changed ci_overlap
#>             0.2800        0.0404        41.3524             FALSE       TRUE
#>             0.1008       -0.0310       -17.6571             FALSE       TRUE
#>             0.5198       -0.0628       -19.6984             FALSE       TRUE
#>             1.0345       -0.0189        -3.5841             FALSE       TRUE
#>             0.1725        0.0120         2.8643             FALSE       TRUE
#>            -0.2559        0.0082         2.0547             FALSE       TRUE
#> 
#> Small-Study Effects Check (Egger-type):
#> Model: SND ~ precision 
#> Method: Egger-type small-study-effects regression with study-cluster robust SE (CR1) 
#> Intercept: -3.8099 SE: 1.2322 t: -3.092 df: 7 p: 0.0175 
#> 
#> SE note:
#> SE rule: reported SEs use the larger of the Hessian-based and OPG-based values when both are finite, otherwise the finite one. Covariance structure: used the fitted MARS within/between-study covariance blocks directly for the publication-bias likelihood.
pb_mv_bb <- publication_bias(
  fit_mv,
  method = "beta_binomial",
  n_bins = 8,
  p_value_tail = "two.sided"
)

summary(pb_mv_bb)
#> Publication Bias Model (MARS)
#> Method: beta_binomial 
#> Structure: UN 
#> Effects: 48  | Studies: 8 
#> P-value tail: two.sided 
#> Convergence: 0 - Converged 
#> Objective: -17.57 
#> 
#> Selection Parameters:
#>  parameter estimate
#>      alpha   1.2740
#>       beta   0.9773
#>     n_bins   8.0000
#> 
#> Selection Weight Profile (bin probabilities):
#>  bin p_lower p_upper weight
#>    1   0.000   0.125 0.0777
#>    2   0.125   0.250 0.0993
#>    3   0.250   0.375 0.1134
#>    4   0.375   0.500 0.1243
#>    5   0.500   0.625 0.1336
#>    6   0.625   0.750 0.1420
#>    7   0.750   0.875 0.1501
#>    8   0.875   1.000 0.1596
#> 
#> Bias-Adjusted Coefficients:
#>                       term estimate std_error z_value p_value
#>  factor(data[[effectID]])1  -0.1744    0.2298 -0.7589  0.4479
#>  factor(data[[effectID]])2  -0.2887    0.1571 -1.8379  0.0661
#>  factor(data[[effectID]])3   0.3471    0.0878  3.9538  0.0001
#>  factor(data[[effectID]])4   0.5116    0.1128  4.5341  0.0000
#>  factor(data[[effectID]])5  -0.4142    0.1094 -3.7847  0.0002
#>  factor(data[[effectID]])6  -0.4337    0.0530 -8.1894  0.0000
#> 
#> Adjusted vs Unadjusted Summary:
#>                       term unadjusted_estimate unadjusted_se
#>  factor(data[[effectID]])1             -0.0977        0.1378
#>  factor(data[[effectID]])2             -0.1756        0.0862
#>  factor(data[[effectID]])3              0.3187        0.0841
#>  factor(data[[effectID]])4              0.5272        0.0334
#>  factor(data[[effectID]])5             -0.4176        0.0459
#>  factor(data[[effectID]])6             -0.4007        0.0418
#>  unadjusted_ci_lower unadjusted_ci_upper estimate std_error adjusted_ci_lower
#>              -0.3678              0.1723  -0.1744    0.2298           -0.6249
#>              -0.3445             -0.0066  -0.2887    0.1571           -0.5967
#>               0.1539              0.4834   0.3471    0.0878            0.1750
#>               0.4618              0.5926   0.5116    0.1128            0.2905
#>              -0.5075             -0.3276  -0.4142    0.1094           -0.6286
#>              -0.4827             -0.3187  -0.4337    0.0530           -0.5376
#>  adjusted_ci_upper estimate_diff percent_change direction_changed ci_overlap
#>             0.2761       -0.0767       -78.4704             FALSE       TRUE
#>             0.0192       -0.1132       -64.4697             FALSE       TRUE
#>             0.5191        0.0284         8.9112             FALSE       TRUE
#>             0.7328       -0.0155        -2.9493             FALSE       TRUE
#>            -0.1997        0.0034         0.8131             FALSE       TRUE
#>            -0.3299       -0.0330        -8.2440             FALSE       TRUE
#> 
#> Small-Study Effects Check (Egger-type):
#> Model: SND ~ precision 
#> Method: Egger-type small-study-effects regression with study-cluster robust SE (CR1) 
#> Intercept: -3.8099 SE: 1.2322 t: -3.092 df: 7 p: 0.0175 
#> 
#> SE note:
#> SE rule: reported SEs use the larger of the Hessian-based and OPG-based values when both are finite, otherwise the finite one. Covariance structure: used the fitted MARS within/between-study covariance blocks directly for the publication-bias likelihood. beta_binomial uses discretized p-value bins; coefficient magnitudes can differ materially from beta_selection.
pb_mv_logit <- publication_bias(
  fit_mv,
  method = "logistic_selection",
  p_value_tail = "two.sided"
)

summary(pb_mv_logit)
#> Publication Bias Model (MARS)
#> Method: logistic_selection 
#> Structure: UN 
#> Effects: 48  | Studies: 8 
#> P-value tail: two.sided 
#> Convergence: 0 - Converged 
#> Objective: -16.75 
#> 
#> Selection Parameters:
#>        parameter estimate
#>  logit_intercept   3.5331
#>      logit_slope  23.1095
#> 
#> Selection Weight Profile (p-value grid):
#>               model p_value weight
#>  logistic_selection  0.0100 1.0000
#>  logistic_selection  0.1189 1.0000
#>  logistic_selection  0.2278 1.0000
#>  logistic_selection  0.3367 1.0000
#>  logistic_selection  0.4456 1.0000
#>  logistic_selection  0.5544 1.0000
#>  logistic_selection  0.6633 1.0000
#>  logistic_selection  0.7722 0.9999
#>  logistic_selection  0.8811 0.9984
#>  logistic_selection  0.9900 0.9774
#> 
#> Bias-Adjusted Coefficients:
#>                       term estimate std_error z_value p_value
#>  factor(data[[effectID]])1  -0.0991    0.2421 -0.4095  0.6822
#>  factor(data[[effectID]])2  -0.2357    0.1671 -1.4110  0.1583
#>  factor(data[[effectID]])3   0.2973    0.1220  2.4359  0.0149
#>  factor(data[[effectID]])4   0.5148    0.0662  7.7805  0.0000
#>  factor(data[[effectID]])5  -0.4169    0.0808 -5.1584  0.0000
#>  factor(data[[effectID]])6  -0.4183    0.0615 -6.7981  0.0000
#> 
#> Adjusted vs Unadjusted Summary:
#>                       term unadjusted_estimate unadjusted_se
#>  factor(data[[effectID]])1             -0.0977        0.1378
#>  factor(data[[effectID]])2             -0.1756        0.0862
#>  factor(data[[effectID]])3              0.3187        0.0841
#>  factor(data[[effectID]])4              0.5272        0.0334
#>  factor(data[[effectID]])5             -0.4176        0.0459
#>  factor(data[[effectID]])6             -0.4007        0.0418
#>  unadjusted_ci_lower unadjusted_ci_upper estimate std_error adjusted_ci_lower
#>              -0.3678              0.1723  -0.0991    0.2421           -0.5736
#>              -0.3445             -0.0066  -0.2357    0.1671           -0.5632
#>               0.1539              0.4834   0.2973    0.1220            0.0581
#>               0.4618              0.5926   0.5148    0.0662            0.3852
#>              -0.5075             -0.3276  -0.4169    0.0808           -0.5753
#>              -0.4827             -0.3187  -0.4183    0.0615           -0.5389
#>  adjusted_ci_upper estimate_diff percent_change direction_changed ci_overlap
#>             0.3753       -0.0014        -1.4307             FALSE       TRUE
#>             0.0917       -0.0602       -34.2677             FALSE       TRUE
#>             0.5365       -0.0214        -6.7152             FALSE       TRUE
#>             0.6445       -0.0123        -2.3418             FALSE       TRUE
#>            -0.2585        0.0006         0.1532             FALSE       TRUE
#>            -0.2977       -0.0176        -4.3802             FALSE       TRUE
#> 
#> Small-Study Effects Check (Egger-type):
#> Model: SND ~ precision 
#> Method: Egger-type small-study-effects regression with study-cluster robust SE (CR1) 
#> Intercept: -3.8099 SE: 1.2322 t: -3.092 df: 7 p: 0.0175 
#> 
#> SE note:
#> SE rule: reported SEs use the larger of the Hessian-based and OPG-based values when both are finite, otherwise the finite one. Covariance structure: used the fitted MARS within/between-study covariance blocks directly for the publication-bias likelihood.

Multivariate coefficients with standard errors:

pb_mv$covariance_mode
#> [1] "fixed_mars_structure"
pb_mv$covariance_structure
#> $structure
#> [1] "UN"
#> 
#> $Tau
#>              [,1]          [,2]         [,3]         [,4]         [,5]
#> [1,]  0.132040025  0.0763014664 -0.031441429  0.003058166 -0.018150222
#> [2,]  0.076301466  0.0441998006 -0.016984791  0.001531606 -0.009860779
#> [3,] -0.031441429 -0.0169847911  0.042292885 -0.007313107  0.009629524
#> [4,]  0.003058166  0.0015316061 -0.007313107  0.001318666 -0.001498453
#> [5,] -0.018150222 -0.0098607790  0.009629524 -0.001498453  0.006384168
#> [6,]  0.002223569  0.0008355477 -0.012254561  0.002277566 -0.002464062
#>               [,6]
#> [1,]  0.0022235691
#> [2,]  0.0008355477
#> [3,] -0.0122545613
#> [4,]  0.0022775661
#> [5,] -0.0024640625
#> [6,]  0.0040360241
pb_mv$coefficients
#>                        term    estimate  std_error    z_value      p_value
#> 1 factor(data[[effectID]])1 -0.05731339 0.17209590 -0.3330317 7.391104e-01
#> 2 factor(data[[effectID]])2 -0.20655358 0.15681039 -1.3172187 1.877653e-01
#> 3 factor(data[[effectID]])3  0.25590590 0.13462471  1.9008836 5.731726e-02
#> 4 factor(data[[effectID]])4  0.50830044 0.26847412  1.8932940 5.831878e-02
#> 5 factor(data[[effectID]])5 -0.40559668 0.29493108 -1.3752253 1.690616e-01
#> 6 factor(data[[effectID]])6 -0.39248053 0.06969686 -5.6312513 1.789068e-08
pb_mv_bb$coefficients
#>                        term   estimate  std_error    z_value      p_value
#> 1 factor(data[[effectID]])1 -0.1744103 0.22982856 -0.7588711 4.479297e-01
#> 2 factor(data[[effectID]])2 -0.2887357 0.15710154 -1.8378924 6.607826e-02
#> 3 factor(data[[effectID]])3  0.3470794 0.08778424  3.9537777 7.692687e-05
#> 4 factor(data[[effectID]])4  0.5116472 0.11284349  4.5341311 5.784109e-06
#> 5 factor(data[[effectID]])5 -0.4141613 0.10942945 -3.7847338 1.538732e-04
#> 6 factor(data[[effectID]])6 -0.4337486 0.05296469 -8.1893924 2.625483e-16
pb_mv_logit$coefficients
#>                        term    estimate  std_error    z_value      p_value
#> 1 factor(data[[effectID]])1 -0.09912321 0.24206557 -0.4094891 6.821808e-01
#> 2 factor(data[[effectID]])2 -0.23571435 0.16706042 -1.4109527 1.582586e-01
#> 3 factor(data[[effectID]])3  0.29728114 0.12204165  2.4358991 1.485483e-02
#> 4 factor(data[[effectID]])4  0.51484944 0.06617171  7.7805071 7.223443e-15
#> 5 factor(data[[effectID]])5 -0.41691705 0.08082318 -5.1583846 2.490896e-07
#> 6 factor(data[[effectID]])6 -0.41826593 0.06152684 -6.7981048 1.060043e-11

The same idea applies to multivariate fits: the publication-bias likelihood is evaluated using the fitted MARS within-study covariance blocks and the fitted between-study Tau matrix, rather than replacing them with a single scalar heterogeneity term.

ref_mv <- extract_full_data_coefficients(fit_mv)
cmp_mv <- compare_pub_bias_3(pb_mv, pb_mv_bb, pb_mv_logit, ref = ref_mv)
cmp_mv
#>                        term estimate_beta_selection se_beta_selection
#> 1 factor(data[[effectID]])1             -0.05731339        0.17209590
#> 2 factor(data[[effectID]])2             -0.20655358        0.15681039
#> 3 factor(data[[effectID]])3              0.25590590        0.13462471
#> 4 factor(data[[effectID]])4              0.50830044        0.26847412
#> 5 factor(data[[effectID]])5             -0.40559668        0.29493108
#> 6 factor(data[[effectID]])6             -0.39248053        0.06969686
#>   estimate_beta_binomial se_beta_binomial estimate_logistic_selection
#> 1             -0.1744103       0.22982856                 -0.09912321
#> 2             -0.2887357       0.15710154                 -0.23571435
#> 3              0.3470794       0.08778424                  0.29728114
#> 4              0.5116472       0.11284349                  0.51484944
#> 5             -0.4141613       0.10942945                 -0.41691705
#> 6             -0.4337486       0.05296469                 -0.41826593
#>   se_logistic_selection estimate_all_data se_all_data
#> 1            0.24206557       -0.09772505  0.13776971
#> 2            0.16706042       -0.17555552  0.08619682
#> 3            0.12204165        0.31868112  0.08406129
#> 4            0.06617171        0.52719552  0.03335209
#> 5            0.08082318       -0.41755660  0.04590156
#> 6            0.06152684       -0.40071392  0.04181907
plot_pub_bias_compare_3(cmp_mv, main = "Multivariate: publication-bias methods with all-data reference")

Jackknife version of the multivariate complete-data fit and each publication-bias model:

set.seed(2026)

becker09_jack <- becker09[seq_len(min(6, nrow(becker09))), , drop = FALSE]

jack_mv_complete <- mars_alt_estimation(
  type = "jackknife",
  data = becker09_jack,
  studyID = "ID",
  effectID = "numID",
  sample_size = "N",
  effectsize_type = "cor",
  varcov_type = "weighted",
  variable_names = c(
    "Cognitive_Performance",
    "Somatic_Performance",
    "Selfconfidence_Performance",
    "Somatic_Cognitive",
    "Selfconfidence_Cognitive",
    "Selfconfidence_Somatic"
  ),
  structure = "UN",
  estimation_method = "MLE",
  jackknife_target = "complete_data"
)

jack_mv_beta_selection <- mars_alt_estimation(
  type = "jackknife",
  data = becker09_jack,
  studyID = "ID",
  effectID = "numID",
  sample_size = "N",
  effectsize_type = "cor",
  varcov_type = "weighted",
  variable_names = c(
    "Cognitive_Performance",
    "Somatic_Performance",
    "Selfconfidence_Performance",
    "Somatic_Cognitive",
    "Selfconfidence_Cognitive",
    "Selfconfidence_Somatic"
  ),
  structure = "UN",
  estimation_method = "MLE",
  jackknife_target = "beta_selection",
  publication_bias_args = list(
    p_value_tail = "two.sided",
    maxit = 80,
    integration_points = 31
  )
)

jack_mv_beta_binomial <- mars_alt_estimation(
  type = "jackknife",
  data = becker09_jack,
  studyID = "ID",
  effectID = "numID",
  sample_size = "N",
  effectsize_type = "cor",
  varcov_type = "weighted",
  variable_names = c(
    "Cognitive_Performance",
    "Somatic_Performance",
    "Selfconfidence_Performance",
    "Somatic_Cognitive",
    "Selfconfidence_Cognitive",
    "Selfconfidence_Somatic"
  ),
  structure = "UN",
  estimation_method = "MLE",
  jackknife_target = "beta_binomial",
  publication_bias_args = list(
    p_value_tail = "two.sided",
    n_bins = 8,
    maxit = 80,
    integration_points = 31
  )
)

jack_mv_logistic <- mars_alt_estimation(
  type = "jackknife",
  data = becker09_jack,
  studyID = "ID",
  effectID = "numID",
  sample_size = "N",
  effectsize_type = "cor",
  varcov_type = "weighted",
  variable_names = c(
    "Cognitive_Performance",
    "Somatic_Performance",
    "Selfconfidence_Performance",
    "Somatic_Cognitive",
    "Selfconfidence_Cognitive",
    "Selfconfidence_Somatic"
  ),
  structure = "UN",
  estimation_method = "MLE",
  jackknife_target = "logistic_selection",
  publication_bias_args = list(
    p_value_tail = "two.sided",
    maxit = 80,
    integration_points = 31
  )
)

jack_mv_complete_stats <- compute_alt_stats(jack_mv_complete, type = "jackknife")
jack_mv_beta_selection_stats <- compute_alt_stats(jack_mv_beta_selection, type = "jackknife")
jack_mv_beta_binomial_stats <- compute_alt_stats(jack_mv_beta_binomial, type = "jackknife")
jack_mv_logistic_stats <- compute_alt_stats(jack_mv_logistic, type = "jackknife")

mv_term <- pb_mv$coefficients$term[1]
jackknife_compare_mv <- rbind(
  complete_data = extract_alt_interval(jack_mv_complete_stats, term = mv_term),
  beta_selection = extract_alt_interval(jack_mv_beta_selection_stats, term = mv_term),
  beta_binomial = extract_alt_interval(jack_mv_beta_binomial_stats, term = mv_term),
  logistic_selection = extract_alt_interval(jack_mv_logistic_stats, term = mv_term)
)

jackknife_compare_mv
#>                    point_estimate    ci_lower   ci_upper
#> complete_data         -0.08983328 -0.20035300 0.01635273
#> beta_selection         0.02405734 -0.08801925 0.06878734
#> beta_binomial         -0.14637973 -0.27612223 0.01877218
#> logistic_selection    -0.07121728 -0.17278774 0.02747445

The multivariate jackknife illustration also uses a small subset so the chunk demonstrates the interface without dominating vignette runtime.

jack_mv_est <- jackknife_compare_mv[, "point_estimate"]
jack_mv_lo <- jackknife_compare_mv[, "ci_lower"]
jack_mv_hi <- jackknife_compare_mv[, "ci_upper"]
xpos_mv <- seq_len(nrow(jackknife_compare_mv))

plot(
  xpos_mv, jack_mv_est,
  ylim = range(c(jack_mv_lo, jack_mv_hi)),
  xaxt = "n", xlab = "", ylab = "Jackknife Estimate",
  pch = 19, col = "#1F78B4",
  main = paste("Multivariate Jackknife for", mv_term)
)
segments(xpos_mv, jack_mv_lo, xpos_mv, jack_mv_hi, lwd = 2, col = "#1F78B4")
abline(h = 0, lty = 2, col = "gray60")
axis(1, at = xpos_mv, labels = rownames(jackknife_compare_mv), las = 2)
title(xlab = "Jackknife Target", line = 6)

Method comparison note: - beta_selection and beta_binomial use different selection-function parameterizations. - Coefficients and SEs can differ materially, especially when beta_binomial uses coarse binning or when Hessian diagnostics indicate weak identifiability. - The all_data reference intervals come from the original MARS fit, while publication-bias intervals come from the publication-bias likelihood with the conservative Hessian-versus-OPG SE rule. - In multilevel and multivariate settings, publication-bias fits now retain the fitted MARS covariance structure through covariance_mode = "fixed_mars_structure".

The same publication-bias interface can be used across univariate, multilevel, and multivariate mars objects, and both methods support funnel plotting.