dplyr and
tidyr packages.dplyr function select.dplyr function
filter.dplyr function
to the input of another function with the ‘pipe’ operator
%>%.mutate.summarize, group_by, and
count to split a data frame into groups of observations,
apply a summary statistics for each group, and then combine the
results.dplyr and
tidyrBracket subsetting is handy, but it can be cumbersome and difficult
to read, especially for complicated operations. Enter
dplyr. dplyr
is a package for making tabular data manipulation easier. It pairs
nicely with tidyr which enables you to
swiftly convert between different data formats for plotting and
analysis.
Packages in R are basically sets of additional functions that let you
do more stuff. The functions we’ve been using so far, like
str() or data.frame(), come built into R;
packages give you access to more of them. Before you use a package for
the first time you need to install it on your machine, and then you
should import it in every subsequent R session when you need it. You
should already have installed the
tidyverse package. This is an
“umbrella-package” that installs several packages useful for data
analysis which work together well such as
tidyr,
dplyr,
ggplot2,
tibble, etc.
The tidyverse package tries to address
3 major problems with some of base R functions:
To load the package type:
library("tidyverse") ## load the tidyverse packages, incl. dplyr
dplyr and
tidyr?The package dplyr provides easy tools
for the most common data manipulation tasks. It is built to work
directly with data frames, with many common tasks optimized by being
written in a compiled language (C++). An additional feature is the
ability to work directly with data stored in an external database. The
benefits of doing this are that the data can be managed natively in a
relational database, queries can be conducted on that database, and only
the results of the query are returned.
This addresses a common problem with R in that all operations are conducted in-memory and thus the amount of data you can work with is limited by available memory. The database connections essentially remove that limitation in that you can connect to a database of many hundreds of GB, conduct queries on it directly, and pull back into R only what you need for analysis.
The package tidyr addresses the common
problem of wanting to reshape your data for plotting and use by
different R functions. Sometimes we want data sets where we have one row
per measurement. Sometimes we want a data frame where each measurement
type has its own column, and rows are instead more aggregated groups -
like countries or decades. Moving back and forth between these formats
is nontrivial, and tidyr gives you tools
for this and more sophisticated data manipulation.
To learn more about dplyr and
tidyr, you may want to check out this handy
data transformation with dplyr
cheatsheet and this one
about tidyr.
For the next two weeks we’ll work with the International Environmental Agreements (IEA) Database, which tracks every treaty, protocol, and amendment that countries have signed to manage the environment, from 19th-century agreements about fishing on border rivers to the Montreal Protocol and the UN climate convention. The database was built by political scientist Ronald Mitchell at the University of Oregon and is now hosted by Université Laval. We’ll use a simplified subset of it, frozen as of October 2026 and trimmed to a handful of columns for teaching. It comes as three linked tables:
iea_agreements.csv: one row per agreement (this
week)iea_memberships.csv: one row per country per agreement
it has signed or joined (next week)iea_countries.csv: one row per country (next week)The codebook
describes every column. Missing values (NA) are left in on
purpose.
We’ll read in our data using the read_csv() function,
from the tidyverse package readr, instead
of read.csv(), the base function for reading in data. We’ll
read it straight from the course website, but you can also download it
into the data folder of your R_DAVIS_2026 project.
agreements <- read_csv("https://ucd-rdavis.github.io/R-DAVIS/data/iea_agreements.csv")
## Rows: 5016 Columns: 10
## ── Column specification ───────────────────────
## Delimiter: ","
## chr (5): agreement_name, scope, instrument, subject, lineage
## dbl (4): iea_id, year_signed, year_in_force, n_members
## lgl (1): terminated
##
## ℹ Use `spec()` to retrieve the full column specification for this data.
## ℹ Specify the column types or set `show_col_types = FALSE` to quiet this message.
## inspect the data
str(agreements)
Notice that the class of the data is now tbl_df This is
referred to as a “tibble”. Remember tibbles are standard data frames
with some extra window dressing:
character are never automatically
converted into factors.Each of the 5,016 rows is one agreement. A few columns we’ll use:
scope: Bilateral (between two countries),
Multilateral (three or more), or Othersubject: the issue area, like
Fish and fisheries or Pollutionyear_signed and year_in_force: when the
agreement was signed, and when it took effect (enough countries had
ratified it)n_members: the number of countries the agreement has
entered into force forWe’re going to learn some of the most common
dplyr functions: select(),
filter(), mutate(), group_by(),
and summarize(). To select columns of a data frame, use
select(). The first argument to this function is the data
frame (agreements), and the subsequent arguments are the
columns to keep.
select(agreements, agreement_name, year_signed, n_members)
To choose rows based on a specific criteria, use
filter(). Here are all the agreements signed in 1992, the
year of the Rio Earth Summit:
filter(agreements, year_signed == 1992)
## # A tibble: 140 × 10
## iea_id agreement_name scope instrument subject lineage year_signed
## <dbl> <chr> <chr> <chr> <chr> <chr> <dbl>
## 1 818 Agreement Between The Go… Bila… Agreement Fish a… Not Ye… 1992
## 2 819 Agreement Between The Go… Bila… Agreement Genera… Not Ye… 1992
## 3 820 Treaty On Development An… Bila… Agreement Freshw… Not Ye… 1992
## 4 821 Agreement Between The Re… Bila… Agreement Genera… Not Ye… 1992
## 5 822 Agreement Between The Re… Bila… Agreement Genera… Not Ye… 1992
## 6 823 Agreement Between The Re… Bila… Agreement Genera… Not Ye… 1992
## 7 824 Agreement Between The Go… Bila… Agreement Genera… Not Ye… 1992
## 8 825 Environmental Cooperatio… Bila… Agreement Genera… Not Ye… 1992
## 9 826 Agreement Between The Fe… Bila… Agreement Freshw… Not Ye… 1992
## 10 827 Agreement Between The Fe… Bila… Agreement Genera… Not Ye… 1992
## # ℹ 130 more rows
## # ℹ 3 more variables: year_in_force <dbl>, terminated <lgl>, n_members <dbl>
filter is used for rows and
select is used for columns. Note that in
select and filter, you do not have to wrap the
column names in quotes (e.g., year_signed works instead of
“year_signed”). The reason is that these dplyr functions
are wired to know that the variable names are vector objects stored
within the tibble/data.frame object, and so standard object naming
conventions work.
What if you want to select and filter at the same time? There are three ways to do this: use intermediate steps, nested functions, or pipes.
With intermediate steps, you create a temporary data frame and use that as input to the next function, like this:
agreements2 <- filter(agreements, n_members > 150)
agreements_big <- select(agreements2, agreement_name, year_signed, n_members)
This is readable, but can clutter up your workspace with lots of objects that you have to name individually. With multiple steps, that can be hard to keep track of.
You can also nest functions (i.e. one function inside of another), like this:
agreements_big <- select(filter(agreements, n_members > 150), agreement_name, year_signed, n_members)
This is handy, but can be difficult to read if too many functions are nested, as R evaluates the expression from the inside out (in this case, filtering, then selecting).
The last option is pipes. Pipes let you
take the output of one function and send it directly to the next, which
is useful when you need to do many things to the same dataset. Pipes in
R look like %>% and are made available via the
magrittr package, installed automatically
with dplyr. If you use RStudio, you can
type the pipe with Ctrl + Shift + M if
you have a PC or Cmd + Shift + M if you
have a Mac.
agreements %>%
filter(n_members > 150) %>%
select(agreement_name, year_signed, n_members)
## # A tibble: 188 × 3
## agreement_name year_signed n_members
## <chr> <dbl> <dbl>
## 1 International Agreement For The Creation Of An Interna… 1924 185
## 2 Constitution Of The Food And Agriculture Organization … 1945 197
## 3 Convention On Wetlands Of International Importance Esp… 1971 174
## 4 Convention On The Prohibition Of The Development, Prod… 1972 166
## 5 Convention For The Protection Of The World Cultural An… 1972 197
## 6 Convention On International Trade In Endangered Specie… 1973 185
## 7 Amendments To Appendices I and II To The Convention On… 1976 183
## 8 Protocol To The International Convention for the Preve… 1978 159
## 9 International Convention On Standards Of Training, Cer… 1978 167
## 10 Amendments To Appendices I and II To The Convention On… 1979 183
## # ℹ 178 more rows
In the above code, we use the pipe to send the
agreements dataset first through filter() to
keep rows where n_members is greater than 150, then through
select() to keep only the agreement_name,
year_signed, and n_members columns. Since
%>% takes the object on its left and passes it as the
first argument to the function on its right, we don’t need to explicitly
include the data frame as an argument to the filter() and
select() functions any more.
Some may find it helpful to read the pipe like the word “then”. For
instance, in the above example, we took the data frame
agreements, then we filtered for rows
with n_members > 150, then we
selected columns agreement_name,
year_signed, and n_members. The
dplyr functions by themselves are somewhat
simple, but by combining them into linear workflows with the pipe, we
can accomplish more complex manipulations of data frames.
If we want to create a new object with this smaller version of the
data, we can assign it a new name. Let’s also sort it with
arrange(), putting the agreements with the most members
first:
agreements_big <- agreements %>%
filter(n_members > 150) %>%
select(agreement_name, year_signed, n_members) %>%
arrange(desc(n_members))
agreements_big
## # A tibble: 188 × 3
## agreement_name year_signed n_members
## <chr> <dbl> <dbl>
## 1 Convention To Combat Desertification In Those Countrie… 1994 200
## 2 Amendment Adding Annex V to the Convention To Combat D… 2000 199
## 3 Convention For The Protection Of The Ozone Layer 1985 198
## 4 Montreal Protocol On Substances That Deplete The Ozone… 1987 198
## 5 United Nations Framework Convention On Climate Change 1992 198
## 6 Adjustments to the Montreal Protocol on Substances tha… 2018 198
## 7 Constitution Of The Food And Agriculture Organization … 1945 197
## 8 Convention For The Protection Of The World Cultural An… 1972 197
## 9 Amendment To The Montreal Protocol On Substances That … 1990 197
## 10 Amendment To The Montreal Protocol On Substances That … 1992 197
## # ℹ 178 more rows
Note that the final data frame is the leftmost part of this expression. The most widely joined agreements are about desertification, the ozone layer, and climate change.
Using pipes, subset the agreements data to include
agreements signed before 1950 and retain only the columns
year_signed, scope, and subject.
Name this dataframe agreements_challenge
agreements_challenge <- agreements %>%
filter(year_signed < 1950) %>%
select(year_signed, scope, subject)
Frequently you’ll want to create new columns based on the values in
existing columns, for example to do unit conversions, or to find the
difference between values in two columns. For this we’ll use
mutate().
To create a new column with the number of years it took each agreement to enter into force:
agreements %>%
mutate(years_to_force = year_in_force - year_signed)
## # A tibble: 5,016 × 11
## iea_id agreement_name scope instrument subject lineage year_signed
## <dbl> <chr> <chr> <chr> <chr> <chr> <dbl>
## 1 1 Agreement Between Austri… Bila… Agreement Freshw… Danube 1858
## 2 2 Fishing Regulations For … Bila… Agreement Fish a… Bidass… 1859
## 3 3 Treaty To Regulate The D… Bila… Agreement Freshw… Meuse 1863
## 4 4 Convention Between Franc… Bila… Agreement Fish a… Not Ye… 1867
## 5 5 Convention Between The G… Bila… Agreement Fish a… Rhine-… 1869
## 6 6 Convention Amending The … Bila… Agreement Freshw… Meuse 1873
## 7 7 Convention Between Baden… Bila… Agreement Fish a… Rhine-… 1875
## 8 8 Convention Between The G… Bila… Agreement Fish a… Not Ye… 1880
## 9 9 Convention Between Franc… Bila… Agreement Fish a… French… 1880
## 10 10 Agreement on Neighbourly… Bila… Agreement Conser… French… 1882
## # ℹ 5,006 more rows
## # ℹ 4 more variables: year_in_force <dbl>, terminated <lgl>, n_members <dbl>,
## # years_to_force <dbl>
You can also create a second new column based on the first new column
within the same call of mutate():
agreements %>%
mutate(years_to_force = year_in_force - year_signed,
decades_to_force = years_to_force / 10)
## # A tibble: 5,016 × 12
## iea_id agreement_name scope instrument subject lineage year_signed
## <dbl> <chr> <chr> <chr> <chr> <chr> <dbl>
## 1 1 Agreement Between Austri… Bila… Agreement Freshw… Danube 1858
## 2 2 Fishing Regulations For … Bila… Agreement Fish a… Bidass… 1859
## 3 3 Treaty To Regulate The D… Bila… Agreement Freshw… Meuse 1863
## 4 4 Convention Between Franc… Bila… Agreement Fish a… Not Ye… 1867
## 5 5 Convention Between The G… Bila… Agreement Fish a… Rhine-… 1869
## 6 6 Convention Amending The … Bila… Agreement Freshw… Meuse 1873
## 7 7 Convention Between Baden… Bila… Agreement Fish a… Rhine-… 1875
## 8 8 Convention Between The G… Bila… Agreement Fish a… Not Ye… 1880
## 9 9 Convention Between Franc… Bila… Agreement Fish a… French… 1880
## 10 10 Agreement on Neighbourly… Bila… Agreement Conser… French… 1882
## # ℹ 5,006 more rows
## # ℹ 5 more variables: year_in_force <dbl>, terminated <lgl>, n_members <dbl>,
## # years_to_force <dbl>, decades_to_force <dbl>
If this runs off your screen and you just want to see the first few
rows, you can use a pipe to view the head() of the data.
(Pipes work with non-dplyr functions, too,
as long as the dplyr or
magrittr package is loaded).
agreements %>%
mutate(years_to_force = year_in_force - year_signed) %>%
select(agreement_name, year_signed, year_in_force, years_to_force) %>%
head()
## # A tibble: 6 × 4
## agreement_name year_signed year_in_force years_to_force
## <chr> <dbl> <dbl> <dbl>
## 1 Agreement Between Austria And Bavari… 1858 1859 1
## 2 Fishing Regulations For The Bidassoa… 1859 1860 1
## 3 Treaty To Regulate The Diversion Of … 1863 NA NA
## 4 Convention Between France And Great … 1867 1868 1
## 5 Convention Between The Grand Duchy O… 1869 1870 1
## 6 Convention Amending The Treaty To Re… 1873 NA NA
Some of these rows are NA, because those agreements
never entered into force (or the IEA database doesn’t know when they
did). If we wanted to remove those we could insert a
filter() in the chain:
agreements %>%
filter(!is.na(year_in_force)) %>%
mutate(years_to_force = year_in_force - year_signed) %>%
select(agreement_name, year_signed, year_in_force, years_to_force) %>%
head()
## # A tibble: 6 × 4
## agreement_name year_signed year_in_force years_to_force
## <chr> <dbl> <dbl> <dbl>
## 1 Agreement Between Austria And Bavari… 1858 1859 1
## 2 Fishing Regulations For The Bidassoa… 1859 1860 1
## 3 Convention Between France And Great … 1867 1868 1
## 4 Convention Between The Grand Duchy O… 1869 1870 1
## 5 Convention Between Baden And Switzer… 1875 1875 0
## 6 Convention Between France And Switze… 1880 1882 2
is.na() is a function that determines whether something
is an NA. The ! symbol negates the result, so
we’re asking for every row where year_in_force is
not an NA.
New columns are also a good way to check your data. An agreement
can’t take effect before it’s signed, so years_to_force
should never be negative. Is it?
agreements %>%
mutate(years_to_force = year_in_force - year_signed) %>%
filter(years_to_force < 0) %>%
select(agreement_name, year_signed, year_in_force)
## # A tibble: 2 × 3
## agreement_name year_signed year_in_force
## <chr> <dbl> <dbl>
## 1 Agreement Between The Government Of Belarus And The… 1998 1900
## 2 Fisheries Partnership Agreement between the Europea… 2008 2006
Two agreements say they entered into force before they were signed.
One of them is off by almost a century, which looks like a typo in the
original record. Real datasets have mistakes like this; a quick
mutate() and filter() is often how you find
them.
Create a new data frame from the agreements data that
meets the following criteria: contains only the
agreement_name column and a new column called
years_to_force containing the number of years between when
the agreement was signed and when it entered into force. In this
years_to_force column, there are no NAs and
all values are 10 or more (slow agreements!). Name this data frame
agreements_slow.
Hint: think about how the commands should be ordered to produce this data frame!
agreements_slow <- agreements %>%
filter(!is.na(year_in_force)) %>%
mutate(years_to_force = year_in_force - year_signed) %>%
filter(years_to_force >= 10) %>%
select(agreement_name, years_to_force)
Many data analysis tasks can be approached using the
split-apply-combine paradigm: split the data into groups, apply
some analysis to each group, and then combine the results.
dplyr makes this very easy through the use
of the group_by() function.
group_by() is often used together with
summarize(), which collapses each group into a single-row
summary of that group. group_by() takes as arguments the
column names that contain the categorical variables for
which you want to calculate the summary statistics. So to compute the
mean number of members by scope:
agreements %>%
group_by(scope) %>%
summarize(mean_members = mean(n_members, na.rm = TRUE))
## # A tibble: 3 × 2
## scope mean_members
## <chr> <dbl>
## 1 Bilateral 2.00
## 2 Multilateral 48.1
## 3 Other 2
No surprise that bilateral agreements average two members. What
happens if you leave out na.rm = TRUE? Try it: a single
NA in a group makes the whole mean NA.
You may also have noticed that the output from these calls doesn’t
run off the screen anymore. It’s one of the advantages of
tbl_df over data frame.
You can also group by multiple columns:
agreements %>%
group_by(subject, scope) %>%
summarize(mean_members = mean(n_members, na.rm = TRUE))
## `summarise()` has regrouped the output.
## ℹ Summaries were computed grouped by subject
## and scope.
## ℹ Output is grouped by subject.
## ℹ Use `summarise(.groups = "drop_last")` to
## silence this message.
## ℹ Use `summarise(.by = c(subject, scope))` for
## per-operation grouping (`?dplyr::dplyr_by`)
## instead.
## # A tibble: 32 × 3
## # Groups: subject [11]
## subject scope mean_members
## <chr> <chr> <dbl>
## 1 Agriculture Bilateral 2.00
## 2 Agriculture Multilateral 27.5
## 3 Agriculture Other NaN
## 4 Conservation of species and biodiversity Bilateral 2
## 5 Conservation of species and biodiversity Multilateral 61.4
## 6 Conservation of species and biodiversity Other 2
## 7 Energy Bilateral 2.00
## 8 Energy Multilateral 30.1
## 9 Energy Other NaN
## 10 Fish and fisheries Bilateral 2.00
## # ℹ 22 more rows
Multilateral agreements are much bigger than bilateral ones on every
subject. But some rows say NaN (“not a number”). Every
agreement in those groups is missing n_members, so once
na.rm = TRUE removes the NAs there’s nothing
left to average.
We can also remove the missing values for n_members
before we attempt to calculate the summary statistics. Because the
missing values are removed first, we can omit na.rm = TRUE
when computing the mean, and the all-NA groups drop out
entirely:
agreements %>%
filter(!is.na(n_members)) %>%
group_by(subject, scope) %>%
summarize(mean_members = mean(n_members))
## `summarise()` has regrouped the output.
## ℹ Summaries were computed grouped by subject
## and scope.
## ℹ Output is grouped by subject.
## ℹ Use `summarise(.groups = "drop_last")` to
## silence this message.
## ℹ Use `summarise(.by = c(subject, scope))` for
## per-operation grouping (`?dplyr::dplyr_by`)
## instead.
## # A tibble: 24 × 3
## # Groups: subject [11]
## subject scope mean_members
## <chr> <chr> <dbl>
## 1 Agriculture Bilateral 2.00
## 2 Agriculture Multilateral 27.5
## 3 Conservation of species and biodiversity Bilateral 2
## 4 Conservation of species and biodiversity Multilateral 61.4
## 5 Conservation of species and biodiversity Other 2
## 6 Energy Bilateral 2.00
## 7 Energy Multilateral 30.1
## 8 Fish and fisheries Bilateral 2.00
## 9 Fish and fisheries Multilateral 13.6
## 10 Freshwater Bilateral 2.00
## # ℹ 14 more rows
Here, again, the output from these calls doesn’t run off the screen
anymore. If you want to display more data, you can use the
print() function at the end of your chain with the argument
n specifying the number of rows to display:
agreements %>%
filter(!is.na(n_members)) %>%
group_by(subject, scope) %>%
summarize(mean_members = mean(n_members)) %>%
print(n = 15)
## `summarise()` has regrouped the output.
## ℹ Summaries were computed grouped by subject
## and scope.
## ℹ Output is grouped by subject.
## ℹ Use `summarise(.groups = "drop_last")` to
## silence this message.
## ℹ Use `summarise(.by = c(subject, scope))` for
## per-operation grouping (`?dplyr::dplyr_by`)
## instead.
## # A tibble: 24 × 3
## # Groups: subject [11]
## subject scope mean_members
## <chr> <chr> <dbl>
## 1 Agriculture Bilateral 2.00
## 2 Agriculture Multilateral 27.5
## 3 Conservation of species and biodiversity Bilateral 2
## 4 Conservation of species and biodiversity Multilateral 61.4
## 5 Conservation of species and biodiversity Other 2
## 6 Energy Bilateral 2.00
## 7 Energy Multilateral 30.1
## 8 Fish and fisheries Bilateral 2.00
## 9 Fish and fisheries Multilateral 13.6
## 10 Freshwater Bilateral 2.00
## 11 Freshwater Multilateral 6.84
## 12 General environmental cooperation Bilateral 2
## 13 General environmental cooperation Multilateral 13.6
## 14 Habitat and ocean Bilateral 2
## 15 Habitat and ocean Multilateral 40.7
## # ℹ 9 more rows
Once the data are grouped, you can also summarize multiple variables at the same time (and not necessarily on the same variable). For instance, we could add a column with the largest membership of any agreement in each group:
agreements %>%
filter(!is.na(n_members)) %>%
group_by(subject, scope) %>%
summarize(mean_members = mean(n_members),
max_members = max(n_members))
## `summarise()` has regrouped the output.
## ℹ Summaries were computed grouped by subject
## and scope.
## ℹ Output is grouped by subject.
## ℹ Use `summarise(.groups = "drop_last")` to
## silence this message.
## ℹ Use `summarise(.by = c(subject, scope))` for
## per-operation grouping (`?dplyr::dplyr_by`)
## instead.
## # A tibble: 24 × 4
## # Groups: subject [11]
## subject scope mean_members max_members
## <chr> <chr> <dbl> <dbl>
## 1 Agriculture Bilateral 2.00 2
## 2 Agriculture Multilater… 27.5 197
## 3 Conservation of species and biodiversity Bilateral 2 2
## 4 Conservation of species and biodiversity Multilater… 61.4 196
## 5 Conservation of species and biodiversity Other 2 2
## 6 Energy Bilateral 2.00 2
## 7 Energy Multilater… 30.1 171
## 8 Fish and fisheries Bilateral 2.00 3
## 9 Fish and fisheries Multilater… 13.6 98
## 10 Freshwater Bilateral 2.00 2
## # ℹ 14 more rows
year_signed, the largest number of members
(max_members), and that number as a percent of the 202
countries in the database (max_pct), arranged
(arrange()) in descending order of
max_members. (This table should have 36 rows, one for each
year)count(). Group the data by
scope and pipe the grouped data into the
count() function. How could you get the same result using
group_by() and summarize()? Hint: see
?n.## Answer 1
agreements %>%
filter(!is.na(n_members), year_signed >= 1990) %>%
group_by(year_signed) %>%
summarize(max_members = max(n_members)) %>%
mutate(max_pct = max_members / 202 * 100) %>%
arrange(desc(max_members))
## # A tibble: 36 × 3
## year_signed max_members max_pct
## <dbl> <dbl> <dbl>
## 1 1994 200 99.0
## 2 2000 199 98.5
## 3 1992 198 98.0
## 4 2018 198 98.0
## 5 1990 197 97.5
## 6 1995 197 97.5
## 7 1997 197 97.5
## 8 1999 197 97.5
## 9 2001 197 97.5
## 10 2007 197 97.5
## # ℹ 26 more rows
## Answer 2
agreements %>%
group_by(scope) %>%
count()
## # A tibble: 3 × 2
## # Groups: scope [3]
## scope n
## <chr> <int>
## 1 Bilateral 3343
## 2 Multilateral 1549
## 3 Other 124
agreements %>%
group_by(scope) %>%
summarize(n = n())
## # A tibble: 3 × 2
## scope n
## <chr> <int>
## 1 Bilateral 3343
## 2 Multilateral 1549
## 3 Other 124
This lesson is adapted from the Data Carpentry: R for Data Analysis and Visualization of Ecological Data Manipulating, analyzing and exporting data with tidyverse materials. Data are from the International Environmental Agreements Database Project, hosted by Université Laval, available at https://www.iea.ulaval.ca, and described in Mitchell et al. (2020), “What We Know (and Could Know) About International Environmental Agreements,” Global Environmental Politics 20(1): 103–121, https://doi.org/10.1162/glep_a_00544. The course files are a simplified subset of the database prepared for teaching (see the codebook); for research, use the full database from the IEADB website.