Reading-In and Writing-Out Data

Author

Robert Ivánek

Published

September 25, 2024

Introduction

In this lecture, we will learn different ways how to read data from the disk into R’s memory, and how to write data out from R back to the disk. Along the way, we’ll learn a little about basic data object types in R, and how to examine created objects.

This handout covers the following topics:

  • Read in tables from text files (.csv / .tab / .tsv files)
  • Data file formats
  • R object (vector) types
  • Write out tables to text files (.csv / .tab / .tsv files)
  • Read in tables from Excel files (.xls / .xlsx files)
  • Saving/Loading Objects (.rdata / .rds files)

Set up

R Packages

We can extend the basic functionality of R by installing additional packages. These packages can be found either in several repositories. The main repositories are the official R package repository CRAN Bioconductor. The source code of many packages can be found in GitHub.

If you want to use extra package, it has to be:

  • installed first (one time action)
  • and loaded before use (at the beginning of each session).

We are going to use tidyverse framework, which is a collection of several packages for data analysis with new modern concept.

We are also going to use additional two packages, which provide functions for reading-in (readxl) and writing out XLS(X) (writexl) files.

All these packages can be installed via “Packages” tab of the GUI Menu:

Packages can be also installed by using “Console” and by typing in the following code:

# install tidyverse packages
install.packages("tidyverse")
# additionally install packages to handle XLS(X) files
install.packages(c("readxl", "writexl"))

Loading the packages

After successful installation we can load them to our R session.

