# Introduction

This tutorial serves as an introduction to network analysis in R using the quanteda, igraph , tidygraph , and ggraph packages.

Networks are a powerful method for visualizing relationships among various elements, such as authors, characters, or words. Network analysis goes beyond mere visualization; it’s a technique for uncovering patterns and structures within complex systems. In essence, network analysis represents relationships as nodes (elements) connected by edges (relationships) which provides a unique perspective for understanding the connections and interactions within your data.

Geared towards beginners and intermediate users of R, this tutorial aims to showcase how to perform network analysis based on textual data and it shows how to visualize networks using R. The primary goal is not to deliver a fully-fledged analysis but rather to demonstrate and exemplify selected useful methods associated with network analysis. This tutorial delves into creating and modifying network graphs, allowing users to explore and compare their statistical properties. By the end, you’ll not only grasp the basics of network analysis but also gain insights into leveraging statistical measures for a more comprehensive understanding of your data.

The entire R markdown document for the sections below can be downloaded here. If you want to render the Rmarkdown notebook on your machine, i.e. knitting the document to a html or pdf file, you need to make sure that you have R installed and you also need to download the bibliography file and store it in the same folder where you store the Rmd file.

Click here to open a Jupyter notebook that allows you to follow this tutorial interactively. This means that you can execute, change, and edit the code used in this tutorial to help you better understand how the code shown here works (make sure you run all code chunks in the order in which they appear - otherwise you will get an error).

Click on this badge to open an notebook-based tool

This tutorial builds on a tutorial on plotting collocation networks by Guillaume Desagulier, a tutorial on network analysis by offered by Alice Miller from the Digital Observatory at the Queensland University of Technology, and this tutorial by Andreas Niekler and Gregor Wiedemann.

# What is Network Analysis?

The most common way to visualize relationships between entities is through networks . Networks, also known as graphs, are powerful tools that represent relationships among entities. They consist of nodes (often depicted as dots) and edges (typically represented as lines) and can be categorized as directed or undirected networks.

• In directed networks, the direction of edges is captured, signifying the flow or relationship from one node to another. An example of a directed network is the trade relationships between countries, where arrows on the edges indicate the direction of exports. The thickness of these arrows can also encode additional information, such as the frequency or strength of the relationship.
• Undirected networks, on the other hand, represent symmetric relationships where the connection between two nodes is mutual. For example, in a social network, the connections between individuals are often undirected, as the relationship between friends is reciprocal.

Network analysis involves exploring the structure and properties of these networks. One key concept is centrality, which identifies the most important nodes in a network. Centrality metrics, such as degree centrality (number of connections) and betweenness centrality (importance in connecting other nodes), help unveil the significance of specific nodes.

In R, there are several packages that provide essential tools for constructing, analyzing, and visualizing networks but here, we will focus on the quanteda.textplots, igraph, tidygraph, and ggraph packages. To showcase how to prepare and generate network graphs, we will visualize the network that the characters in William Shakespeare’s Romeo and Juliet form.

Preparation and session set up

Certainly! Here’s a corrected and improved version of the passage:

This tutorial is built with and uses R (the R programming language). If you haven’t installed R or are new to it, you can find an introduction and more information on how to use R here. In order to execute the scripts and code chunks presented in this tutorial without errors, we need to install specific packages from the R library. Prior to delving into the code below, please install the required packages by running the code provided in this paragraph. If you’ve already installed the mentioned packages, feel free to skip ahead and disregard this section. To install the necessary packages, execute the following code. Note that it may take some time (between 1 and 5 minutes) to install all the libraries, so don’t worry if it takes a little while.

# install packages
install.packages("flextable")
install.packages("GGally")
install.packages("ggraph")
install.packages("igraph")
install.packages("Matrix")
install.packages("network")
install.packages("quanteda")
install.packages("sna")
install.packages("tidygraph")
install.packages("tidyverse")
install.packages("tm")
install.packages("tibble")
install.packages("quanteda.textplots")
# install klippy for copy-to-clipboard button in code chunks
install.packages("remotes")
remotes::install_github("rlesur/klippy")

# activate packages
library(flextable)
library(GGally)
library(ggraph)
library(gutenbergr)
library(igraph)
library(Matrix)
library(network)
library(quanteda)
library(sna)
library(tidygraph)
library(tidyverse)
library(tm)
library(tibble)
# activate klippy for copy-to-clipboard button
klippy::klippy()

