Learning Objectives

  • Describe the purpose of the dplyr and tidyr packages.
  • Select certain columns in a data frame with the dplyr function select.
  • Select certain rows in a data frame according to filtering conditions with the dplyr function filter.
  • Link the output of one dplyr function to the input of another function with the ‘pipe’ operator %>%.
  • Add new columns to a data frame that are functions of existing columns with mutate.
  • Use the split-apply-combine concept for data analysis.
  • Use 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.


Data Manipulation using dplyr and tidyr

Bracket 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:

  1. The results from a base R function sometimes depend on the type of data.
  2. Using R expressions in a non standard way, which can be confusing for new learners.
  3. Hidden arguments, having default operations that new learners are not aware of.

To load the package type:

library("tidyverse")    ## load the tidyverse packages, incl. dplyr

What are 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.

Our data: international environmental agreements

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:

  1. When you print a tibble, R displays the data type of each column under its name; it prints only the first few rows of data and only as many columns as fit on one screen.
  2. Columns of class 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 Other
  • subject: the issue area, like Fish and fisheries or Pollution
  • year_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 for

Selecting columns and filtering rows

We’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.

Pipes

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.

Challenge

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

ANSWER
agreements_challenge <- agreements %>%
    filter(year_signed < 1950) %>%
    select(year_signed, scope, subject)


Mutate

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.

Challenge

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!

ANSWER
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)


Group by and summarize

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

Challenge

  1. What was the largest number of members of any agreement signed in each year since 1990? Return a table with three columns: 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)
  2. Try out a new function, 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
## 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.