Tony Tsai

May the force of science be with you

Dec 4, 2014 - 17 minute read - Comments - Influenza

SIRS Model with Time Dependent Transmission Rate

1 SIRS Model

The SIRS model is simply an extension of the SIR model as it allows members of the recovered class to rejoin the susceptible class at a defined rate, which integrates the impact of waning immunity following antigenic drift. Similar to the SIR model, a fixed population without births and deaths is considered in the SIRS model. The standard form of SIRS model is described as $$ \begin{equation} \begin{aligned} & \frac{dS}{dt} = fR - \frac{\beta SI}{N} \\ & \frac{dI}{dt} = \frac{\beta SI}{N} - \gamma I \\ & \frac{dR}{dt} = \gamma I - fR \end{aligned} \label{eq1} \end{equation} $$ where \(f\) is the average loss of immunity rate of recovered indiviuals, and the other notations are the same as the SIR model. Note that SIR model is a special case of SIRS model under condition of \(f = 0\). Hence, it is possible to create a general function to solve both SIR and SIRS models.

As only two of the three ODEs are independent, \(R(t)\) does not have to be treated explicitly and using ODEs of \(S(t)\) and \(I(t)\) to describe the dynamic of SIRS model are adequate. Substitute \(R(t) = N - S(t) - I(t)\) into ODE of \(S(t)\) and the formula is simplified as follows $$ \begin{equation} \begin{aligned} & \frac{dS}{dt} = f(N - S - I) - \frac{\beta SI}{N} \\ & \frac{dI}{dt} = \frac{\beta SI}{N} - \gamma I \end{aligned} \label{eq2} \end{equation} $$

The form of SIRS model described in Dushoff et al. (2004) and Shaman et al. (2010) is as follows $$ \begin{equation} \begin{aligned} \label{eq3} & \frac{dS}{dt} = \frac{N - S - I}{L} - \frac{\beta SI}{N} \\ & \frac{dI}{dt} = \frac{\beta SI}{N} - \frac{I}{D} \end{aligned} \end{equation} $$ where \(L\) is the average duration of immunity of recoverd indiviuals, and \(D\) is the mean infectious period of infected indiviuals. Comparing with ODEs \((\ref{eq2})\), \(L = \frac{1}{f}\) and \(D = \frac{1}{\gamma}\) are derived.

In the following subsections, RK4 method is applied to solve SIRS model in different forms.

1.1 RK4SIRS.std function

The iterative formula of RK4 method for solving SIRS model in the standard form \((\ref{eq1})\) can be easily derived and won’t be presented here. The following codes firstly define four helper functions. Three of them describe \(S(t)\), \(I(t)\) and \(R(t)\) compartments of SIRS model. The other one defines the infection rate (see Table 1 in Dushoff et al. (2004)) to calculate cummulative incidence. Different from SIR model, the infection rate is only the second term of \(S(t)\) compartment in SIRS model. Therefore, an additional helper function is needed to separately define the infection rate.

# define S, I, R compartments of SIRS model
dSdt.std <- function(t, S, I, R) {
  return(f * R - beta * S * I / N)
}

dIdt.std <- function(t, S, I) {
  return(beta * S * I / N - gamma * I)
}

dRdt.std <- function(t, I, R) {
  return(gamma * I - f * R)
}

# define infection rate
dCIdt.std <- function(t, S, I) {
  return(beta * S * I / N)
}

The iterative algorithm for calculating cummumcaltive incidence resembles those of \(S(t)\), \(I(t)\) and \(R(t)\). The implementation of RK4 method for SIRS model is presented as follows.