Once you have installed R, RStudio, and have also initiated the session by executing the code shown above, you are good to go.

# Data preparation

In network analysis, it’s crucial to have at least one table indicating the start and end points of edges (lines connecting nodes). Additionally, two additional tables providing information on node size/type and edge size/type are valuable. In the upcoming sections, we’ll create these tables from raw data. Alternatively, you can generate network graphs by uploading tables containing the necessary information.

We’ll generate a network showing the frequency of characters in William Shakespeare’s Romeo and Juliet appearing in the same scene. Our focus is on investigating the networks of personas in Shakespeare’s Romeo and Juliet, and thus, we’ll load this renowned work of fiction.

## Creating a matrix

We start by loading the data which represents a table that contains the personas that are present during a sub-scene as well as how many contributions they make and how often they occur.

# load data
net_dat <- read.delim("https://slcladal.github.io/data/romeo_tidy.txt", sep = "\t")
First 15 rows of rom data.

actscene

person

contrib

occurrences

ACT I_SCENE I

BENVOLIO

24

7

ACT I_SCENE I

CAPULET

2

9

ACT I_SCENE I

FIRST CITIZEN

1

2

ACT I_SCENE I

1

10

ACT I_SCENE I

MONTAGUE

6

3

ACT I_SCENE I

PRINCE

1

3

ACT I_SCENE I

ROMEO

16

14

ACT I_SCENE I

TYBALT

2

3

ACT I_SCENE II

BENVOLIO

5

7

ACT I_SCENE II

CAPULET

3

9

ACT I_SCENE II

PARIS

2

5

ACT I_SCENE II

ROMEO

11

14

ACT I_SCENE II

SERVANT

8

3

ACT I_SCENE III

JULIET

5

11

ACT I_SCENE III

11

10

We now transform that table into a co-occurrence matrix.

net_cmx <- crossprod(table(net_dat[1:2]))
diag(net_cmx) <- 0
net_df <- as.data.frame(net_cmx)
First 5 rows and 5 columns of the of romeoco-occurrence matrix.

Persona

BALTHASAR

BENVOLIO

CAPULET

FIRST CITIZEN

FIRST SERVANT

BALTHASAR

0

0

1

0

0

BENVOLIO

0

0

3

2

1

CAPULET

1

3

0

1

2

FIRST CITIZEN

0

2

1

0

0

FIRST SERVANT

0

1

2

0

0

The data shows how often a character has appeared with each other character in the play - only Friar Lawrence and Friar John were excluded because they only appear in one scene where they talk to each other.

# Network Visualization

There are various different ways to visualize a network structure. We will focus on two packages for network visualization here and exemplify how you can visualize networks in R.

## Quanteda Networks

The quanteda package contains many very useful functions for analyzing texts. Among these functions is the textplot_network function which provides a very handy way to display networks. The advantage of the network plots provided by or generated with the quanteda package is that you can create them with very little code. However, this comes at a cost as these visualizations cannot be modified easily (which means that their design is not very flexible compared to other methods for generating network visualizations).

In a first step, we transform the text vectors of the romeo data into a document-feature matrix using the dfm function.

# create a document feature matrix
net_dfm <- quanteda::as.dfm(net_df)
# create feature co-occurrence matrix
net_fcm <- quanteda::fcm(net_dfm, tri = F)
# inspect data
head(net_fcm)
## Feature co-occurrence matrix of: 6 by 18 features.
##                 features
## features         BALTHASAR BENVOLIO CAPULET FIRST CITIZEN FIRST SERVANT
##   BALTHASAR              1       25      31            11             6
##   BENVOLIO              25       39      93            39            27
##   CAPULET               31       93      65            42            39
##   FIRST CITIZEN         11       39      42             6            10
##   FIRST SERVANT          6       27      39            10             3
##   FRIAR LAWRENCE        20       53      74            18            17
##                 features
## features         FRIAR LAWRENCE JULIET LADY CAPULET MERCUTIO MONTAGUE
##   BALTHASAR                  20     26           31       11       17
##   BENVOLIO                   53     87           99       42       55
##   CAPULET                    74    131          117       52       65
##   FIRST CITIZEN              18     32           36       24       29
##   FIRST SERVANT              17     40           42       12       15
##   FRIAR LAWRENCE             15     61           72       23       32
## [ reached max_nfeat ... 8 more features ]

This feature-co-occurrence matrix can then serve as the input for the textplot_network function which already generates a nice network graph.

