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 ggraph package.


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 in ggplot2 is a poor fit for graph and network visualizations due to its reliance on tabular data input. ggraph is an extension of the ggplot2 API tailored to graph visualizations and provides the same flexible approach to building up plots layer by layer.

Although presented as an improvement upon ggplot2, 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 tidygraph.


We download, install, and load three packages: igraph, ggraph, and 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("", header=T, sep="\t")
edges_data <- read.table("", 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 edges_data;
  • vertices, a data frame with v.attr in the first column;
  • directed which, 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):
 [1] food       --hoard        hoard      --masks        hoard      --supplies     hoard      --toilet      
 [5] hoard      --paper        cash       --hoard        hoard      --items        goods      --hoard       
 [9] groceries  --hoard        hoard      --ventilators  hoard      --price        hoard      --profiteering
[13] face       --hoard        hoard      --wealth       equipment  --hoard        hoard      --products    
[17] commodities--hoard        hoard      --profiteers   hoard      --things       hoard      --panic       
[21] hoard      --misuse       essentials --hoard        hand       --hoard        bottles    --hoard       
[25] drugs      --hoard        drug       --hoard        behaviour  --hoard        devices    --hoard       
[29] 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 tidygraph counterpart:

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:

The tbl_graph class is a thin wrapper around an igraph object that provides methods for manipulating the graph using the tidy API. As it is just a subclass of igraph every 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. »(;
  • 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 (color='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 (color=colorVals).
  • 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.

Fig. 1. A graph of hoardstockpile, and their nominal collocates in the Coronavirus Corpus with ggraph. The edges appear as bezier curves.

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')
Fig. 2. A graph of hoardstockpile, and their nominal collocates in the Coronavirus Corpus with ggraph. The edges are weighted.

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 (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.

Cite this article as: Guillaume Desagulier, "Plotting collocation networks with R and ggraph," in Around the word, 19/05/2020,

Guillaume Desagulier

UMR 7114 MoDyCo — Université Paris 8, CNRS, Université Paris Nanterre, Institut Universitaire de France.

Vous aimerez aussi...

Laisser un commentaire

Votre adresse de messagerie ne sera pas publiée. Les champs obligatoires sont indiqués avec *

Ce site utilise Akismet pour réduire les indésirables. En savoir plus sur comment les données de vos commentaires sont utilisées.