Bayesian safety monitoring

Blinded pooled AE data

Input blinded pooled AE data. Define Bayesian beta prior distribution. Separate the data by AE preferred term. Process interim looks in chronological order. Update the posterior distribution using pooled AE counts and total subjects. Treat the previous posterior as the prior for the next interim look. Generate posterior simulations for the pooled AE rate. Estimate the posterior mean AE rate and 95% credible interval. Calculate the posterior probability that the pooled AE rate exceeds the safety threshold (rate_threshold = 0.10). Apply predefined decision rules. Classify each AE term as: NO SIGNAL WATCH ALERT Generate a blinded pooled Bayesian safety monitoring table.

# =========================================================
# Blinded / Pooled Bayesian Safety Monitoring
# Aggregated AE Monitoring without Treatment Group
# Beta-Binomial Sequential Updating
# =========================================================

bayes_pooled_safety_monitor <- function(
    pooled_data,
    alpha_prior = 0.5,
    beta_prior  = 0.5,
    rate_threshold = 0.10,
    prob_threshold_watch = 0.80,
    prob_threshold_alert = 0.90,
    nsim = 100000,
    seed = 123
) {
  
  set.seed(seed)
  
  ae_terms <- unique(pooled_data$ae_term)
  looks <- sort(unique(pooled_data$look))
  
  results <- data.frame()
  
  for (ae in ae_terms) {
    
    dat_ae <- pooled_data[pooled_data$ae_term == ae, ]
    
    alpha_post <- alpha_prior
    beta_post  <- beta_prior
    
    for (lk in looks) {
      
      dat_lk <- dat_ae[dat_ae$look == lk, ]
      
      if (nrow(dat_lk) == 0) next
      
      ae_count <- dat_lk$ae_count
      n_total  <- dat_lk$n_total
      
      # Sequential Bayesian updating
      # Previous posterior becomes current prior
      alpha_post <- alpha_post + ae_count
      beta_post  <- beta_post  + n_total - ae_count
      
      # Posterior simulation for pooled AE rate
      p_pooled <- rbeta(nsim, alpha_post, beta_post)
      
      post_mean_rate <- mean(p_pooled)
      ci_rate <- quantile(p_pooled, c(0.025, 0.975))
      
      prob_exceed <- mean(p_pooled > rate_threshold)
      
      signal_level <- ifelse(
        prob_exceed >= prob_threshold_alert,
        "ALERT",
        ifelse(
          prob_exceed >= prob_threshold_watch,
          "WATCH",
          "NO SIGNAL"
        )
      )
      
      ae_cum <- alpha_post - alpha_prior
      n_cum  <- alpha_post + beta_post - alpha_prior - beta_prior
      
      results <- rbind(
        results,
        data.frame(
          look = lk,
          ae_term = ae,
          ae_cum = ae_cum,
          n_cum = n_cum,
          observed_pooled_rate = ae_cum / n_cum,
          posterior_alpha = alpha_post,
          posterior_beta = beta_post,
          posterior_mean_rate = post_mean_rate,
          rate_ci_lower = ci_rate[1],
          rate_ci_upper = ci_rate[2],
          rate_threshold = rate_threshold,
          prob_rate_gt_threshold = prob_exceed,
          signal_level = signal_level
        )
      )
    }
  }
  
  return(results)
}


# =========================================================
# Example blinded pooled safety data
# Each row = one AE term at one interim look
# =========================================================

pooled_data <- data.frame(
  look = c(
    1,1,1,
    2,2,2,
    3,3,3,
    4,4,4
  ),
  
  ae_term = c(
    "Nausea", "Headache", "Liver enzyme increased",
    "Nausea", "Headache", "Liver enzyme increased",
    "Nausea", "Headache", "Liver enzyme increased",
    "Nausea", "Headache", "Liver enzyme increased"
  ),
  
  ae_count = c(
    37, 32, 9,
    28, 27, 12,
    31, 29, 15,
    35, 31, 17
  ),
  
  n_total = c(
    400, 400, 400,
    300, 300, 300,
    300, 300, 300,
    400, 400, 400
  )
)



pooled_results <- bayes_pooled_safety_monitor(
  pooled_data = pooled_data,
  alpha_prior = 0.5,
  beta_prior = 0.5,
  rate_threshold = 0.10,
  prob_threshold_watch = 0.80,
  prob_threshold_alert = 0.90,
  nsim = 100000,
  seed = 123
)

