What is a contact graph
Building a contact graph
Assume that we have \(M\) households and \(K\) institudions like age-care, prisons, hospitals Allocate these spatially
library('spatstat')
## Warning: package 'spatstat' was built under R version 3.6.2
## Loading required package: spatstat.data
## Loading required package: nlme
## Loading required package: rpart
##
## spatstat 1.64-1 (nickname: 'Help you I can, yes!')
## For an introduction to spatstat, type 'beginner'
##
## Note: R version 3.6.0 (2019-04-26) is more than a year old; we strongly recommend upgrading to the latest version
HHpos <- rpoispp(2, win=owin(c(0,10),c(0,10)),nsim=1)
INSpos <- rpoispp(.2, win=owin(c(0,10),c(0,10)),nsim=1)
plot(HHpos$x,HHpos$y,bty='n',xaxt='n',yaxt='n',xlab='Lat',ylab='long')
lines(INSpos$x,INSpos$y,type='p',pch='+',col='red')
For these
M <- length(HHpos$x)
K <- length(INSpos$x)
locations, we allocate residents
HHres <- rpois(M,2)+1
plot(table(HHres))
INSres <- rpois(K,20)+1
plot(table(INSres))
Create affiliation matrix
totallocs <- M+K
AllLocs <- c(HHres,INSres)
AllLocsPos <- matrix(0,totallocs,2)
AllLocsPos[1:M,1] <- HHpos$x
AllLocsPos[1:M,2] <- HHpos$y
AllLocsPos[(M+1):totallocs,1] <- INSpos$x
AllLocsPos[(M+1):totallocs,2] <- INSpos$y
npep <- sum(HHres)+ sum(INSres)
AFF <- matrix(0,npep,totallocs)
INDpos <- matrix(0,c(sum(HHres)+ sum(INSres) ), 2)
k <- 0
for (i in c(1:totallocs))
{
AFF[c(k+1):c(k+AllLocs[i]),i] <- 1
INDpos[c(k+1):c(k+AllLocs[i]),1] <- AllLocsPos[i,1]
INDpos[c(k+1):c(k+AllLocs[i]),2] <- AllLocsPos[i,2]
k<- k+AllLocs[i]
}
Plot affiliation matrix
library('sna')
## Loading required package: statnet.common
##
## Attaching package: 'statnet.common'
## The following object is masked from 'package:base':
##
## order
## Loading required package: network
## network: Classes for Relational Data
## Version 1.15 created on 2019-04-01.
## copyright (c) 2005, Carter T. Butts, University of California-Irvine
## Mark S. Handcock, University of California -- Los Angeles
## David R. Hunter, Penn State University
## Martina Morris, University of Washington
## Skye Bender-deMoll, University of Washington
## For citation information, type citation("network").
## Type help("network-package") to get started.
## sna: Tools for Social Network Analysis
## Version 2.4 created on 2016-07-23.
## copyright (c) 2005, Carter T. Butts, University of California-Irvine
## For citation information, type citation("sna").
## Type help(package="sna") to get started.
##
## Attaching package: 'sna'
## The following objects are masked from 'package:spatstat':
##
## is.connected, maxflow
## The following object is masked from 'package:nlme':
##
## gapply
library('network')
plot.sociomatrix(AFF, drawlab=FALSE, diaglab=FALSE, drawlines =FALSE)
Distance matrix
Following Daraganova et al. (2012) we calculate the dyadic distances \[D_{ij}=||P_i-P_j||^2\]
D <- matrix(0,npep,npep)
for (i in c(1: (npep-1)))
{
for (j in c((i+1): npep))
{
D[i,j] <- sum( (INDpos[i,]-INDpos[j,])^2 )
D[j,i] <- D[i,j]
}
}
Deterministic foci model
Assuming that there may only be close contact between people in the same location we have the following deterministic graph \[ \Pr(X_{ij}=1|D_{ij}) = \left\{ \begin{array}{lr} 1 ,&\text{if } D_{ij}=0 \\ 0,&\text{if } D_{ij}>0 \end{array} \right. {\text{.}} \]
ADJ <- matrix(0,npep,npep)
ADJ <- D==0
diag(ADJ) <- 0
plot( as.network( ADJ, directed=FALSE))
Attenuated power-law for distance interaction function
We may add ties between foci according to an attenuated power-law. Following Daraganova et al. (2012) we define the model \[ \Pr( X_{ij}= 1 | D_{ij})= \frac{ p_b}{1+\alpha D_{ij}^{\gamma}} \] Setting \(p_b=1\) we get a model that a tie-probability of one for members of the same foci with a tie-probability then decreasing according to a power-law.
Define a random spatial graph function
spat.bern <- function(D=NULL,pb=NULL,alpha=NULL,gamma=NULL,off.set=1000)
{
require('sna')
n <- nrow(D)
Dtemp <- D+off.set
Dtemp[D==0] <- 0
tie.prob <- pb/(1+alpha*Dtemp^gamma)
diag(tie.prob) <- 0
ADJ <- rgraph(n , m=1, tprob = tie.prob, mode='graph')
ADJ
}
Example graph
Set paramters and plot graph
ADJ <- spat.bern(D=D,pb=1,alpha=2,gamma=1)
gden(ADJ)
## [1] 0.009729958
plot(as.network(ADJ, directed=FALSE),vertex.cex=degree(ADJ)*0.01+0.01 )
Clearly, since institutions have more members they have more opportunities to form ties to people in other institutions - this does not seem realistic
Partition the affiliation matrix \(A=(B,C)\), where \(B\) are affiliations to households and \(C\) affiliations to institutions. Change the baseline parameter \(p_b\) for institutions and households to
\[ p_{bij} = \left\{ \begin{array}{lr} p_{B} ,&\text{if } B_i^{\top } {\bf{1}}>0 \text{ and } B_j^{\top } {\bf{1}}>0 \\ p_{BC} ,&\text{if } B_i^{\top } {\bf{1}}>0 \text{ and } C_j^{\top } {\bf{1}}>0 \\ p_{CC} ,&\text{if } C_i^{\top } {\bf{1}}>0 \text{ and } C_j^{\top } {\bf{1}}>0 \\ p_{C} ,&\text{if } C_i^{\top } C_j >0 \end{array} \right. {\text{.}} \]
Pb <- matrix(0,npep,npep)
numpepinHH <- sum(HHres)
numpepinINS <- sum(INSres)
Pb[1:numpepinHH ,1:numpepinHH ] <- 1
CC <- AFF[,(M +1):(M +K)] %*% t(AFF[,(M +1):(M+K)])
Pb[CC>0] <- 0.5
ADJ <- spat.bern(D=D,pb=Pb,alpha=0.5,gamma=1)
gden(ADJ)
## [1] 0.005937662
plot(as.network(ADJ, directed=FALSE),vertex.cex=degree(ADJ)*0.01+0.01 )
Workplaces
Allocate each person to workplaces. We have to consider the sizes of workplaces as well as the number of workplaces per person. For convenince, assume that people in institutions do not work and that a number of peopel do not have phsycal workplaces. Let us start with assuming that the numpepinHH
have an average of 0.75 workplaces and that the average workplace size is 5.
ave.workplace.size <- 5
ave.num.work <- 0.75
num.works <- matrix(0,npep,1)
num.works[1:numpepinHH] <- rpois(numpepinHH,lambda=ave.num.work)
set.num.works <- ceiling(sum(num.works)/ave.workplace.size)
WORK <- matrix(0,npep,set.num.works)
this.place <- rpois(set.num.works, lambda =(ave.workplace.size-1) )+1
for (i in c(1:set.num.works))
{
need.work <- which(num.works>0 & rowSums(WORK)< num.works)
sampsize <- min(length(need.work ), this.place[i] )
hire <- sample(need.work,size= sampsize, replace = FALSE)
WORK[hire,i] <- 1
}
Adding direct contacts in workplaces
We can increase the probability of people being close contacts if they belong to the same workplace.
\[ {\rm{logit}} \left\{ \Pr(X_{ij}=1 | W_{ij}= 1) \right\} - {\rm{logit}} \left\{ \Pr(X_{ij}=1 | W_{ij}= 0) \right\} = \phi \]
Construct the co-worker matrix
W <- WORK %*% t(WORK)
diag(W) <- 0
Now, use an ERGM to simulate the network \[ p_{\theta}(X) = \exp \left\{ \theta^{\top} z(x) - \psi(\theta) \right\} \]
Moderate work mixing
require('ergm')
## Loading required package: ergm
##
## ergm: version 3.10.4, created on 2019-06-10
## Copyright (c) 2019, Mark S. Handcock, University of California -- Los Angeles
## David R. Hunter, Penn State University
## Carter T. Butts, University of California -- Irvine
## Steven M. Goodreau, University of Washington
## Pavel N. Krivitsky, University of Wollongong
## Martina Morris, University of Washington
## with contributions from
## Li Wang
## Kirk Li, University of Washington
## Skye Bender-deMoll, University of Washington
## Chad Klumb
## Based on "statnet" project software (statnet.org).
## For license and citation information see statnet.org/attribution
## or type citation("ergm").
## NOTE: Versions before 3.6.1 had a bug in the implementation of the bd()
## constriant which distorted the sampled distribution somewhat. In
## addition, Sampson's Monks datasets had mislabeled vertices. See the
## NEWS and the documentation for more details.
## NOTE: Some common term arguments pertaining to vertex attribute and
## level selection have changed in 3.10.0. See terms help for more
## details. Use 'options(ergm.term=list(version="3.9.4"))' to use old
## behavior.
num.edges <- 5*dim(ADJ)[1] # simulating ERGM for large networks is almost impossible
ADJ.2 <- rgnm(1, nv=dim(ADJ)[1], m = num.edges, mode = "graph") # unless you fix the density
net <- as.network(ADJ,directed=FALSE)
net %v% 'inhome' <- rowSums(AFF[,(M +1):(M +K)] )
mod.1 <- net ~ nodecov('inhome')+ # indegree effect for sex
edgecov( log( D+0.001 ) ) + # distance interaction
edgecov( CC ) + # homophily on being in same home
edgecov( W ) + # homophily on being in same wrkplace
altkstar(1.1, fixed=TRUE) + #
gwesp(decay=log(2), fixed=TRUE)
coefficients <- c( -6, # low baseline for ties of people in institutions
-5, # distance decay
2, # being in same home
.1, # forming ties to work mates
.1, # positive effect for super spreaders
.1) # friends of my friends tend to be friends
sim.net <- simulate(mod.1,coef=coefficients, control = control.simulate.formula(MCMC.burnin = 100000),constraints=~edges)
## Warning: `set_attrs()` is deprecated as of rlang 0.3.0
## This warning is displayed once per session.
plot(sim.net,vertex.cex=degree(sim.net)*0.01+0.01 )
Strong work mixing
coefficients <- c(-6, # low baseline for ties of people in institutions
-5, # distance decay
2, # being in same home
3, # forming ties to work mates
.1, # positive effect for super spreaders
.1) # friends of my friends tend to be friends
sim.net.2 <- simulate(mod.1,coef=coefficients, control = control.simulate.formula(MCMC.burnin = 100000),constraints=~edges)
plot(sim.net.2,vertex.cex=degree(sim.net)*0.01+0.01 )
Increasing number of super spreaders
coefficients <- c(-6, # low baseline for ties of people in institutions
-5, # distance decay
2, # being in same home
3, # forming ties to work mates
.5, # positive effect for super spreaders
.1) # friends of my friends tend to be friends
sim.net.3 <- simulate(mod.1,coef=coefficients, control = control.simulate.formula(MCMC.burnin = 500000),constraints=~edges)
plot(sim.net.3,vertex.cex=degree(sim.net)*0.01+0.01 )
Disease model
Assume that a person can either be suceptible (S), infected (I), or recovered (R). Assuming that you cannot get reinfected you can only go from S to I to R. We assume that given that you are I, you are infectious for a stochastic amount of time with an average of 5.5 days. Once you stop being infections you transition to R. The assumption is that when you are symptomatic, you isolate. Approximating the process in discrete-time, an individual \(i\) who is S at day \(t\) transitions to I with a probability \(p_i( \mathcal{A}_t)\), where $ _t$ denotes the state of the system at time \(t\) \[ p_i( \mathcal{A}_t) = \left\{ \begin{array}{lr} \frac{\exp [\alpha \sum_{j \neq i} I_j x_{ij}]}{1+\exp [\alpha \sum_{j \neq i} I_j x_{ij}]} ,&\text{if } \sum_{j \neq i} I_j x_{ij}>0 \text{ and } S_i=1\\ 0,&\text{ otherwise } \end{array} \right. {\text{.}} \]
Incremental updates
New infections
newcases <- function(I=NULL,S=NULL,alpha=NULL, X=NULL){
n <- length(I)
neighbours <- X %*% I
mu <- alpha * neighbours # linear predictor
prob <- 1/( 1+ exp(-mu))
prob[neighbours==0] <- 0
new.case <- runif(n)<prob
I <- I + S*new.case
I
}
New recovered
Assume a geometric distribution, where the probability of success is \(p\). Denote the time until a success \(T\), then \(\Pr(T=1)=p\). The probability of \(T=2\) is \(\Pr(T=2)=(1-p)p\), of \(T=3\) \(\Pr(T=3)=(1-p)^2p\), and so on. The expected number of trials until a sucess is \(1/p\). Here, we set \(p=0.18\) to get an expected incubation period of 5.5 days.
newrecov <- function(I=NULL,R=NULL,p=NULL){
n <- length(I)
new.case <- runif(n)<p
R <- R+ I*new.case
R
}
Update infections
Having the current SIR state, we update using the function
up.date.sir <- function(SIR = NULL, alpha = NULL, p =NULL, X=NULL)
{
I <- newcases(I=SIR$I,S=SIR$S,alpha=alpha, X=X)
SIR$I <- I
SIR$S[I==1] <- 0
R <- newrecov(I=SIR$I,R=SIR$R,p=p)
SIR$R <- R
SIR$I[R==1] <- 0
SIR
}
Iterating
Initialise with one infercted
run.epidem <- function(ADJ=NULL,iterations=100,alpha=NULL,p=NULL,num.pat.zero=1,in.home=NULL)
{
n <- dim(ADJ)[1]
SIR <- data.frame(S=matrix(1,n,1),I=matrix(0,n,1),R=matrix(0,n,1))
sim.sir <-matrix(0,iterations,4)
patient.zero <- sample(c(1:n),size=num.pat.zero)
SIR$S[patient.zero] <- 0
SIR$I[patient.zero] <- 1
sim.sir[1,] <- c(colSums(SIR),sum(SIR[in.home==1,2]) )
for (t in c(2:iterations))
{
SIR <- up.date.sir(SIR = SIR, alpha = alpha, p =p, X=ADJ)
sim.sir[t,] <- c(colSums(SIR),sum(SIR[in.home==1,2]) )
}
sim.sir
}
Different Recovery/Removal rates
The removal rate depends on the adherence to isolation when developing symptoms. Additionally, this depends on the proportion of infections that result in no symptoms.
For a fixed network, vary recovery paramter from very small to the average contagion period being around 5.5 days. Note, for \(p=0.001\), the average incubabion period is 1000 days.
ADJ <- as.matrix.network(sim.net.3)
n <- nrow(ADJ)
alpha <- .5
iterations <- 100
p.nums <- seq(from=0.001,to =0.18,length.out = 25)
par(mfrow=c(5,5), mar=c(0,0,0,0))
for (k in c(1:25)){
sim.sir <- run.epidem(ADJ=ADJ,iterations=100,alpha=alpha,p=p.nums[k],num.pat.zero=10, in.home=rowSums(AFF[,(M +1):(M +K)] ))
plot(sim.sir[,2],type='l',ylim=range(sim.sir[,c(2:3)] ), xlab=NA,ylab=NA,main=NA,xaxt='n')
lines(sim.sir[,3],col='red')
lines(sim.sir[,4],col='blue')
grid( col = "lightgray", lty = "dotted")
}
More connected network
The contact network used in the previous example has several components
invisible( capture.output( cd<-component.dist(ADJ) ) )
plot(table(cd$csize))
The disease cannot jump between compponents. We can introduce some more heterogeneity in the number of contacts by increasing super-spreaders and reducing the distance decay.
num.edges <- 5*dim(ADJ)[1]
ADJ.2 <- rgnm(1, nv=dim(ADJ)[1], m = num.edges, mode = "graph")
net <- as.network(ADJ,directed=FALSE)
net %v% 'inhome' <- rowSums(AFF[,(M +1):(M +K)] )
mod.1 <- net ~ nodecov('inhome')+ # indegree effect for sex
edgecov( log( D+0.001 ) ) + # distance interaction
edgecov( CC ) + # homophily on being in same home
edgecov( W )+ # homophily on being in same wrkplace
altkstar(1.1, fixed=TRUE) + #
gwesp(decay=log(2), fixed=TRUE)
coefficients <- c(-6, # low baseline for ties of people in institutions
-1.5, # distance decay
0.5, # being in same home
0.7, # forming ties to work mates
0.75, # positive effect for super spreaders
0.3) # friends of my friends tend to be friends
sim.net.4 <- simulate(mod.1,coef=coefficients, control = control.simulate.formula(MCMC.burnin = 500000),constraints=~edges)
plot(sim.net.4,vertex.cex=degree(sim.net)*0.01+0.01 )
invisible( capture.output( cd<-component.dist(sim.net.4) ) )
plot(table(cd$csize))
This network is dominated by one large component and many isolated nodes.
ADJ <- as.matrix.network(sim.net.4)
n <- nrow(ADJ)
alpha <- .5
iterations <- 100
p.nums <- seq(from=0.001,to =0.18,length.out = 25)
par(mfrow=c(5,5), mar=c(0,0,0,0))
for (k in c(1:25)){
sim.sir.A <- run.epidem(ADJ=ADJ,iterations=100,alpha=alpha,p=p.nums[k],num.pat.zero=10,in.home=rowSums(AFF[,(M +1):(M +K)] ))
plot(sim.sir.A[,2],type='l',ylim=range(sim.sir.A[,c(2:3)] ), xlab=NA,ylab=NA,main=NA,xaxt='n')
lines(sim.sir.A[,3],col='red')
lines(sim.sir.A[,4],col='blue')
grid( col = "lightgray", lty = "dotted")
}
Difference in connectivity
Let’s look at the \(p=0.18\) incubation parameter for the graph with a without super spreaders
plot(sim.sir[,2],type='l',ylim=range(sim.sir[,2],sim.sir.A[,2] ), xlab=NA,ylab=NA,main=NA,xaxt='n')
lines( sim.sir.A[,2], col='blue')
grid( col = "lightgray", lty = "dotted")
It seems that even with radically higher connectivity, zero community detection is achieved with low incubation period
References
Daraganova, Galina, Pip Pattison, Johan Koskinen, Bill Mitchell, Anthea Bill, Martin Watts, and Scott Baum. 2012. “Networks and Geography: Modelling Community Network Structures as the Outcome of Both Spatial and Network Processes.” Social Networks 34 (1). Elsevier: 6–17.