Plotting collocation networks with R: ‘hoard’ vs. ‘stockpile’ in the Coronavirus Corpus
This post is a follow-up to the previous one on graph theory and corpus linguistics. I show how to plot a graph of collocation networks with R and the
igraph package. The case study focuses on the nominal collocates of two near-synonymous verbs in the brand new Coronavirus Corpus: hoard and stockpile.
Graphs are linguistically relevant
A graph consists of vertices (nodes) and edges (links). In Fig. 1a, each circle is a node and each line is an edge. Each edge denotes a relationship between two nodes. The relationship is symmetric, i.e. undirected.
The edges of a graph may be asymmetric, i.e. have a direction associated with them. The graph in Fig. 1b illustrates this asymmetry. This graph is directed.
In a study on collocations, words are going to be the nodes and edges are going to stand for their co-occurrence. The attributes of a graph may be assigned linguistically relevant features. For example, the frequency of a constituent has a correlate in the importance of the node: frequent nodes may be represented with a larger size than infrequent nodes. The co-occurrence frequency of at least two nodes has a correlate in the number of edges between nodes: frequent co-occurrence can be visualized by means of either multiple edges or one edge whose thickness is indexed on frequency or some association metric.
The collocates of hoard and stockpile in the Coronavirus corpus
On May 15th, Mark Davies announced the release of the Coronavirus Corpus (Davies, 2020). Here is an excerpt from the official announcement, which I received by email:
The Coronavirus Corpus is designed to be the definitive record of the social, cultural, and economic impact of the coronavirus (COVID-19) in 2020 and beyond, and it is part of the English-Corpora.org suite of corpora, which offer unparalleled insight into genre-based, historical, and dialectal variation in English.
The corpus is currently about 270 million words in size, and it continues to grow by 3-4 million words each day. (For example, there are already 4 million words of text for yesterday, May 14). At this rate, the corpus may be 500-600 million words in size by August 2020.Mark Davies, 05/15/2020
I decided to use this brand-new and still-growing corpus to compare the collocates of two near-synonyms: hoard and stockpile. These two words appeared in the early days of the COVID-19 crisis, as people around the world started panick-buying toilet paper and other survival-related items. My goal is to plot a graph of the relations between the near-synonyms and their nominal collocates.
The data consist of the top 100 nominal collocates of the verbs hoard and stockpile in the Coronavirus Corpus on May 17, 2020. The dataset is available for download below.
I will be using the
igraph package because it is flexible, and thoroughly documented (Kolaczyk and Csárdi 2014; Arnold and Tilton 2015). Note that other interesting packages exist:
network (Butts 2008, 2015),
tidygraph (Pedersen, 2019), and
ggraph (Pedersen, 2020).1
First, install and load the
rm(list=ls(all=TRUE)) install.packages("igraph") library(igraph)
Next, two files are needed: (a) an edge list and (b) node attributes. An edge list is a list of connections between the nodes. Node attributes (or vertex attributes) list all the nodes and their properties. We load them.
v.attr <- read.table("https://www.nakala.fr/data/11280/1f0d5c4c", header=T, sep="\t") e.list <- read.table("https://www.nakala.fr/data/11280/cfd3793e", header=T, sep="\t")
v.attr contains the node attributes.
> head(v.attr) Name coll_freq 1 alcohol 17 2 allocation 4 3 ammunition 3 4 amounts 24 5 antibiotics 5 6 anticipation 8
We have two attributes: the name of each node and the number of times it is found as a collocate of hoard and stockpile (
coll_freq). Hoard and stockpile are also listed in the file, along with their overall frequencies. The names will be used to label the nodes, and the frequencies to adjust node sizes.
e.list is the edge list. Each row stands for an edge in the graph. Each cooccurrence of hoard/stockpile and an object noun is signalled by an edge. The first column contains the origins of the edges (
from) and the second column the end points (
> head(e.list) from to MI 1 hoard food 6.41 2 hoard masks 5.59 3 hoard supplies 6.72 4 hoard toilet 8.63 5 hoard paper 7.19 6 hoard cash 6.22
Here, I chose to weight the edges with a mutual information score (
MI). The higher the score, the stronger the association between hoard/stockpile and its collocate.
First, we create a graph object with the
graph.data.frame() function. We point to the edge list, the node attributes, and specify that we want an undirected graph.
G <- graph.data.frame(e.list, vertices=v.attr, directed=FALSE)
We obtain a graph object (
G) which we can inspect.
> G IGRAPH c067608 UN-- 154 202 -- + attr: name (v/c), coll_freq (v/n), MI (e/n) + edges from c067608 (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
Among other things, the output says that we have a graph that is undirected (U) and named (N) with 154 nodes and 202 edges. In
V stands for ‘vertex’ and
E for ‘edge’. We index the size of each node on collocation frequency,
v.size <- V(G)$coll_freq
and we assign a label to each node.
v.label <- V(G)$name
We also specify that mutual information is used to weight the edges of the graph.
E(G)$weight <- E(G)$MI
One crucial aspect of graphs is centrality. Graph centrality is a measure of how important a node is in the context of the entire graph. Here, it will be applied to detect the most prototypical collocations. Arguably, the three most popular measures of centrality are:
- degree centrality (nodes are ranked according to the number of edges to which they are connected),
- eigenvector centrality (nodes connected to important nodes are assigned a higher weight), and
- betweenness centrality (nodes are ranked according to how many pairs of nodes linked by the shortest path they are connected to).
We shall use eigenvector centrality to spot the most influential nodes. Eigenvector centrality is calculated with the
eigenCent <- evcent(G)$vector
The function outputs a list. We are interested in the vector element of the list. It is a vector that assigns each node a numerical score between 0 and 1. Let us take a look at the first twenty nodes.
> head(eigenCent, 20) alcohol allocation ammunition amounts antibiotics anticipation armaments 0.09555761 0.04980499 0.06276331 0.13068878 0.06118577 0.06941148 0.11279366 backlogs balances banners beans behavior behaviors behaviour 0.09915926 0.05746730 0.10345654 0.07431191 0.12330265 0.08163260 0.12269568 behaviours billions black-marketing blackmarketing bog boom 0.08881516 0.04867818 0.16395582 0.17832095 0.18386351 0.06781997
We are going to use the centrality score to color the nodes. This score is known to drop dramatically. See it for yourself by entering:
plot(sort(eigenCent, decreasing=TRUE), type="l")
We must therefore rearrange the centrality scores into quantized buckets known as ‘bins’. This is how you do it.
bins <- unique(quantile(eigenCent, seq(0,1,length.out=30))) vals <- cut(eigenCent, bins, labels=FALSE, include.lowest=TRUE)
Each bin is now assigned a color along a rainbow continuum skewed to the reds (for higher scores) and yellows (for lower scores). The color values are assigned to the color attributes of the nodes. The plotting function will shade the nodes according to their eigenvector centralities.
colorVals <- rev(heat.colors(length(bins)))[vals] V(G)$color <- colorVals
Next, we need to select a layout. I choose the Fruchterman-Reingold layout.
l <- layout.fruchterman.reingold(G)
A layout is an algorithm that defines the shape of the network graph. Although there are tons of layouts to choose from (see https://igraph.org/r/doc/layout_.html), I like using the Fruchterman-Reingold layout for plotting collocation networks involving near-synonyms because it captures the force-dynamics at work when attractions and repulsions are at play. This idea is summarized below:
The Fruchterman-Reingold Algorithm is a force-directed layout algorithm. The idea of a force directed layout algorithm is to consider a force between any two nodes. In this algorithm, the nodes are represented by steel rings and the edges are springs between them. The attractive force is analogous to the spring force and the repulsive force is analogous to the electrical force.https://github.com/gephi/gephi/wiki/Fruchterman-Reingold
Finally, we plot the graph with the desired parameters. Note that to keep the nodes and the labels from being too large, I have log-modified
plot(G, layout=l, edge.width=E(G)$weight, vertex.size=log(v.size), vertex.label=v.label, vertex.label.cex=log(v.size), vertex.label.color="black")
The resulting graph is displayed in Fig. 3.
Among other things, the graph allows you to see that:
- The nodes that correspond to hoard and stockpile are roughly the same size, an indication that they collocate with the same number of tokens given the top 100 types that were extracted from the corpus;
- the shared collocates in the middle of the graph are characterized by high centrality scores and are strongly specific to the COVID-19 crisis. They belong to the following classes:
- FOOD: food, foods, flour, meat, groceries,
- SHOPPING FOR SELF-SUFFICIENCY-RELATED SUPPLIES: essentials, necessities, amounts, quantities, supplies, goods, shoppers, stuff, materials, fuel, commodities, staples, cash,
- RESTROOM SUPPLIES: toilet, loo, bog, paper, rolls,
- MEDICAL SUPPLIES & EQUIPMENT: ventilators, (face)masks, bottles, medication(s), medicines, alcohol, (hand)-sanitizer, hydroxychloroquine, drugs, prescription, vaccines
- BEHAVIOR: behavio(u)r, panic-buying, panic.
- The most central nouns that are distinctive of hoard denote PROFITEERING: profiteering, profiteers, overpricing, black(-)marketing, price-gouging, smuggling, smugglers, misuse, wealth.
- No such clear specialization emerges among the most central nouns that are specific to stockpile (sns, high-grade, sinograin, recyclables, overkill, armaments, ness). However, among the less central nouns, we find terms that are representative of the concerns that are specific to the COVID-19 crisis: respirators, PPE (personal protective equipment), antibiotics, chemicals, gowns, warehouse(s), frenzy, etc. This is not surprising given the (intentional) thematic bias of the corpus.
The division of labor between hoard and stockpile depicted above is typical of near-synonyms. We learn as much about each synonym by inspecting the collocates they have in common as by inspecting their distinctive collocates.
When you work with the
igraph package, there are some things you need to know. First, for some unknown reason, if you do not switch off R between two graphs, you may end up with some strange result, including when you enter
Second, the vertex size and the labels will often be way too large. To avoid that, on top of log-modifying the sizes, enclose the plot call within a postscript graphics device as follows:
postscript("graph.eps", horizontal = FALSE, onefile = FALSE, paper = "special", height=60, width=60) plot(G, layout=l, edge.width=E(G)$weight, vertex.size=log(v.size), vertex.label=v.label, vertex.label.cex=log(v.size), vertex.label.color="black", vertex.label.dist=0.2, vertex.label.family="Arial") dev.off()
The above prints the graph on a giant virtual sheet (height=60, width=60). If this does not work, modify the height and the width until you reach a satisfying result. Also, do not hesitate to fiddle with the
igraph arguments, especially
Third, each time you call the plot function, this will generate a graph with identical relative distances but the overall node positions will be slightly different. To prevent this from happening, and for reproducible results, choose a number (any number) and include it as an argument of
set.seed(17)) before generating the graph object.
As you will soon realize, plotting network graphs involves its share of heuristic manipulations. But once you have found the right combination, the result is gratifying!
Arnold, Taylor, and Lauren Tilton. 2015. Humanities Data in R: Exploring Networks, Geospatial Data, Images, and Text. Quantitative Methods in the Humanities and Social Sciences. New York: Springer.
Butts C (2015).
network: Classes for Relational Data. The Statnet Project (http://www.statnet.org). R package version 220.127.116.11.
Butts C (2008). “
network: a Package for Managing Relational Data in R.” Journal of Statistical Software, 24(2).
Davies, Mark. 2020. The Coronavirus Corpus. https://www.english-corpora.org/corona/ (last accessed 05.17.2020).
Kolaczyk, Eric D., and Gábor Csárdi. 2014. Statistical Analysis of Network Data with R. Use R!, xii, 386 New York, London: Springer.
Pedersen, Thomas Lin. 2019.
tidygraph: A Tidy API for Graph Manipulation. R package version 1.1.2. https://CRAN.R-project.org/package=tidygraph
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
ggraphis based on
igraphbut the former improves upon the latter with respect to visualization.