Beyond the elicited informative prior, regulatory submissions typically require one or more alternative prior specifications to demonstrate robustness. bayprior provides three well-established alternatives:
Prior type
Function
Method
Reference
MAP (from historical trials)
map_prior()
Random-effects meta-analysis with explicit tau prior
Roever et al. (2021)
Robust mixture
robust_prior()
Mixes informative + vague
Schmidli et al. (2014)
Sceptical
sceptical_prior()
Centred at null effect
Spiegelhalter et al. (1994)
Power prior
calibrate_power_prior()
Down-weights historical data
Ibrahim & Chen (2000)
Robust Mixture Prior
Concept
A robust prior protects against prior misspecification by mixing the informative elicited prior with a vague (diffuse) component:
The vague component — a wide Normal centred at the informative prior mean — ensures the posterior is never completely dominated by a conflicting prior. The default weight is \(w = 0.20\) (80% informative, 20% vague).
Deriving the Informative Prior from Historical Trials (MAP)
Concept
The examples above assume the informative prior fed into robust_prior() already exists (from elicitation, or otherwise). In practice, that informative component is often itself derived from multiple historical trials via a random-effects meta-analysis. map_prior() performs this step directly: given trial-level effect estimates and standard errors, it returns a meta-analytic-predictive (MAP) prior suitable for passing straight into robust_prior().
Unlike treating the between-trial heterogeneity (the parameter \(\tau\)) as fixed, map_prior() always places an explicit prior on \(\tau\), following the weakly-informative recommendations of Roever et al. (2021). This matters because how much heterogeneity is assumed among historical trials directly affects how much borrowing is ultimately justified — an assumption that should be stated and justified explicitly, not left as an unstated default. Sensible outcome-specific defaults are built in; see ?resolve_tau_prior.
map_prior() fits this model itself, entirely in base R: the posterior of \(\mu\) given \(\tau\) has a closed form, so only the marginal posterior of \(\tau\) needs numerical integration (stats::integrate()). bayprior has no dependency, direct or optional, on any external meta-analysis package for this. See ?map_prior for the underlying model.
Usage
# Five historical control-arm trials -- i.e. a single-arm log-odds scale,# not a two-arm log-odds RATIO. This distinction matters: see below.y <-c(0.10, -0.05, 0.22, 0.05, 0.15)se <-c(0.12, 0.15, 0.10, 0.18, 0.14)map <-map_prior( y, se,outcome_type ="single_arm_log_odds",label ="Historical control (log-odds)")print(map)#> #> -- bayprior: Historical control (log-odds) --#> • Distribution : NORMAL#> • Parameters : mu = 0.1115, sigma = 0.0944#> • Method : map elicitation#> • Expert : map_historical#> • Mean (SD) : 0.1115 (0.0944)#> • 95% CrI : [-0.0736, 0.2966]
map_prior() returns an ordinary bayprior object (a Normal approximation to the marginal posterior of the meta-analytic mean, integrating over \(\tau\)), so it can be used exactly like any elicited prior:
The example above assumes y/se are already computed. In practice they’re usually derived from each historical trial’s own reported summary statistics (event counts, arm means/SDs, …), and metafor::escalc() is the standard tool for that conversion. historical_effect_sizes() is a thin wrapper around it that maps map_prior()’s outcome_type vocabulary onto the matching metafor measure, so raw trial data can be turned into y/se in one step:
# The same five trials as above, but as originally reported -- event# counts (xi out of ni) rather than pre-computed log-odds estimates.trials <-data.frame(xi =c(11, 9, 14, 10, 12),ni =c(45, 40, 55, 48, 42))es <-historical_effect_sizes("single_arm_log_odds", trials)es[, c("xi", "ni", "y", "se")]#> xi ni y se#> 1 11 45 -1.1284653 0.3468730#> 2 9 40 -1.2367626 0.3786412#> 3 14 55 -1.0745147 0.3095461#> 4 10 48 -1.3350011 0.3554093#> 5 12 42 -0.9162907 0.3415650map_from_counts <-map_prior(y = es$y, se = es$se,outcome_type ="single_arm_log_odds",label ="Historical control (from raw counts)")print(map_from_counts)#> #> -- bayprior: Historical control (from raw counts) --#> • Distribution : NORMAL#> • Parameters : mu = -1.1311, sigma = 0.203#> • Method : map elicitation#> • Expert : map_historical#> • Mean (SD) : -1.1311 (0.203)#> • 95% CrI : [-1.529, -0.7332]
This is not the only way to get y/se – they can equally be read by hand off a published point estimate and 95% CI (e.g. se = (log(upper) - log(lower)) / (2 * 1.96) on whatever scale the CI was reported) – and historical_effect_sizes() is only usable if metafor is installed (it is a Suggests dependency, not Imports, so it is not required just to use map_prior() itself). See ?historical_effect_sizes for the full column-name mapping across outcome types – two-arm odds ratios, standardised/raw mean differences, incidence-rate ratios, and correlations, in addition to the single-arm case shown here.
Choosing the Heterogeneity Prior Scale
outcome_type is not just a label: unless tau_prior is supplied explicitly, it selects the default \(\tau\) prior from Roever et al. (2021)’s outcome-specific recommendations, via resolve_tau_prior():
resolve_tau_prior("log_or") # two-arm log-odds ratio#> $family#> [1] "half_normal"#> #> $scale#> [1] 0.5resolve_tau_prior("single_arm_log_odds") # e.g. a historical control rate#> $family#> [1] "half_normal"#> #> $scale#> [1] 1
Note that these give different scales (0.5 vs 1.0), despite both being “log-odds” in casual speech – they are different quantities with different heterogeneity properties, and conflating them silently applies the wrong default. "mean_difference" (raw, unstandardised units) has no preset at all – its scale is entirely endpoint-specific (mmHg, cm, days, …), so map_prior() requires tau_prior to be supplied explicitly for it:
# An explicit tau_prior always overrides the outcome_type preset, and is# required (map_prior() errors otherwise) for "mean_difference".map_tight <-map_prior(y, se, tau_prior =list(family ="half_normal", scale =0.1))map_wide <-map_prior(y, se, tau_prior =list(family ="half_normal", scale =1.0))cat("Tight tau prior -- posterior SD:", round(map_tight$fit_summary$sd, 4), "\n")#> Tight tau prior -- posterior SD: 0.0683cat("Wide tau prior -- posterior SD:", round(map_wide$fit_summary$sd, 4), "\n")#> Wide tau prior -- posterior SD: 0.0944
Allowing more heterogeneity (a wider \(\tau\) prior) generally widens the resulting informative prior, which in turn results in a more conservative (less confident) prior being carried forward into robust_prior().
Inspecting the Heterogeneity Posterior
The full posterior distribution of \(\tau\) (evaluated on a plotting grid whose range is chosen automatically from the fitted posterior, not a fixed guess) is available via $tau_posterior, useful for checking whether the data are actually informative about heterogeneity or whether the posterior for \(\tau\) still closely tracks the prior:
plot(map$tau_posterior$tau, map$tau_posterior$density, type ="l",xlab =expression(tau), ylab ="Posterior density",main ="Posterior distribution of the heterogeneity SD")
Sceptical Prior
Concept
The sceptical prior (Spiegelhalter & Freedman, 1994) represents the view of a conservative regulator who is sceptical of a treatment effect. It is centred at the null value of the treatment effect with width calibrated to a chosen strength of scepticism.
This sensitivity prior follows Spiegelhalter and Freedman’s (1994) approach for trials using informative priors: the trial conclusions should hold even under a prior that places most mass at “no effect.”
Normal Family (Mean Differences, Log Odds Ratios)
For continuous or log-scale quantities where the null is typically 0:
sc_weak <-sceptical_prior(null_value =0, family ="normal", strength ="weak",label ="Log OR (weak sceptic)")sc_moderate <-sceptical_prior(null_value =0, family ="normal", strength ="moderate",label ="Log OR (moderate sceptic)")sc_strong <-sceptical_prior(null_value =0, family ="normal", strength ="strong",label ="Log OR (strong sceptic)")cat("Weak SD: ", sc_weak$fit_summary$sd, "\n")#> Weak SD: 1cat("Moderate SD:", sc_moderate$fit_summary$sd, "\n")#> Moderate SD: 0.5cat("Strong SD: ", sc_strong$fit_summary$sd, "\n")#> Strong SD: 0.25
plot(sc_moderate)
The SD mapping by strength:
Strength
SD
Interpretation
weak
1.0
Vague scepticism — wide prior around null
moderate
0.5
2-SD departure from null has ~5% prior probability
strong
0.25
Very concentrated at null — very sceptical
Beta Family (Response Rates)
For binary endpoints, null_value must be in \((0, 1)\) — it represents the null response rate, not a difference:
# Null response rate of 20%: sceptic believes treatment is no better than 20%sc_beta <-sceptical_prior(null_value =0.20,family ="beta",strength ="moderate",label ="Response rate (sceptical)")plot(sc_beta)
Log-Normal Family (Hazard Ratios)
For hazard ratios, the null is HR = 1, which corresponds to null_value = 0 on the log scale:
sc_hr <-sceptical_prior(null_value =0, # log(1) = 0, i.e. HR = 1family ="lognormal",strength ="moderate",label ="Hazard ratio (sceptical)")plot(sc_hr)
Enthusiastic vs Sceptical Pair
A common regulatory sensitivity practice is to present conclusions under both an enthusiastic prior (favouring treatment benefit) and a sceptical prior, following Spiegelhalter and Freedman (1994):
enthusiastic <-elicit_beta(mean =0.45, sd =0.08,method ="moments", label ="Response rate (enthusiastic)")sceptical <-sceptical_prior(null_value =0.20, family ="beta", strength ="moderate",label ="Response rate (sceptical)")data_obs <-list(type ="binary", x =18, n =40)post_enth <- bayprior:::.conjugate_update(enthusiastic, data_obs)post_scep <- bayprior:::.conjugate_update(sceptical, data_obs)cat("Posterior mean (enthusiastic):", round(post_enth$fit_summary$mean, 3), "\n")#> Posterior mean (enthusiastic): 0.45cat("Posterior mean (sceptical): ", round(post_scep$fit_summary$mean, 3), "\n")#> Posterior mean (sceptical): 0.356cat("Posterior SD (enthusiastic): ", round(post_enth$fit_summary$sd, 3), "\n")#> Posterior SD (enthusiastic): 0.056cat("Posterior SD (sceptical): ", round(post_scep$fit_summary$sd, 3), "\n")#> Posterior SD (sceptical): 0.059
Power Prior
Concept
The power prior (Ibrahim & Chen, 2000) provides a principled method for incorporating historical data by down-weighting it by a factor \(\delta \in (0, 1]\):
where \(D_0\) is the historical data and \(\delta\) controls how much weight it receives. \(\delta = 1\) fully incorporates the historical data (standard Bayesian updating); \(\delta \to 0\) ignores it entirely.
Calibrating \(\delta\)
calibrate_power_prior() selects \(\delta\) to achieve a target Bayes Factor between the historical-data-informed prior and the current likelihood — ensuring the historical data is incorporated only to the extent it is compatible with current data:
base <-elicit_beta(mean =0.50,sd =0.20,method ="moments",label ="Response rate")calib <-calibrate_power_prior(historical_data =list(type ="binary", x =12, n =40),current_data =list(type ="binary", x =18, n =50),base_prior = base,target_bf =3,delta_grid =seq(0.05, 1.0, by =0.05),method ="bayes_factor")print(calib)#> #> -- Power Prior Calibration --#> • Method : bayes_factor#> • Target BF : 3#> • Optimal delta : 0.05#> • Power prior mean: 0.4448#> • Power prior SD : 0.173
plot(calib)
The calibration curves show:
Top panel: Bayes Factor vs \(\delta\). The dashed red line is the target BF; the dotted green line marks the optimal \(\delta\).
Bottom panel: Box p-value vs \(\delta\). Values below 0.05 indicate conflict at that weight.
Compatibility Method
Alternatively, select \(\delta\) to be the largest value for which the historical-data-informed prior shows no conflict with the current data:
calib_compat <-calibrate_power_prior(historical_data =list(type ="binary", x =12, n =40),current_data =list(type ="binary", x =18, n =50),base_prior = base,method ="compatibility",delta_grid =seq(0.05, 1.0, by =0.05))cat("Optimal delta (BF method): ", calib$delta_opt, "\n")#> Optimal delta (BF method): 0.05cat("Optimal delta (compatibility method):", calib_compat$delta_opt, "\n")#> Optimal delta (compatibility method): 1
Power Prior for Other Families
Power prior updating is supported for Beta, Normal, Gamma, Log-Normal, and Mixture priors. For a Normal prior with continuous historical data:
base_norm <-elicit_normal(mean =0.0, sd =0.5,method ="moments", label ="Mean difference")calib_norm <-calibrate_power_prior(historical_data =list(type ="continuous", x =0.35, sd =0.3, n =60),current_data =list(type ="continuous", x =0.42, sd =0.3, n =80),base_prior = base_norm,target_bf =3,delta_grid =seq(0.05, 1.0, by =0.10),method ="bayes_factor")print(calib_norm)#> #> -- Power Prior Calibration --#> • Method : bayes_factor#> • Target BF : 3#> • Optimal delta : 0.95#> • Power prior mean: 0.3478#> • Power prior SD : 0.0396
Choosing the Right Alternative Prior
library(knitr)kable(data.frame(Situation =c("No conflict, regulatory requirement","Mild conflict detected","Severe conflict detected","Historical data available","Enthusiastic/sceptical pair for sensitivity analysis" ),`Recommended prior`=c("Robust mixture (w = 0.20)","Robust mixture (w = 0.30-0.40)","Sceptical prior (moderate-strong)","Power prior (calibrated)","Sceptical prior as second arm" ),check.names =FALSE), align ="ll")
Situation
Recommended prior
No conflict, regulatory requirement
Robust mixture (w = 0.20)
Mild conflict detected
Robust mixture (w = 0.30-0.40)
Severe conflict detected
Sceptical prior (moderate-strong)
Historical data available
Power prior (calibrated)
Enthusiastic/sceptical pair for sensitivity analysis
Sceptical prior as second arm
Including Robust Priors in the Regulatory Report
All three robust prior types — robust mixture, sceptical, and power prior — are automatically included in the downloaded prior justification report when they have been computed in the session. Pass them directly to prior_report():
# After running the analyses above...prior_report(prior = prior,conflict = cd,sensitivity = sa,robust_prior = rob, # adds "Robust Mixture" section to reportsceptical_prior = scep, # adds "Sceptical Prior" section to reportpower_prior = calib, # adds "Power Prior" section with calibration tableoutput_format ="html",output_file ="prior_justification_report",trial_name ="TRIAL-001",sponsor ="BioPharma Ltd",author ="J. Smith, Biostatistician")
Each section in the report includes a parameter summary table and the corresponding density or calibration plot. The compliance checklist in the report automatically marks “Robust / sceptical prior computed” as Complete when any of the three types is supplied.
When using the Shiny app, the robust priors flow into the report automatically — simply run the analyses in the Robust Priors panel before clicking Download Report.
Note:prior_report() requires devtools::install(), not just devtools::load_all(). Quarto spawns a fresh R session that requires the package to be properly installed.
References
Ibrahim, J. G. & Chen, M.-H. (2000). Power prior distributions for regression models. Statistical Science, 15, 46–60.
Schmidli, H., Gsteiger, S., Roychoudhury, S., O’Hagan, A., Spiegelhalter, D., & Neuenschwander, B. (2014). Robust meta-analytic-predictive priors in clinical trials with historical control information. Biometrics, 70, 1023–1032.
Spiegelhalter, D. J., Freedman, L. S., & Parmar, M. K. B. (1994). Bayesian approaches to randomized trials. Journal of the Royal Statistical Society A, 157, 357–416.
Gravestock, I. & Held, L. (2017). Adaptive power priors with empirical Bayes for clinical trials. Pharmaceutical Statistics, 16, 349–360.
Roever, C., Bender, R., Dias, S., Schmid, C. H., Schmidli, H., Sturtz, S., Weber, S., & Friede, T. (2021). On weakly informative prior distributions for the heterogeneity parameter in Bayesian random-effects meta-analysis. Research Synthesis Methods, 12(4), 448–474.