Now we generate a network graph using the textplot_network function from the quanteda.textplots package. This function has the following arguments:

• x: a fcm or dfm object
• min_freq: a frequency count threshold or proportion for co-occurrence frequencies of features to be included (default = 0.5),
• omit_isolated: if TRUE, features do not occur more frequent than min_freq will be omitted (default = TRUE),
• edge_color: color of edges that connect vertices (default = “#1F78B4”),
• edge_alpha: opacity of edges ranging from 0 to 1.0 (default = 0.5),
• edge_size: size of edges for most frequent co-occurrence (default = 2),
• vertex_color: color of vertices (default = “#4D4D4D”),
• vertex_size: size of vertices (default = 2),
• vertex_labelcolor: color of texts. Defaults to the same as vertex_color,
• vertex_labelfont: font-family of texts,
• vertex_labelsize: size of vertex labels in mm. Defaults to size 5. Supports both integer values and vector values (default = 5),
• offset: if NULL (default), the distance between vertices and texts are determined automatically,
quanteda.textplots::textplot_network(
x = net_fcm,                    # a fcm or dfm object
min_freq = 0.5,                   # frequency count threshold or proportion for co-occurrence frequencies (default = 0.5)
edge_alpha = 0.5,                 # opacity of edges ranging from 0 to 1.0 (default = 0.5)
edge_color = "gray",            # color of edges that connect vertices (default = "#1F78B4")
edge_size = 2,                    # size of edges for most frequent co-occurrence (default = 2)
# calculate the size of vertex labels for the network plot
vertex_labelsize = net_dfm %>%
# convert the dfm object to a data frame
quanteda::convert(to = "data.frame") %>%
# exclude the 'doc_id' column
dplyr::select(-doc_id) %>%
# calculate the sum of row values for each row
rowSums() %>%
# apply the natural logarithm to the resulting sums
log(),
vertex_color = "#4D4D4D",         # color of vertices (default = "#4D4D4D")
vertex_size = 2                   # size of vertices (default = 2)
)

We now turn to generating tidy networks with is more complex but also offers more flexibility and options for customization.

## Tidy Networks

We now turn to a different method for generating networks that is extremely flexible.

First, we define the nodes and we can also add information about the nodes that we can use later on (such as frequency information).

# create a new data frame 'va' using the 'net_dat' data
net_dat %>%
# rename the 'person' column to 'node' and 'occurrences' column to 'n'
dplyr::rename(node = person,
n = occurrences) %>%
# group the data by the 'node' column
dplyr::group_by(node) %>%
# summarize the data, calculating the total occurrences ('n') for each 'node'
dplyr::summarise(n = sum(n)) -> va
Personas and their frequencies of occurrence in Shakespeare's *Romeo and Juliet*.

node

n

BALTHASAR

4

BENVOLIO

49

CAPULET

81

FIRST CITIZEN

4

FIRST SERVANT

4

FRIAR LAWRENCE

49

JULIET

121

100

MERCUTIO

16

MONTAGUE

9

NURSE

121

PARIS

25

PETER

4

PRINCE

9

ROMEO

196

SECOND SERVANT

9

SERVANT

9

TYBALT

9

The next part is optional but it can help highlight important information. We add a column with additional information to our nodes table.

# define family
mon <- c("ABRAM", "BALTHASAR", "BENVOLIO", "LADY MONTAGUE", "MONTAGUE", "ROMEO")
cap <- c("CAPULET", "CAPULET’S COUSIN", "FIRST SERVANT", "GREGORY", "JULIET", "LADY CAPULET", "NURSE", "PETER", "SAMPSON", "TYBALT")
oth <- c("APOTHECARY", "CHORUS", "FIRST CITIZEN", "FIRST MUSICIAN", "FIRST WATCH", "FRIAR JOHN" , "FRIAR LAWRENCE", "MERCUTIO", "PAGE", "PARIS", "PRINCE", "SECOND MUSICIAN", "SECOND SERVANT", "SECOND WATCH", "SERVANT", "THIRD MUSICIAN")
# create color vectors
va <- va %>%
dplyr::mutate(type = dplyr::case_when(node %in% mon ~ "MONTAGUE",
node %in% cap ~ "CAPULET",
TRUE ~ "Other"))
va
## # A tibble: 18 × 3
##    node               n type
##    <chr>          <int> <chr>
##  1 BALTHASAR          4 MONTAGUE
##  2 BENVOLIO          49 MONTAGUE
##  3 CAPULET           81 CAPULET
##  4 FIRST CITIZEN      4 Other
##  5 FIRST SERVANT      4 CAPULET
##  6 FRIAR LAWRENCE    49 Other
##  7 JULIET           121 CAPULET
##  8 LADY CAPULET     100 CAPULET
##  9 MERCUTIO          16 Other
## 10 MONTAGUE           9 MONTAGUE
## 11 NURSE            121 CAPULET
## 12 PARIS             25 Other
## 13 PETER              4 CAPULET
## 14 PRINCE             9 Other
## 15 ROMEO            196 MONTAGUE
## 16 SECOND SERVANT     9 Other
## 17 SERVANT            9 Other
## 18 TYBALT             9 CAPULET
Updated and enriched nodes table

node

n

type

BALTHASAR

4

MONTAGUE

BENVOLIO

49

MONTAGUE

CAPULET

81

CAPULET

FIRST CITIZEN

4

Other

FIRST SERVANT

4

CAPULET

FRIAR LAWRENCE

49

Other

JULIET

121

CAPULET

100

CAPULET

MERCUTIO

16

Other

MONTAGUE

9

MONTAGUE

NURSE

121

CAPULET

PARIS

25

Other

PETER

4

CAPULET

PRINCE

9

Other

ROMEO

196

MONTAGUE

Now, we define the edges, i.e., the connections between nodes and, again, we can add information in separate variables that we can use later on.

# create a new data frame 'ed' using the 'dat' data
ed <- net_df %>%
# add a new column 'from' with row names
dplyr::mutate(from = rownames(.)) %>%
# reshape the data from wide to long format using 'gather'
tidyr::gather(to, n, BALTHASAR:TYBALT) %>%
# remove zero frequencies
dplyr::filter(n != 0)
Edges between nodes

from

to

n

CAPULET

BALTHASAR

1

FRIAR LAWRENCE

BALTHASAR

1

JULIET

BALTHASAR

1

BALTHASAR

1

MONTAGUE

BALTHASAR

1

PARIS

BALTHASAR

1

PRINCE

BALTHASAR

1

ROMEO

BALTHASAR

2

CAPULET

BENVOLIO

3

FIRST CITIZEN

BENVOLIO

2

FIRST SERVANT

BENVOLIO

1

JULIET

BENVOLIO

1

BENVOLIO

2

MERCUTIO

BENVOLIO

4

MONTAGUE

BENVOLIO

2

Now that we have generated tables for the edges and the nodes, we can generate a graph object.

ig <- igraph::graph_from_data_frame(d=ed, vertices=va, directed = FALSE)

We will also add labels to the nodes as follows:

tg <- tidygraph::as_tbl_graph(ig) %>%
tidygraph::activate(nodes) %>%
dplyr::mutate(label=name)

When we now plot our network, it looks as shown below.

# set seed (so that the exact same network graph is created every time)
set.seed(12345)

# create a graph using the 'tg' data frame with the Fruchterman-Reingold layout
tg %>%
ggraph::ggraph(layout = "fr") +

# add arcs for edges with various aesthetics
geom_edge_arc(colour = "gray50",
lineend = "round",
strength = .1,
aes(edge_width = ed$n, alpha = ed$n)) +

# add points for nodes with size based on log-transformed 'v.size' and color based on 'va$Family' geom_node_point(size = log(va$n) * 2,
aes(color = va$type)) + # add text labels for nodes with various aesthetics geom_node_text(aes(label = name), repel = TRUE, point.padding = unit(0.2, "lines"), size = sqrt(va$n),
colour = "gray10") +