library(tidyverse)
── Attaching core tidyverse packages ─────────────────── tidyverse 2.0.0 ──
✔ dplyr     1.1.4     ✔ readr     2.1.5
✔ forcats   1.0.0     ✔ stringr   1.5.1
✔ ggplot2   3.5.1     ✔ tibble    3.2.1
✔ lubridate 1.9.3     ✔ tidyr     1.3.1
✔ purrr     1.0.2     
── Conflicts ───────────────────────────────────── tidyverse_conflicts() ──
✖ dplyr::filter() masks stats::filter()
✖ dplyr::lag()    masks stats::lag()
ℹ Use the conflicted package (<http://conflicted.r-lib.org/>) to force all conflicts to become errors
library(readxl)
library(writexl)

Data files

Before we can read data files into R, we need to download some example files from the course webpage. These files are provided under “Code, Datasets” for each lecture.

Once you have downloaded the file, move it to somewhere organised, e.g. using the following steps:

  1. Go to your working_directory for this course.
  2. Create a new sub-directory and call it data.
  3. Move the newly downloaded files inside the data directory.

Alternatively we can also download the file via R in the “Console”:

# first we need to create a `data` directory
dir.create("data")
# then we can download the file
download.file(url = "https://ivanek.github.io/introductionToR/data/penguins.csv",
              destfile = "data/penguins.csv")

Reminders from Session 1

First, a few reminders of useful functions/operators seen in Session 1:

# Assignment of some values to an object
x <- 1:5

# Function to list objects in your workspace
# see also RStudio's Environment pane.
ls()

# Function to remove object x from workspace
rm(x)

# How to get help 
?ls
?rm

# "Where am I?"
getwd()

# Changing working directory
setwd("/take/me/to/this/directory/")

Data File formats

There are plenty of file formats for storing data that can be read into R, or written out from it. These can vary in many ways:

  • Plain text, e.g. .csv.
  • More advanced text files, e.g. .xml.
  • Compressed, e.g. .zip or .gz.
  • Open or Proprietary formats, e.g. .hdf5 and .xlsx, respectively.
  • Proprietary statistics formats, e.g. .dta for STATA, .sav for SPSS.
  • R formats, e.g. .rds and .rdata.

We will focus on the two commonly found formats in biomedical research, i.e. mostly plain text format .csv, Excel files .xls and .xlsx, and of course on the R formats.

Read in tables from text files (.csv / .tab / .tsv files)

Using the GUI interface of RStudio

In case of these plain text files, we mostly assume that they contain data in a tabular format, where individual columns are separated by a delimiter (,, ;, or \t)

We can navigate to the file data/penguins.csv in the “File” tab of the RStudio. We can directly open the file (“View”) or load it into the current R session by selecting “Import Dataset…” option in the context menu.

This will open new window where we can set few parameters for the import. On the bottom-right side we can see the code which will be executed once we press “Import” button. Let’s copy it to the script, which would allows us to reproduce our steps in the future.

We can now also explore the different options for the import. Try to change and check how this is reflected in the code:

  • “First Row as Names”
  • “Open Data Viewer”
  • “Delimiter”
  • “NA” values

Exercise 1

Choose the best options to import the dataset.

penguins <- read_csv("data/penguins.csv")

Exercise 2

Create a script and save all code used so far into it.

The new script can be opened via menu “File” -> “New File” -> “R Script”. Save it into your working directory under the name: ‘lecture_02.R’. Do not forget to include the loading of the libraries. Optionally, add there also code block, which was used to download the file.

library(tidyverse)
library(readxl)
library(writexl)
library(readr)

# first we need to create a `data` directory
dir.create("data")
# then we can download the file
download.file(url = "https://ivanek.github.io/introductionToR/data/penguins.csv",
              destfile = "data/penguins.csv")

penguins <- read_csv("data/penguins.csv")

Explore the resulting object (GUI)

We have loaded the file into variable penguins. Let’s view it using the “Environment tab”:

This dataset is coming from the R package palmerpenguins. It was specifically designed for data exploration and visualization. There are three different species of penguins in this dataset, collected from three islands in the Palmer Archipelago, Antarctica. Description of individual columns can be found in the help page of the dataset penguins.

Exercise 3

In order to be able to access the help page, we need to install the package first. Install the package palmerpenguins and find out what are the individual columns in the dataset. Help page can be found in the “Help” tab.

install.packages("palmerpenguins")
library(palmerpenguins)
?penguins

Exploring the object in details

We can also explore the object directly in console by typing in variable name:

penguins
# A tibble: 344 × 8
   species island    bill_length_mm bill_depth_mm flipper_length_mm
   <chr>   <chr>              <dbl>         <dbl>             <dbl>
 1 Adelie  Torgersen           39.1          18.7               181
 2 Adelie  Torgersen           39.5          17.4               186
 3 Adelie  Torgersen           40.3          18                 195
 4 Adelie  Torgersen           NA            NA                  NA
 5 Adelie  Torgersen           36.7          19.3               193
 6 Adelie  Torgersen           39.3          20.6               190
 7 Adelie  Torgersen           38.9          17.8               181
 8 Adelie  Torgersen           39.2          19.6               195
 9 Adelie  Torgersen           34.1          18.1               193
10 Adelie  Torgersen           42            20.2               190
# ℹ 334 more rows
# ℹ 3 more variables: body_mass_g <dbl>, sex <chr>, year <dbl>

We can see, that in contrary to the viewer, only part of the dataset was printed on scree. At the same time, in the header we can read: # A tibble: 344 × 8. The tibble is a modern alternative to original data.frame. The word is coming from Australian spelling of table.

To some extent, it is similar to “Excel” sheet.

tibble

  • structure to store tabular data
  • it is always rectangular, each column has the same number of rows.
  • every column (named) can store only one data type, exception is column with list
  • there are no row names, only index
  • it has a special print method, which by default shows only first 10 rows and only columns which fit to the screen
  • there is also a main difference in sub-setting, but this is the topic of the next lecture
Comparison of tibble to data.frame

data.frame

  • part of base R, still the most frequently used object type
  • similar to tibble also stores tabular data, each column can contain only a single data type
  • can have row names
  • there is no default print method, the entire table is printed on the screen!
  • supports arithmetic operations on all columns at once
  • suffers from inconsistent behaviors (subsetting, value recycling while creating)

One can easily convert between the tibble and the data.frame:

  • as_tibble()
  • as.data.frame()

Individual Columns of the tibble

We could see that the default print method also shows short description under each column name (<chr>, <dbl>). The function read_csv tried to guess the appropriate format for each of them. Typically a column contain a vector with values.

Graphics taken from: https://dcl-prog.stanford.edu/list-columns.html

Graphics taken from: https://dcl-prog.stanford.edu/list-columns.html
Warning

tibble column can contain also more complicated structures like list. And the list can contain anything, so potentially it can be a list of tibbles.

For the simple vectors, there are:

  • character: to store string data
species <- c("Gentoo", "Adelie", "Gentoo", "Chinstrap", "Adelie")
class(species)
[1] "character"
is.character(species)
[1] TRUE
  • double or numeric: to store floating point numbers
bill_length_mm <- c(44.5, 38.6, 45.3, 52.8, 37.3)
class(bill_length_mm) # numeric is the name of the type, implicit class
[1] "numeric"
is.numeric(bill_length_mm) 
[1] TRUE
is.double(bill_length_mm) # double is the name of type
[1] TRUE
  • integer: to store whole numbers
body_mass_g <- c(4100, 3800, 4300, 4550, 3775)
class(body_mass_g) # numeric!
[1] "numeric"
# for integers we need to specifically ask for that representation
body_mass_g <- as.integer(body_mass_g)
class(body_mass_g)
[1] "integer"
is.integer(body_mass_g)
[1] TRUE
# or directly while creating it, by adding `L` to each value
body_mass_g <- c(4100L, 3800L, 4300L, 4550L, 3775L)
class(body_mass_g)
[1] "integer"
is.integer(body_mass_g)
[1] TRUE
  • logical: only TRUE or FALSE values
rare <- c(TRUE, TRUE, FALSE, TRUE, FALSE)
class(rare)
[1] "logical"
is.logical(rare)
[1] TRUE
  • factor: similar to character but uses different representation of the data. Typically used with few categories (sex, education, …). There is a collection of unique labels (“male”, “female”) called levels and the vector of indices pointing to the appropriate level. This was the object makes much smaller footprint in the memory.
sex <- c("male", "female", "female", "female", "female")
sex <- factor(sex, levels=c("female", "male"))
class(sex)
[1] "factor"
is.factor(sex)
[1] TRUE
sex
[1] male   female female female female
Levels: female male
# levels
levels(sex)
[1] "female" "male"  
# individual values
as.integer(sex) # in fact there are only integers pointing to the individual level 
[1] 2 1 1 1 1
# replace all values with `f` and `m`
# we can only replace the levels -> the individual values are just integers == indices!
levels(sex) <- c("f", "m")
  • various representations for date and times

For details see the package lubridate.

library(lubridate)

collected <- c("2007-06-01", "2009-04-15", "2009-07-20", "2009-09-16", "2009-12-24")
collected <- ymd(collected)
class(collected)
[1] "Date"
# special functions to extract specific information
year(collected)
[1] 2007 2009 2009 2009 2009
month(collected)
[1]  6  4  7  9 12
day(collected)
[1]  1 15 20 16 24

Functions to explore the objects

Often we are interested in general properties of the object, like length, size, dimensions, unique values and maybe a general overview of the structure. Here are the most commonly used functions to explore the properties of a vector:

# length of a vector
length(species)
[1] 5
# unique values
unique(species)
[1] "Gentoo"    "Adelie"    "Chinstrap"
# general structure
str(species)
 chr [1:5] "Gentoo" "Adelie" "Gentoo" "Chinstrap" "Adelie"
str(sex)
 Factor w/ 2 levels "f","m": 2 1 1 1 1
# summary
summary(species)
   Length     Class      Mode 
        5 character character 
summary(bill_length_mm)
   Min. 1st Qu.  Median    Mean 3rd Qu.    Max. 
   37.3    38.6    44.5    43.7    45.3    52.8 
summary(sex)
f m 
4 1 

For two dimensional objects (with rows and columns), we can use additionally:

# dimensions
dim(penguins)
[1] 344   8
# number of rows
nrow(penguins)
[1] 344
# number of columns
ncol(penguins)
[1] 8
# general structure
str(penguins)
spc_tbl_ [344 × 8] (S3: spec_tbl_df/tbl_df/tbl/data.frame)
 $ species          : chr [1:344] "Adelie" "Adelie" "Adelie" "Adelie" ...
 $ island           : chr [1:344] "Torgersen" "Torgersen" "Torgersen" "Torgersen" ...
 $ bill_length_mm   : num [1:344] 39.1 39.5 40.3 NA 36.7 39.3 38.9 39.2 34.1 42 ...
 $ bill_depth_mm    : num [1:344] 18.7 17.4 18 NA 19.3 20.6 17.8 19.6 18.1 20.2 ...
 $ flipper_length_mm: num [1:344] 181 186 195 NA 193 190 181 195 193 190 ...
 $ body_mass_g      : num [1:344] 3750 3800 3250 NA 3450 ...
 $ sex              : chr [1:344] "male" "female" "female" NA ...
 $ year             : num [1:344] 2007 2007 2007 2007 2007 ...
 - attr(*, "spec")=
  .. cols(
  ..   species = col_character(),
  ..   island = col_character(),
  ..   bill_length_mm = col_double(),
  ..   bill_depth_mm = col_double(),
  ..   flipper_length_mm = col_double(),
  ..   body_mass_g = col_double(),
  ..   sex = col_character(),
  ..   year = col_double()
  .. )
 - attr(*, "problems")=<externalptr> 
# summary of each column
summary(penguins)
   species             island          bill_length_mm  bill_depth_mm  
 Length:344         Length:344         Min.   :32.10   Min.   :13.10  
 Class :character   Class :character   1st Qu.:39.23   1st Qu.:15.60  
 Mode  :character   Mode  :character   Median :44.45   Median :17.30  
                                       Mean   :43.92   Mean   :17.15  
                                       3rd Qu.:48.50   3rd Qu.:18.70  
                                       Max.   :59.60   Max.   :21.50  
                                       NA's   :2       NA's   :2      
 flipper_length_mm  body_mass_g       sex                 year     
 Min.   :172.0     Min.   :2700   Length:344         Min.   :2007  
 1st Qu.:190.0     1st Qu.:3550   Class :character   1st Qu.:2007  
 Median :197.0     Median :4050   Mode  :character   Median :2008  
 Mean   :200.9     Mean   :4202                      Mean   :2008  
 3rd Qu.:213.0     3rd Qu.:4750                      3rd Qu.:2009  
 Max.   :231.0     Max.   :6300                      Max.   :2009  
 NA's   :2         NA's   :2                                       

There are also special functions to display the first or last few elements/rows of an object:

head(penguins)
# A tibble: 6 × 8
  species island bill_length_mm bill_depth_mm flipper_length_mm body_mass_g
  <chr>   <chr>           <dbl>         <dbl>             <dbl>       <dbl>
1 Adelie  Torge…           39.1          18.7               181        3750
2 Adelie  Torge…           39.5          17.4               186        3800
3 Adelie  Torge…           40.3          18                 195        3250
4 Adelie  Torge…           NA            NA                  NA          NA
5 Adelie  Torge…           36.7          19.3               193        3450
6 Adelie  Torge…           39.3          20.6               190        3650
# ℹ 2 more variables: sex <chr>, year <dbl>
tail(penguins)
# A tibble: 6 × 8
  species island bill_length_mm bill_depth_mm flipper_length_mm body_mass_g
  <chr>   <chr>           <dbl>         <dbl>             <dbl>       <dbl>
1 Chinst… Dream            45.7          17                 195        3650
2 Chinst… Dream            55.8          19.8               207        4000
3 Chinst… Dream            43.5          18.1               202        3400
4 Chinst… Dream            49.6          18.2               193        3775
5 Chinst… Dream            50.8          19                 210        4100
6 Chinst… Dream            50.2          18.7               198        3775
# ℹ 2 more variables: sex <chr>, year <dbl>

And we can also open a built-in viewer for the data by calling:

View(penguins)

Create a tibble manually

We can also create tibble manually in R. We can specify the individual columns and their values. Let’s create the following table:

species n avg_body_mass_g
Adelie 152 3701
Chinstrap 68 3733
Gentoo 124 5076
tibble(
  species = c("Adelie", "Chinstrap", "Gentoo"), 
  n = c(152L, 68L, 124L), 
  avg_body_mass_g = c(3701, 3733, 5076)
  )
# A tibble: 3 × 3
  species       n avg_body_mass_g
  <chr>     <int>           <dbl>
1 Adelie      152            3701
2 Chinstrap    68            3733
3 Gentoo      124            5076

Exercise 4

In the palmerpenguins there is additional dataset penguins_raw. Can you create a summary table (tibble) with names of both datasets (“penguins”, “penguins_raw”) in first column, with number of rows of each dataset in the second column and and third column with number of columns of each dataset?

library(palmerpenguins)
dim(penguins)
dim(penguins_raw)

tibble(dataset=c("penguins", "penguins_raw"),
       nrows = c(344, 344),
       ncols = c(8, 17))

Writing out data from R session

Writing out the data can be done only via the console. Similarly to reading in, there is a dedicated function write_csv which can export tabular data (tibble or data.frame) to a CSV file.

write_csv(penguins, file="data/penguins_exported.csv")

Exercise 5

Export the penguins as TSV file and try to import it again into R. TSV stands for tab separated values. Hint: use function apropos("tsv") to find out which function contain ‘tsv’ as part of their name. You can also try to import it via the GUI.

# write it out
write_tsv(penguins, file="data/penguins.tsv")

# read it in again
penguins_tsv <- read_tsv("data/penguins.tsv") 
# or use again the GUI

Exchanging data with Excel tables (.xls / .xlsx files)

Reading from Excel

To read Excel files (i.e. files with .xls or .xlsx extension), there are several R packages available. We recommend installing (if necessary) and using the readxl package.

The functions in this package read data from one sheet in the Excel file. The data in this sheet should be tabular, with samples in rows, and variables in columns. There can be a header row, but only a single header row is supported. Avoid leaving blank rows/columns before your data table. Cell formatting in Excel (borders, fill, how numbers are rendered) are typically not carried across. Be aware of date conversions!

The read_excel() function looks at the file name at determines whether it is a .xls or .xlsx file, calling sub-functions read_xls() or read_xlsx(). By default, it assumes you want to read the maximal range of data available on the 1st sheet of the file, and that the first row contains a header. (NB: cannot deal with “multi-row headers”) - all these behaviors can be changed with arguments to the function, for details check the help.

library(readxl)

# load using default settings
penguins_xlsx <- read_xlsx("data/penguins.xlsx")

penguins_xlsx
# A tibble: 344 × 8
   species island    bill_length_mm bill_depth_mm flipper_length_mm
   <chr>   <chr>              <dbl>         <dbl>             <dbl>
 1 Adelie  Torgersen           39.1          18.7               181
 2 Adelie  Torgersen           39.5          17.4               186
 3 Adelie  Torgersen           40.3          18                 195
 4 Adelie  Torgersen           NA            NA                  NA
 5 Adelie  Torgersen           36.7          19.3               193
 6 Adelie  Torgersen           39.3          20.6               190
 7 Adelie  Torgersen           38.9          17.8               181
 8 Adelie  Torgersen           39.2          19.6               195
 9 Adelie  Torgersen           34.1          18.1               193
10 Adelie  Torgersen           42            20.2               190
# ℹ 334 more rows
# ℹ 3 more variables: body_mass_g <dbl>, sex <chr>, year <dbl>
Note

We can verify if the original and newly read object are identical.

identical(penguins, penguins_xlsx)
[1] FALSE

The two R objects are not identical. There is a subtle difference in classes.

Object penguins is a spec_tbl_db, which is special subclass of tibble generated by readr package. While penguins_xlsx is a normal tibble.

class(penguins)
[1] "spec_tbl_df" "tbl_df"      "tbl"         "data.frame" 
class(penguins_xlsx)
[1] "tbl_df"     "tbl"        "data.frame"
identical(as_tibble(penguins), penguins_xlsx)
[1] TRUE

Writing to Excel

The usage of function write_xlsx() from R package writexl is relatively straightforward and mimics the behavior of write_csv and similar functions from the same family.

library(writexl)
write_xlsx(penguins, "data/penguins_out.xlsx")

Exercise 6

Import the file penguins_extra.xlsx into R. How would you fix the formatting issue while importing the file into R? You can preview the file in Excel or try to use the import in the GUI if RStudio.

Fixing it in Excel is not accepted, this goes far beyond the content of this course :)

