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
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.