# RK4 method for the numerical integration of SIRS model in standard form
RK4SIRS.std <- function(n, beta, gamma, f, S0, I0, R0 = 0, dt = 1, incidence = FALSE) {
  N <<- S0 + I0 + R0
  
  S <- c(S0, rep(0, n))
  I <- c(I0, rep(0, n))
  R <- c(R0, rep(0, n))
  CI <- c(I0, rep(0, n))  # cummulative incidence
  for (i in 1:n) {
    Si <- S[i]
    Ii <- I[i]
    Ri <- R[i]
    CIi <- CI[i]
    
    S.k1 <- dSdt.std(i, Si, Ii, Ri)
    I.k1 <- dIdt.std(i, Si, Ii)
    R.k1 <- dRdt.std(i, Ii, Ri)
    CI.k1 <- dCIdt.std(i, Si, Ii)
    
    S.k2 <- dSdt.std(i + dt / 2, Si + dt / 2 * S.k1, Ii + dt / 2 * I.k1, Ri + dt / 2 * R.k1)
    I.k2 <- dIdt.std(i + dt / 2, Si + dt / 2 * S.k1, Ii + dt / 2 * I.k1)
    R.k2 <- dRdt.std(i + dt / 2, Ii + dt / 2 * I.k1, Ri + dt / 2 * R.k1)
    CI.k2 <- dCIdt.std(i + dt / 2, Si + dt / 2 * S.k1, Ii + dt / 2 * I.k1)
    
    S.k3 <- dSdt.std(i + dt / 2, Si + dt / 2 * S.k2, Ii + dt / 2 * I.k2, Ri + dt / 2 * R.k2)
    I.k3 <- dIdt.std(i + dt / 2, Si + dt / 2 * S.k2, Ii + dt / 2 * I.k2)
    R.k3 <- dRdt.std(i + dt / 2, Ii + dt / 2 * I.k2, Ri + dt / 2 * R.k2)
    CI.k3 <- dCIdt.std(i + dt / 2, Si + dt / 2 * S.k2, Ii + dt / 2 * I.k2)
    
    S.k4 <- dSdt.std(i + dt, Si + dt * S.k3, Ii + dt * I.k3, Ri + dt * R.k3)
    I.k4 <- dIdt.std(i + dt, Si + dt * S.k3, Ii + dt * I.k3)
    R.k4 <- dRdt.std(i + dt, Ii + dt * I.k3, Ri + dt * R.k3)
    CI.k4 <- dCIdt.std(i + dt, Si + dt * S.k3, Ii + dt * I.k3)
    
    S[i + 1] <- Si + dt / 6 * (S.k1 + 2 * S.k2 + 2 * S.k3 + S.k4)
    I[i + 1] <- Ii + dt / 6 * (I.k1 + 2 * I.k2 + 2 * I.k3 + I.k4)
    R[i + 1] <- Ri + dt / 6 * (R.k1 + 2 * R.k2 + 2 * R.k3 + R.k4)
    CI[i + 1] <- CIi + dt / 6 * (CI.k1 + 2 * CI.k2 + 2 * CI.k3 + CI.k4)
  }
  if (!incidence) {
    return(data.frame(n = 0:n, S = S, I = I, R = R))
  } else {
    # newly infected per day (incidence)
    inc = c(I0, diff(CI))
    return(data.frame(n = 0:n, S = S, I = I, R = R, inc = inc, cum.inc = CI))
  }
}

1.2 RK4SIRS function

There are many repetitive computations in the implementation of RK4SIRS.std function, especially for calculating cummulative incidence. Actually, the system of ODEs of SIRS model describes the transition of an individual between susceptible, infectious, and recovered states. For SIRS model in simplified form \((\ref{eq2})\), only susceptible and infectious states are necessary to represent the system state and the system state at time \(t\) is usually expressed as state vector \(\mathbf{Z}(t)\equiv (S(t), I(t))\). As presented in Table 1, infection, recovery, and immunity loss events can change the system state at a rate of \(\frac{\beta SI}{N}\), \(\gamma I\), and \(f(N-S-I)\), respectively.

Table: Table 1. Markov chain transition rate (source: Dushoff et al. (2004))

Event Change Rate


Infection \((S,I)\rightarrow (S-1,I+1)\) \(\frac{\beta SI}{N}\)
Recovery \((S,I)\rightarrow (S, I-1)\) \(\gamma I\)
Immunity loss \((S,I)\rightarrow (S+1, I)\) \(f(N-S-I)\)

Instead of directly defining \(S(t)\) and \(I(t)\) compartments of SIRS model, using helper functions to define the events can avoid redundant computing as events describe the changes in system state at fine-grained scale. According to Table 1, RK4 method for solving SIRS model in simplified form \((\ref{eq2})\) can be easily implemented by slightly modified from RK4SIRS.std function.