print(pooled_results)
##        look                ae_term ae_cum n_cum observed_pooled_rate
## 2.5%      1                 Nausea     37   400           0.09250000
## 2.5%1     2                 Nausea     65   700           0.09285714
## 2.5%2     3                 Nausea     96  1000           0.09600000
## 2.5%3     4                 Nausea    131  1400           0.09357143
## 2.5%4     1               Headache     32   400           0.08000000
## 2.5%5     2               Headache     59   700           0.08428571
## 2.5%6     3               Headache     88  1000           0.08800000
## 2.5%7     4               Headache    119  1400           0.08500000
## 2.5%8     1 Liver enzyme increased      9   400           0.02250000
## 2.5%9     2 Liver enzyme increased     21   700           0.03000000
## 2.5%10    3 Liver enzyme increased     36  1000           0.03600000
## 2.5%11    4 Liver enzyme increased     53  1400           0.03785714
##        posterior_alpha posterior_beta posterior_mean_rate rate_ci_lower
## 2.5%              37.5          363.5          0.09357433    0.06700927
## 2.5%1             65.5          635.5          0.09344477    0.07289916
## 2.5%2             96.5          904.5          0.09635352    0.07891565
## 2.5%3            131.5         1269.5          0.09387029    0.07923488
## 2.5%4             32.5          368.5          0.08108063    0.05664443
## 2.5%5             59.5          641.5          0.08488912    0.06545687
## 2.5%6             88.5          912.5          0.08837508    0.07154270
## 2.5%7            119.5         1281.5          0.08529928    0.07127494
## 2.5%8              9.5          391.5          0.02367332    0.01116501
## 2.5%9             21.5          679.5          0.03065876    0.01923362
## 2.5%10            36.5          964.5          0.03645383    0.02576675
## 2.5%11            53.5         1347.5          0.03819711    0.02882286
##        rate_ci_upper rate_threshold prob_rate_gt_threshold signal_level
## 2.5%      0.12396641            0.1                0.31575    NO SIGNAL
## 2.5%1     0.11619047            0.1                0.26676    NO SIGNAL
## 2.5%2     0.11543935            0.1                0.33952    NO SIGNAL
## 2.5%3     0.10967540            0.1                0.21217    NO SIGNAL
## 2.5%4     0.10968857            0.1                0.08846    NO SIGNAL
## 2.5%5     0.10663429            0.1                0.08008    NO SIGNAL
## 2.5%6     0.10687180            0.1                0.10018    NO SIGNAL
## 2.5%7     0.10047802            0.1                0.02824    NO SIGNAL
## 2.5%8     0.04055128            0.1                0.00000    NO SIGNAL
## 2.5%9     0.04467052            0.1                0.00000    NO SIGNAL
## 2.5%10    0.04896431            0.1                0.00000    NO SIGNAL
## 2.5%11    0.04875933            0.1                0.00000    NO SIGNAL

Unblinded pooled AE data by treatment groups

Input interim safety data for multiple AE preferred terms. Specify Bayesian beta prior distributions. Separate data by AE term. Perform sequential Bayesian updating at each interim look. Update posterior distributions using AE counts and non-AE counts. Generate posterior simulations using beta distributions. Estimate treatment and control AE rates. Calculate posterior Risk Difference (RD) and Risk Ratio (RR). Compute posterior probabilities: Pr(RD > threshold) Pr(RR > threshold) (rd_threshold = 0.03, rr_threshold = 1.5), Apply predefined Bayesian safety decision rules. Classify signals as: NO SIGNAL WATCH ALERT Generate interim Bayesian safety monitoring tables for DMC/safety review.

# =========================================================
# Industrial-style Bayesian Safety Signal Monitoring
# Multiple AE Preferred Terms
# Beta-Binomial Sequential Updating
# =========================================================