# adjust edge width and alpha scales
scale_edge_width(range = c(0, 2.5)) +
scale_edge_alpha(range = c(0, .3)) +

# set graph background color to white
theme_graph(background = "white") +

# adjust legend position to the top
theme(legend.position = "top",
# suppress legend title
legend.title = element_blank()) +

# remove edge width and alpha guides from the legend
guides(edge_width = FALSE,
edge_alpha = FALSE)

# Network Statistics

In addition to visualizing networks, we will analyze the network and extract certain statistics about the network that tell us about structural properties of networks.

To extract the statistics, we use the edge object generated above (called ed) and then repeat each combination as often as it occurred based on the value in the Frequency column.

dg <- ed[rep(seq_along(ed$n), ed$n), 1:2]
rownames(dg) <- NULL

The resulting object (dg) looks as shown below.

First 15 rows of dg data.

from

to

CAPULET

BALTHASAR

FRIAR LAWRENCE

BALTHASAR

JULIET

BALTHASAR

BALTHASAR

MONTAGUE

BALTHASAR

PARIS

BALTHASAR

PRINCE

BALTHASAR

ROMEO

BALTHASAR

ROMEO

BALTHASAR

CAPULET

BENVOLIO

CAPULET

BENVOLIO

CAPULET

BENVOLIO

