library(igraph) # For network functionality
library(data.table)
library(ggplot2)
09. Networks (solutions)
Practical 1. Introduction to the igraph package
1. Building and plotting graphs
# Complete graph with 4 nodes
<- make_full_graph(4)
gr print(gr)
IGRAPH e377ec4 U--- 4 6 -- Full graph
+ attr: name (g/c), loops (g/l)
+ edges from e377ec4:
[1] 1--2 1--3 1--4 2--3 2--4 3--4
plot(gr)
Question (1): Run the plot(gr) line multiple times. The plot changes. Does the graph change?
Answer: The locations of the numbered nodes may change, but the network itself is not changing.
Plot different graphs each with 16 vertices, but different connections between vertices:
<- make_full_graph(16)
gr plot(gr)
<- make_ring(16)
gr plot(gr)
<- make_ring(16, circular = FALSE)
gr plot(gr)
<- make_lattice(c(4, 4))
gr plot(gr)
Compare a connected graph with 16 vertices to an Erdős-Rényi G(n, p) graph with 16 vertices:
plot(make_full_graph(16), layout = layout_in_circle)
plot(sample_gnp(16, 0.2), layout = layout_in_circle)
Other random graphs: The “small-world” model by Watts and Strogatz, where there are connections between neighbours, some of which are randomly rewired:
plot(sample_smallworld(1, 16, 2, 0.1), layout = layout_in_circle)
The “preferential attachment” model by Barabási and Albert, which is built by adding nodes one at a time, and each time a node is added, it is connected to other nodes, where the connection is more likely to be made to a node that already has more connections (a “rich get richer” dynamic).
plot(sample_pa(16, directed = FALSE))
2. Getting and setting properties of the graph
Start by making a new ‘lattice’ graph:
<- make_lattice(c(5, 5))
network print(network)
IGRAPH fa88a6b U--- 25 40 -- Lattice graph
+ attr: name (g/c), dimvector (g/n), nei (g/n), mutual (g/l), circular
| (g/l)
+ edges from fa88a6b:
[1] 1-- 2 1-- 6 2-- 3 2-- 7 3-- 4 3-- 8 4-- 5 4-- 9 5--10 6-- 7
[11] 6--11 7-- 8 7--12 8-- 9 8--13 9--10 9--14 10--15 11--12 11--16
[21] 12--13 12--17 13--14 13--18 14--15 14--19 15--20 16--17 16--21 17--18
[31] 17--22 18--19 18--23 19--20 19--24 20--25 21--22 22--23 23--24 24--25
plot(network)
Some simple calculations: vcount()
or ecount()
give the number of vertices or edges in the graph; degree()
gives the number of neighbours of each vertex.
vcount(network)
[1] 25
ecount(network)
[1] 40
degree(network)
[1] 2 3 3 3 2 3 4 4 4 3 3 4 4 4 3 3 4 4 4 3 2 3 3 3 2
With igraph
, you can get and set attributes of the entire graph using the $
operator. For example, let’s set the graph’s layout to a grid:
$layout <- layout_on_grid(network)
networkprint(network)
IGRAPH fa88a6b U--- 25 40 -- Lattice graph
+ attr: name (g/c), dimvector (g/n), nei (g/n), mutual (g/l), circular
| (g/l), layout (g/n)
+ edges from fa88a6b:
[1] 1-- 2 1-- 6 2-- 3 2-- 7 3-- 4 3-- 8 4-- 5 4-- 9 5--10 6-- 7
[11] 6--11 7-- 8 7--12 8-- 9 8--13 9--10 9--14 10--15 11--12 11--16
[21] 12--13 12--17 13--14 13--18 14--15 14--19 15--20 16--17 16--21 17--18
[31] 17--22 18--19 18--23 19--20 19--24 20--25 21--22 22--23 23--24 24--25
plot(network)
You can also modify properties of the vertices and of the edges, using V()
and E()
respectively.
V(network)$color <- "azure"
E(network)$color <- "pink"
plot(network) # lovely
You can use V(network)[[]]
or E(network)[[]]
to see the properties of the vertices/edges laid out as a data frame:
V(network)[[]]
+ 25/25 vertices, from fa88a6b:
color
1 azure
2 azure
3 azure
4 azure
5 azure
6 azure
7 azure
8 azure
9 azure
10 azure
11 azure
12 azure
13 azure
14 azure
15 azure
16 azure
17 azure
18 azure
19 azure
20 azure
21 azure
22 azure
23 azure
24 azure
25 azure
E(network)[[]]
+ 40/40 edges from fa88a6b:
tail head tid hid color
1 1 2 1 2 pink
2 1 6 1 6 pink
3 2 3 2 3 pink
4 2 7 2 7 pink
5 3 4 3 4 pink
6 3 8 3 8 pink
7 4 5 4 5 pink
8 4 9 4 9 pink
9 5 10 5 10 pink
10 6 7 6 7 pink
11 6 11 6 11 pink
12 7 8 7 8 pink
13 7 12 7 12 pink
14 8 9 8 9 pink
15 8 13 8 13 pink
16 9 10 9 10 pink
17 9 14 9 14 pink
18 10 15 10 15 pink
19 11 12 11 12 pink
20 11 16 11 16 pink
21 12 13 12 13 pink
22 12 17 12 17 pink
23 13 14 13 14 pink
24 13 18 13 18 pink
25 14 15 14 15 pink
26 14 19 14 19 pink
27 15 20 15 20 pink
28 16 17 16 17 pink
29 16 21 16 21 pink
30 17 18 17 18 pink
31 17 22 17 22 pink
32 18 19 18 19 pink
33 18 23 18 23 pink
34 19 20 19 20 pink
35 19 24 19 24 pink
36 20 25 20 25 pink
37 21 22 21 22 pink
38 22 23 22 23 pink
39 23 24 23 24 pink
40 24 25 24 25 pink
The “color” attribute is now also listed when we print the network:
network
IGRAPH fa88a6b U--- 25 40 -- Lattice graph
+ attr: name (g/c), dimvector (g/n), nei (g/n), mutual (g/l), circular
| (g/l), layout (g/n), color (v/c), color (e/c)
+ edges from fa88a6b:
[1] 1-- 2 1-- 6 2-- 3 2-- 7 3-- 4 3-- 8 4-- 5 4-- 9 5--10 6-- 7
[11] 6--11 7-- 8 7--12 8-- 9 8--13 9--10 9--14 10--15 11--12 11--16
[21] 12--13 12--17 13--14 13--18 14--15 14--19 15--20 16--17 16--21 17--18
[31] 17--22 18--19 18--23 19--20 19--24 20--25 21--22 22--23 23--24 24--25
Finally, we can also use brackets [] to change properties of only certain vertices/edges.
V(network)[12]$color <- "orange"
plot(network)
V(network)[color == "orange"]$color <- "pink"
plot(network)
Question (2): What do the above lines do?
Answer: Change the vertex with index 12 into orange. Then change all orange vertices from orange to pink.
Pink is contagious:
V(network)[.nei(color == "pink")]$color <- "pink"
plot(network)
Question (3): What is the code above? What happens if you re-run the last two lines above several times?
Answer: Change the colors of vertices neighboring a pink vertex to pink. The number of pink points in this network gradually increase.
Other interesting attributes for vertices include:
V(network)$label <- NA # text label for the vertices (set to NA for no labels)
V(network)$size <- 5 # size of vertex markers
V(network)$shape <- "square" # shape of markers
plot(network)
# See ?igraph.plotting for more.
Bonus: Code a network model
Here is one possible way of doing it…
<- make_lattice(c(5, 5)) network
Set all vertices to “susceptible” except for one “infected”
V(network)$state <- "S"
V(network)[1]$state <- "I"
# Pick plotting colours
<- c(S = "lightblue", I = "red", R = "pink")
colours
# Print and loop through time steps
<- 10
t_max for (t in 1:t_max)
{# Plot network
plot(network,
vertex.color = colours[V(network)$state],
layout = layout_on_grid,
main = paste("t = ", t))
# Pause so we can see animation
Sys.sleep(1.0)
# Find "infector" vertices
<- V(network)[state == "I"]
infectors
# Infect susceptible neighbours of infectors
V(network)[.nei(infectors) & state == "S"]$state <- "I"
# Recover infectors
V(network)[infectors]$state <- "R"
}
Practical 2. A network model of mpox transmission
library(igraph)
library(data.table)
library(ggplot2)
1. Setting up the network
Set up a transmission network of n nodes by preferential attachment with affinity proportional to degree^m.
<- function(n, d, layout = layout_nicely)
create_network
{# Create the network by preferential attachment,
# passing on the parameters n and power
<- sample_pa(n, d, directed = FALSE)
network
# Add the "state" attribute to the vertices of
#the network, which can be "S", "I", "R", or "V".
# Start out everyone as susceptible ...
V(network)$state <- "S"
# ... except make 5 random individuals infectious.
V(network)$state[sample(vcount(network), 5, prob = degree(network))] <- "I"
# Reorder vertices so they go in order from least to most connected. This
# is to help with degree-targeted vaccination, and also to make the
# most connected vertices plot on top so they don't get hidden.
<- permute(network, rank(degree(network), ties.method = "first"))
network # Set the network layout so it doesn't change every time it's plotted.
$layout <- layout(network)
network
return (network)
}
## See how the parameter d to create_network changes the network structure
<- create_network(40, 0)
net plot(net)
<- create_network(40, 1)
net plot(net)
<- create_network(40, 2)
net plot(net)
Question (1): Try varying the d
parameter for create_network
between 0 and 2. What changes about the network?
Answer: All vertices are increasingly connected through the same vertex.
Plot a network, highlighting the degree of each node by different colours.
<- function(network)
plot_degree
{# Set up palette
<- hcl.colors(5, "Zissou 1")
colors
# Classify nodes by degree
<- cut(degree(network),
deg breaks = c(1, 2, 5, 10, 20, Inf),
labels = c("1", "2-4", "5-9", "10-19", "20+"),
include.lowest = TRUE, right = FALSE)
# Plot network
plot(network,
vertex.color = colors[deg],
vertex.label = NA,
vertex.size = 4)
legend("topright", levels(deg), fill = colors, title = "Degree")
}
## Look at the degree distribution plotted for different values of d
<- create_network(500, 1)
net plot_degree(net)
<- create_network(500, 1.5)
net plot_degree(net)
<- create_network(500, 2)
net plot_degree(net)
Question (2): This time we’ve created a network with 500 nodes. Try varying the d
parameter again and plotting the generated networks. Are the results similar to what you saw before with 40 nodes?
Answer: Yes.
Plot a network, colouring by state (S/I/R/V).
<- function(network)
plot_state
{# Set up palette
<- c(S = "lightblue", I = "red", R = "darkblue", V = "white")
colors
# Plot network
plot(network,
vertex.color = colors[V(network)$state],
vertex.label = NA,
vertex.size = 4)
legend("topright", names(colors), fill = colors, title = "State")
}
## Test the plot_state function
<- create_network(500, 1)
net plot_state(net)
2. Running the model
Enact one step of the network model: infectious individuals infect susceptible neighbours with probability \(p\), and recover after one time step.
<- function(net, p)
network_step
{# Identify all susceptible neighbours of infectious individuals,
# who are "at risk" of infection
<- V(net)[state == "S" & .nei(state == "I")]
at_risk
# Use the transmission probability to select who gets exposed from
# among those at risk
<- at_risk[runif(length(at_risk)) < p]
exposed
# All currently infectious individuals will recover
V(net)[state == "I"]$state <- "R"
# All exposed individuals become infectious
V(net)[exposed]$state <- "I"
return (net)
}
<- create_network(500, 1)
net <- network_step(net, p = 0.8)
net plot_state(net)
Question (3): After creating your network with the first line above, run the last two lines repeatedly to watch the network model evolve.
Run the transmission model on the network with maximum simulation time t_max
and transmission probability p; plot the network as the model is running if
animate = TRUE
.
<- function(net, t_max, p, animate = FALSE)
run_model
{# Plot network degree
if (animate) {
plot_degree(net)
mtext("Network created")
Sys.sleep(2.0)
}
# Set up results
<- list()
dt
# Iterate over each time step
for (t in 0:t_max)
{# Store results
length(dt) + 1]] <- data.table(
dt[[S = sum(V(net)$state == "S"),
I = sum(V(net)$state == "I"),
R = sum(V(net)$state == "R"),
V = sum(V(net)$state == "V")
)
# Plot current state
if (animate) {
Sys.sleep(0.5)
plot_state(net)
mtext(paste0("t = ", t))
}
# Stop early if no infectious individuals are left
if (!any(V(net)$state == "I")) {
break;
}
# Run one step of the network model
<- network_step(net, p)
net
}
# Return results, including empirical calculation of Rt
<- rbindlist(dt, idcol = "t")
results $Rt <- results$I / shift(results$I, 1) # new infections per new infection last time step
resultsreturn (results)
}
Run an example simulation
<- create_network(500, 2)
net <- run_model(net, 100, 0.8, TRUE) res
<- head(res$S,1) - tail(res$S,1)
outbreak_size print(outbreak_size)
[1] 371
Question (4): How does the preferential attachment parameter (create_network
parameter d
) affect the final outbreak size?
Answer: changing d from 1 to 2 increase the outbreak size, changing d from 2 to 4 doesn’t change the outbreak sizes by much.
Bonus: vaccination and multiple runs
Vaccinate a fraction v of the nodes in the network. The parameter k, between -1 and 1, determines the association between network degree and vaccination.
<- function(network, v, k)
vaccinate_network
{# Count total population (n) and number to vaccinate (nv)
<- vcount(network)
n <- rbinom(1, n, v)
nv
# If k > 0, vaccinate the nv most-connected individuals; if k <= 0,
# vaccinate the nv least-connected individuals.
if (k > 0) {
<- (n - nv + 1):n
target else {
} <- 1:nv
target
}V(network)[target]$state <- "V"
# Now randomly shuffle the state of a fraction 1 - abs(k) of individuals.
<- which(rbinom(n, 1, 1 - abs(k)) == 1)
shuffle V(network)[shuffle]$state <- sample(V(network)[shuffle]$state)
return (network)
}
Run the model nsim
times with parameters in params (n, d, v, k, t_max, p), showing the animated network the first nanim
times, and returning a data.table
with the results of each simulation.
<- function(params, nsim, nanim = 1)
run_scenario
{<- list()
results
for (sim in 1:nsim)
{<- create_network(params$n, params$d)
net <- vaccinate_network(net, params$v, params$k)
net
<- run_model(net, params$t_max, params$p, animate = sim <= nanim)
results[[sim]]
cat(".")
}cat("\n");
<- rbindlist(results, idcol = "run")
results return (results)
}
Test multiple runs
<- list(
params n = 500,
d = 0,
p = 0.8,
v = 0.3,
k = -0.5,
t_max = 100
)
# Do a test run!
<- run_scenario(params, nsim = 50) x
..................................................
ggplot(x) +
geom_line(aes(x = t, y = R, group = run))
Return to the practical here.