Author: Ndoh Penn
Date: 2026-05-29

# Sensitivity Analysis

#### Ndoh Penn

#### 2026-05-29

# Why Sensitivity Analysis Is Required

Regulatory guidelines — including the FDA Draft Guidance on Bayesian Statistical Methods (2026) and ICH E9(R1) — require demonstrating that trial conclusions are robust to plausible variations in prior specification.

bayprior provides two dedicated functions:

- `sensitivity_grid()` — evaluates posterior mean, SD, and efficacy probability
- `sensitivity_cri()` — focuses on credible interval width and bounds

> Sensitivity analysis is fully independent of conflict diagnostics. You can run it without first running `prior_conflict()` by supplying `data_summary` directly.

# Supported Data Types

`type =` | Conjugate update | `param_grid` names
--- | --- | ---
`"binary"` | Beta-Binomial | `alpha`, `beta`
`"continuous"` | Normal-Normal | `mu`, `sigma`
`"poisson"` | Gamma-Poisson | `shape`, `rate`
`"survival"` | Gamma-Exponential | `shape`, `rate`

# Binary Data

    prior    <- elicit_beta(mean = 0.30, sd = 0.10, method = "moments",
                            label = "Response rate")
    data_obs <- list(type = "binary", x = 14, n = 40)

    sa <- sensitivity_grid(
      prior        = prior,
      data_summary = data_obs,
      param_grid   = list(alpha = seq(1, 8, 1), beta = seq(2, 20, 2)),
      target       = c("posterior_mean", "prob_efficacy"),
      threshold    = 0.30
    )
    sa$influence_scores

    ## posterior_mean  prob_efficacy
    ##      0.1940984      0.8184416

    plot_tornado(sa)

    plot_sensitivity(sa, target = "posterior_mean")

## Credible Interval Sensitivity

    cri_sa <- sensitivity_cri(
      prior        = prior,
      data_summary = data_obs,
      param_grid   = list(alpha = seq(1, 8, 1), beta = seq(2, 20, 2)),
      cri_level    = 0.95
    )
    cri_sa$influence_scores

    ##      cri_lower      cri_upper      cri_width posterior_mean   posterior_sd
    ##     0.15946768     0.21751316     0.06678301     0.19409836     0.01716165

    plot_sensitivity(cri_sa, target = "cri_width")

# Poisson / Count Data

Sensitivity with Poisson data uses Gamma-Poisson conjugate updating.

    prior_ae  <- elicit_gamma(mean = 0.15, sd = 0.06, method = "moments",
                               label = "AE rate (per person-year)")
    data_pois <- list(type = "poisson", x = 18, n = 120)

    sa_pois <- sensitivity_grid(
      prior        = prior_ae,
      data_summary = data_pois,
      param_grid   = list(shape = seq(2, 10, 1), rate = seq(5, 40, 5)),
      target       = c("posterior_mean", "prob_efficacy"),
      threshold    = 0.20
    )
    sa_pois$influence_scores

    ## posterior_mean  prob_efficacy
    ##      0.0990000      0.6908443

    plot_tornado(sa_pois)

    plot_sensitivity(sa_pois, target = "posterior_mean")

# Survival / Time-to-Event Data

Sensitivity with survival data uses Gamma-Exponential conjugate updating.

    prior_hz  <- elicit_exponential(mean = 0.05, method = "moments",
                                     label = "OS hazard rate")
    data_surv <- list(type = "survival", x = 30, n = 600)

    sa_surv <- sensitivity_grid(
      prior        = prior_hz,
      data_summary = data_surv,
      param_grid   = list(shape = seq(1, 5, 0.5), rate = seq(5, 30, 5)),
      target       = c("posterior_mean", "prob_efficacy"),
      threshold    = 0.10
    )
    sa_surv$influence_scores

    ## posterior_mean  prob_efficacy
    ##   2.033320e-03   8.021621e-06

    plot_tornado(sa_surv)

    cri_surv <- sensitivity_cri(
      prior        = prior_hz,
      data_summary = data_surv,
      param_grid   = list(shape = seq(1, 5, 0.5), rate = seq(5, 30, 5)),
      cri_level    = 0.95
    )
    plot_sensitivity(cri_surv, target = "cri_width")

# Continuous Endpoints

    prior_cont <- elicit_normal(mean = 0.0, sd = 0.3, method = "moments",
                                 label = "Log odds ratio")
    sa_cont <- sensitivity_grid(
      prior        = prior_cont,
      data_summary = list(type = "continuous", x = 0.20, sd = 0.25, n = 60),
      param_grid   = list(mu = seq(-0.5, 0.5, 0.1), sigma = seq(0.1, 0.8, 0.1)),
      target       = c("posterior_mean", "posterior_sd")
    )
    plot_tornado(sa_cont)

# Interpreting Influence Scores

Score | Sensitivity | Implication
--- | --- | ---
< 0.05 | Not sensitive | Prior has negligible influence
0.05 – 0.15 | Moderate | Report alongside primary estimate
> 0.15 | Sensitive | Consider robust prior; emphasise data

# Mixture Prior Sensitivity

    e1  <- elicit_beta(mean = 0.25, sd = 0.08, method = "moments",
                       expert_id = "E1", label = "ORR")
    e2  <- elicit_beta(mean = 0.40, sd = 0.10, method = "moments",
                       expert_id = "E2", label = "ORR")
    mix <- aggregate_experts(list(E1 = e1, E2 = e2), weights = c(0.5, 0.5))

    sa_mix <- sensitivity_grid(
      prior        = mix,
      data_summary = list(type = "binary", x = 14, n = 40),
      param_grid   = list(alpha = seq(1, 8, 1), beta = seq(2, 16, 2)),
      target       = "posterior_mean"
    )
    plot_tornado(sa_mix)

Note: for mixture priors, the grid varies the dominant component’s parameters. A compatibility warning is shown in the Shiny app.

# Regulatory Reporting

Include in the clinical study report:

1. Influence score table — one row per posterior quantity
2. Tornado plot — ordered from most to least sensitive
3. Heatmap — for the primary efficacy quantity
4. Classification statement — sensitive / not sensitive per quantity

All three are generated automatically by `prior_report()` when a `bayprior_sensitivity` object is supplied.

# References

- FDA (2026). Draft Guidance: Bayesian Statistical Methods for Drug and Biological Products.
- ICH E9(R1) (2019). Addendum on Estimands and Sensitivity Analysis.