# define infection rate
inf.rate <- function(t, S, I) {
  return(beta * S * I / N)
}

# define recovery rate
rec.rate <- function(t, I) {
  return(gamma * I)
}

# define immunity loss rate
ilos.rate <- function(t, S, I) {
  return(f * (N - S - I))
}

# RK4 method for numerical integration of SIRS model in simplified form
RK4SIRS <- function(n, beta, gamma, f, S0, I0, R0 = 0, dt = 1, incidence = FALSE) {
  N <<- S0 + I0 + R0
  
  S <- c(S0, rep(0, n))
  I <- c(I0, rep(0, n))
  R <- c(R0, rep(0, n))
  CI <- c(I0, rep(0, n))  # cummulative incidence
  for (i in 1:n) {
    Si <- S[i]
    Ii <- I[i]
    Ri <- R[i]
    CIi <- CI[i]
    
    Einf <- inf.rate(i, Si, Ii)  # event of infection
    Erec <- rec.rate(i, Ii)  # event of recovery
    Eilos <- ilos.rate(i, Si, Ii)  # event of immunity loss
    S.k1 <- Eilos - Einf
    I.k1 <- Einf - Erec
    CI.k1 <- Einf
    
    Einf <- inf.rate(i + dt / 2, Si + dt / 2 * S.k1, Ii + dt / 2 * I.k1)
    Erec <- rec.rate(i + dt / 2, Ii + dt / 2 * I.k1)
    Eilos <- ilos.rate(i + dt / 2, Si + dt / 2 * S.k1, Ii + dt / 2 * I.k1)
    S.k2 <- Eilos - Einf
    I.k2 <- Einf - Erec
    CI.k2 <- Einf
    
    Einf <- inf.rate(i + dt / 2, Si + dt / 2 * S.k2, Ii + dt / 2 * I.k2)
    Erec <- rec.rate(i + dt / 2, Ii + dt / 2 * I.k2)
    Eilos <- ilos.rate(i + dt / 2, Si + dt / 2 * S.k2, Ii + dt / 2 * I.k2)
    S.k3 <- Eilos - Einf
    I.k3 <- Einf - Erec
    CI.k3 <- Einf
    
    Einf <- inf.rate(i + dt, Si + dt * S.k3, Ii + dt * I.k3)
    Erec <- rec.rate(i + dt, Ii + dt * I.k3)
    Eilos <- ilos.rate(i + dt, Si + dt * S.k3, Ii + dt * I.k3)
    S.k4 <- Eilos - Einf
    I.k4 <- Einf - Erec
    CI.k4 <- Einf
    
    S[i + 1] <- Si + dt / 6 * (S.k1 + 2 * S.k2 + 2 * S.k3 + S.k4)
    I[i + 1] <- Ii + dt / 6 * (I.k1 + 2 * I.k2 + 2 * I.k3 + I.k4)
    CI[i + 1] <- CIi + dt / 6 * (CI.k1 + 2 * CI.k2 + 2 * CI.k3 + CI.k4)
  }
  R <- N - S - I
  
  if (!incidence) {
    return(data.frame(n = 0:n, S = S, I = I, R = R))
  } else {
    # newly infected per day (incidence)
    inc = c(I0, diff(CI))
    return(data.frame(n = 0:n, S = S, I = I, R = R, inc = inc, cum.inc = CI))
  }
}

1.3 rk4 function

Using rk4 function from deSolve package to solve SIRS model only needs to redefine the model and the vector of parameters used in SIRS model.

# vector of parameters used in SIRS model
params <- c(beta = beta, gamma = gamma, f = f)
# definition of SIRS model
SIRS <- function(t, y, params) {
  with(as.list(c(params, y)), {
    dS <- f * R - beta * S * I / N
    dI <- beta * S * I / N - gamma * I
    dR <- gamma * I - f * R
    list(c(dS, dI, dR))
  })
}

