Plotting collocation networks with R and ggraph
In a previous post, I showed how to use the
igraph package for R to plot a graph of the nominal collocates of the verbs hoard and stockpile in the Coronavirus Corpus (Davies, 2020). In this post, I show how to do the same with the
On February 2017, Thomas Lin Pedersen announced the release of
ggraph on CRAN. The motivation for
ggraphis the following:
The grammar of graphics as implemented inhttps://cran.r-project.org/web/packages/ggraph/index.html
ggplot2is a poor fit for graph and network visualizations due to its reliance on tabular data input.
ggraphis an extension of the
ggplot2API tailored to graph visualizations and provides the same flexible approach to building up plots layer by layer.
Although presented as an improvement upon
ggraph also plays the role of an
igraph add-on to bring the flexibility of the tidyverse grammar and more graphics settings into it. Naturally, because of its native tidyverse coloration,
ggraph is well suited to the logic of
We download, install, and load three packages:
rm(list=ls(all=TRUE)) install.library("igraph") install.library("ggraph") install.library("tidygraph") library(igraph) library(ggraph) library(tidygraph)
The data is the same as used previously. It consists of two files: a table of vertex attributes (
v.attr) and an edge list (
edges_data). We load them.
v.attr <- read.table("https://www.nakala.fr/data/11280/1f0d5c4c", header=T, sep="\t") edges_data <- read.table("https://www.nakala.fr/data/11280/cfd3793e", header=T, sep="\t")
We make a graph object with the
graph_from_data_frame function from
igraph. The function takes three arguments:
d, the data frame with the
vertices, a data frame with
v.attrin the first column;
directedwhich, set to FALSE, specifies that we want an undirected graph.
ig <- graph_from_data_frame(d=edges_data, vertices=v.attr, directed = FALSE)
Let us inspect the graph object:
> ig IGRAPH 4bca3fb UN-- 154 202 -- + attr: name (v/c), coll_freq (v/n), MI (e/n) + edges from 4bca3fb (vertex names):  food --hoard hoard --masks hoard --supplies hoard --toilet  hoard --paper cash --hoard hoard --items goods --hoard  groceries --hoard hoard --ventilators hoard --price hoard --profiteering  face --hoard hoard --wealth equipment --hoard hoard --products  commodities--hoard hoard --profiteers hoard --things hoard --panic  hoard --misuse essentials --hoard hand --hoard bottles --hoard  drugs --hoard drug --hoard behaviour --hoard devices --hoard  hoard --overpricing amounts --hoard hoard --sanitizer dollars --hoard + ... omitted several edges
The output says that we have a graph that is undirected (U) and named (N) with 154 nodes and 202 edges. The vertices have
coll_freq (i.e. collocation frequency) as a quantitative attribute, and the edges are weighted with
MI (i.e. mutual information).
For the sake of illustration, let us compare the
igraph object to its
tg <- tidygraph::as_tbl_graph(ig) %>% activate(nodes) %>% mutate(label=name)
R outputs the following:
> tg # A tbl_graph: 154 nodes and 202 edges # # An undirected simple graph with 1 component # # Node Data: 154 x 3 (active) name coll_freq label <chr> <int> <chr> 1 alcohol 17 alcohol 2 allocation 4 allocation 3 ammunition 3 ammunition 4 amounts 24 amounts 5 antibiotics 5 antibiotics 6 anticipation 8 anticipation # … with 148 more rows # # Edge Data: 202 x 3 from to MI <int> <int> <dbl> 1 45 60 6.41 2 60 76 5.59 3 60 137 6.72 # … with 199 more rows
tg is a tibble graph (
tbl_graph). As the
tidygraph documentation indicates:
tbl_graphclass is a thin wrapper around an
igraphobject that provides methods for manipulating the graph using the tidy API. As it is just a subclass of
igraphevery igraph method will work as expected.
tidygraph bridges the gap between
igraph and the tidyverse, which means that we can rely on
igraph while benefiting from the intuitive grammar of the tidyverse.
We index the size of each node on collocation frequency,
v.size <- V(tg)$coll_freq
and specify that mutual information is used to weight the edges of the graph.
E(tg)$weight <- E(tg)$MI
As previously, we use eigenvector centrality to spot the most influential nodes by:
- rearranging the centrality scores into quantized buckets known as ‘bins;
- assigning a color to each bin along a heat continuum skewed to the reds (for higher centrality scores) and yellows (for lower centrality scores).
eigenCent <- evcent(tg)$vector bins <- unique(quantile(eigenCent, seq(0,1,length.out=30))) vals <- cut(eigenCent, bins, labels=FALSE, include.lowest=TRUE) colorVals <- rev(heat.colors(length(bins)))[vals]
Finally, we plot the graph with the desired parameters. Note that the grammar is typical of the tidyverse, as shown by the pipe operator (
tg %>% ggraph(layout="stress") + geom_edge_diagonal(alpha = .2, color='white') + geom_node_point(size=log(v.size)*2, color=colorVals) + geom_node_text(aes(label = name), repel = TRUE, point.padding = unit(0.2, "lines"), size=log(v.size), colour='white') + theme_graph(background = 'grey20')
Here is what each line means:
ggraph(layout="stress"): “This layout is related to the stress-minimization algorithm known as Kamada-Kawai (available as the ‘kk’ layout), but uses another optimization strategy.”(https://rdrr.io/cran/ggraph/man/layout_tbl_graph_stress.html);
geom_edge_diagonal(alpha = .2, color='white'): this draws edges as diagonal bezier curves rather than straight lines. A level of transparency is added (
alpha = .2) and the edges are printed in white (
geom_node_point(size=log(v.size)*2, color=colorVals): the size of each node is indexed on the log of collocation frequency times two (
size=log(v.size)*2) and the color of each node is indexed on its centrality score (
geom_node_text(aes(label = name), repel = TRUE, point.padding = unit(0.2, "lines"), size=log(v.size), colour='white'): each node is assigned a label (
aes(label = name)). The position of labels is optimized to avoid overlapping text (
repel = TRUE). Labels are in white (
colour='white‘). Padding is added around the labeled data points (
point.padding = unit(0.2, "lines"));
theme_graph(background = 'grey20'): the background of the graph is colored in a dark shade of grey.
The results is an elegant graph (Fig. 1). See this previous post for a short analysis.
Note that we have not indexed edge width on MI. To do so, remove the
geom_edge_diagonal parameter and replace it with:
geom_edge_link(alpha = .25, colour='white, aes(width = weight)). The code is now:
tg %>% ggraph(layout="stress") + geom_edge_link(alpha = .25, colour='white', aes(width = weight)) + geom_node_point(size=log(v.size)*2, color=colorVals) + geom_node_text(aes(label = name), repel = TRUE, point.padding = unit(0.2, "lines"), size=log(v.size), colour="white") + theme_graph(background = 'grey20')
In a future post, I plan to conduct a diachronic study of collocation shifts to see how panick-buying has evolved along the whole COVID-19 crisis.
Davies, Mark. 2020. The Coronavirus Corpus. https://www.english-corpora.org/corona/ (last accessed 05.17.2020).
Pedersen, Thomas Lin. 2020.
ggraph: An Implementation of Grammar of Graphics for Graphs and Networks. R package version 2.0.2. https://CRAN.R-project.org/package=ggraph