Overview
The mhtopt package implements the economically optimal
multiple hypothesis testing (MHT) framework from Viviano, Wuthrich, and
Niehaus (2026). Unlike ad hoc procedures (Bonferroni, Holm,
Benjamini-Hochberg), this package derives the correct
adjustment from the structure of research costs.
Key insight: The optimal per-test significance level
is (Proposition 4.1):
\[
\alpha^*(J) = \frac{C(J, n)}{b \cdot \bar{\omega}(J)}
\]
where \(C(J, n)\) is the research
cost function, \(b\) is the
researcher’s benefit from a rejection, and \(\bar{\omega}(J)\) is the sum of hypothesis
weights. This simplifies to closed-form expressions depending on the
cost structure.
Package functions
mht_est() |
Main function: test J treatment coefficients from a regression |
mht_critical() |
Compute optimal critical values for given J |
mht_test() |
Apply adjustment to a vector of p-values |
mht_cost_estimate() |
Estimate cost function parameters from data |
mht_table() |
Generate reference tables of critical values |
Installation
# From CRAN (once published)
install.packages("mhtopt")
# Development version from GitHub
# remotes::install_github("dviviano/mhtopt", subdir = "r/mhtopt")
Quick Start
# After running a regression with multiple treatments
fit <- lm(outcome ~ treat1 + treat2 + treat3 + controls, data = mydata)
# Test all treatment coefficients with optimal MHT adjustment
result <- mht_est(fit,
vars = c("treat1", "treat2", "treat3"),
alpha_bar = 0.05)
# View results: shows rejections under 5 procedures
print(result)
The function returns rejection decisions under five procedures: -
Optimal — derived from cost structure (default: FDA
calibration) - Bonferroni — traditional FWER control -
Holm — step-down Bonferroni -
Benjamini-Hochberg — FDR control -
Unadjusted — no MHT correction
When to Adjust?
The paper’s central insight: the need for MHT adjustment depends on
the research cost function — specifically, whether
costs exhibit returns to scale with respect to the number of
hypotheses.
Cost structure determines the optimal correction
Fixed costs dominate (cf_share near
1, or beta near 0): Adding more hypotheses barely increases
costs → researchers have incentive to “test many and get lucky” →
optimal correction approaches Bonferroni
Variable costs dominate (cf_share
near 0, or beta near 1): Each hypothesis costs
proportionally more → cost itself deters excessive testing →
little/no adjustment needed
Intermediate (empirically relevant): Optimal
threshold lies between Bonferroni and unadjusted, determined by cost
parameters
Default calibrations
Linear model (FDA calibration):
# Default: cf_share = 0.46, J_bar = 3 (from Sertkaya et al. 2016)
mht_critical(J = 5, alpha_bar = 0.05)
#>
#> ------------------------------------------------------------
#> Optimal MHT Critical Values
#> Viviano, Wuthrich, and Niehaus (2026)
#> ------------------------------------------------------------
#>
#> Cost model: Linear (Eq. 27)
#> Number of hypotheses: 5
#> Benchmark alpha: 0.0500
#> Sample size ratio: 1.00
#>
#> ------------------------------------------------------------
#> Optimal test size: 0.021250
#> Optimal z-threshold: 2.0286
#>
#> Bonferroni size: 0.010000
#> Bonferroni z-thresh: 2.3263
#> Sidak size: 0.010206
#> Sidak z-threshold: 2.3187
#> Unadjusted size: 0.050000
#> ------------------------------------------------------------
Cobb-Douglas (J-PAL calibration):
# beta = 0.13, iota = 0.075 (from J-PAL data, Appendix A)
mht_critical(J = 5, alpha_bar = 0.05, model = "cobbdouglas")
#>
#> ------------------------------------------------------------
#> Optimal MHT Critical Values
#> Viviano, Wuthrich, and Niehaus (2026)
#> ------------------------------------------------------------
#>
#> Cost model: Cobb-Douglas (Appendix A)
#> Number of hypotheses: 5
#> Benchmark alpha: 0.0500
#> Sample size ratio: 1.00
#>
#> ------------------------------------------------------------
#> Optimal test size: 0.012327
#> Optimal z-threshold: 2.2468
#>
#> Bonferroni size: 0.010000
#> Bonferroni z-thresh: 2.3263
#> Sidak size: 0.010206
#> Sidak z-threshold: 2.3187
#> Unadjusted size: 0.050000
#> ------------------------------------------------------------
Notice: Cobb-Douglas is more conservative because beta ≈ 0
implies near-constant marginal cost (fixed-cost-like).
Main Function: mht_est()
Tests J coefficients from a fitted regression model.
Basic usage
mht_est(fit,
vars = NULL, # coefficient names (default: all non-intercept)
alpha_bar, # benchmark significance level
onesided = TRUE, # one-sided (positive) vs two-sided
model = "linear", # "linear" or "cobbdouglas"
cf_share = 0.46, # fixed cost share (linear model)
J_bar = 3, # avg arms per trial (linear model)
nm_ratio = 1.0, # sample size ratio n/m
mbar = NULL, # benchmark arm size (overrides nm_ratio)
beta = 0.13, # arms elasticity (Cobb-Douglas)
iota = 0.075) # sample elasticity (Cobb-Douglas)
Supported model classes
lm — base R linear models
glm — generalized linear models
lm_robust, iv_robust — from
estimatr (cluster/HC standard errors)
fixest — from fixest (feols,
fepois, etc.)
- Any class with a
coef(summary(fit)) method
Return value
A data.frame with columns:
term — coefficient name
estimate, std_error, t_stat,
p_value — from the model
reject_optimal, reject_bonferroni,
reject_holm, reject_bh,
reject_unadjusted — rejection indicators
Attributes: alpha_opt, alpha_bonf,
alpha_bar, J, model,
onesided, nm_ratio
Examples
Multiple treatment arms:
library(estimatr)
fit <- lm_robust(outcome ~ treat_A + treat_B + treat_C + controls,
fixed_effects = ~village,
clusters = ~hh_id,
data = df)
result <- mht_est(fit,
vars = c("treat_A", "treat_B", "treat_C"),
alpha_bar = 0.05)
# Extract optimal alpha used
attr(result, "alpha_opt")
# How many hypotheses rejected under each procedure?
colSums(result[, c("reject_optimal", "reject_bonferroni",
"reject_holm", "reject_bh", "reject_unadjusted")])
Two-sided test:
fit_logit <- glm(uptake ~ treat1 + treat2 + controls,
family = binomial,
data = df)
mht_est(fit_logit,
vars = c("treat1", "treat2"),
alpha_bar = 0.05,
onesided = FALSE)
Using benchmark sample size (mbar):
# If you know typical arm size is 500 observations
# and your study has N = 1500 with J = 3 arms
# then nm_ratio = (1500/3) / 500 = 1.0
fit <- lm(outcome ~ treat1 + treat2 + treat3, data = df)
mht_est(fit,
vars = c("treat1", "treat2", "treat3"),
alpha_bar = 0.05,
mbar = 500) # automatically computes nm_ratio
Custom cost parameters:
# Suppose you estimated cost parameters from your data
est <- mht_cost_estimate(cost, arms, sample_size, alpha_bar = 0.05)
mht_est(fit,
vars = c("treat1", "treat2", "treat3"),
alpha_bar = 0.05,
model = "cobbdouglas",
beta = est$beta,
iota = est$iota)
Supporting Functions
mht_critical() — Compute Optimal Critical Values
The core formula. Computes \(\alpha^*\) for given J and cost model.
# Linear model (default)
mht_critical(J = 5, alpha_bar = 0.05)
#>
#> ------------------------------------------------------------
#> Optimal MHT Critical Values
#> Viviano, Wuthrich, and Niehaus (2026)
#> ------------------------------------------------------------
#>
#> Cost model: Linear (Eq. 27)
#> Number of hypotheses: 5
#> Benchmark alpha: 0.0500
#> Sample size ratio: 1.00
#>
#> ------------------------------------------------------------
#> Optimal test size: 0.021250
#> Optimal z-threshold: 2.0286
#>
#> Bonferroni size: 0.010000
#> Bonferroni z-thresh: 2.3263
#> Sidak size: 0.010206
#> Sidak z-threshold: 2.3187
#> Unadjusted size: 0.050000
#> ------------------------------------------------------------
# Cobb-Douglas
mht_critical(J = 5, alpha_bar = 0.05, model = "cobbdouglas")
#>
#> ------------------------------------------------------------
#> Optimal MHT Critical Values
#> Viviano, Wuthrich, and Niehaus (2026)
#> ------------------------------------------------------------
#>
#> Cost model: Cobb-Douglas (Appendix A)
#> Number of hypotheses: 5
#> Benchmark alpha: 0.0500
#> Sample size ratio: 1.00
#>
#> ------------------------------------------------------------
#> Optimal test size: 0.012327
#> Optimal z-threshold: 2.2468
#>
#> Bonferroni size: 0.010000
#> Bonferroni z-thresh: 2.3263
#> Sidak size: 0.010206
#> Sidak z-threshold: 2.3187
#> Unadjusted size: 0.050000
#> ------------------------------------------------------------
# Larger sample size => less conservative
mht_critical(J = 5, alpha_bar = 0.05, nm_ratio = 2.0)
#>
#> ------------------------------------------------------------
#> Optimal MHT Critical Values
#> Viviano, Wuthrich, and Niehaus (2026)
#> ------------------------------------------------------------
#>
#> Cost model: Linear (Eq. 27)
#> Number of hypotheses: 5
#> Benchmark alpha: 0.0500
#> Sample size ratio: 2.00
#>
#> ------------------------------------------------------------
#> Optimal test size: 0.035312
#> Optimal z-threshold: 1.8079
#>
#> Bonferroni size: 0.010000
#> Bonferroni z-thresh: 2.3263
#> Sidak size: 0.010206
#> Sidak z-threshold: 2.3187
#> Unadjusted size: 0.050000
#> ------------------------------------------------------------
Cost model formulas:
Linear (Eq. 26 of the paper): \[
\alpha^* = \alpha_{\text{bar}} \left[ \frac{1 + r/J}{1 + r} + \frac{n/m
- 1}{1 + r} \right]
\] where \(r = \frac{c_f \cdot
\bar{J}}{c_v \cdot m} = \frac{\text{cf\_share} \cdot J_{\text{bar}}}{1 -
\text{cf\_share}}\)
Cobb-Douglas: \[
\alpha^* = \alpha_{\text{bar}} \cdot J^{\beta - 1} \cdot (n/m)^\iota
\]
mht_test() — Test a Vector of P-Values
Use when you have pre-computed p-values (not from a regression).
pvals <- c(0.003, 0.015, 0.030, 0.048, 0.120, 0.500)
result <- mht_test(p = pvals, alpha_bar = 0.05)
print(result)
#>
#> -----------------------------------------------------------------
#> Multiple Hypothesis Testing Results
#> Viviano, Wuthrich, and Niehaus (2026)
#> -----------------------------------------------------------------
#>
#> Hypotheses tested: 6
#> Benchmark alpha: 0.0500
#> Cost model: Linear
#>
#> -------------------------------------------------------
#> Procedure Test size Rejections
#> -------------------------------------------------------
#> Optimal (model-based) 0.020052 2
#> Bonferroni 0.008333 1
#> Holm (step-down) 0.0500 * 1
#> BH (FDR control) 0.0500 * 2
#> Unadjusted 0.050000 4
#> -------------------------------------------------------
#> * Step-wise; effective threshold varies by rank
#>
#> Detailed results:
#> p_value reject_optimal reject_bonferroni reject_holm reject_bh
#> 0.003 TRUE TRUE TRUE TRUE
#> 0.015 TRUE FALSE FALSE TRUE
#> 0.030 FALSE FALSE FALSE FALSE
#> 0.048 FALSE FALSE FALSE FALSE
#> 0.120 FALSE FALSE FALSE FALSE
#> 0.500 FALSE FALSE FALSE FALSE
#> reject_unadjusted
#> TRUE
#> TRUE
#> TRUE
#> TRUE
#> FALSE
#> FALSE
Alternatively, pass z-statistics instead of p-values:
zstats <- c(3.2, 2.1, 1.8, 1.3, 0.8, -0.2)
mht_test(z = zstats, alpha_bar = 0.05)
#>
#> -----------------------------------------------------------------
#> Multiple Hypothesis Testing Results
#> Viviano, Wuthrich, and Niehaus (2026)
#> -----------------------------------------------------------------
#>
#> Hypotheses tested: 6
#> Benchmark alpha: 0.0500
#> Cost model: Linear
#>
#> -------------------------------------------------------
#> Procedure Test size Rejections
#> -------------------------------------------------------
#> Optimal (model-based) 0.020052 2
#> Bonferroni 0.008333 1
#> Holm (step-down) 0.0500 * 1
#> BH (FDR control) 0.0500 * 1
#> Unadjusted 0.050000 3
#> -------------------------------------------------------
#> * Step-wise; effective threshold varies by rank
#>
#> Detailed results:
#> p_value reject_optimal reject_bonferroni reject_holm reject_bh
#> 0.0006871379 TRUE TRUE TRUE TRUE
#> 0.0178644206 TRUE FALSE FALSE FALSE
#> 0.0359303191 FALSE FALSE FALSE FALSE
#> 0.0968004846 FALSE FALSE FALSE FALSE
#> 0.2118553986 FALSE FALSE FALSE FALSE
#> 0.5792597094 FALSE FALSE FALSE FALSE
#> reject_unadjusted
#> TRUE
#> TRUE
#> TRUE
#> FALSE
#> FALSE
#> FALSE
mht_table() — Generate Reference Tables
Reproduces Table 1 from the paper (or custom tables).
# Default: reproduces paper Table 1 exactly
# (J = 1..9 plus infinity, multiple alpha_bar and nm_ratio values)
mht_table()
#>
#> =====================================================================================================================================================
#> Optimal Critical Values (Linear (Eq. 27) model)
#> Viviano, Wuthrich, and Niehaus (2026)
#> =====================================================================================================================================================
#>
#> n/m=50% n/m=100% n/m=150% n/m=200% Sidak
#> -----------------------------------------------------------------------------------------------------------------------------------------------------
#> |J| a=0.025 a=0.050 a=0.100 a=0.150 a=0.025 a=0.050 a=0.100 a=0.150 a=0.025 a=0.050 a=0.100 a=0.150 a=0.025 a=0.050 a=0.100 a=0.150 a=0.025 a=0.050
#> -----------------------------------------------------------------------------------------------------------------------------------------------------
#> 1 0.021 0.043 0.086 0.129 0.025 0.050 0.100 0.150 0.029 0.057 0.114 0.171 0.032 0.064 0.128 0.192 0.025 0.050
#> 2 0.012 0.025 0.050 0.075 0.016 0.032 0.064 0.096 0.020 0.039 0.078 0.117 0.023 0.046 0.092 0.138 0.013 0.025
#> 3 0.010 0.019 0.038 0.057 0.013 0.026 0.052 0.078 0.017 0.033 0.066 0.099 0.020 0.040 0.080 0.120 0.008 0.017
#> 4 0.008 0.016 0.032 0.048 0.012 0.023 0.046 0.069 0.015 0.030 0.060 0.090 0.019 0.037 0.074 0.111 0.006 0.013
#> 5 0.007 0.014 0.028 0.043 0.011 0.021 0.042 0.064 0.014 0.028 0.057 0.085 0.018 0.035 0.071 0.106 0.005 0.010
#> 6 0.007 0.013 0.026 0.039 0.010 0.020 0.040 0.060 0.014 0.027 0.054 0.081 0.017 0.034 0.068 0.102 0.004 0.009
#> 7 0.006 0.012 0.024 0.036 0.010 0.019 0.038 0.058 0.013 0.026 0.052 0.079 0.017 0.033 0.067 0.100 0.004 0.007
#> 8 0.006 0.012 0.023 0.035 0.009 0.019 0.037 0.056 0.013 0.026 0.051 0.077 0.016 0.033 0.065 0.098 0.003 0.006
#> 9 0.006 0.011 0.022 0.033 0.009 0.018 0.036 0.054 0.013 0.025 0.050 0.075 0.016 0.032 0.064 0.096 0.003 0.006
#> Inf 0.004 0.007 0.014 0.021 0.007 0.014 0.028 0.042 0.011 0.021 0.042 0.063 0.014 0.028 0.056 0.084 0.000 0.000
#> -----------------------------------------------------------------------------------------------------------------------------------------------------
# Custom table
mht_table(alpha_bar = 0.05,
J_range = 1:10,
nm_ratios = c(0.5, 1.0, 1.5, 2.0))
#>
#> =====================================================
#> Optimal Critical Values (Linear (Eq. 27) model)
#> Viviano, Wuthrich, and Niehaus (2026)
#> =====================================================
#>
#> n/m=50% n/m=100% n/m=150% n/m=200% Sidak
#> -----------------------------------------------------
#> |J| a=0.050 a=0.050 a=0.050 a=0.050 a=0.025 a=0.050
#> -----------------------------------------------------
#> 1 0.043 0.050 0.057 0.064 0.025 0.050
#> 2 0.025 0.032 0.039 0.046 0.013 0.025
#> 3 0.019 0.026 0.033 0.040 0.008 0.017
#> 4 0.016 0.023 0.030 0.037 0.006 0.013
#> 5 0.014 0.021 0.028 0.035 0.005 0.010
#> 6 0.013 0.020 0.027 0.034 0.004 0.009
#> 7 0.012 0.019 0.026 0.033 0.004 0.007
#> 8 0.012 0.019 0.026 0.033 0.003 0.006
#> 9 0.011 0.018 0.025 0.032 0.003 0.006
#> 10 0.011 0.018 0.025 0.032 0.003 0.005
#> -----------------------------------------------------
Returns a formatted table and stores all values in
attr(result, "values").
Estimating Cost Functions
mht_cost_estimate() — Estimate Parameters from
Data
If you have data on research project costs, you can estimate cost
function parameters.
Cobb-Douglas model (default):
Estimates: \(\log(C) = \text{const} + \beta
\log(J) + \iota \log(n) + \text{controls}\)
Returns \(\beta\) and \(\iota\) with hypothesis tests: - \(H_0: \beta = 0\) ⇒ Bonferroni is optimal -
\(H_0: \beta = 1\) ⇒ no adjustment
needed
# Example data: cost, number of arms, sample size
projects <- data.frame(
cost = c(500000, 750000, 1200000, 600000, 900000),
arms = c(2, 3, 5, 2, 4),
n = c(1000, 1500, 2000, 800, 1200)
)
est <- mht_cost_estimate(
cost = projects$cost,
arms = projects$arms,
sample_size = projects$n,
alpha_bar = 0.05,
model = "cobbdouglas"
)
# View estimates
print(est)
# Use in mht_est()
fit <- lm(outcome ~ treat1 + treat2 + treat3, data = mydata)
mht_est(fit,
vars = c("treat1", "treat2", "treat3"),
alpha_bar = 0.05,
model = "cobbdouglas",
beta = est$beta,
iota = est$iota)
Linear model:
est_linear <- mht_cost_estimate(
cost = projects$cost,
arms = projects$arms,
sample_size = projects$n,
alpha_bar = 0.05,
model = "linear_share"
)
# Returns cf_share (fixed cost share)
mht_est(fit,
vars = c("treat1", "treat2", "treat3"),
alpha_bar = 0.05,
cf_share = est_linear$cf_share)
Worked Example
# Simulate a simple experiment
set.seed(123)
n <- 300
df <- data.frame(
treat1 = rbinom(n, 1, 0.33),
treat2 = rbinom(n, 1, 0.33),
treat3 = rbinom(n, 1, 0.33)
)
df$outcome <- 0.3 * df$treat1 + 0.2 * df$treat2 + 0.05 * df$treat3 + rnorm(n)
# Estimate effects
fit <- lm(outcome ~ treat1 + treat2 + treat3, data = df)
summary(fit)
# Apply optimal MHT adjustment
result <- mht_est(fit,
vars = c("treat1", "treat2", "treat3"),
alpha_bar = 0.05)
print(result)
# Compare: how many rejections under each procedure?
colSums(result[, grep("^reject_", names(result))])
One-Sided vs Two-Sided Tests
Default behavior (onesided = TRUE): tests \(H_0: \beta_j \leq 0\) vs \(H_1: \beta_j > 0\).
Justification (Remark 4.1, paper v10): When the
status quo is “no intervention,” a negative effect means the status quo
holds (no social value lost by failing to reject). One-sided tests are
appropriate.
For two-sided tests (Appendix B.1):
mht_est(fit,
vars = c("treat1", "treat2", "treat3"),
alpha_bar = 0.05,
onesided = FALSE)
P-values are multiplied by 2 and compared against the same \(\alpha^*\).
Sample Size Adjustment
The nm_ratio parameter captures how your study’s sample
size compares to a benchmark:
\[
\text{nm\_ratio} = \frac{n/J}{m}
\]
where: - \(n\) = total sample size
in your study - \(J\) = number of
hypotheses/arms - \(m\) = benchmark
per-arm sample size
Larger studies are more expensive → optimal \(\alpha^*\) is less conservative
(encourages research):
# Small study (half the benchmark size)
mht_critical(J = 3, alpha_bar = 0.05, nm_ratio = 0.5)
#>
#> ------------------------------------------------------------
#> Optimal MHT Critical Values
#> Viviano, Wuthrich, and Niehaus (2026)
#> ------------------------------------------------------------
#>
#> Cost model: Linear (Eq. 27)
#> Number of hypotheses: 3
#> Benchmark alpha: 0.0500
#> Sample size ratio: 0.50
#>
#> ------------------------------------------------------------
#> Optimal test size: 0.019010
#> Optimal z-threshold: 2.0746
#>
#> Bonferroni size: 0.016667
#> Bonferroni z-thresh: 2.1280
#> Sidak size: 0.016952
#> Sidak z-threshold: 2.1212
#> Unadjusted size: 0.050000
#> ------------------------------------------------------------
# Benchmark size
mht_critical(J = 3, alpha_bar = 0.05, nm_ratio = 1.0)
#>
#> ------------------------------------------------------------
#> Optimal MHT Critical Values
#> Viviano, Wuthrich, and Niehaus (2026)
#> ------------------------------------------------------------
#>
#> Cost model: Linear (Eq. 27)
#> Number of hypotheses: 3
#> Benchmark alpha: 0.0500
#> Sample size ratio: 1.00
#>
#> ------------------------------------------------------------
#> Optimal test size: 0.026042
#> Optimal z-threshold: 1.9424
#>
#> Bonferroni size: 0.016667
#> Bonferroni z-thresh: 2.1280
#> Sidak size: 0.016952
#> Sidak z-threshold: 2.1212
#> Unadjusted size: 0.050000
#> ------------------------------------------------------------
# Large study (double the benchmark)
mht_critical(J = 3, alpha_bar = 0.05, nm_ratio = 2.0)
#>
#> ------------------------------------------------------------
#> Optimal MHT Critical Values
#> Viviano, Wuthrich, and Niehaus (2026)
#> ------------------------------------------------------------
#>
#> Cost model: Linear (Eq. 27)
#> Number of hypotheses: 3
#> Benchmark alpha: 0.0500
#> Sample size ratio: 2.00
#>
#> ------------------------------------------------------------
#> Optimal test size: 0.040104
#> Optimal z-threshold: 1.7495
#>
#> Bonferroni size: 0.016667
#> Bonferroni z-thresh: 2.1280
#> Sidak size: 0.016952
#> Sidak z-threshold: 2.1212
#> Unadjusted size: 0.050000
#> ------------------------------------------------------------
The mbar argument automates this: if you know the
typical per-arm sample size in your field, pass it via
mbar, and the function computes nm_ratio
automatically from the model’s sample size.
References
Viviano, D., Wuthrich, K., & Niehaus, P. (2026). A Model of
Multiple Hypothesis Testing. arXiv:2104.13367v10.
Sertkaya, A., Wong, H. H., Jessup, A., & Beleche, T. (2016). Key
cost drivers of pharmaceutical clinical trials in the United States.
Clinical Trials, 13(2), 117-126.