FIRST CITIZEN

BENVOLIO

FIRST CITIZEN

BENVOLIO

FIRST SERVANT

BENVOLIO

## Degree centrality

We now generate an edge list from the dg object and then extract the degree centrality. The degree centrality reflects how many edges each node has with the most central node having the highest value.

dgg <- graph.edgelist(as.matrix(dg), directed = T)
# extract degree centrality
igraph::degree(dgg) %>%
as.data.frame() %>%
tibble::rownames_to_column("node") %>%
dplyr::rename(degree centrality = 2) %>%
dplyr::arrange(-degree centrality) -> dc_tbl
Degree centrality

node

degree centrality

ROMEO

108

CAPULET

92

90

NURSE

76

JULIET

72

BENVOLIO

68

MONTAGUE

44

PRINCE

44

TYBALT

44

PARIS

42

FRIAR LAWRENCE

40

SECOND SERVANT

32

MERCUTIO

30

SERVANT

30

FIRST CITIZEN

28

## Central node

Next, we extract the most central node.

names(igraph::degree(dgg))[which(igraph::degree(dgg) == max(igraph::degree(dgg)))]
## [1] "ROMEO"

## Betweenness centrality

We now extract the betweenness centrality. Betweenness centrality provides a measure of how important nodes are for information flow between nodes in a network. The node with the highest betweenness centrality creates the shortest paths in the network. The higher a node’s betweenness centrality, the more important it is for the efficient flow of goods in a network.

igraph::betweenness(dgg) %>%
as.data.frame() %>%
tibble::rownames_to_column("node") %>%
dplyr::rename(betweenness centrality = 2) %>%
dplyr::arrange(-betweenness centrality) -> bc_tbl
Betweenness centrality

node

betweenness centrality

ROMEO

27.62437026

16.27686423

CAPULET

15.62321868

BENVOLIO

9.61512099

NURSE

7.40145363

JULIET

5.55471008

TYBALT

3.19940849

MONTAGUE

2.18220323

PRINCE

2.18220323

PARIS

1.85942942

FRIAR LAWRENCE

1.09118044

MERCUTIO

0.84421390

PETER

0.26841707

SERVANT

0.23874480

FIRST CITIZEN

0.03846154

We now extract the node with the highest betweenness centrality.

names(igraph::betweenness(dgg))[which(igraph::betweenness(dgg) == max(igraph::betweenness(dgg)))]
## [1] "ROMEO"

## Closeness

In addition, we extract the closeness statistic of all edges in the dg object by using the closeness function from the igraph package. Closeness centrality refers to the shortest paths between nodes. The distance between two nodes represents the length of the shortest path between them. The closeness of a node is the average distance from that node to all other nodes.

igraph::closeness(dgg) %>%
as.data.frame() %>%
tibble::rownames_to_column("node") %>%
dplyr::rename(closeness = 2) %>%
dplyr::arrange(-closeness) -> c_tbl
Closeness statistic

node

closeness

0.05882353

ROMEO

0.05882353

CAPULET

0.05555556

BENVOLIO

0.05263158

JULIET

0.05000000

NURSE

0.04761905

TYBALT

0.04761905

MONTAGUE

0.04545455

PARIS

0.04545455

PRINCE

0.04545455

FRIAR LAWRENCE

0.04166667

SERVANT

0.04166667

FIRST SERVANT

0.04000000

MERCUTIO

0.04000000

SECOND SERVANT

0.04000000

We now extract the node with the highest closeness.

names(igraph::closeness(dgg))[which(igraph::closeness(dgg) == max(igraph::closeness(dgg)))]
## [1] "LADY CAPULET" "ROMEO"

We have reached the end of this tutorial and you now know how to create and modify networks in R and how you can highlight aspects of your data.

# Citation & Session Info

Schweinberger, Martin. 2024. Network Analysis using R. Brisbane: The University of Queensland. url: https://ladal.edu.au/net.html (Version 2024.04.24).

