Code
import sympy as sp
# Define symbols
= sp.symbols('B C p r', positive=True)
B, C, p, r
# Net benefits
= B*p - C
net_pre = B*p*r - C*p
net_react
# When is pre-emptive better?
= sp.solve(net_pre - net_react > 0, p) threshold_p
Jong-Hoon Kim
June 27, 2025
When managing infectious disease risks such as cholera, limited vaccine supply often forces decision-makers to choose between pre-emptive and reactive vaccination campaigns. This post explores a cost-benefit model comparing the two strategies using analytical expressions and numerical simulations.
We aim to answer: Under what conditions is it better to vaccinate before an outbreak occurs, and when is it more beneficial to wait and respond reactively?
Let:
If we vaccinate before any outbreak, the expected benefit is \(B \cdot p\). However, we always incur the campaign cost \(C\), so the net expected benefit is:
\[ \text{Net}_{\text{pre}} = Bp - C \]
For reactive vaccination, the benefit is only realized if an outbreak occurs (probability \(p\)), and even then, only \(rB\) is saved due to delays. The cost \(C\) is only paid if an outbreak occurs. Hence:
\[ \text{Net}_{\text{react}} = Bpr - Cp \]
\[ \text{Net}_{\text{pre}} - \text{Net}_{\text{react}} = pB(1 - r) - C(1 - p) \]
Pre-emptive is better if:
\[ pB(1 - r) > C(1 - p) \]
Solving for \(p\) gives the threshold outbreak probability \(p^*\):
\[ p > \frac{C}{B(1 - r) + C} \]
library(ggplot2)
library(dplyr)
# Parameters
r_vals <- seq(0.01, 0.99, by = 0.01)
p_vals <- seq(0.01, 0.99, by = 0.01)
B <- 1
C_vals <- c(0.1, 0.5, 1, 3)
# Create grid
grid <- expand.grid(p = p_vals, r = r_vals, C = C_vals) %>%
mutate(
p_star = C / (B * (1 - r) + C),
decision = ifelse(p > p_star, "Preemptive", "Reactive")
)
# Create threshold lines for each C
threshold_lines <- data.frame()
for (C_val in C_vals) {
df_line <- data.frame(
r = r_vals,
p_thresh = C_val / (B * (1 - r_vals) + C_val),
C = C_val
)
threshold_lines <- bind_rows(threshold_lines, df_line)
}
# Create labeled plot
ggplot(grid, aes(x = r, y = p)) +
geom_line(data = threshold_lines, aes(x = r, y = p_thresh, group = C),
color = "black", size = 1, inherit.aes = FALSE) +
scale_y_continuous(limits=c(0,1))+
scale_x_continuous(limits=c(0,1))+
geom_text(data = data.frame(C = rep(C_vals, each = 2),
label = rep(c("Preemptive", "Reactive"), 4),
r = rep(0.05, 8),
p = rep(c(0.95, 0.05), 4)),
aes(x = r, y = p, label = label), hjust = 0,
color = "black", size = 4, inherit.aes = FALSE) +
scale_fill_manual(values = c("Preemptive" = "steelblue", "Reactive" = "tomato")) +
facet_wrap(~C, labeller = label_bquote(B/C == .(B/C))) +
labs(
title = "Vaccination Strategy Decision with Threshold",
x = "Relative Reactive Benefit (r)",
y = "Outbreak Probability (p)",
fill = "Strategy"
) +
theme_light() +
theme(legend.position = 'top')
This plot shows that reactive vaccination is better when \(p\) is small, but pre-emptive takes over beyond a break-even point that depends on \(r\).
We define two additional parameters:
\(\phi\): Vaccination may provide benefits even in the absence of an outbreak—for example, through future immunity or the prevention of future outbreaks. This applies to both pre-emptive and reactive strategies. If the primary concern is outbreaks occurring within the current year, then \(\phi\) can be interpreted as the potential preventive benefit of vaccination against outbreaks in subsequent years, measured as a fraction of the full benefit, \(B\).
\(D\): The cost or penalty associated with unused vaccines (e.g., due to expiry or logistical waste). This applied to the reactive strategy.
If we vaccinate before any outbreak, the expected benefit includes two parts:
The cost \(C\) is always incurred. So the net expected benefit is:
\[ \text{Net}_{\text{pre}} = pB + (1 - p)\phi B - C \]
Reactive benefit occurs only if an outbreak occurs. In that case:
So the net expected benefit is:
\[ \text{Net}_{\text{react}} = p(r+\phi)B - pC - (1 - p)D \]
-B*p*(phi + r) + B*p - B*phi*(p - 1) + C*p - C - D*(p - 1)
\[ \text{Net}_{\text{pre}} - \text{Net}_{\text{react}} = Bp(1 - r - \phi) + B(1 - p)\phi - C(1 - p) + D(1 - p) \]
Pre-emptive vaccination is better when this expression is positive. The threshold for \(p\) can be derived based on values of \(B, C, D, r, \phi\):
\[ p > \frac{- \phi B + C - D}{(1- 2 \phi - r)B + C - D} \]
library(ggplot2)
library(dplyr)
B <- 1
D <- 0.02
phi <- 0.1
r_vals <- seq(0.1, 0.9, by = 0.01)
p_vals <- seq(0.1, 0.9, by = 0.01)
C_vals <- c(0.1, 0.5, 1, 3)
compute_net <- function(p, r, B=1, C, phi=0.1, D=0.02) {
net_pre <- B * p + (1 - p) * B * phi - C
net_react <- B * p * (r + phi) - p * C - (1 - p) * D
return(data.frame(p = p, r = r,
net_pre = net_pre,
net_react = net_react))
}
# Compute threshold for each r
grid <- expand.grid(p = p_vals, r = r_vals, C = C_vals)
grid <- grid %>%
mutate(pre = compute_net(p=p,r=r,C=C)$net_pre,
react = compute_net(p=p,r=r,C=C)$net_react,
decision = ifelse(pre > react, "Preemptive", "Reactive"))
# Create labeled plot
ggplot(grid, aes(x = r, y = p)) +
geom_line(data = threshold_lines, aes(x = r, y = p_thresh, group = C),
color = "black", size = 1, inherit.aes = FALSE) +
scale_y_continuous(limits=c(0,1))+
scale_x_continuous(limits=c(0,1))+
geom_text(data = data.frame(C = rep(C_vals, each = 2),
label = rep(c("Preemptive", "Reactive"), 4),
r = rep(0.05, 8),
p = rep(c(0.95, 0.05), 4)),
aes(x = r, y = p, label = label), hjust = 0,
color = "black", size = 4, inherit.aes = FALSE) +
scale_fill_manual(values = c("Preemptive" = "steelblue", "Reactive" = "tomato")) +
facet_wrap(~C, labeller = label_bquote(B/C == .(B/C))) +
labs(
title = "Vaccination Strategy Decision with Threshold",
x = "Relative Reactive Benefit (r)",
y = "Outbreak Probability (p)",
fill = "Strategy"
) +
theme_light() +
theme(legend.position = 'top')
We consider a strategic vaccination scenario where oral cholera vaccines (OCVs) are in limited supply. Specifically, the available stockpile is sufficient to cover only 10% of all subpopulations. Epidemiological data suggest that, within the anticipated time frame, fewer than 10% of subpopulations are likely to experience an outbreak. This implies that a fully reactive strategy—in which vaccines are deployed only in response to confirmed outbreaks—could, in principle, respond to all outbreaks without exceeding supply.
However, a recent risk assessment indicates that 20% of the population exceeds the outbreak probability threshold \(p^*\) above which preemptive vaccination is expected to yield greater benefit than reactive vaccination for a given region.
This raises a strategic dilemma:
Should all subpopulations above the preemptive threshold be vaccinated in advance, despite limited vaccine availability and the potential need for reactive response elsewhere?
This scenario highlights the trade-off between targeting high-risk areas proactively versus preserving flexibility to respond where outbreaks actually occur, especially under uncertainty and supply constraints. That is, we need to consider an opportunity cost of preemptive vaccination under Limited vaccine supply.
Assumptions:
Vaccinate top \(k\) high-risk regions:
\[\text{Benefit}_{\text{pre}} = \sum_{i=1}^{k} p_i B \]
Let \(\mu_k = \sum_{i=k+1}^{n} p_i\): expected number of outbreaks in unvaccinated regions.
Expected reactive benefit (up to \(M - k\) responses):
\[ \text{Benefit}_{\text{react}} = rB \cdot \min(M - k, \mu_k) \]
\[ \text{Total Benefit}_k = B \left( \sum_{i=1}^{k} p_i + r \cdot \min(M - k, \mu_k) \right) \]
Vaccinating region \(k+1\) preemptively:
Opportunity cost:
\[ \text{Opportunity Cost}(k+1) = rB \cdot \mathbb{P}(X_k \ge M - k) \]
where \(X_k \sim \text{Binomial}(n - k, \bar{p}_k)\) or \(\text{Poisson}(\mu_k)\).
Preemptively vaccinate region \(i\) only if:
\[ p_i > r \cdot \mathbb{P}(\text{reactive demand exceeds remaining doses}) \]
Using Poisson approximation:
\[ \mathbb{P}(X_k \ge M - k) \approx 1 - \sum_{x=0}^{M-k-1} \frac{e^{-\mu_k} \mu_k^x}{x!} \]
So:
\[ \text{Opportunity Cost} \approx rB \left(1 - \sum_{x=0}^{M-k-1} \frac{e^{-\mu_k} \mu_k^x}{x!} \right) \]
Quantity | Expression |
---|---|
Preemptive threshold (p^* = ) | |
Preemptive benefit (\(k\) regions) | $_{i=1}^k p_i B $ |
Reactive expected benefit | $ rB (M-k, _k) $ |
Total expected benefit | $ B ( _{i=1}^k p_i + r (M - k, _k) ) $ |
Opportunity cost of dose $ k+1 $ | $ rB (X_k M - k) $ |
Adjusted decision rule | Vaccinate if $ p_{k+1} > r (X_k M - k) $ |
# Load necessary libraries
library(ggplot2)
library(dplyr)
# Simulation parameters
set.seed(123)
n <- 100 # Total subpopulations
M <- 10 # Vaccine budget (10% of regions)
B <- 1 # Benefit of preventing an outbreak
C <- 0.4 # Cost of vaccination
r <- 0.8 # Relative reactive benefit
phi <- 0.05
D <- 0.1
# Threshold for preemptive action
p_star <- (- phi*B + C - D) / ((1- 2*phi - r)*B + C - D)
# p_star <- C / (B * (1 - r) + C)
# Generate synthetic outbreak probabilities
p_i <- sort(runif(n, 0.01, 0.5), decreasing = TRUE)
# Simulate outbreak events (Bernoulli trial based on p_i)
n_sim <- 1000
results <- data.frame()
for (k in 0:M) {
# Preemptively vaccinate top-k at-risk regions
preempt_indices <- 1:k
reactive_pool <- (k+1):n
total_cases_averted <- numeric(n_sim)
for (s in 1:n_sim) {
outbreaks <- rbinom(n, 1, p_i)
# Preemptive: full benefit if outbreak occurs
preempt_impact <- sum(outbreaks[preempt_indices]) * B
# Reactive: respond to up to (M - k) actual outbreaks in unvaccinated group
reactive_indices <- reactive_pool[outbreaks[reactive_pool] == 1]
max_reactive <- min(length(reactive_indices), M - k)
reactive_impact <- max_reactive * r * B
total_cases_averted[s] <- preempt_impact + reactive_impact
}
results <- rbind(results,
data.frame(
k_preempt = k,
mean_cases_averted = mean(total_cases_averted),
sd = sd(total_cases_averted)
))
}
# Plot
ggplot(results, aes(x = k_preempt, y = mean_cases_averted)) +
geom_line(size = 1.2) +
geom_ribbon(aes(ymin = mean_cases_averted - sd,
ymax = mean_cases_averted + sd),
fill = "skyblue", alpha = 0.3) +
labs(title = "Expected Cases Averted vs. Number of Preemptive Vaccinations",
x = "Number of Preemptive Vaccinated Regions (k)",
y = "Mean Cases Averted (with SD band)") +
theme_minimal()
# Load necessary libraries
library(ggplot2)
library(dplyr)
# Simulation parameters
set.seed(123)
n <- 100 # Total subpopulations
M <- 10 # Vaccine budget (10% of regions)
B <- 1 # Benefit of preventing an outbreak
C <- 0.4 # Cost of vaccination
phi <- 0.05
D <- 0.1
# Outbreak probabilities
p_i <- sort(runif(n, 0.01, 0.5), decreasing = TRUE)
# Sim settings
n_sim <- 1000
r_values <- seq(0.2, 1, by = 0.2) # Explore multiple r values
results <- data.frame()
for (r in r_values) {
# Recalculate p_star (optional for reference)
p_star <- (- phi*B + C - D) / ((1- 2*phi - r)*B + C - D)
for (k in 0:M) {
preempt_indices <- 1:k
reactive_pool <- (k+1):n
total_cases_averted <- numeric(n_sim)
for (s in 1:n_sim) {
outbreaks <- rbinom(n, 1, p_i)
# Preemptive impact
preempt_impact <- sum(outbreaks[preempt_indices]) * B
# Reactive impact
reactive_indices <- reactive_pool[outbreaks[reactive_pool] == 1]
max_reactive <- min(length(reactive_indices), M - k)
reactive_impact <- max_reactive * r * B
total_cases_averted[s] <- preempt_impact + reactive_impact
}
results <- rbind(results,
data.frame(
r = r,
k_preempt = k,
mean_cases_averted = mean(total_cases_averted),
sd = sd(total_cases_averted)
))
}
}
# Plot
ggplot(results, aes(x = k_preempt, y = mean_cases_averted, color = factor(r))) +
geom_line(size = 1.2) +
geom_ribbon(aes(ymin = mean_cases_averted - sd,
ymax = mean_cases_averted + sd,
fill = factor(r)), alpha = 0.2, color = NA) +
labs(title = "Expected Cases Averted by Preemptive Vaccination and Reactive Effectiveness",
x = "Number of Preemptive Vaccinated Regions (k)",
y = "Mean Cases Averted (with SD band)",
color = "Reactive effectiveness (r)",
fill = "Reactive effectiveness (r)") +
theme_minimal()+
theme(legend.position = 'top')
We consider a constrained setting where \(r\) is not fixed and depends on the duration of the outbreak, in addition to the previous version.
Longer outbreaks allow more time for detection and response, and thus yield more effective reactive campaigns. We model \(r\) as a function of outbreak duration \(d\), i.e., \(r = f(d)\).
# Load necessary libraries
library(ggplot2)
library(dplyr)
# Simulation parameters
set.seed(123)
n <- 1000
M <- 10
B <- 1
C <- 0.8
phi <- 0.05
D <- 0.01
# Control parameter for r(d)
alpha <- 1.4 # >1: delayed growth, <1: rapid saturation
# Outbreak probabilities
p_i <- sort(runif(n, 0.01, 0.9), decreasing = TRUE)
# Duration depends on p_i
min_d <- 3
max_d <- 156
duration <- min_d + (max_d - min_d) * (p_i - min(p_i)) / (max(p_i) - min(p_i))
# Reactive effectiveness with parameter alpha
r_d <- ((duration - min_d) / (max_d - min_d)) ^ alpha
r_d <- pmin(r_d, 1) # cap at 1
# Optional preview
# plot(p_i, r_d, type = "l")
# Simulation loop
n_sim <- 1000
results <- data.frame()
for (k in 0:M) {
preempt_indices <- 1:k
reactive_pool <- (k + 1):n
total_cases_averted <- numeric(n_sim)
for (s in 1:n_sim) {
outbreaks <- rbinom(n, 1, p_i)
# Preemptive impact
preempt_impact <- sum(outbreaks[preempt_indices]) * B
# Reactive impact
reactive_indices <- reactive_pool[outbreaks[reactive_pool] == 1]
reactive_impact <- if (length(reactive_indices) > 0) {
top_reactive <- head(reactive_indices, M - k)
sum(r_d[top_reactive] * B)
} else 0
total_cases_averted[s] <- preempt_impact + reactive_impact
}
results <- rbind(results, data.frame(
k_preempt = k,
mean_cases_averted = mean(total_cases_averted),
sd = sd(total_cases_averted)
))
}
# Plot simulation result
ggplot(results, aes(x = k_preempt, y = mean_cases_averted)) +
geom_line(size = 1.2, color = "steelblue") +
geom_ribbon(aes(ymin = mean_cases_averted - sd,
ymax = mean_cases_averted + sd),
fill = "skyblue", alpha = 0.3) +
labs(title = paste0("Expected Cases Averted vs. k (alpha = ", alpha, ")"),
x = "Number of Preemptively Vaccinated Regions (k)",
y = "Mean Cases Averted (with SD)") +
theme_minimal()
Symbol | Meaning |
---|---|
\(r(d)\) | Duration-dependent effectiveness of reactive vaccination |
\(\phi\) | Benefit from proactive vaccination even if no outbreak |
\(D\) | Cost of unused vaccine under reactive plan |
\(p^*\) | Threshold outbreak probability above which preemptive vaccination is favored |
Final threshold formula:
\[{p^* = \frac{- \phi B + C - D}{(1 - 2\phi - r(d)) B + C - D}}\]
This formulation reflects operational complexity, vaccine scarcity, and time-sensitive effectiveness, guiding more nuanced vaccine allocation strategies in outbreak-prone settings.
When outbreak risk is high enough, pre-emptive vaccination pays off even if the outbreak doesn’t occur. When outbreak risk is low, and reactive campaigns are effective (\(r\) close to 1) and is more cost-effective. The decision threshold depends critically on the relative cost \(C\) and the delay penalty \((1 - r)\) in addition to \(p\) and \(r\)
Even under limited vaccine supply, careful cost-benefit modeling can guide whether to vaccinate early or wait. This blog post outlined a tractable analytical framework and practical tools (in Python and R) to support such decisions. Extensions could include waning immunity, spatial correlations, or dynamic outbreaks.