--- title: "Prior-Data Conflict Diagnostics" author: "Ndoh Penn" date: "`r Sys.Date()`" output: rmarkdown::html_vignette vignette: > %\VignetteIndexEntry{Prior-Data Conflict Diagnostics} %\VignetteEngine{knitr::rmarkdown} %\VignetteEncoding{UTF-8} --- ```{r setup, include=FALSE} knitr::opts_chunk$set(echo = TRUE, fig.width = 7, fig.height = 4, message = FALSE, warning = FALSE) library(bayprior) ``` # Why Conflict Diagnostics Matter Before updating a prior with trial data it is essential to check whether the two are compatible. Substantial prior-data conflict indicates one of three things: the prior was mis-specified, the data are anomalous, or the model is wrong. Regulators increasingly require evidence that conflict was assessed and addressed. **bayprior** provides four complementary univariate diagnostics and one multivariate diagnostic, following Box (1980) and related work. The conflict module supports **four data types**: binary, continuous, Poisson/count, and survival. --- # Supported Data Types | Data type | `type =` argument | Conjugate update | Typical endpoint | |---|---|---|---| | Binary | `"binary"` | Beta–Binomial | Response rate, ORR | | Continuous | `"continuous"` | Normal–Normal | Mean difference, HbA1c | | Poisson / count | `"poisson"` | Gamma–Poisson | Adverse event rate | | Survival | `"survival"` | Gamma–Exponential | Hazard rate, OS, PFS | --- # Univariate Conflict: `prior_conflict()` ## Binary Data ```{r binary-prior} prior <- elicit_beta( mean = 0.30, sd = 0.10, method = "moments", label = "Response rate" ) ``` ```{r binary-none} # No-conflict: data consistent with prior cd_none <- prior_conflict( prior = prior, data_summary = list(type = "binary", x = 13, n = 40), alpha = 0.05 ) print(cd_none) ``` ```{r binary-mild} # Mild conflict cd_mild <- prior_conflict( prior = prior, data_summary = list(type = "binary", x = 20, n = 40) ) print(cd_mild) ``` ```{r binary-severe} # Severe conflict cd_severe <- prior_conflict( prior = prior, data_summary = list(type = "binary", x = 35, n = 40) ) print(cd_severe) ``` --- # The Four Diagnostics Explained ## 1. Box's Prior Predictive P-value $$p_{\text{Box}} = 2\Phi\!\left(-\left|\frac{\hat\theta - \mu_\pi} {\sqrt{\sigma_\pi^2 + \text{SE}^2}}\right|\right)$$ **Interpretation:** $p < 0.05$ flags conflict at the 5% level. ## 2. Surprise Index $$S = \left|\frac{\hat\theta - \mu_\pi} {\sqrt{\sigma_\pi^2 + \text{SE}^2}}\right|$$ **Interpretation:** $S > 2$ moderate; $S > 3$ high surprise. ## 3. KL Divergence $$\text{KL}(P \| Q) = \log\frac{\sigma_Q}{\sigma_P} + \frac{\sigma_P^2 + (\mu_P - \mu_Q)^2}{2\sigma_Q^2} - \frac12$$ **Interpretation:** $> 1$ indicates large information distance. ## 4. Bhattacharyya Overlap Coefficient $$\text{BC} = \exp\!\left( -\frac{(\mu_P - \mu_Q)^2}{4(\sigma_P^2 + \sigma_Q^2)}\right) \cdot \left(\frac{2\sigma_P\sigma_Q} {\sigma_P^2 + \sigma_Q^2}\right)^{1/2}$$ **Interpretation:** 1 = identical distributions; 0 = no overlap. ## Threshold Summary ```{r thresholds, echo=FALSE} knitr::kable(data.frame( Diagnostic = c("Box p-value", "Surprise index", "KL divergence", "Bhattacharyya overlap"), `No conflict` = c(">= 0.05", "< 2", "< 0.5", "> 0.6"), `Mild conflict` = c("0.01-0.05", "2-3", "0.5-1", "0.3-0.6"), `Severe conflict` = c("< 0.01", "> 3", "> 1", "< 0.3"), check.names = FALSE ), align = "lrrr") ``` --- # Prior-Likelihood-Posterior Overlay ```{r overlay-none} plot_prior_likelihood( prior, data_summary = list(type = "binary", x = 13, n = 40), show_posterior = TRUE ) ``` *No conflict: prior and likelihood overlap substantially.* ```{r overlay-severe} plot_prior_likelihood( prior, data_summary = list(type = "binary", x = 35, n = 40), show_posterior = TRUE ) ``` *Severe conflict: prior (blue) and likelihood (orange) widely separated.* --- # Continuous Endpoint Conflict ```{r continuous} prior_norm <- elicit_normal( mean = 0.0, sd = 0.3, method = "moments", label = "Log odds ratio" ) # No conflict: observed log OR consistent with prior prior_conflict( prior_norm, data_summary = list(type = "continuous", x = 0.15, sd = 0.20, n = 80) ) ``` --- # Poisson / Count Data Conflict {#poisson} Poisson/count data arises for endpoints such as adverse event rates, where the observed data consists of a count of events over an exposure period (person-time). The conjugate update uses a **Gamma-Poisson** model: $$\text{Prior: Gamma}(\alpha, \beta) \quad\Rightarrow\quad \text{Posterior: Gamma}(\alpha + x,\; \beta + n)$$ where $x$ = observed events and $n$ = total person-time. ```{r poisson-setup} # Gamma prior on adverse event rate # Expert believes the AE rate is ~0.15 events per person-year (SD 0.06) prior_ae <- elicit_gamma( mean = 0.15, sd = 0.06, method = "moments", label = "Adverse event rate (per person-year)", expert_id = "Safety Expert" ) print(prior_ae) ``` ```{r poisson-noconflict} # Observed: 12 AEs over 100 person-years => rate 0.12 — consistent with prior cd_pois_none <- prior_conflict( prior = prior_ae, data_summary = list(type = "poisson", x = 12, n = 100), alpha = 0.05 ) print(cd_pois_none) ``` ```{r poisson-conflict} # Observed: 40 AEs over 100 person-years => rate 0.40 — much higher than prior cd_pois_conf <- prior_conflict( prior = prior_ae, data_summary = list(type = "poisson", x = 40, n = 100) ) print(cd_pois_conf) ``` ```{r poisson-overlay} plot_prior_likelihood( prior_ae, data_summary = list(type = "poisson", x = 40, n = 100), show_posterior = TRUE ) ``` --- # Survival / Time-to-Event Data Conflict {#survival} Survival data conflict arises for endpoints like OS and PFS hazard rates. The observed data consists of event counts over a total follow-up time (person-months). The conjugate update uses a **Gamma-Exponential** model: $$\text{Prior: Gamma}(\alpha, \beta) \quad\Rightarrow\quad \text{Posterior: Gamma}(\alpha + d,\; \beta + T)$$ where $d$ = observed events and $T$ = total follow-up time. An Exponential prior (constant hazard) is equivalent to $\text{Gamma}(1, \lambda)$. ```{r survival-setup} # Exponential prior on hazard rate # Expert believes median OS ≈ 20 months => hazard ≈ 0.05 prior_hz <- elicit_exponential( mean = 0.05, method = "moments", label = "OS hazard rate", expert_id = "Clinical Expert" ) print(prior_hz) ``` ```{r survival-noconflict} # Observed: 20 deaths over 400 person-months => hazard 0.05 — consistent cd_surv_none <- prior_conflict( prior = prior_hz, data_summary = list(type = "survival", x = 20, n = 400), alpha = 0.05 ) print(cd_surv_none) ``` ```{r survival-conflict} # Observed: 60 deaths over 400 person-months => hazard 0.15 — much higher cd_surv_conf <- prior_conflict( prior = prior_hz, data_summary = list(type = "survival", x = 60, n = 400) ) print(cd_surv_conf) ``` ```{r survival-overlay} plot_prior_likelihood( prior_hz, data_summary = list(type = "survival", x = 60, n = 400), show_posterior = TRUE ) ``` Also works with a Gamma prior on the hazard rate: ```{r survival-gamma} prior_hz_gamma <- elicit_gamma( mean = 0.05, sd = 0.02, method = "moments", label = "OS hazard rate (Gamma prior)" ) prior_conflict( prior_hz_gamma, data_summary = list(type = "survival", x = 20, n = 400) ) ``` --- # Compatibility Validation bayprior warns when the prior family is atypical for the chosen data type, helping users avoid inadvertent misspecification: ```{r compat-warning} # Warning: Beta prior with Poisson data is unusual prior_beta <- elicit_beta(mean = 0.15, sd = 0.06, method = "moments", label = "Rate") # The app shows a warning notification; the function still runs with # a Normal approximation cd_warn <- prior_conflict( prior_beta, data_summary = list(type = "poisson", x = 15, n = 100) ) ``` --- # Mixture Prior Conflict All diagnostics work transparently with mixture priors via a Normal approximation from the mixture's `fit_summary`: ```{r mixture-conflict} e1 <- elicit_beta(mean = 0.25, sd = 0.08, method = "moments", expert_id = "E1", label = "ORR") e2 <- elicit_beta(mean = 0.40, sd = 0.12, method = "moments", expert_id = "E2", label = "ORR") mix <- aggregate_experts(list(E1 = e1, E2 = e2), weights = c(0.5, 0.5)) prior_conflict(mix, list(type = "binary", x = 18, n = 40)) ``` --- # Multivariate Conflict: `conflict_mahalanobis()` For trials with co-primary endpoints, the Mahalanobis distance provides an omnibus multivariate conflict test: ```{r mahalanobis} conflict_mahalanobis( prior_means = c(0.35, 0.60), prior_cov = matrix(c(0.010, 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"), alpha = 0.05 ) ``` --- # Conflict Severity Classification | Severity | Recommended action | |---|---| | **None** | Proceed — prior is consistent with data | | **Mild** | Consider reporting both prior-weighted and likelihood-only estimates | | **Severe** | Revise prior or use a robust/power prior; report sensitivity | --- # References - Box, G. E. P. (1980). Sampling and Bayes' inference in scientific modelling and robustness. *JRSS-A*, 143, 383–430. - Bhattacharyya, A. (1943). On a measure of divergence between two statistical populations. *Bulletin of the Calcutta Mathematical Society*, 35, 99–109.