penguins_extra <- read_excel("data/penguins_extra.xlsx", skip = 2)

Saving/Loading R Objects

Rather than keeping data in text or Excel files, it can be helpful to save the objects that are in R’s memory for future use in R, thus circumventing the need to read and prepare the data each time. This can be useful to introduce breakpoints into your analysis workflow, or to share prepared data.

Note that R saves objects in RDS/RData files in a compressed format, so these will not be view-able with a text editor.

Saving/Loading single or multiple named objects at once (.Rdata files)

The complete session (all objects), or a selection of objects, can be stored/retrieved with:

  • save()
  • load()
  • convenient file extension: .RData

The objects will be loaded with the names they originally had.

x <- 1:5
y <- "high"


# The following 2 commands are equivalent
save(list = c("x", "y") , file="obj1.rdata")
# or
save(x,y, file="obj1.rdata")


# Save all objects you have in the session
save(list = ls(), file="obj1.rdata")


# Delete / change the variables
rm(x)
y <- "low" 

# Load them up from saved file
load(file="obj1.rdata")

# Let us check the variables we just read in
# the variables are updated/re-created to 
# whatever was in the RData file.
x
y

Note on RStudio

By default, RStudio asks to save all the objects in your current session to a hidden .RData file upon exit, and restores it upon return. This behavior can be changed in RStudio’s preferences (Preferences > General). We recommend disabling this, to ensure that your workspace is always clean upon restarting. Otherwise, your environment can get cluttered, slow to load/save, and you might end up with object dependencies you did not plan for. Keeping your environment clean from session to session also forces you to keep completing your R script, so that commands can be executed again.