The above three functions are tested with the same study case of the spread of Hong Kong flu in New York city and they all give the same results. Note that the parameter \(f = \frac{1}{L} = \frac{1}{3.86 \times 365}\) comes from the parameters combination for generation of synthetic truth in Shaman and Karspeck (2012). Here, only results given by RK4SIRS function are presented to avoid redundancy.

# simulate the spread of Hong Kong flu of NYC
S0 <- 7900000
I0 <- 10
beta <- 1 / 2
gamma <- 1 / 3  # gamma = 1 / D
f <- 1 / (3.86 * 365)  # L = 3.86 years, f = 1 / L
n <- 200

r <- RK4SIRS(n, beta, gamma, f, S0, I0, incidence = TRUE)
# plot S, I, R curves
library(reshape2)
r.plot <- melt(r, id = "n", measure = c("S", "I", "R", "inc", "cum.inc"))
library(ggplot2)
p <- ggplot(r.plot, aes(x = n, y = value, group = variable, color = variable))
p + geom_line() + 
  scale_colour_discrete(name = "Legend", 
                        breaks = c("S", "I", "R", "inc", "cum.inc"), 
                        labels = c("Susceptible", "Infected ", "Recovered", "Incidence", 
                                   "Cumulative incidence")) + 
  ggtitle("Spread of Hong Kong Flu in New York City")
# peak of the infected
which.max(r$I)
## [1] 75
max(r$I)
## [1] 499451.1
# peak of incidence
which.max(r$inc)
## [1] 73
max(r$inc)
## [1] 174175.2

1.4 Benchmark

RK4SIRS.std, RK4SIRS and rk4 are separately runned 1000 times for benchmark performance analysis.

####
library(deSolve)
# initial (state) values for SIR model
y0 <- c(S = S0, I = I0, R = 0)
# vector of time steps
times <- 0:n
# vector of parameters used in SIRS model
params <- c(beta = beta, gamma = gamma, f = f)
# definition of SIRS model
SIRS <- function(t, y, params) {
  with(as.list(c(params, y)), {
    dS <- f * R - beta * S * I / N
    dI <- beta * S * I / N - gamma * I
    dR <- gamma * I - f * R
    list(c(dS, dI, dR))
  })
}
####
# benchmark
library(microbenchmark)
compare <- microbenchmark(rk4(y0, times, SIRS, params), 
                          RK4SIRS(n, beta, gamma, f, S0, I0, incidence = TRUE), 
                          RK4SIRS.std(n, beta, gamma, f, S0, I0, incidence = TRUE), 
                          times = 10)
# change expr for display
compare$expr <- gsub("rk4(y0, times, SIRS, params)", "rk4", 
                     compare$expr, fixed = TRUE)
compare$expr <- gsub("RK4SIRS(n, beta, gamma, f, S0, I0, incidence = TRUE)", "RK4SIRS", 
                     compare$expr, fixed = TRUE)
compare$expr <- gsub("RK4SIRS.std(n, beta, gamma, f, S0, I0, incidence = TRUE)", 
                     "RK4SIRS.std", compare$expr, fixed = TRUE)
compare
## Unit: milliseconds
##         expr      min        lq      mean    median        uq       max neval
##          rk4 11.46048 11.619344 13.132164 11.709227 12.170580 22.100666    10
##      RK4SIRS  2.66024  2.858313  3.158156  2.890771  2.994426  5.543579    10
##  RK4SIRS.std  4.17314  4.258929 13.824397  4.366474  4.504573 99.067068    10
# plot comparing results
library(ggplot2)
p <- autoplot(compare)
## Coordinate system already present. Adding new coordinate system, which will
## replace the existing one.
p + geom_violin(aes(color = expr, fill = expr)) + 
  theme(legend.position = "none")

As shown in the above figure, RK4SIRS function runs the fastest. Subsequently, SIRS model will be solved in simplified form \eqref{eq:2}.

1.5 Basic Reproductive Number \(R_0\)

Technically, the basic reproductive number \(R_0\) is defined as the number of secondary infections caused by a single infective introduced into a population made up entirely of susceptible individuals over the course of the infection of this single infective. For the susceptible population ($S_0 \approx N$), this infective individual makes \(\beta\) contacts per unit time producing new infections with a mean infectious period of \(D\). Therefore, the basic reproduction number is $$ \begin{equation} \label{eq4} R_0 = \beta D = \frac{\beta}{\gamma} \end{equation} $$

