The hardware and bandwidth for this mirror is donated by dogado GmbH, the Webhosting and Full Service-Cloud Provider. Check out our Wordpress Tutorial.
If you wish to report a bug, or if you are interested in having us mirror your free-software or open-source project, please feel free to contact us at mirror[@]dogado.de.
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) |
A robust prior protects against prior misspecification by mixing the informative elicited prior with a vague (diffuse) component:
\[\pi_{\text{robust}}(\theta) = (1 - w) \cdot \pi_{\text{informative}}(\theta) + w \cdot \pi_{\text{vague}}(\theta)\]
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).
informative <- elicit_beta(
mean = 0.30,
sd = 0.08,
method = "moments",
label = "Response rate"
)
rob <- robust_prior(
informative = informative,
vague_weight = 0.20,
label = "Robust mixture prior"
)
plot(rob)cat("Informative component weight:", 1 - rob$vague_weight, "\n")
#> Informative component weight: 0.8
cat("Vague component weight: ", rob$vague_weight, "\n")
#> Vague component weight: 0.2
cat("Mixture mean:", round(rob$fit_summary$mean, 4), "\n")
#> Mixture mean: 0.3
cat("Mixture SD: ", round(rob$fit_summary$sd, 4), "\n")
#> Mixture SD: 0.3649A higher vague weight makes the prior more diffuse and reduces its influence on the posterior:
oldpar <- par(mfrow = c(1, 1))
weights <- c(0.10, 0.20, 0.30, 0.50)
cols <- c("#185FA5", "#1D9E75", "#D85A30", "#888780")
x <- seq(0, 0.8, length.out = 300)
plot(x, bayprior:::.eval_density_vec(informative, x),
type = "l", lwd = 2, col = "#185FA5",
xlab = "Response rate", ylab = "Density",
main = "Effect of vague weight on robust prior",
ylim = c(0, 6))
for (i in seq_along(weights)) {
r <- robust_prior(informative, vague_weight = weights[i])
lines(x, bayprior:::.eval_density_vec(r, x),
col = cols[i], lwd = 1.5, lty = i + 1)
}
legend("topright",
legend = c("Informative",
paste0("w = ", weights)),
col = c("#185FA5", cols),
lwd = 2,
lty = c(1, 2, 3, 4, 5),
bty = "n", cex = 0.8)The vague component’s SD defaults to 10× the informative prior’s SD.
Adjust with vague_sd:
rob_narrow <- robust_prior(informative, vague_weight = 0.20,
vague_sd = 2 * informative$fit_summary$sd)
rob_wide <- robust_prior(informative, vague_weight = 0.20,
vague_sd = 20 * informative$fit_summary$sd)
cat("Narrow vague SD: ", round(rob_narrow$components$vague$fit_summary$sd, 3), "\n")
#> Narrow vague SD: 0.16
cat("Default vague SD:", round(rob$components$vague$fit_summary$sd, 3), "\n")
#> Default vague SD: 0.8
cat("Wide vague SD: ", round(rob_wide$components$vague$fit_summary$sd, 3), "\n")
#> Wide vague SD: 1.6The 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.
# 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)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:
y and se from Raw Trial DataThe 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.3415650
map_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)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.
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.5
resolve_tau_prior("single_arm_log_odds") # e.g. a historical control rate
#> $family
#> [1] "half_normal"
#>
#> $scale
#> [1] 1Note 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.0683
cat("Wide tau prior -- posterior SD:", round(map_wide$fit_summary$sd, 4), "\n")
#> Wide tau prior -- posterior SD: 0.0944Allowing 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().
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")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.”
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: 1
cat("Moderate SD:", sc_moderate$fit_summary$sd, "\n")
#> Moderate SD: 0.5
cat("Strong SD: ", sc_strong$fit_summary$sd, "\n")
#> Strong SD: 0.25The 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 |
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)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 = 1
family = "lognormal",
strength = "moderate",
label = "Hazard ratio (sceptical)"
)
plot(sc_hr)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.45
cat("Posterior mean (sceptical): ", round(post_scep$fit_summary$mean, 3), "\n")
#> Posterior mean (sceptical): 0.356
cat("Posterior SD (enthusiastic): ", round(post_enth$fit_summary$sd, 3), "\n")
#> Posterior SD (enthusiastic): 0.056
cat("Posterior SD (sceptical): ", round(post_scep$fit_summary$sd, 3), "\n")
#> Posterior SD (sceptical): 0.059The power prior (Ibrahim & Chen, 2000) provides a principled method for incorporating historical data by down-weighting it by a factor \(\delta \in (0, 1]\):
\[\pi(\theta | D_0, \delta) \propto L(\theta | D_0)^\delta \cdot \pi_0(\theta)\]
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.
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)The calibration curves show:
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.05
cat("Optimal delta (compatibility method):", calib_compat$delta_opt, "\n")
#> Optimal delta (compatibility method): 1Power 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)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 |
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 report
sceptical_prior = scep, # adds "Sceptical Prior" section to report
power_prior = calib, # adds "Power Prior" section with calibration table
output_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()requiresdevtools::install(), not justdevtools::load_all(). Quarto spawns a fresh R session that requires the package to be properly installed.
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.
These binaries (installable software) and packages are in development.
They may not be fully stable and should be used with caution. We make no claims about them.
Health stats visible at Monitor.