Saving/Loading single objects as RDS files

If you do not need to save multiple objects, and/or want the loaded object to have a different name, single to-be-named objects can be stored/retrieved with:

  • dput() and dget() (text)
  • saveRDS() and readRDS() (.rds file extension)

Text-based

We’re mentioning dput() here as it can be quite useful for creating the textual representations of data in R needed to share them in places such as forums where you’re asking for help!

dput(head(penguins))
structure(list(species = c("Adelie", "Adelie", "Adelie", "Adelie", 
"Adelie", "Adelie"), island = c("Torgersen", "Torgersen", "Torgersen", 
"Torgersen", "Torgersen", "Torgersen"), bill_length_mm = c(39.1, 
39.5, 40.3, NA, 36.7, 39.3), bill_depth_mm = c(18.7, 17.4, 18, 
NA, 19.3, 20.6), flipper_length_mm = c(181, 186, 195, NA, 193, 
190), body_mass_g = c(3750, 3800, 3250, NA, 3450, 3650), sex = c("male", 
"female", "female", NA, "female", "male"), year = c(2007, 2007, 
2007, 2007, 2007, 2007)), row.names = c(NA, -6L), class = c("tbl_df", 
"tbl", "data.frame"))

RDS-based

RDS files contain a single object, without a name (like the output from dput()), but compressed (so non-readable).