Actually, the basic reproductive number is often compared to 1 to determine whether an epidemic occurs or the disease simply dies out.

2 Time Dependent Transmission Rate \(\beta(t)\)

Equation (4) in Shaman et al. (2010) shows the relationship between basic reproductive number \(R_0(t)\) and specific humidity \(q(t)\) at time \(t\). Combining with equation \((\ref{eq4})\), it’s easy to deduce that transmission rate \(\beta\) is also modulated by specific humidity \(q\) and time dependent. Hence, replacing the constant \(\beta\) in ODEs \((\ref{eq2})\) with time dependent \(\beta (t)\) can make SIRS model more reasonable. The resulting SIRS model with time dependent transmission rate \(\beta (t)\) is expressed as follow $$ \begin{equation} \begin{aligned} \label{eq:5} & \frac{dS}{dt} = f(N - S - I) - \frac{\beta (t) SI}{N} \\ & \frac{dI}{dt} = \frac{\beta (t) SI}{N} - \gamma I \end{aligned} \end{equation} $$

2.1 Implementation

The RK4 method for numerical integration of SIRS model with time dependent transmission rate \(\beta(t)\) can be easily implemented by adding parameters to helper event functions.

# define infection rate
inf.rate <- function(t, beta, S, I) {
  return(beta * S * I / N)
}

# define recovery rate
rec.rate <- function(t, gamma, I) {
  return(gamma * I)
}

# define immunity loss rate
ilos.rate <- function(t, f, S, I) {
  return(f * (N - S - I))
}

# beta is a numeric vector of length n or 1
RK4SIRS <- function(n, beta, gamma, f, S0, I0, R0 = 0, dt = 1, incidence = FALSE) {
  N <<- S0 + I0 + R0
  
  S <- c(S0, rep(0, n))
  I <- c(I0, rep(0, n))
  R <- c(R0, rep(0, n))
  CI <- c(I0, rep(0, n))  # cummulative incidence
  for (i in 1:n) {
    bti <- beta[i]
    Si <- S[i]
    Ii <- I[i]
    CIi <- CI[i]
    
    Einf <- inf.rate(i, bti, Si, Ii)  # event of infection
    Erec <- rec.rate(i, gamma, Ii)  # event of recovery
    Eilos <- ilos.rate(i, f, Si, Ii)  # event of immunity loss
    S.k1 <- Eilos - Einf
    I.k1 <- Einf - Erec
    CI.k1 <- Einf
    
    Einf <- inf.rate(i + dt / 2, bti, Si + dt / 2 * S.k1, Ii + dt / 2 * I.k1)
    Erec <- rec.rate(i + dt / 2, gamma, Ii + dt / 2 * I.k1)
    Eilos <- ilos.rate(i + dt / 2, f, Si + dt / 2 * S.k1, Ii + dt / 2 * I.k1)
    S.k2 <- Eilos - Einf
    I.k2 <- Einf - Erec
    CI.k2 <- Einf
    
    Einf <- inf.rate(i + dt / 2, bti, Si + dt / 2 * S.k2, Ii + dt / 2 * I.k2)
    Erec <- rec.rate(i + dt / 2, gamma, Ii + dt / 2 * I.k2)
    Eilos <- ilos.rate(i + dt / 2, f, Si + dt / 2 * S.k2, Ii + dt / 2 * I.k2)
    S.k3 <- Eilos - Einf
    I.k3 <- Einf - Erec
    CI.k3 <- Einf
    
    Einf <- inf.rate(i + dt, bti, Si + dt * S.k3, Ii + dt * I.k3)
    Erec <- rec.rate(i + dt, gamma, Ii + dt * I.k3)
    Eilos <- ilos.rate(i + dt, f, Si + dt * S.k3, Ii + dt * I.k3)
    S.k4 <- Eilos - Einf
    I.k4 <- Einf - Erec
    CI.k4 <- Einf
    
    S[i + 1] <- Si + dt / 6 * (S.k1 + 2 * S.k2 + 2 * S.k3 + S.k4)
    I[i + 1] <- Ii + dt / 6 * (I.k1 + 2 * I.k2 + 2 * I.k3 + I.k4)
    CI[i + 1] <- CIi + dt / 6 * (CI.k1 + 2 * CI.k2 + 2 * CI.k3 + CI.k4)
  }
  R <- N - S - I
  
  if (!incidence) {
    return(data.frame(n = 0:n, S = S, I = I, R = R))
  } else {
    # newly infected per day (incidence)
    inc = c(I0, diff(CI))
    return(data.frame(n = 0:n, S = S, I = I, R = R, inc = inc, cum.inc = CI))
  }
}

