Pipes in R
Pipes are an extremely useful tool from the magrittr
package1 that allow you to express a sequence of multiple operations. They can greatly simplify your code and make your operations more intuitive. However they are not the only way to write your code and combine multiple operations. In fact, for many years the pipe did not exist in R. How else did people write their code?
Suppose we have the following assignment:
Using the
penguins
dataset, calculate the average body mass for Adelie penguins on different islands.
Okay, first let’s load our libraries and check out the data frame.
library(tidyverse)
library(palmerpenguins)
penguins
## # A tibble: 344 × 8
## species island bill_length_mm bill_depth_mm flipper_…¹ body_…² 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 fema… 2007
## 3 Adelie Torgersen 40.3 18 195 3250 fema… 2007
## 4 Adelie Torgersen NA NA NA NA <NA> 2007
## 5 Adelie Torgersen 36.7 19.3 193 3450 fema… 2007
## 6 Adelie Torgersen 39.3 20.6 190 3650 male 2007
## 7 Adelie Torgersen 38.9 17.8 181 3625 fema… 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
## # … with 334 more rows, and abbreviated variable names ¹flipper_length_mm,
## # ²body_mass_g
## # ℹ Use `print(n = ...)` to see more rows
We can decompose the problem into a series of discrete steps:
- Filter
penguins
to only keep observations where the species is “Adelie” - Group the filtered
penguins
data frame by island - Summarize the grouped and filtered
penguins
data frame by calculating the average body mass
But how do we implement the code?
Intermediate steps
One option is to save each step as a new object:
penguins_1 <- filter(penguins, species == "Adelie")
penguins_2 <- group_by(penguins_1, island)
(penguins_3 <- summarize(penguins_2, body_mass = mean(body_mass_g, na.rm = TRUE)))
## # A tibble: 3 × 2
## island body_mass
## <fct> <dbl>
## 1 Biscoe 3710.
## 2 Dream 3688.
## 3 Torgersen 3706.
Why do we not like doing this? We have to name each intermediate object. Here I just append a number to the end, but this is not good self-documentation. What should we expect to find in penguins_2
? It would be nicer to have an informative name, but there isn’t a natural one. Then we have to remember how the data exists in each intermediate step and remember to reference the correct one. What happens if we misidentify the data frame?
penguins_1 <- filter(penguins, species == "Adelie")
penguins_2 <- group_by(penguins_1, island)
(penguins_3 <- summarize(penguins_1, body_mass = mean(body_mass_g, na.rm = TRUE)))
## # A tibble: 1 × 1
## body_mass
## <dbl>
## 1 3701.
We don’t get the correct answer. Worse, we don’t get an explicit error message because the code, as written, works. R can execute this command for us and doesn’t know to warn us that we used penguins_1
instead of penguins_2
.
Overwrite the original
Instead of creating intermediate objects, let’s just replace the original data frame with the modified form.
penguins <- filter(penguins, species == "Adelie")
penguins <- group_by(penguins, island)
(penguins <- summarize(penguins, body_mass = mean(body_mass_g, na.rm = TRUE)))
## # A tibble: 3 × 2
## island body_mass
## <fct> <dbl>
## 1 Biscoe 3710.
## 2 Dream 3688.
## 3 Torgersen 3706.
This works, but still has a couple of problems. What happens if I make an error in the middle of the operation? I need to rerun the entire operation from the beginning. With your own data sources, this means having to read in the .csv
file all over again to restore a fresh copy.
Function composition
We could string all the function calls together into a single object and forget assigning it anywhere.
summarize(
group_by(
filter(
penguins,
species == "Adelie"
),
island
),
body_mass = mean(body_mass_g, na.rm = TRUE)
)
## # A tibble: 3 × 2
## island body_mass
## <fct> <dbl>
## 1 Biscoe 3710.
## 2 Dream 3688.
## 3 Torgersen 3706.
But now we have to read the function from the inside out. Even worse, what happens if we cram it all into a single line?
summarize(group_by(filter(penguins, species == "Adelie"), island), body_mass = mean(body_mass_g, na.rm = TRUE))
## # A tibble: 3 × 2
## island body_mass
## <fct> <dbl>
## 1 Biscoe 3710.
## 2 Dream 3688.
## 3 Torgersen 3706.
This is not intuitive for humans. Again, the computer will handle it just fine, but if you make a mistake debugging it will be a pain.
Back to the pipe
penguins %>%
filter(species == "Adelie") %>%
group_by(island) %>%
summarize(body_mass = mean(body_mass_g, na.rm = TRUE))
## # A tibble: 3 × 2
## island body_mass
## <fct> <dbl>
## 1 Biscoe 3710.
## 2 Dream 3688.
## 3 Torgersen 3706.
Piping is the clearest syntax to implement, as it focuses on actions, not objects. Or as Hadley would say:
[I]t focuses on verbs, not nouns.
magrittr
automatically passes the output from the first line into the next line as the input.
This is how I explain the 'pipe' to #rstats newbies... pic.twitter.com/VdAFTLzijy
— We are R-Ladies (@WeAreRLadies) September 13, 2019
This is why tidyverse
functions always accept a data frame as the first argument.
Important tips for piping
- Remember though that you don’t assign anything within the pipes - that is, you should not use
<-
inside the piped operation. Only use this at the beginning if you want to save the output - Remember to add the pipe
%>%
at the end of each line involved in the piped operation. A good rule of thumb: RStudio will automatically indent lines of code that are part of a piped operation. If the line isn’t indented, it probably hasn’t been added to the pipe. If you have an error in a piped operation, always check to make sure the pipe is connected as you expect.
Session Info
sessioninfo::session_info()
## ─ Session info ───────────────────────────────────────────────────────────────
## setting value
## version R version 4.2.1 (2022-06-23)
## os macOS Monterey 12.3
## system aarch64, darwin20
## ui X11
## language (EN)
## collate en_US.UTF-8
## ctype en_US.UTF-8
## tz America/New_York
## date 2022-08-22
## pandoc 2.18 @ /Applications/RStudio.app/Contents/MacOS/quarto/bin/tools/ (via rmarkdown)
##
## ─ Packages ───────────────────────────────────────────────────────────────────
## package * version date (UTC) lib source
## blogdown 1.10 2022-05-10 [2] CRAN (R 4.2.0)
## bookdown 0.27 2022-06-14 [2] CRAN (R 4.2.0)
## bslib 0.4.0 2022-07-16 [2] CRAN (R 4.2.0)
## cachem 1.0.6 2021-08-19 [2] CRAN (R 4.2.0)
## cli 3.3.0 2022-04-25 [2] CRAN (R 4.2.0)
## digest 0.6.29 2021-12-01 [2] CRAN (R 4.2.0)
## evaluate 0.16 2022-08-09 [1] CRAN (R 4.2.1)
## fastmap 1.1.0 2021-01-25 [2] CRAN (R 4.2.0)
## here 1.0.1 2020-12-13 [2] CRAN (R 4.2.0)
## htmltools 0.5.3 2022-07-18 [2] CRAN (R 4.2.0)
## jquerylib 0.1.4 2021-04-26 [2] CRAN (R 4.2.0)
## jsonlite 1.8.0 2022-02-22 [2] CRAN (R 4.2.0)
## knitr 1.39 2022-04-26 [2] CRAN (R 4.2.0)
## magrittr 2.0.3 2022-03-30 [2] CRAN (R 4.2.0)
## R6 2.5.1 2021-08-19 [2] CRAN (R 4.2.0)
## rlang 1.0.4 2022-07-12 [2] CRAN (R 4.2.0)
## rmarkdown 2.14 2022-04-25 [2] CRAN (R 4.2.0)
## rprojroot 2.0.3 2022-04-02 [2] CRAN (R 4.2.0)
## rstudioapi 0.13 2020-11-12 [2] CRAN (R 4.2.0)
## sass 0.4.2 2022-07-16 [2] CRAN (R 4.2.0)
## sessioninfo 1.2.2 2021-12-06 [2] CRAN (R 4.2.0)
## stringi 1.7.8 2022-07-11 [2] CRAN (R 4.2.0)
## stringr 1.4.0 2019-02-10 [2] CRAN (R 4.2.0)
## xfun 0.31 2022-05-10 [1] CRAN (R 4.2.0)
## yaml 2.3.5 2022-02-21 [2] CRAN (R 4.2.0)
##
## [1] /Users/soltoffbc/Library/R/arm64/4.2/library
## [2] /Library/Frameworks/R.framework/Versions/4.2-arm64/Resources/library
##
## ──────────────────────────────────────────────────────────────────────────────
The basic
%>%
pipe is automatically imported as part of thetidyverse
library. If you wish to use any of the extra tools frommagrittr
as demonstrated in R for Data Science, you need to explicitly loadmagrittr
. ↩︎