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
gr <- make_full_graph(4)
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:
gr <- make_full_graph(16)
plot(gr)
gr <- make_ring(16)
plot(gr)
gr <- make_ring(16, circular = FALSE)
plot(gr)
gr <- make_lattice(c(4, 4))
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:
network <- make_lattice(c(5, 5))
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:
network$layout <- layout_on_grid(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), 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:
networkIGRAPH 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…
network <- make_lattice(c(5, 5))Set all vertices to “susceptible” except for one “infected”
V(network)$state <- "S"
V(network)[1]$state <- "I"
# Pick plotting colours
colours <- c(S = "lightblue", I = "red", R = "pink")
# Print and loop through time steps
t_max <- 10
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
infectors <- V(network)[state == "I"]
# 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.
create_network <- function(n, d, layout = layout_nicely)
{
# Create the network by preferential attachment,
# passing on the parameters n and power
network <- sample_pa(n, d, directed = FALSE)
# 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.
network <- permute(network, rank(degree(network), ties.method = "first"))
# Set the network layout so it doesn't change every time it's plotted.
network$layout <- layout(network)
return (network)
}
## See how the parameter d to create_network changes the network structure
net <- create_network(40, 0)
plot(net)
net <- create_network(40, 1)
plot(net)
net <- create_network(40, 2)
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.
plot_degree <- function(network)
{
# Set up palette
colors <- hcl.colors(5, "Zissou 1")
# Classify nodes by degree
deg <- cut(degree(network),
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
net <- create_network(500, 1)
plot_degree(net)
net <- create_network(500, 1.5)
plot_degree(net)
net <- create_network(500, 2)
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).
plot_state <- function(network)
{
# Set up palette
colors <- c(S = "lightblue", I = "red", R = "darkblue", V = "white")
# 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
net <- create_network(500, 1)
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.
network_step <- function(net, p)
{
# Identify all susceptible neighbours of infectious individuals,
# who are "at risk" of infection
at_risk <- V(net)[state == "S" & .nei(state == "I")]
# Use the transmission probability to select who gets exposed from
# among those at risk
exposed <- at_risk[runif(length(at_risk)) < p]
# All currently infectious individuals will recover
V(net)[state == "I"]$state <- "R"
# All exposed individuals become infectious
V(net)[exposed]$state <- "I"
return (net)
}
net <- create_network(500, 1)
net <- network_step(net, p = 0.8)
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.
run_model <- function(net, t_max, p, animate = FALSE)
{
# Plot network degree
if (animate) {
plot_degree(net)
mtext("Network created")
Sys.sleep(2.0)
}
# Set up results
dt <- list()
# Iterate over each time step
for (t in 0:t_max)
{
# Store results
dt[[length(dt) + 1]] <- data.table(
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
net <- network_step(net, p)
}
# Return results, including empirical calculation of Rt
results <- rbindlist(dt, idcol = "t")
results$Rt <- results$I / shift(results$I, 1) # new infections per new infection last time step
return (results)
}Run an example simulation
net <- create_network(500, 2)
res <- run_model(net, 100, 0.8, TRUE)





outbreak_size <- head(res$S,1) - tail(res$S,1)
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.
vaccinate_network <- function(network, v, k)
{
# Count total population (n) and number to vaccinate (nv)
n <- vcount(network)
nv <- rbinom(1, n, v)
# If k > 0, vaccinate the nv most-connected individuals; if k <= 0,
# vaccinate the nv least-connected individuals.
if (k > 0) {
target <- (n - nv + 1):n
} else {
target <- 1:nv
}
V(network)[target]$state <- "V"
# Now randomly shuffle the state of a fraction 1 - abs(k) of individuals.
shuffle <- which(rbinom(n, 1, 1 - abs(k)) == 1)
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.
run_scenario <- function(params, nsim, nanim = 1)
{
results <- list()
for (sim in 1:nsim)
{
net <- create_network(params$n, params$d)
net <- vaccinate_network(net, params$v, params$k)
results[[sim]] <- run_model(net, params$t_max, params$p, animate = sim <= nanim)
cat(".")
}
cat("\n");
results <- rbindlist(results, idcol = "run")
return (results)
}Test multiple runs
params <- list(
n = 500,
d = 0,
p = 0.8,
v = 0.3,
k = -0.5,
t_max = 100
)
# Do a test run!
x <- run_scenario(params, nsim = 50)












..................................................
ggplot(x) +
geom_line(aes(x = t, y = R, group = run))
Return to the practical here.