2.2 Simulation

The following simulation example is adapted from Fig. 1a in Dushoff et al. (2004).

S0 <- 500000
I0 <- 100  # I0 has little effect on the dynamic equilibrium
R0 <- 0
N <- S0 + I0 + R0
L <- 4  # yr
f <- 1 / L
D <- 0.02  # yr
gamma <- 1 / D
dt <- 0.01  # yr
t <- seq(from = 0, to = 20, by = dt)
beta0 <- 500  # persons per yr
beta1 <- 0.04
beta <- beta0 * (1 + beta1 * cos(2 * pi * t))
n <- length(t[-1])

r <- RK4SIRS(n, beta, gamma, S0, I0, f = f, dt = dt, incidence = TRUE)
r <- cbind(t, r)
# plot S, I, R curves
library(reshape2)
r.plot <- melt(r, id = "t", measure = c("S", "I", "R", "inc", "cum.inc"))
# change levels for display
levels(r.plot$variable) <- c("Susceptible", "Infected", "Recovered", "Incidence", 
                             "Cummulative incidence")
library(ggplot2)
p <- ggplot(r.plot, aes(x = t, y = value, group = variable, color = variable))
p + geom_line() + 
  xlab("Time (years)") + 
  facet_wrap(~ variable, scales = "free") + 
  ggtitle("Simulation") + 
  theme_bw(base_size = 12) + 
  theme(legend.position = "none")

In Fig. 1a simulation, the basic reproductive number \(R_0=D\beta_0=\) 10, and the intrinsic period of oscillation \(T=2\pi \sqrt{\frac{DL}{R_0-1}}=\) 0.59. As shown in above figures, after starting simulation for several years, the compartments of SIRS model (including incidence) are in dynamic equilibrium. To show the details of deterministic equilibrium, the following codes only plot the subset results of 10 ~ 20 years.

r.eqb <- r[r$t >= 10, ]
r.plot <- melt(r.eqb, id = "t", measure = c("S", "I", "R", "inc", "cum.inc"))
levels(r.plot$variable) <- c("Susceptible", "Infected", "Recovered", "Incidence", 
                             "Cummulative incidence")
p <- ggplot(r.plot, aes(x = t, y = value, group = variable, color = variable))
p + geom_line() + 
  scale_x_continuous(breaks = 10:20, labels = 10:20, expand = c(0, 0)) + 
  xlab("Time (years)") + 
  facet_wrap(~ variable, scales = "free", ncol = 1) + 
  ggtitle("Subset Results of Equilibrium") + 
  theme_bw(base_size = 14) + 
  theme(legend.position = "none")

The Fig. 1b simulation is very similar to Fig. 1a, but with different parameters of \(L=8\) yr, \(D=0.025\) yr, and \(\beta_0=400\) persons per yr. The resulting plots of simulation are presented.

L <- 8  # yr
D <- 0.025  # yr
beta0 <- 400  # persons per yr

In Fig. 1b simulation, the basic reproductive number \(R_0=\) 10, and the intrinsic period of oscillation \(T=\) 0.94. From the Infected panel of Subset Results of Equilibrium figure, strong oscillations (noticing the y-axis range) due to resonance are observed. This is because comparing with Fig. 1a simulation, while the basic reprodutive number \(R_0\) keeps the same, the period of endogenous SIRS oscillation \(T\) is much more near the period of seasonal forcing (1 year) (see Dushoff et al. (2004) for further details).