x <- 1:5
y <- "high"


## save variables individually 
saveRDS(x, file="x.rds")
saveRDS(y, file="y.rds")


## make a change
y <- "low"


## reload and compare
x.new <- readRDS(file="x.rds")
identical(x, x.new)

## not identical, due to the change above
y.new <- readRDS(file="y.rds")
identical(y, y.new)

Exercise 7

Export the manually created tibble with dataset names, number of rows and columns in each dataset as a plain text and as a RDS object.

# lets store it in a variable first to save typing
tab <- tibble(dataset=c("penguins", "penguins_raw"),
       nrows = c(344, 344),
       ncols = c(8, 17))

dput(tab)

saveRDS(tab, file="data/summary_table.rds")

Getting help

If you need a hand:

  • Look at the R help pages.
  • Ask us your questions during the face-to-face lecture.
  • You can also write them on the Etherpad on ADAM.
  • Search on Google.
  • Talk to colleagues.
  • Ask on forums.

Further material

Session info

sessioninfo::session_info()
─ Session info ──────────────────────────────────────────────────────────
 setting  value
 version  R version 4.4.1 Patched (2024-09-18 r87181)
 os       macOS Sequoia 15.0
 system   aarch64, darwin24.0.0
 ui       X11
 language (EN)
 collate  en_US.UTF-8
 ctype    en_US.UTF-8
 tz       Europe/Zurich
 date     2024-09-24
 pandoc   3.4 @ /opt/homebrew/bin/ (via rmarkdown)