@manual{schweinberger2024net,
author = {Schweinberger, Martin},
title = {Network Analysis using R},
year = {2024},
organization = "The University of Queensland, Australia. School of Languages and Cultures},
edition = {2024.04.24}
}
sessionInfo()
## R version 4.3.2 (2023-10-31 ucrt)
## Platform: x86_64-w64-mingw32/x64 (64-bit)
## Running under: Windows 11 x64 (build 22621)
##
## Matrix products: default
##
##
## locale:
## [1] LC_COLLATE=English_Australia.utf8  LC_CTYPE=English_Australia.utf8
## [3] LC_MONETARY=English_Australia.utf8 LC_NUMERIC=C
## [5] LC_TIME=English_Australia.utf8
##
## time zone: Australia/Brisbane
## tzcode source: internal
##
## attached base packages:
## [1] stats     graphics  grDevices utils     datasets  methods   base
##
## other attached packages:
##  [1] tm_0.7-13            NLP_0.2-1            lubridate_1.9.3
##  [4] forcats_1.0.0        stringr_1.5.1        dplyr_1.1.4
## [10] tibble_3.2.1         tidyverse_2.0.0      tidygraph_1.3.1
## [13] sna_2.7-2            statnet.common_4.9.0 quanteda_4.0.1
## [16] network_1.18.2       Matrix_1.6-5         igraph_2.0.3
## [19] gutenbergr_0.2.4     ggraph_2.2.1         GGally_2.2.1
## [22] ggplot2_3.5.0        flextable_0.9.5
##
## loaded via a namespace (and not attached):
##  [1] gridExtra_2.3             rlang_1.1.3
##  [3] magrittr_2.0.3            compiler_4.3.2
##  [5] systemfonts_1.0.6         vctrs_0.6.5
##  [7] httpcode_0.3.0            pkgconfig_2.0.3
##  [9] crayon_1.5.2              fastmap_1.1.1
## [11] labeling_0.4.3            utf8_1.2.4
## [13] promises_1.3.0            rmarkdown_2.26
## [15] tzdb_0.4.0                ragg_1.3.0
## [17] xfun_0.43                 cachem_1.0.8
## [19] jsonlite_1.8.8            highr_0.10
## [21] later_1.3.2               uuid_1.2-0
## [23] tweenr_2.0.3              parallel_4.3.2
## [25] stopwords_2.3             R6_2.5.1
## [27] bslib_0.7.0               stringi_1.8.3
## [29] RColorBrewer_1.1-3        jquerylib_0.1.4
## [31] assertthat_0.2.1          Rcpp_1.0.12
## [33] knitr_1.46                klippy_0.0.0.9500
## [35] httpuv_1.6.15             timechange_0.3.0
## [37] tidyselect_1.2.1          rstudioapi_0.16.0
## [39] yaml_2.3.8                viridis_0.6.5
## [41] curl_5.2.1                lattice_0.21-9
## [43] plyr_1.8.9                shiny_1.8.1.1
## [47] coda_0.19-4.1             evaluate_0.23
## [49] ggstats_0.6.0             polyclip_1.10-6
## [51] zip_2.3.1                 xml2_1.3.6
## [53] pillar_1.9.0              generics_0.1.3
## [55] hms_1.1.3                 munsell_0.5.1
## [57] scales_1.3.0              xtable_1.8-4
## [59] quanteda.textplots_0.94.4 slam_0.1-50
## [61] glue_1.7.0                gdtools_0.3.7
## [63] tools_4.3.2               gfonts_0.2.0
## [65] data.table_1.15.4         graphlayouts_1.1.1
## [67] fastmatch_1.1-4           grid_4.3.2
## [69] colorspace_2.1-0          ggforce_0.4.2
## [71] cli_3.6.2                 textshaping_0.3.7
## [73] officer_0.6.5             fontBitstreamVera_0.1.1
## [75] fansi_1.0.6               viridisLite_0.4.2
## [77] gtable_0.3.4              sass_0.4.9
## [79] digest_0.6.35             fontquiver_0.2.1
## [81] ggrepel_0.9.5             crul_1.4.2
## [83] farver_2.1.1              memoise_2.0.1
## [85] htmltools_0.5.8.1         lifecycle_1.0.4
## [87] mime_0.12                 fontLiberation_0.1.0
## [89] openssl_2.1.2             MASS_7.3-60