bayes_safety_monitor_industry <- function(
    safety_data,
    alpha_prior = 0.5,
    beta_prior  = 0.5,
    rd_threshold = 0.03,
    rr_threshold = 1.5,
    prob_threshold_watch = 0.80,
    prob_threshold_alert = 0.90,
    nsim = 100000,
    seed = 123
) {
  
  set.seed(seed)
  
  ae_terms <- unique(safety_data$ae_term)
  looks <- sort(unique(safety_data$look))
  
  results <- data.frame()
  
  for (ae in ae_terms) {
    
    dat_ae <- safety_data[safety_data$ae_term == ae, ]
    
    alpha_trt <- alpha_prior
    beta_trt  <- beta_prior
    
    alpha_ctl <- alpha_prior
    beta_ctl  <- beta_prior
    
    for (lk in looks) {
      
      dat_lk <- dat_ae[dat_ae$look == lk, ]
      
      if (nrow(dat_lk) == 0) next
      
      ae_trt <- dat_lk$ae_trt
      n_trt  <- dat_lk$n_trt
      
      ae_ctl <- dat_lk$ae_ctl
      n_ctl  <- dat_lk$n_ctl
      
      # Sequential Bayesian updating
      alpha_trt <- alpha_trt + ae_trt
      beta_trt  <- beta_trt  + n_trt - ae_trt
      
      alpha_ctl <- alpha_ctl + ae_ctl
      beta_ctl  <- beta_ctl  + n_ctl - ae_ctl
      
      # Posterior simulation
      p_trt <- rbeta(nsim, alpha_trt, beta_trt)
      p_ctl <- rbeta(nsim, alpha_ctl, beta_ctl)
      
      rd <- p_trt - p_ctl
      rr <- p_trt / p_ctl
      
      prob_rd_signal <- mean(rd > rd_threshold)
      prob_rr_signal <- mean(rr > rr_threshold)
      
      # Industrial-style traffic light decision
      signal_level <- ifelse(
        prob_rd_signal >= prob_threshold_alert | prob_rr_signal >= prob_threshold_alert,
        "ALERT",
        ifelse(
          prob_rd_signal >= prob_threshold_watch | prob_rr_signal >= prob_threshold_watch,
          "WATCH",
          "NO SIGNAL"
        )
      )
      
      results <- rbind(
        results,
        data.frame(
          look = lk,
          ae_term = ae,
          
          trt_ae_cum = alpha_trt - alpha_prior,
          trt_n_cum  = alpha_trt + beta_trt - alpha_prior - beta_prior,
          trt_rate   = (alpha_trt - alpha_prior) /
            (alpha_trt + beta_trt - alpha_prior - beta_prior),
          
          ctl_ae_cum = alpha_ctl - alpha_prior,
          ctl_n_cum  = alpha_ctl + beta_ctl - alpha_prior - beta_prior,
          ctl_rate   = (alpha_ctl - alpha_prior) /
            (alpha_ctl + beta_ctl - alpha_prior - beta_prior),
          
          post_mean_rd = mean(rd),
          rd_ci_lower  = quantile(rd, 0.025),
          rd_ci_upper  = quantile(rd, 0.975),
          
          post_mean_rr = mean(rr),
          rr_ci_lower  = quantile(rr, 0.025),
          rr_ci_upper  = quantile(rr, 0.975),
          
          prob_rd_gt_threshold = prob_rd_signal,
          prob_rr_gt_threshold = prob_rr_signal,
          
          signal_level = signal_level
        )
      )
    }
  }
  
  return(results)
}


# =========================================================
# Example safety data
# Each row = one AE term at one interim look
# =========================================================

safety_data <- data.frame(
  look = c(
    1,1,1,
    2,2,2,
    3,3,3,
    4,4,4
  ),
  
  ae_term = c(
    "Nausea", "Headache", "Liver enzyme increased",
    "Nausea", "Headache", "Liver enzyme increased",
    "Nausea", "Headache", "Liver enzyme increased",
    "Nausea", "Headache", "Liver enzyme increased"
  ),
  
  ae_trt = c(
    25, 18, 6,
    18, 15, 8,
    20, 16, 10,
    22, 17, 12
  ),
  
  n_trt = c(
    200, 200, 200,
    150, 150, 150,
    150, 150, 150,
    200, 200, 200
  ),
  
  ae_ctl = c(
    12, 14, 3,
    10, 12, 4,
    11, 13, 5,
    13, 14, 5
  ),
  
  n_ctl = c(
    200, 200, 200,
    150, 150, 150,
    150, 150, 150,
    200, 200, 200
  )
)


