--- title: "Introduction to bayprior" author: "Ndoh Penn" date: "`r Sys.Date()`" output: rmarkdown::html_vignette: toc: true toc_depth: 3 number_sections: true vignette: > %\VignetteIndexEntry{Introduction to bayprior} %\VignetteEngine{knitr::rmarkdown} %\VignetteEncoding{UTF-8} --- ```{r setup, include = FALSE} knitr::opts_chunk$set( collapse = TRUE, comment = "#>", fig.width = 6, fig.height = 3.8, out.width = "95%", dpi = 120, warning = FALSE, message = FALSE ) ``` # Overview **bayprior** is an R package for principled Bayesian prior elicitation, conflict detection, and sensitivity analysis in clinical trials. It supports the documented, systematic prior construction process called for in the FDA's 2026 draft guidance, *Use of Bayesian Methodology in Clinical Trials of Drug and Biological Products*, and implements the SHELF elicitation framework (O'Hagan et al., 2006). The core analytical workflow is: 1. **Elicit** — Specify a prior from expert knowledge or historical data 2. **Pool** — Combine priors from multiple experts into a consensus prior 3. **Diagnose** — Detect conflict between the prior and observed data 4. **Analyse** — Assess how sensitive posterior conclusions are to prior choice 5. **Robust** — Construct robust, sceptical, or power priors as alternatives 6. **Report** — Generate a regulatory-ready prior justification report --- # Prior Elicitation ## Beta Prior (Binary Endpoints) The Beta distribution is the natural conjugate prior for a binary response rate $\theta \in (0, 1)$. `elicit_beta()` supports two elicitation methods. ### Quantile Matching The expert specifies quantiles directly — for example: "I believe the true response rate has a 5% chance of being below 10%, a median of around 30%, and a 95% chance of being below 60%." ```{r beta-quantile} library(bayprior) prior_q <- elicit_beta( quantiles = c("0.05" = 0.10, "0.50" = 0.30, "0.95" = 0.60), expert_id = "Expert_1", label = "Response rate (treatment arm)" ) print(prior_q) ``` ### Moment Matching Alternatively, elicit the prior mean and standard deviation directly: ```{r beta-moments} prior_m <- elicit_beta( mean = 0.30, sd = 0.10, method = "moments", expert_id = "Expert_1", label = "Response rate (treatment arm)" ) plot(prior_m) ``` The shaded region shows the 95% credible interval and the dashed vertical line marks the prior mean. ## Normal Prior (Continuous Endpoints) For continuous quantities such as mean differences or log odds ratios: ```{r normal-prior} prior_nor <- elicit_normal( quantiles = c("0.025" = -0.5, "0.50" = 0.20, "0.975" = 0.90), label = "Log odds ratio" ) print(prior_nor) ``` ## Gamma Prior (Rate / Count Endpoints) For positive-valued quantities such as Poisson event rates or median survival: ```{r gamma-prior} prior_gam <- elicit_gamma( mean = 5, sd = 2, method = "moments", label = "Median OS (months)" ) plot(prior_gam) ``` ## Log-Normal Prior (Hazard Ratios) For multiplicative quantities that must remain positive: ```{r lognormal-prior} prior_ln <- elicit_lognormal( quantiles = c("0.05" = 0.40, "0.50" = 0.70, "0.95" = 1.20), label = "Hazard ratio (treatment vs control)" ) plot(prior_ln) ``` ## Roulette Method The roulette method (Oakley & O'Hagan, 2010) converts chip allocations across pre-defined bins into a parametric prior via quantile matching. It is available interactively through the Shiny app and programmatically here: ```{r roulette} prior_rou <- elicit_roulette( chips = c(0L, 2L, 5L, 8L, 5L, 2L, 1L), breaks = seq(0, 0.7, by = 0.1), family = "beta", label = "Response rate" ) print(prior_rou) ``` --- # Expert Pooling When multiple experts contribute, their priors are aggregated into a single consensus prior via `aggregate_experts()`. ## Linear Pooling Linear pooling produces a weighted mixture of the individual priors. It is the most common approach in clinical trial settings (externally Bayesian): ```{r linear-pooling} p1 <- elicit_beta(mean = 0.25, sd = 0.08, method = "moments", expert_id = "E1", label = "Response rate") p2 <- elicit_beta(mean = 0.35, sd = 0.10, method = "moments", expert_id = "E2", label = "Response rate") p3 <- elicit_beta(mean = 0.30, sd = 0.09, method = "moments", expert_id = "E3", label = "Response rate") consensus <- aggregate_experts( priors = list(E1 = p1, E2 = p2, E3 = p3), weights = c(0.40, 0.30, 0.30), method = "linear" ) print(consensus) plot(consensus) ``` `aggregate_experts()` automatically computes pairwise Bhattacharyya coefficients and warns when expert disagreement is substantial. ## Manual Mixture Construction You can also build a mixture prior directly from components: ```{r manual-mixture} mix <- elicit_mixture( components = list(p1, p2), weights = c(0.5, 0.5), label = "50-50 pooled prior" ) plot(mix) ``` --- # Prior-Data Conflict Diagnostics Before updating the prior with trial data it is good practice to check compatibility between the prior and the observed data. `prior_conflict()` computes four complementary diagnostics: | Diagnostic | Interpretation | |---|---| | Box p-value | Prior predictive p-value; < 0.05 flags conflict | | Surprise index | Standardised distance; > 3 indicates high surprise | | KL divergence | Information distance from prior to likelihood | | Bhattacharyya overlap | Distributional overlap; < 0.3 is concerning | ```{r conflict} prior <- elicit_beta( mean = 0.30, sd = 0.10, method = "moments", label = "Response rate" ) # Observed: 18 events in 40 patients cd <- prior_conflict( prior = prior, data_summary = list(type = "binary", x = 18, n = 40), # also: "continuous", "poisson", "survival" alpha = 0.05 ) print(cd) ``` Visualise the prior-likelihood-posterior overlay to assess compatibility graphically: ```{r overlay, fig.height = 4} plot_prior_likelihood( prior, data_summary = list(type = "binary", x = 18, n = 40), show_posterior = TRUE ) ``` When the prior (blue) and likelihood (orange) overlap substantially there is no meaningful conflict, and the posterior (green) represents a credible update. --- # Sensitivity Analysis Regulatory guidelines require demonstrating that trial conclusions are robust to plausible prior variations. `sensitivity_grid()` evaluates how posterior inferences change across a grid of hyperparameter values. Note: the grids below are intentionally coarse for vignette build speed. In practice, use finer grids (e.g. `seq(1, 8, 0.5)`) to obtain smooth surfaces. ```{r sensitivity, cache = TRUE} sa <- sensitivity_grid( prior = prior, data_summary = list(type = "binary", x = 14, n = 40), # also: "continuous", "poisson", "survival" param_grid = list(alpha = seq(1, 6, 1), beta = seq(2, 14, 2)), target = c("posterior_mean", "prob_efficacy"), threshold = 0.30 ) ``` ## Tornado Plot The tornado plot ranks posterior quantities by their range across the hyperparameter grid. Wider bars indicate higher prior sensitivity: ```{r tornado, fig.height = 3} plot_tornado(sa) ``` ## Influence Heatmap The heatmap shows how the posterior mean changes as both hyperparameters vary. The orange diamond marks the reference prior: ```{r heatmap, fig.height = 4} plot_sensitivity(sa, target = "posterior_mean") ``` ## Credible Interval Sensitivity `sensitivity_cri()` focuses specifically on the posterior credible interval width — a key regulatory quantity: ```{r cri-sensitivity, cache = TRUE, fig.height = 4} cri_sa <- sensitivity_cri( prior = prior, data_summary = list(type = "binary", x = 14, n = 40), # also: "continuous", "poisson", "survival" param_grid = list(alpha = seq(1, 6, 1), beta = seq(2, 14, 2)), cri_level = 0.95, threshold = 0.30 ) plot_sensitivity(cri_sa, target = "cri_width") ``` --- # Robust and Sceptical Priors When prior-data conflict is detected, or when a conservative regulatory stance is required, bayprior provides ready-made alternative prior constructions. ## Robust Mixture Prior A robust prior mixes an informative component with a vague (diffuse) component (Schmidli et al., 2014). The vague component ensures the posterior is never dominated by a conflicting informative prior. The default `vague_weight = 0.20` gives an 80/20 informative/vague split: ```{r robust, fig.height = 3.5} 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) ``` ## Sceptical Prior A sceptical prior is centred at the null value of the treatment effect and represents a conservative regulatory stance (Spiegelhalter et al., 1994). For a **Normal** family (e.g. log odds ratio, mean difference), `null_value` is the null treatment effect (typically 0): ```{r sceptical-normal, fig.height = 3.5} sc_norm <- sceptical_prior( null_value = 0, family = "normal", strength = "moderate", label = "Log odds ratio (sceptical)" ) plot(sc_norm) ``` For a **Beta** family (binary response rate), `null_value` must be in (0, 1): ```{r sceptical-beta, fig.height = 3.5} sc_beta <- sceptical_prior( null_value = 0.20, # null response rate of 20% family = "beta", strength = "moderate", label = "Response rate (sceptical)" ) plot(sc_beta) ``` --- # Power Prior Calibration When relevant historical data exist, a power prior down-weights the historical evidence by $\delta \in (0, 1]$ before incorporating it. `calibrate_power_prior()` selects $\delta$ to achieve a target Bayes Factor (Ibrahim & Chen, 2000; Gravestock & Held, 2017). ```{r power-prior, cache = TRUE} 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.10, 1.0, by = 0.10), # coarse grid for vignette speed method = "bayes_factor" ) print(calib) plot(calib) ``` The calibration curves show the Bayes Factor (top) and Box p-value (bottom) across the $\delta$ grid. The vertical dotted green line marks the optimal weight and the horizontal dashed lines mark the target BF and $\alpha = 0.05$. --- # Multivariate Conflict Diagnostics For trials with co-primary endpoints, `conflict_mahalanobis()` extends the conflict check to the multivariate setting. The Mahalanobis distance has a known $\chi^2$ reference distribution under compatibility: ```{r mahal} mv <- conflict_mahalanobis( prior_means = c(0.35, 0.60), prior_cov = matrix(c(0.01, 0.003, 0.003, 0.015), 2, 2), obs_means = c(0.55, 0.58), obs_cov = matrix(c(0.008, 0.002, 0.002, 0.010), 2, 2) / 50, labels = c("Response rate", "OS rate") ) print(mv) ``` The `marginal_z` scores identify which endpoint contributes most to any detected conflict. --- # Generating a Prior Justification Report Once the analysis is complete, `prior_report()` renders a self-contained regulatory-ready document in HTML, PDF, or Word format. The report includes the prior specification, conflict diagnostics, sensitivity visualisations, and an FDA/EMA compliance checklist. ```{r report, eval = FALSE} # prior_report() calls rmarkdown::render() internally and cannot be run # inside a vignette build. Run interactively after loading bayprior. prior_report( prior = prior, conflict = cd, sensitivity = sa, output_format = "html", # or "pdf" or "docx" trial_name = "TRIAL-001", sponsor = "BioPharma Ltd", author = "J. Smith", notes = paste0( "Prior based on Phase 2 internal data and two external expert ", "elicitations. Sensitivity analysis confirms robustness across a ", "wide range of prior hyperparameter values." ) ) ``` --- # Complete Worked Example The following end-to-end example mirrors a realistic binary-endpoint trial workflow, combining three expert opinions and assessing sensitivity. ```{r full-example, cache = TRUE, fig.height = 4} # 1. Elicit priors from three experts using different methods e1 <- elicit_beta(mean = 0.28, sd = 0.08, method = "moments", expert_id = "E1", label = "ORR") e2 <- elicit_beta(mean = 0.35, sd = 0.10, method = "moments", expert_id = "E2", label = "ORR") e3 <- elicit_beta( quantiles = c("0.10" = 0.15, "0.50" = 0.30, "0.90" = 0.52), expert_id = "E3", label = "ORR" ) # 2. Pool into a consensus prior consensus <- aggregate_experts( priors = list(E1 = e1, E2 = e2, E3 = e3), weights = c(0.40, 0.35, 0.25), method = "linear" ) # 3. Check conflict with interim data (20 responses in 55 patients) data_obs <- list(type = "binary", x = 20, n = 55) cd_full <- prior_conflict(consensus, data_obs) print(cd_full) # 4. Visualise prior-likelihood-posterior plot_prior_likelihood(consensus, data_obs, show_posterior = TRUE) ``` ```{r full-sensitivity, cache = TRUE, fig.height = 3} # 5. Sensitivity analysis (coarse grid for vignette speed) sa_full <- sensitivity_grid( prior = consensus, data_summary = data_obs, param_grid = list(alpha = seq(1, 6, 1), beta = seq(2, 12, 2)), target = c("posterior_mean", "prob_efficacy"), threshold = 0.25 ) plot_tornado(sa_full) ``` ```{r full-heatmap, cache = TRUE, fig.height = 4} plot_sensitivity(sa_full, target = "prob_efficacy") ``` ```{r full-robust, fig.height = 3.5} # 6. Robust alternative in case conflict worsens at final analysis rob_full <- robust_prior(consensus, vague_weight = 0.20) plot(rob_full) ``` --- # Function Reference | Function | Purpose | |---|---| | `elicit_beta()` | Beta prior for binary endpoints — support (0, 1) | | `elicit_normal()` | Normal prior for continuous endpoints | | `elicit_gamma()` | Gamma prior for rate/count endpoints | | `elicit_lognormal()` | Log-normal prior for hazard ratios | | `elicit_exponential()` | Exponential prior for constant hazard / Poisson rates | | `elicit_weibull()` | Weibull prior for non-constant hazard survival times | | `elicit_roulette()` | Chip-based roulette elicitation | | `elicit_mixture()` | Manual mixture prior construction | | `aggregate_experts()` | Pool multiple expert priors | | `prior_conflict()` | Univariate prior-data conflict diagnostics | | `conflict_mahalanobis()` | Multivariate conflict diagnostics | | `sensitivity_grid()` | Grid sensitivity of posterior quantities | | `sensitivity_cri()` | Credible interval sensitivity | | `plot_sensitivity()` | Heatmap / line plot of sensitivity results | | `plot_tornado()` | Tornado plot of prior influence | | `plot_prior_likelihood()` | Prior-likelihood-posterior overlay | | `robust_prior()` | Robust mixture prior | | `sceptical_prior()` | Sceptical prior centred at null | | `calibrate_power_prior()` | Calibrated power prior from historical data | | `prior_report()` | Regulatory prior justification report | --- # References Box, G. E. P. (1980). Sampling and Bayes' inference in scientific modelling and robustness. *Journal of the Royal Statistical Society A*, 143, 383–430. Gravestock, I. & Held, L. (2017). Adaptive power priors with empirical Bayes for clinical trials. *Pharmaceutical Statistics*, 16, 349–360. Ibrahim, J. G. & Chen, M.-H. (2000). Power prior distributions for regression models. *Statistical Science*, 15, 46–60. O'Hagan, A., Buck, C. E., Daneshkhah, A., Eiser, J. R., Garthwaite, P. H., Jenkinson, D. J., Oakley, J. E., & Rakow, T. (2006). *Uncertain Judgements: Eliciting Experts' Probabilities*. Wiley. Oakley, J. E. & O'Hagan, A. (2010). SHELF: the Sheffield Elicitation Framework. University of Sheffield. 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.