# Load in required packages, deSolve and socialmixr
# If either package is not installed, install using the install.packages() function
library(deSolve)
library(socialmixr)
05b. Using a contact matrix
This is a short guide to how you might use a contact matrix in a model. This uses the socialmixr
package to generate a contact matrix from POLYMOD data for the UK, but you can also define your own contact matrices.
First step is to load in the required packages.
We now use socialmixr
’s contact_matrix
function to build a contact matrix from POLYMOD data for the UK. The age.limits
argument below specifies that we will consider 10-year age bands, from 0-9 up to 60-69 and finally 70+.
We then extract the contact matrix itself into mat
, and gather some info on the number of groups and their names.
<- contact_matrix(polymod, countries = "United Kingdom",
polymod_data age.limits = seq(0, 70, by = 10))
Removing participants that have contacts without age information. To change this behaviour, set the 'missing.contact.age' option
<- polymod_data$matrix
mat <- nrow(mat)
n_groups <- rownames(mat) group_names
For more information on socialmixr, try consulting the “socialmixr” vignette by running: vignette("socialmixr", package = "socialmixr")
.
Let’s take a look at what we have just created:
print(mat)
contact.age.group
age.group [0,10) [10,20) [20,30) [30,40) [40,50) [50,60) [60,70)
[0,10) 5.1522843 1.2487310 1.0964467 2.0203046 1.203046 0.6091371 0.3604061
[10,20) 1.2318841 8.0628019 1.1739130 1.7294686 1.710145 0.7004831 0.3719807
[20,30) 1.2711864 1.4406780 3.6016949 1.8898305 1.754237 1.1016949 0.4661017
[30,40) 1.9384615 1.1230769 1.5153846 2.7615385 1.730769 1.2076923 0.6153846
[40,50) 0.7863248 1.8547009 1.6153846 2.1794872 2.897436 1.3504274 0.6837607
[50,60) 0.5583333 0.8916667 1.6333333 1.5500000 1.483333 1.8583333 0.8166667
[60,70) 0.7096774 0.5913978 1.1720430 1.7204301 1.505376 1.4301075 1.1720430
70+ 0.2758621 1.0000000 0.6896552 0.4482759 1.413793 0.9310345 1.2068966
contact.age.group
age.group 70+
[0,10) 0.1421320
[10,20) 0.1545894
[20,30) 0.2033898
[30,40) 0.2923077
[40,50) 0.5982906
[50,60) 0.6333333
[60,70) 0.5483871
70+ 1.3448276
print(n_groups)
[1] 8
print(group_names)
[1] "[0,10)" "[10,20)" "[20,30)" "[30,40)" "[40,50)" "[50,60)" "[60,70)"
[8] "70+"
Note the usual pattern with a higher number of contacts along the diagonal. Base R’s matrix plotting functions are a bit tricky, but the lattice
package has a relatively easy to use function:
::levelplot(mat) lattice
Now let’s start bringing together our model function, initial state, parameters, and times, as normal for an ODE model.
First, the model function:
# Define model function
<- function(times, state, parms)
SIR_conmat_model
{# Get variables
<- state[1:n_groups]
S <- state[(n_groups + 1):(2 * n_groups)]
I <- state[(2 * n_groups + 1):(3 * n_groups)]
R <- S + I + R
N # Extract parameters
<- parms[["p"]]
p <- parms[["cm"]]
cm <- parms[["gamma"]]
gamma # Build force of infection
<- (p * cm %*% (I/N))[,1]
lambda # Define differential equations
<- -lambda * S
dS <- lambda * S - gamma * I
dI <- gamma * I
dR <- list(c(dS, dI, dR))
res return (res)
}
There’s a lot to unpack here, so let’s go step by step.
First, our state
vector needs to have n_groups
versions of the S compartment, n_groups
versions of the I compartment, and n_groups
versions of the R compartment. Within the vector, we’ll lay it out in this order: \((S_1, S_2, ..., S_n, I_1, I_2, ..., I_n, R_1, R_2, ..., R_n)\). So, in the lines
# Get variables
<- state[1:n_groups]
S <- state[(n_groups + 1):(2 * n_groups)]
I <- state[(2 * n_groups + 1):(3 * n_groups)]
R <- S + I + R N
we’re extracting S
, I
, and R
each as vectors holding n_groups
numbers each. Then we’re defining N
as the sum of those vectors; it will also hold n_groups
numbers, the first one corresponding to the 0-9 age group, the second one corresponding to the 10-19 age group, and so on.
Next we extract parameters:
# Extract parameters
<- parms[["p"]]
p <- parms[["cm"]]
cm <- parms[["gamma"]] gamma
Notice here we are using double brackets [[
instead of single brackets [[
like we normally do. This is because cm
will be our contact matrix, and you can’t really store a matrix inside a vector. You can store it in a list
though, which takes double brackets to extract elements.
Now we calculate our force of infection.
# Build force of infection
<- (p * cm %*% (I/N))[,1] lambda
This is a bit cryptic, so let’s break it down. What we are doing here is calculating \(\lambda_i = \sum_j c_{ij} I_j/N_j\), for all \(i\). One way of doing this would be with a for
loop:
= rep(0, n_groups)
lambda for (i in 1:n_groups) {
<- sum(p * cm[i, ] * I/N)
lambda[i] }
The line lambda <- (p * cm %*% (I/N))[,1]
is equivalent to this, and uses matrix multiplication %*%
to achieve what it’s doing. This will run a bit faster in R than the for
loop, but either way of calculating the force of infection, lambda (\(\lambda\)), is probably fine.
Finally we define and return the differential equations:
# Define differential equations
<- -lambda * S
dS <- lambda * S - gamma * I
dI <- gamma * I
dR <- list(c(dS, dI, dR))
res return (res)
Remember, S, I, and R are vectors, so dS, dI, and dR are also vectors with length n_groups
. The line res <- list(c(dS, dI, dR))
sticks them all together into a vector with n_groups
\(\,\times 3\) elements. Then this gets returned.
OK, that’s the end of the model function. Carrying on, we set up the parameters and times (remember, parms
will be a list
this time instead of a vector):
# Define parameters
<- list(p = 0.05, cm = mat, gamma = 0.2) # NOT c(p = 0.05, cm = mat, gamma = 0.2) !
parms
# Define time to run model
<- seq(from = 0, to = 50, by = 1) times
And define our initial conditions:
# Define initial conditions
<- seq(1000, 650, length.out = n_groups)
N0 <- rep(1, n_groups)
I0 <- N0 - I0
S0 <- rep(0, n_groups)
R0 names(S0) <- paste0("S", 1:n_groups)
names(I0) <- paste0("I", 1:n_groups)
names(R0) <- paste0("R", 1:n_groups)
<- c(S0, I0, R0) state
Note that this sets up the population size so there are 1000 0-9 year olds, 650 70+ year olds, and intermediate numbers in between. Let’s look at our initial state to make sure it’s correct:
print(state)
S1 S2 S3 S4 S5 S6 S7 S8 I1 I2 I3 I4 I5 I6 I7 I8 R1 R2 R3 R4
999 949 899 849 799 749 699 649 1 1 1 1 1 1 1 1 0 0 0 0
R5 R6 R7 R8
0 0 0 0
Looks good.
Now all we need to do is solve the ODE system and plot it as before:
# Solve equations
<- ode(y = state,
output_raw times = times,
func = SIR_conmat_model,
parms = parms)
# Convert to data frame for easy extraction of columns
<- as.data.frame(output_raw)
output
# Plot output
par(mfrow = c(1, 1))
matplot(output$time, output[, 2:ncol(output)], type = "l", xlab = "Time (years)", ylab = "Number of people")
We use matplot
here to quickly plot a large number of different lines, each stored in a different column of a matrix. Cool plot, but not so easy to see what’s going on, so let’s split it out by S versus I versus R:
# Make a 2x2 grid
par(mfrow = c(2, 2))
# Plot S, I, R on separate plots
matplot(output$time, output[, (0 * n_groups + 2):(1 * n_groups + 1)], type = "l",
xlab = "Time (years)", ylab = "Susceptible", col = rainbow(n_groups), lty = 1)
matplot(output$time, output[, (1 * n_groups + 2):(2 * n_groups + 1)], type = "l",
xlab = "Time (years)", ylab = "Infectious", col = rainbow(n_groups), lty = 1)
matplot(output$time, output[, (2 * n_groups + 2):(3 * n_groups + 1)], type = "l",
xlab = "Time (years)", ylab = "Recovered", col = rainbow(n_groups), lty = 1)
# Put a legend in the bottom right of the grid
plot.new()
legend("center", legend = rownames(mat), col = rainbow(n_groups), lty = 1)
# Put plot grid back to 1x1
par(mfrow = c(1, 1))