results <- bayes_safety_monitor_industry(
  safety_data = safety_data,
  alpha_prior = 0.5,
  beta_prior = 0.5,
  rd_threshold = 0.03,
  rr_threshold = 1.5,
  prob_threshold_watch = 0.80,
  prob_threshold_alert = 0.90,
  nsim = 100000,
  seed = 123
)

print(results)
##        look                ae_term trt_ae_cum trt_n_cum   trt_rate ctl_ae_cum
## 2.5%      1                 Nausea         25       200 0.12500000         12
## 2.5%1     2                 Nausea         43       350 0.12285714         22
## 2.5%2     3                 Nausea         63       500 0.12600000         33
## 2.5%3     4                 Nausea         85       700 0.12142857         46
## 2.5%4     1               Headache         18       200 0.09000000         14
## 2.5%5     2               Headache         33       350 0.09428571         26
## 2.5%6     3               Headache         49       500 0.09800000         39
## 2.5%7     4               Headache         66       700 0.09428571         53
## 2.5%8     1 Liver enzyme increased          6       200 0.03000000          3
## 2.5%9     2 Liver enzyme increased         14       350 0.04000000          7
## 2.5%10    3 Liver enzyme increased         24       500 0.04800000         12
## 2.5%11    4 Liver enzyme increased         36       700 0.05142857         17
##        ctl_n_cum   ctl_rate post_mean_rd  rd_ci_lower rd_ci_upper post_mean_rr
## 2.5%         200 0.06000000   0.06475031  0.008539925  0.12262569     2.207732
## 2.5%1        350 0.06285714   0.05973239  0.017268639  0.10309281     2.015455
## 2.5%2        500 0.06600000   0.05991181  0.023757257  0.09664350     1.950160
## 2.5%3        700 0.06571429   0.05559498  0.025377782  0.08630537     1.875863
## 2.5%4        200 0.07000000   0.01987871 -0.033449206  0.07421376     1.363311
## 2.5%5        350 0.07428571   0.01985931 -0.020899893  0.06135786     1.308490
## 2.5%6        500 0.07800000   0.01986602 -0.015304058  0.05500190     1.281890
## 2.5%7        700 0.07571429   0.01852973 -0.010654453  0.04800970     1.264626
## 2.5%8        200 0.01500000   0.01493359 -0.014796248  0.04711834     2.593392
## 2.5%9        350 0.02000000   0.01997352 -0.005233462  0.04663045     2.228528
## 2.5%10       500 0.02400000   0.02394430  0.001061153  0.04779217     2.124369
## 2.5%11       700 0.02428571   0.02710806  0.007377200  0.04771286     2.209444
##        rr_ci_lower rr_ci_upper prob_rd_gt_threshold prob_rr_gt_threshold
## 2.5%     1.0969031    4.124930              0.88718              0.83788
## 2.5%1    1.2066847    3.235803              0.91483              0.85526
## 2.5%2    1.2864879    2.871535              0.94786              0.88037
## 2.5%3    1.3198084    2.619995              0.95107              0.88408
## 2.5%4    0.6618169    2.534547              0.35085              0.32189
## 2.5%5    0.7804511    2.082672              0.31289              0.25022
## 2.5%6    0.8402951    1.883183              0.28503              0.19142
## 2.5%7    0.8819021    1.764985              0.21844              0.14422
## 2.5%8    0.5374136    8.559516              0.15592              0.65067
## 2.5%9    0.8419059    5.090006              0.21442              0.73345
## 2.5%10   1.0298007    4.028217              0.29846              0.79423
## 2.5%11   1.2200271    3.801509              0.38152              0.88640
##        signal_level
## 2.5%          WATCH
## 2.5%1         ALERT
## 2.5%2         ALERT
## 2.5%3         ALERT
## 2.5%4     NO SIGNAL
## 2.5%5     NO SIGNAL
## 2.5%6     NO SIGNAL
## 2.5%7     NO SIGNAL
## 2.5%8     NO SIGNAL
## 2.5%9     NO SIGNAL
## 2.5%10    NO SIGNAL
## 2.5%11        WATCH

Based on a comprehensive assessment of these factors—including AE severity, SAEs, AESIs, time-to-onset, exposure-adjusted incidence, medical review, multiplicity, and benefit-risk—a judgment is rendered.