<- function(u, p, t){
seir_vacc_trial_jl # vaccine recipients lose partial immunity over time.
# this is implemented in a way that individuals in the V state
# decreases and transitions to the S state
<- u[1]; E <- u[2]; I <- u[3]; R <- u[4]; C <- u[5]
S
# vaccinated cohort
<- u[6]; # vaccinated and partially protected
V <- u[7]; # vaccinated but immunity waned and fully susceptible
VS <- u[8]; VI <- u[9]; VR <- u[10]; VC <- u[11]
VE
<- S + E + I + R
pop <- V + VS + VE + VI + VR
popV
<- p[1] # 1/latent period
epsilon <- p[2] # 1/duration of infectiousness
gamma <- p[3] # transmission rate
beta <- p[4] # death rate is applied and population size decreases over time
mu <- p[5] # 1 / duration of natural immunity
omega <- p[6] # 1 / duration of partial vaccine-derived immunity
omega_v <- p[7] # vaccine efficacy
ve # vaccinated and unvaccinated population mix randomly
<- beta * (I+VI) / (pop + popV) # force of infection
foi
<- epsilon
muEI <- gamma
muIR <- omega
muRS <- omega_v
muVS
# differential equations
<- - foi*S + muRS*R - mu*S
dS <- foi*S - muEI*E - mu*E
dE <- muEI*E - muIR*I - mu*I
dI <- muIR*I - muRS*R - mu*R
dR <- muEI*E
dC
<- - foi*(1-ve)*V - muVS*V - mu*V
dV <- - foi*VS + muVS*V + muRS*VR - mu*VS
dVS <- foi*((1-ve)*V + VS) - muEI*VE - mu*VE
dVE <- muEI*VE - muIR*VI - mu*VI
dVI <- muIR*VI - muRS*VR - mu*VR
dVR <- muEI*VE
dVC
return(c(dS,dE,dI,dR,dC,dV,dVS,dVE,dVI,dVR,dVC))
}
Waning of vaccine effectiveness
The protection derived from vaccination often wanes over time and require the second or the third doses (so-called booster doses). For example, the study showed the efficacy of cholera vaccines over five years. The vaccine efficacy (VE) over the period seems to indicate that the VE wanes over time. In this post, we will develop a \(SEIR\) model to illustrate the waning of vaccine-derived immunity.
\(SEIR\) model with vaccination
diffeqr
package is used use Julia’s DifferentialEquations
library
library(diffeqr)
<- diffeqr::diffeq_setup() de
We assume that cholera transmission is in the steady state in the setting where the vaccine efficacy trial is examined. Algebraic solutions to the steady states (shown below) are used as initial conditions to generate initial conditions.
<- "((gamma + mu) * (epsilon + mu)) / (beta * epsilon)"
Ss <- "- ((gamma + mu) * (mu + omega) * ((gamma + mu) * (epsilon + mu) - beta * epsilon)) / (beta * epsilon * (gamma * (epsilon + mu + omega) + (epsilon + mu) * (mu + omega)))"
Es
<- "((mu + omega) * (beta * epsilon - (gamma + mu) * (epsilon + mu))) / (beta * omega * (gamma + epsilon + mu) + beta * (gamma + mu) * (epsilon + mu))"
Is <- "(beta * gamma * epsilon - gamma * (gamma + mu) * (epsilon + mu)) / (beta * omega * (gamma + epsilon + mu) + beta * (gamma + mu) * (epsilon + mu))" Rs
Simulation of a clinical trial
The basic idea is to use the steady states as initial conditions and therefore, cholera transmission is sustained as new susceptibles are supplied through birth and waning of vaccine-derived and natural immunity. In this setting, a clinical trial is implemented by moving some portion of the population to the vaccinated cohort. Note that the proportion of the vaccinated population, \(f\), is important in the subsequent dynamics. For example, we know that if \(f> 1- \frac{1}{R_0}\), cholera infections will die out. In this case, we can not measure the relative risk of cholera among vaccinated and unvaccinated population over a period based on the cumulative incidence. Even if \(f < 1- \frac{1}{R_0}\), moving some portion of the population into vaccinated states will perturb the steady states that were determined based on and a series of outbreaks may follow before the system reaches new steady states eventually.
Since our objective is to vaccine efficacy in this dynamic population based on cumulative incidence among vaccinated and unvaccinated people, and we used the steady states without the vaccinated population, we can set the proportion of the vaccinated population small that the system more or less maintains the states without vaccination. Wh
<- 1e5 # unvaccinated population
N <- 1e-4 # a very small proportion of the population is vaccinated
f <- 2
R0 <- 1/1.4
epsilon <- 1/2
gamma <- R0*gamma
beta # mu <- 1/(65*365) # natural death rate
<- 0 # natural death rate
mu <- 1/(40*365) # natural immunity waning rate
omega <- 1/(10*365) # vaccine-derived immunity waning rate
omega_v <- 0.7 # vaccine efficacy (instantaneous hazard ratio)
ve # vaccine efficacy based on CI will differ and we will eventually estimate
# this parameter along with omega_v
<- c(epsilon=epsilon, gamma=gamma, beta=beta, mu=mu,
params omega=omega, omega_v=omega_v, ve=ve)
# steady states for given parameters
<- list(S=Ss, E=Es, I=Is, R=Rs)
states0 <- sapply(states0, function(x) eval(parse(text = x)))
steadys0 # initial distribution of the population across the states
<- c(steadys0*N,C=0,V=0,VS=0,VE=0,VI=0,VR=0,VC=0)
u0 c("V","VE","VI","VR")] <- as.numeric(steadys0*N*f/(1-f))
u0[
<- 20.0*365 # measure vaccin
tend <- c(0.0, tend)
tspan
<- de$ODEProblem(seir_vacc_trial_jl, u0, tspan, params)
prob <- de$solve(prob, de$Tsit5(), saveat=1)
sol
<- sapply(sol$u, identity)
mat <- as.data.frame(t(mat))
udf <- cbind(data.frame(t=sol$t), udf)
out names(out) <- c("t","S","E","I","R","C","V","VS","VE","VI","VR","VC")
# cumulative incidence
plot(1:nrow(out)/365, out[,"C"], type="l", ylab="cumulative incidence", xlab="year", col="black")
lines(1:nrow(out)/365,out[,"VC"], col="firebrick")
legend("topleft",
legend=c("No vaccine", "Vaccine"),
col=c("black", "firebrick"),
lty= 1,
bty = "n",
cex = 1.0,
text.col = "black",
horiz = F ,
inset = c(0.02,0.02))
Measure vaccine impact
<- c("S","E","I","R")
novacc <- c("V","VS","VE","VI","VR")
vacc <- rep(NA, nrow(out)) # direct vaccine effectiveness measured daily
dve for(i in 1:nrow(out)){
<- 1 - (out[i,"VC"]/sum(out[i,vacc]))/(out[i,"C"]/sum(out[i,novacc]))
dve[i]
}plot(1:length(dve)/365,dve, type="l", ylab="direct VE", xlab="year")
Estimating VE and \(\omega_V\)
Let’s suppose we want to the following dataset showing the vaccine effec
<- c(76,72,68,63,58,52,46,39,32,24,15)/100 ve_obs
Create a function to measure vaccine efficacy from the model based on cumulative incidence
<- function(p, times, N=1e5, f=1e-4){
measure_vacc_eff "ve"] <- p[1]
params["omega_v"] <- p[2]
params[<- c(steadys0*N,C=0,V=N,VS=0,VE=0,VI=0,VR=0,VC=0)
u0 c("V","VE","VI","VR")] <- as.numeric(steadys0*N*f/(1-f))
u0[<- de$ODEProblem(seir_vacc_trial_jl, u0, c(0.0, max(times)+1), params)
prob <- de$solve(prob, de$Tsit5(), saveat=1)
sol <- sapply(sol$u, identity)
mat <- as.data.frame(t(mat))
udf <- cbind(data.frame(t=sol$t), udf)
out names(out) <- c("t","S","E","I","R","C",
"V","VS","VE","VI","VR","VC")
<- c("S","E","I","R")
novacc <- c("V","VS","VE","VI","VR")
vacc
<- rep(NA, length(times))
ve_sim for (i in 1:length(times)) {
<- 1 -
ve_sim[i] "VC"])/sum(out[times[i],vacc])) /
((out[times[i],"C"])/sum(out[times[i],novacc]))
((out[times[i],
}return(ve_sim)
}
Define sum of squared difference to evaluate the difference between the model and the observation
# the initial VE is not measured from the model
= seq(6,by=6,length.out=10)*30 # times to measure VE
times <- function(p){
ssq <- measure_vacc_eff(p, times=times)
ve_sim sum((ve_obs - c(p[1],ve_sim))^2)
}# check for some predetermined ve and omega
# ssq(p=c(0.8,1/3650)) must be smaller than ssq(p=c(0.4,1/365))
ssq(p=c(0.8,1/3650))
[1] 0.5765074
ssq(p=c(0.4,1/365))
[1] 1.260319
Use the nlminb
to identify the parameter values that minimize the ssq.
<- nlminb(c(0.2, 1/(2*365)), objective=ssq,
fit lower=c(0.1,1/(1000*365)),
upper=c(0.99,0.99))
ve_obs
[1] 0.76 0.72 0.68 0.63 0.58 0.52 0.46 0.39 0.32 0.24 0.15
round(c(fit$par[1], measure_vacc_eff(p=fit$par, times)), digits=2)
[1] 0.83 0.73 0.65 0.58 0.52 0.46 0.42 0.38 0.34 0.31 0.29
# png("vacc_eff_waning.png")
plot(0:10/2, ve_obs, col="firebrick",
ylim=c(0,1), xlab="year", ylab="direct VE",
type="p", pch=0)
lines(0:10/2,c(fit$par[1], measure_vacc_eff(p=fit$par, times)),
col="black")
legend("topright",
legend=c("Data", "Model"),
col=c("firebrick", "black"),
lty= c(0,1),
pch=c(0,NA),
bty = "n",
cex = 1.0,
text.col = "black",
horiz = F ,
inset = c(0.02,0.02))
# dev.off()