Data manipulation with dplyr
Pick rows, choose columns, create variables and sort — one verb at a time
By the end of this lesson you will be able to:
- chain operations with the native pipe
|>and read a pipeline aloud; - keep rows with
filter()and choose columns withselect()and its helpers; - create and recode variables with
mutate(),if_else()andcase_when(); - sort rows with
arrange()anddesc(), and relate them to base Rorder(); - tidy up a table with
rename(),relocate(),distinct(),slice_*()andcount().
library(dplyr)
library(tibble)1. Why a “grammar” of data manipulation?
Most of the time spent on a real analysis is not spent on statistics: it is spent getting the data into shape. Keep only the adult patients. Convert grams to kilograms. Sort by date. Count how many observations per group.
Base R can do all of this with brackets, but the code quickly becomes hard to read. The dplyr package gives each of these tasks a verb — a function with a clear name — and all verbs work the same way:
- the first argument is a data frame;
- the other arguments describe what to do, using column names without quotes;
- the result is a new data frame (the original is never modified).
We will use penguins throughout. Converting it to a tibble once keeps the printed output short:
penguins <- as_tibble(penguins)
penguins#> # A tibble: 344 × 8
#> species island bill_len bill_dep flipper_len body_mass sex year
#> <fct> <fct> <dbl> <dbl> <int> <int> <fct> <int>
#> 1 Adelie Torgersen 39.1 18.7 181 3750 male 2007
#> 2 Adelie Torgersen 39.5 17.4 186 3800 female 2007
#> 3 Adelie Torgersen 40.3 18 195 3250 female 2007
#> 4 Adelie Torgersen NA NA NA NA <NA> 2007
#> 5 Adelie Torgersen 36.7 19.3 193 3450 female 2007
#> 6 Adelie Torgersen 39.3 20.6 190 3650 male 2007
#> 7 Adelie Torgersen 38.9 17.8 181 3625 female 2007
#> 8 Adelie Torgersen 39.2 19.6 195 4675 male 2007
#> 9 Adelie Torgersen 34.1 18.1 193 3475 <NA> 2007
#> 10 Adelie Torgersen 42 20.2 190 4250 <NA> 2007
#> # ℹ 334 more rows
2. The pipe |>: “and then”
Suppose we want the Gentoo penguins, then only three columns, then sorted by mass. Without a pipe, we either nest the calls (read from the inside out) or create intermediate objects:
arrange(select(filter(penguins, species == "Gentoo"),
species, sex, body_mass), body_mass)#> # A tibble: 124 × 3
#> species sex body_mass
#> <fct> <fct> <int>
#> 1 Gentoo female 3950
#> 2 Gentoo <NA> 4100
#> 3 Gentoo female 4150
#> 4 Gentoo female 4200
#> 5 Gentoo female 4200
#> 6 Gentoo female 4200
#> 7 Gentoo female 4300
#> 8 Gentoo female 4300
#> 9 Gentoo female 4350
#> 10 Gentoo female 4375
#> # ℹ 114 more rows
The pipe |> takes what is on its left and passes it as the first argument of the function on its right. x |> f(y) is the same as f(x, y). Read it aloud as “and then”:
penguins |> # take penguins, and then
filter(species == "Gentoo") |> # keep the Gentoo, and then
select(species, sex, body_mass) |> # keep three columns, and then
arrange(body_mass) # sort by body mass#> # A tibble: 124 × 3
#> species sex body_mass
#> <fct> <fct> <int>
#> 1 Gentoo female 3950
#> 2 Gentoo <NA> 4100
#> 3 Gentoo female 4150
#> 4 Gentoo female 4200
#> 5 Gentoo female 4200
#> 6 Gentoo female 4200
#> 7 Gentoo female 4300
#> 8 Gentoo female 4300
#> 9 Gentoo female 4350
#> 10 Gentoo female 4375
#> # ℹ 114 more rows
The two chunks produce the same result, but the second one reads in the order in which things happen. Put one verb per line, with |> at the end of each line.
|> vs %>%
The native pipe |> has been part of R since version 4.1. Older code (and many web pages) use %>% from the magrittr package, which behaves almost identically for everyday use. In this course we always use |>. The keyboard shortcut in RStudio and Positron is Ctrl/Cmd + Shift + M (tick “Use native pipe operator” in the options).
3. filter(): keep rows
filter() keeps the rows for which a condition is TRUE.
penguins |>
filter(species == "Adelie", island == "Dream", flipper_len > 190)#> # A tibble: 22 × 8
#> species island bill_len bill_dep flipper_len body_mass sex year
#> <fct> <fct> <dbl> <dbl> <int> <int> <fct> <int>
#> 1 Adelie Dream 36.4 17 195 3325 female 2007
#> 2 Adelie Dream 39.2 21.1 196 4150 male 2007
#> 3 Adelie Dream 40.8 18.4 195 3900 male 2007
#> 4 Adelie Dream 44.1 19.7 196 4400 male 2007
#> 5 Adelie Dream 42.3 21.2 191 4150 male 2007
#> 6 Adelie Dream 37.3 17.8 191 3350 female 2008
#> 7 Adelie Dream 41.3 20.3 194 3550 male 2008
#> 8 Adelie Dream 35.7 18 202 3550 female 2008
#> 9 Adelie Dream 41.1 18.1 205 4300 male 2008
#> 10 Adelie Dream 40.8 18.9 208 4300 male 2008
#> # ℹ 12 more rows
Conditions separated by commas must all be true (logical “and”). For “or” use |. The comparison operators are the ones you met with vectors: ==, !=, <, <=, >, >=.
Three helpers make conditions shorter and clearer:
# Value in a set: %in%
penguins |> filter(species %in% c("Adelie", "Chinstrap")) |> nrow()#> [1] 220
# Value in a range (both ends included): between()
penguins |> filter(between(body_mass, 3500, 4000)) |> nrow()#> [1] 99
# Missing values: is.na()
penguins |> filter(is.na(sex))#> # A tibble: 11 × 8
#> species island bill_len bill_dep flipper_len body_mass sex year
#> <fct> <fct> <dbl> <dbl> <int> <int> <fct> <int>
#> 1 Adelie Torgersen NA NA NA NA <NA> 2007
#> 2 Adelie Torgersen 34.1 18.1 193 3475 <NA> 2007
#> 3 Adelie Torgersen 42 20.2 190 4250 <NA> 2007
#> 4 Adelie Torgersen 37.8 17.1 186 3300 <NA> 2007
#> 5 Adelie Torgersen 37.8 17.3 180 3700 <NA> 2007
#> 6 Adelie Dream 37.5 18.9 179 2975 <NA> 2007
#> 7 Gentoo Biscoe 44.5 14.3 216 4100 <NA> 2007
#> 8 Gentoo Biscoe 46.2 14.4 214 4650 <NA> 2008
#> 9 Gentoo Biscoe 47.3 13.8 216 4725 <NA> 2009
#> 10 Gentoo Biscoe 44.5 15.7 217 4875 <NA> 2009
#> 11 Gentoo Biscoe NA NA NA NA <NA> 2009
%in%is cleaner thanspecies == "Adelie" | species == "Chinstrap".between(x, 3500, 4000)meansx >= 3500 & x <= 4000.- The last table shows the 11 penguins whose sex was not recorded.
filter(!is.na(sex))keeps the others. filter()drops rows where the condition isNA, so penguins with missingbody_massare silently excluded from thebetween()count.
4. select(): choose columns
penguins |> select(species, island, body_mass)#> # A tibble: 344 × 3
#> species island body_mass
#> <fct> <fct> <int>
#> 1 Adelie Torgersen 3750
#> 2 Adelie Torgersen 3800
#> 3 Adelie Torgersen 3250
#> 4 Adelie Torgersen NA
#> 5 Adelie Torgersen 3450
#> 6 Adelie Torgersen 3650
#> 7 Adelie Torgersen 3625
#> 8 Adelie Torgersen 4675
#> 9 Adelie Torgersen 3475
#> 10 Adelie Torgersen 4250
#> # ℹ 334 more rows
penguins |> select(-year, -island) # everything except#> # A tibble: 344 × 6
#> species bill_len bill_dep flipper_len body_mass sex
#> <fct> <dbl> <dbl> <int> <int> <fct>
#> 1 Adelie 39.1 18.7 181 3750 male
#> 2 Adelie 39.5 17.4 186 3800 female
#> 3 Adelie 40.3 18 195 3250 female
#> 4 Adelie NA NA NA NA <NA>
#> 5 Adelie 36.7 19.3 193 3450 female
#> 6 Adelie 39.3 20.6 190 3650 male
#> 7 Adelie 38.9 17.8 181 3625 female
#> 8 Adelie 39.2 19.6 195 4675 male
#> 9 Adelie 34.1 18.1 193 3475 <NA>
#> 10 Adelie 42 20.2 190 4250 <NA>
#> # ℹ 334 more rows
penguins |> select(species:bill_dep) # a range of columns#> # A tibble: 344 × 4
#> species island bill_len bill_dep
#> <fct> <fct> <dbl> <dbl>
#> 1 Adelie Torgersen 39.1 18.7
#> 2 Adelie Torgersen 39.5 17.4
#> 3 Adelie Torgersen 40.3 18
#> 4 Adelie Torgersen NA NA
#> 5 Adelie Torgersen 36.7 19.3
#> 6 Adelie Torgersen 39.3 20.6
#> 7 Adelie Torgersen 38.9 17.8
#> 8 Adelie Torgersen 39.2 19.6
#> 9 Adelie Torgersen 34.1 18.1
#> 10 Adelie Torgersen 42 20.2
#> # ℹ 334 more rows
Selection helpers pick columns by their name or type:
penguins |> select(species, starts_with("bill"))#> # A tibble: 344 × 3
#> species bill_len bill_dep
#> <fct> <dbl> <dbl>
#> 1 Adelie 39.1 18.7
#> 2 Adelie 39.5 17.4
#> 3 Adelie 40.3 18
#> 4 Adelie NA NA
#> 5 Adelie 36.7 19.3
#> 6 Adelie 39.3 20.6
#> 7 Adelie 38.9 17.8
#> 8 Adelie 39.2 19.6
#> 9 Adelie 34.1 18.1
#> 10 Adelie 42 20.2
#> # ℹ 334 more rows
penguins |> select(where(is.numeric))#> # A tibble: 344 × 5
#> bill_len bill_dep flipper_len body_mass year
#> <dbl> <dbl> <int> <int> <int>
#> 1 39.1 18.7 181 3750 2007
#> 2 39.5 17.4 186 3800 2007
#> 3 40.3 18 195 3250 2007
#> 4 NA NA NA NA 2007
#> 5 36.7 19.3 193 3450 2007
#> 6 39.3 20.6 190 3650 2007
#> 7 38.9 17.8 181 3625 2007
#> 8 39.2 19.6 195 4675 2007
#> 9 34.1 18.1 193 3475 2007
#> 10 42 20.2 190 4250 2007
#> # ℹ 334 more rows
Other helpers: ends_with(), contains(), everything(), all_of(vec) (names stored in a character vector). where() takes a function that returns TRUE/FALSE for each column: where(is.factor), where(is.numeric).
5. mutate(): create or modify columns
mutate() adds new columns computed from existing ones (or overwrites them, if you reuse a name).
penguins |>
mutate(
body_mass_kg = body_mass / 1000,
bill_ratio = bill_len / bill_dep
) |>
select(species, body_mass_kg, bill_ratio)#> # A tibble: 344 × 3
#> species body_mass_kg bill_ratio
#> <fct> <dbl> <dbl>
#> 1 Adelie 3.75 2.091
#> 2 Adelie 3.8 2.270
#> 3 Adelie 3.25 2.239
#> 4 Adelie NA NA
#> 5 Adelie 3.45 1.902
#> 6 Adelie 3.65 1.908
#> 7 Adelie 3.625 2.185
#> 8 Adelie 4.675 2
#> 9 Adelie 3.475 1.884
#> 10 Adelie 4.25 2.079
#> # ℹ 334 more rows
Columns created earlier in the same mutate() can be used immediately afterwards.
Two-way recoding: if_else()
penguins |>
mutate(heavy = if_else(body_mass > 4500, "heavy", "light")) |>
count(heavy)#> # A tibble: 3 × 2
#> heavy n
#> <chr> <int>
#> 1 heavy 115
#> 2 light 227
#> 3 <NA> 2
if_else(condition, value_if_true, value_if_false). Where the condition is NA, the result is NA — which is honest: we do not know whether a penguin with unknown mass is heavy.
Many categories: case_when()
penguins |>
mutate(
size = case_when(
body_mass < 3500 ~ "small",
body_mass < 4500 ~ "medium",
body_mass >= 4500 ~ "large",
.default = NA
)
) |>
count(size)#> # A tibble: 4 × 2
#> size n
#> <chr> <int>
#> 1 large 118
#> 2 medium 153
#> 3 small 71
#> 4 <NA> 2
Each line is condition ~ value. Conditions are checked in order, and the first one that is true wins: a penguin of 3000 g matches the first line and never reaches the second. .default gives the value when no condition is true.
Turning a numeric variable into classes like this is called discretisation. It is handy for tables, but it throws information away: for statistical models, keep the original number whenever you can.
6. arrange(): sort rows
Base R first: sort() and order()
x <- c(5, 1, 10)
sort(x) # the sorted values#> [1] 1 5 10
order(x) # the positions that would sort x#> [1] 2 1 3
x[order(x)] # using those positions#> [1] 1 5 10
To sort a data frame in base R we put order() in the row position:
head(penguins[order(penguins$body_mass), c("species", "body_mass")], 3)#> # A tibble: 3 × 2
#> species body_mass
#> <fct> <int>
#> 1 Chinstrap 2700
#> 2 Adelie 2850
#> 3 Adelie 2850
# descending: put a minus in front (numeric columns only)
head(penguins[order(-penguins$body_mass), c("species", "body_mass")], 3)#> # A tibble: 3 × 2
#> species body_mass
#> <fct> <int>
#> 1 Gentoo 6300
#> 2 Gentoo 6050
#> 3 Gentoo 6000
# by species, then by decreasing bill length
head(penguins[order(penguins$species, -penguins$bill_len),
c("species", "bill_len")], 3)#> # A tibble: 3 × 2
#> species bill_len
#> <fct> <dbl>
#> 1 Adelie 46
#> 2 Adelie 45.8
#> 3 Adelie 45.6
The dplyr way
penguins |> arrange(body_mass)#> # A tibble: 344 × 8
#> species island bill_len bill_dep flipper_len body_mass sex year
#> <fct> <fct> <dbl> <dbl> <int> <int> <fct> <int>
#> 1 Chinstrap Dream 46.9 16.6 192 2700 female 2008
#> 2 Adelie Biscoe 36.5 16.6 181 2850 female 2008
#> 3 Adelie Biscoe 36.4 17.1 184 2850 female 2008
#> 4 Adelie Biscoe 34.5 18.1 187 2900 female 2008
#> 5 Adelie Dream 33.1 16.1 178 2900 female 2008
#> 6 Adelie Torgersen 38.6 17 188 2900 female 2009
#> 7 Chinstrap Dream 43.2 16.6 187 2900 female 2007
#> 8 Adelie Biscoe 37.9 18.6 193 2925 female 2009
#> 9 Adelie Dream 37.5 18.9 179 2975 <NA> 2007
#> 10 Adelie Dream 37 16.9 185 3000 female 2007
#> # ℹ 334 more rows
penguins |> arrange(desc(body_mass))#> # A tibble: 344 × 8
#> species island bill_len bill_dep flipper_len body_mass sex year
#> <fct> <fct> <dbl> <dbl> <int> <int> <fct> <int>
#> 1 Gentoo Biscoe 49.2 15.2 221 6300 male 2007
#> 2 Gentoo Biscoe 59.6 17 230 6050 male 2007
#> 3 Gentoo Biscoe 51.1 16.3 220 6000 male 2008
#> 4 Gentoo Biscoe 48.8 16.2 222 6000 male 2009
#> 5 Gentoo Biscoe 45.2 16.4 223 5950 male 2008
#> 6 Gentoo Biscoe 49.8 15.9 229 5950 male 2009
#> 7 Gentoo Biscoe 48.4 14.6 213 5850 male 2007
#> 8 Gentoo Biscoe 49.3 15.7 217 5850 male 2007
#> 9 Gentoo Biscoe 55.1 16 230 5850 male 2009
#> 10 Gentoo Biscoe 49.5 16.2 229 5800 male 2008
#> # ℹ 334 more rows
penguins |> arrange(species, desc(bill_len))#> # A tibble: 344 × 8
#> species island bill_len bill_dep flipper_len body_mass sex year
#> <fct> <fct> <dbl> <dbl> <int> <int> <fct> <int>
#> 1 Adelie Torgersen 46 21.5 194 4200 male 2007
#> 2 Adelie Torgersen 45.8 18.9 197 4150 male 2008
#> 3 Adelie Biscoe 45.6 20.3 191 4600 male 2009
#> 4 Adelie Dream 44.1 19.7 196 4400 male 2007
#> 5 Adelie Torgersen 44.1 18 210 4000 male 2009
#> 6 Adelie Dream 43.2 18.5 192 4100 male 2008
#> 7 Adelie Biscoe 43.2 19 197 4775 male 2009
#> 8 Adelie Torgersen 43.1 19.2 197 3500 male 2009
#> 9 Adelie Torgersen 42.9 17.6 196 4700 male 2008
#> 10 Adelie Torgersen 42.8 18.5 195 4250 male 2008
#> # ℹ 334 more rows
desc() works for any type (numbers, text, factors), while the minus trick only works for numbers. With several columns, ties in the first are broken by the second. In both approaches, missing values go last.
7. Small but useful verbs
penguins |>
rename(mass_g = body_mass, flipper_mm = flipper_len) |> # new = old
relocate(year, .before = species) # move a column#> # A tibble: 344 × 8
#> year species island bill_len bill_dep flipper_mm mass_g sex
#> <int> <fct> <fct> <dbl> <dbl> <int> <int> <fct>
#> 1 2007 Adelie Torgersen 39.1 18.7 181 3750 male
#> 2 2007 Adelie Torgersen 39.5 17.4 186 3800 female
#> 3 2007 Adelie Torgersen 40.3 18 195 3250 female
#> 4 2007 Adelie Torgersen NA NA NA NA <NA>
#> 5 2007 Adelie Torgersen 36.7 19.3 193 3450 female
#> 6 2007 Adelie Torgersen 39.3 20.6 190 3650 male
#> 7 2007 Adelie Torgersen 38.9 17.8 181 3625 female
#> 8 2007 Adelie Torgersen 39.2 19.6 195 4675 male
#> 9 2007 Adelie Torgersen 34.1 18.1 193 3475 <NA>
#> 10 2007 Adelie Torgersen 42 20.2 190 4250 <NA>
#> # ℹ 334 more rows
distinct() keeps unique rows (or unique combinations of the columns you name):
penguins |> distinct(species, island)#> # A tibble: 5 × 2
#> species island
#> <fct> <fct>
#> 1 Adelie Torgersen
#> 2 Adelie Biscoe
#> 3 Adelie Dream
#> 4 Gentoo Biscoe
#> 5 Chinstrap Dream
The slice_*() family selects rows by position:
penguins |> slice_head(n = 3) # first 3 rows#> # A tibble: 3 × 8
#> species island bill_len bill_dep flipper_len body_mass sex year
#> <fct> <fct> <dbl> <dbl> <int> <int> <fct> <int>
#> 1 Adelie Torgersen 39.1 18.7 181 3750 male 2007
#> 2 Adelie Torgersen 39.5 17.4 186 3800 female 2007
#> 3 Adelie Torgersen 40.3 18 195 3250 female 2007
penguins |> slice_max(body_mass, n = 3) # 3 heaviest#> # A tibble: 4 × 8
#> species island bill_len bill_dep flipper_len body_mass sex year
#> <fct> <fct> <dbl> <dbl> <int> <int> <fct> <int>
#> 1 Gentoo Biscoe 49.2 15.2 221 6300 male 2007
#> 2 Gentoo Biscoe 59.6 17 230 6050 male 2007
#> 3 Gentoo Biscoe 51.1 16.3 220 6000 male 2008
#> 4 Gentoo Biscoe 48.8 16.2 222 6000 male 2009
penguins |> slice_min(bill_len, n = 1, by = species) # shortest bill per species#> # A tibble: 3 × 8
#> species island bill_len bill_dep flipper_len body_mass sex year
#> <fct> <fct> <dbl> <dbl> <int> <int> <fct> <int>
#> 1 Adelie Dream 32.1 15.5 188 3050 female 2009
#> 2 Gentoo Biscoe 40.9 13.7 214 4650 female 2007
#> 3 Chinstrap Dream 40.9 16.6 187 3200 female 2008
slice_max() keeps ties: if several penguins share the third-highest mass, they are all returned (use with_ties = FALSE to get exactly n rows). The argument by = repeats the operation within each group — more on groups in the next lesson. For random rows, use slice_sample(n = 5) (after set.seed()).
count() counts the rows for each combination of values:
penguins |> count(species)#> # A tibble: 3 × 2
#> species n
#> <fct> <int>
#> 1 Adelie 152
#> 2 Chinstrap 68
#> 3 Gentoo 124
penguins |> count(species, sex, sort = TRUE)#> # A tibble: 8 × 3
#> species sex n
#> <fct> <fct> <int>
#> 1 Adelie female 73
#> 2 Adelie male 73
#> 3 Gentoo male 61
#> 4 Gentoo female 58
#> 5 Chinstrap female 34
#> 6 Chinstrap male 34
#> 7 Adelie <NA> 6
#> 8 Gentoo <NA> 5
Each row of a count() result is one combination of the grouping variables, and n is the number of units with that combination. These are absolute frequencies; you will turn them into proportions and contingency tables in Frequency tables. Note the NA rows for sex: count() does not hide missing values, which is exactly what you want when checking data.
8. Putting it together
A typical pipeline answers a question from start to finish. Among penguins with known sex, which five have the largest bill-to-depth ratio, and on which island do they live?
penguins |>
filter(!is.na(sex)) |>
mutate(bill_ratio = bill_len / bill_dep) |>
select(species, island, sex, bill_ratio) |>
arrange(desc(bill_ratio)) |>
slice_head(n = 5)#> # A tibble: 5 × 4
#> species island sex bill_ratio
#> <fct> <fct> <fct> <dbl>
#> 1 Gentoo Biscoe male 3.613
#> 2 Gentoo Biscoe male 3.510
#> 3 Gentoo Biscoe male 3.506
#> 4 Gentoo Biscoe female 3.492
#> 5 Gentoo Biscoe male 3.459
Notice that nothing has been saved: penguins is unchanged. To keep a result, assign it: result <- penguins |> ....
=instead of==infilter().filter(species = "Adelie")gives an error; comparison needs==.species == "Adelie" | "Gentoo"does not mean what it seems. Writespecies %in% c("Adelie", "Gentoo").- Forgetting to assign. A pipeline only prints its result;
penguinsis not modified unless you writepenguins <- penguins |> .... - Quoting column names in dplyr verbs (
select("species")works, butfilter("body_mass" > 4000)compares two constants!). Use bare names. - Expecting exactly
nrows fromslice_max(). Ties are kept by default. - Order matters in
case_when(). Put the most specific conditions first.
9. Check your understanding
10. Exercises
Interactive (run in your browser)
★ Exercise 1 — filter and count. How many Adelie penguins from the island of Dream have a flipper longer than 190 mm?
Text values need quotes: "Adelie", "Dream". The third condition is flipper_len > 190.
penguins |>
filter(species == "Adelie", island == "Dream", flipper_len > 190) |>
nrow()22 penguins.
★★ Exercise 2 — mutate and recode. Create body_mass_kg (mass in kilograms) and a variable size that is "small" below 3.5 kg, "medium" from 3.5 up to 4.5 kg (excluded), and "large" from 4.5 kg. Then count the penguins in each size class.
Divide body_mass by 1000. Conditions are checked in order, so the second one can simply be body_mass_kg < 4.5.
penguins |>
mutate(
body_mass_kg = body_mass / 1000,
size = case_when(
body_mass_kg < 3.5 ~ "small",
body_mass_kg < 4.5 ~ "medium",
body_mass_kg >= 4.5 ~ "large"
)
) |>
count(size)About 71 small, 153 medium and 118 large penguins, plus 2 with unknown mass (NA), which match no condition.
★★ Exercise 3 — debug. The code below should list species, sex and body mass of Adelie and Chinstrap penguins, from heaviest to lightest. It contains three mistakes. Fix them.
Look at (1) the comparison operator and the “or” condition, (2) the end of the select() line, (3) the sort direction.
penguins |>
filter(species %in% c("Adelie", "Chinstrap")) |>
select(species, sex, body_mass) |>
arrange(desc(body_mass))The missing |> after select() is the most common bug in pipelines: R runs the first part, prints it, then tries to run arrange(body_mass) alone and fails.
★★★ Exercise 4 — predict the output. Before running, predict how many rows this returns. Then run it and explain any surprise.
Three species × 3 rows = 9 rows… unless there are ties. Look at the Gentoo rows.
penguins |>
slice_max(body_mass, n = 3, by = species, with_ties = FALSE) |>
select(species, body_mass)The original returns 10 rows: two Gentoo penguins share the third-highest mass (6000 g) and slice_max() keeps both. Adding with_ties = FALSE returns exactly 9.
Practice on your own computer
★★ Exercise 5 — base R vs dplyr. With mtcars, list the three cars with the highest mpg among those with 4 cylinders, showing only mpg, cyl and hp. Do it twice: once with order() and brackets, once with dplyr (keep the car names with rownames_to_column()).
# base R
cars4 <- mtcars[mtcars$cyl == 4, c("mpg", "cyl", "hp")]
head(cars4[order(-cars4$mpg), ], 3)
# dplyr
mtcars |>
rownames_to_column("model") |>
filter(cyl == 4) |>
select(model, mpg, cyl, hp) |>
arrange(desc(mpg)) |>
slice_head(n = 3)Both give the Toyota Corolla, Fiat 128 and Honda Civic. The dplyr version is longer but each line does one thing.
Summary
| Verb | What it does | Example |
|---|---|---|
filter() |
keep rows | filter(x > 0, g %in% c("a", "b")) |
select() |
choose columns | select(a, starts_with("b"), where(is.numeric)) |
mutate() |
create / modify columns | mutate(z = x / y) |
if_else(), case_when() |
recode | case_when(x < 0 ~ "neg", .default = "pos") |
arrange(), desc() |
sort rows | arrange(g, desc(x)) |
order() (base) |
sorting positions | df[order(-df$x), ] |
rename(), relocate() |
rename / move columns | rename(new = old) |
distinct() |
unique rows | distinct(g1, g2) |
slice_head(), slice_max(), slice_sample() |
rows by position | slice_max(x, n = 3) |
count() |
frequencies | count(g, sort = TRUE) |
- Every verb takes a data frame and returns a new one; nothing is modified in place.
- The pipe
|>passes the result on its left as the first argument of the function on its right: read it as “and then”, and write one verb per line. filter()drops rows whose condition isNA; check missing values withfilter(is.na(x))orcount().