─ Packages ──────────────────────────────────────────────────────────────
 package     * version date (UTC) lib source
 BiocManager   1.30.25 2024-08-28 [1] CRAN (R 4.4.1)
 BiocStyle   * 2.33.1  2024-06-12 [1] Bioconductor 3.20 (R 4.4.1)
 bit           4.5.0   2024-09-20 [1] CRAN (R 4.4.1)
 bit64         4.5.2   2024-09-22 [1] CRAN (R 4.4.1)
 cellranger    1.1.0   2016-07-27 [1] CRAN (R 4.4.0)
 cli           3.6.3   2024-06-21 [1] CRAN (R 4.4.1)
 colorspace    2.1-1   2024-07-26 [1] CRAN (R 4.4.1)
 crayon        1.5.3   2024-06-20 [1] CRAN (R 4.4.1)
 digest        0.6.37  2024-08-19 [1] CRAN (R 4.4.1)
 dplyr       * 1.1.4   2023-11-17 [1] CRAN (R 4.4.0)
 evaluate      1.0.0   2024-09-17 [1] CRAN (R 4.4.1)
 fansi         1.0.6   2023-12-08 [1] CRAN (R 4.4.0)
 fastmap       1.2.0   2024-05-15 [1] CRAN (R 4.4.0)
 forcats     * 1.0.0   2023-01-29 [1] CRAN (R 4.4.0)
 generics      0.1.3   2022-07-05 [1] CRAN (R 4.4.0)
 ggplot2     * 3.5.1   2024-04-23 [1] CRAN (R 4.4.0)
 glue          1.7.0   2024-01-09 [1] CRAN (R 4.4.0)
 gtable        0.3.5   2024-04-22 [1] CRAN (R 4.4.0)
 hms           1.1.3   2023-03-21 [1] CRAN (R 4.4.0)
 htmltools     0.5.8.1 2024-04-04 [1] CRAN (R 4.4.0)
 htmlwidgets   1.6.4   2023-12-06 [1] CRAN (R 4.4.0)
 jsonlite      1.8.9   2024-09-20 [1] CRAN (R 4.4.1)
 knitr       * 1.48    2024-07-07 [1] CRAN (R 4.4.1)
 lifecycle     1.0.4   2023-11-07 [1] CRAN (R 4.4.0)
 lubridate   * 1.9.3   2023-09-27 [1] CRAN (R 4.4.0)
 magrittr      2.0.3   2022-03-30 [1] CRAN (R 4.4.0)
 munsell       0.5.1   2024-04-01 [1] CRAN (R 4.4.0)
 pillar        1.9.0   2023-03-22 [1] CRAN (R 4.4.0)
 pkgconfig     2.0.3   2019-09-22 [1] CRAN (R 4.4.0)
 png         * 0.1-8   2022-11-29 [1] CRAN (R 4.4.0)
 purrr       * 1.0.2   2023-08-10 [1] CRAN (R 4.4.0)
 R6            2.5.1   2021-08-19 [1] CRAN (R 4.4.0)
 readr       * 2.1.5   2024-01-10 [1] CRAN (R 4.4.0)
 readxl      * 1.4.3   2023-07-06 [1] CRAN (R 4.4.0)
 rlang         1.1.4   2024-06-04 [1] CRAN (R 4.4.1)
 rmarkdown     2.28    2024-08-17 [1] CRAN (R 4.4.1)
 rstudioapi    0.16.0  2024-03-24 [1] CRAN (R 4.4.0)
 scales        1.3.0   2023-11-28 [1] CRAN (R 4.4.0)
 sessioninfo   1.2.2   2021-12-06 [1] CRAN (R 4.4.0)
 stringi       1.8.4   2024-05-06 [1] CRAN (R 4.4.0)
 stringr     * 1.5.1   2023-11-14 [1] CRAN (R 4.4.0)
 tibble      * 3.2.1   2023-03-20 [1] CRAN (R 4.4.0)
 tidyr       * 1.3.1   2024-01-24 [1] CRAN (R 4.4.0)
 tidyselect    1.2.1   2024-03-11 [1] CRAN (R 4.4.0)
 tidyverse   * 2.0.0   2023-02-22 [1] CRAN (R 4.4.0)
 timechange    0.3.0   2024-01-18 [1] CRAN (R 4.4.0)
 tzdb          0.4.0   2023-05-12 [1] CRAN (R 4.4.0)
 utf8          1.2.4   2023-10-22 [1] CRAN (R 4.4.0)
 vctrs         0.6.5   2023-12-01 [1] CRAN (R 4.4.0)
 vroom         1.6.5   2023-12-05 [1] CRAN (R 4.4.0)
 withr         3.0.1   2024-07-31 [1] CRAN (R 4.4.1)
 writexl     * 1.5.0   2024-02-09 [1] CRAN (R 4.4.0)
 xfun          0.47    2024-08-17 [1] CRAN (R 4.4.1)
 yaml          2.3.10  2024-07-26 [1] CRAN (R 4.4.1)

 [1] /Library/Frameworks/R.framework/Versions/4.4/Resources/library

─────────────────────────────────────────────────────────────────────────