From a3647333269b206789b0ef2a02eccab376ac0915 Mon Sep 17 00:00:00 2001 From: Tim Dennis Date: Sat, 8 Aug 2026 16:11:25 -0700 Subject: [PATCH] Modernize pipe operator and recode() usage Convert %>% to the native pipe |> throughout episodes 1, 3, 4, and 5, matching the current tidyverse style guide recommendation for R 4.1+ (this lesson already requires R 4.5+). Replace dplyr::recode() (superseded) with case_match() in episodes 3 and 5, including an explicit .default to preserve recode()'s pass-through behavior for values not in the mapping list. Verified case_match() output is identical to recode() output on the actual dataset (episodes/data/books.csv), and that .default isn't currently masking any data issue since every raw code value is covered by the mapping. Updated accompanying prose (objectives, headings, exercise text, keypoints) to match. Verified: sandpaper::validate_lesson() passes, and all four episodes knit cleanly from a fresh session with no rendered errors. --- episodes/01-intro-to-r.Rmd | 4 +- .../03-data-cleaning-and-transformation.Rmd | 160 +++++++++--------- episodes/04-data-viz-ggplot.Rmd | 16 +- episodes/05-reproducible-reports.Rmd | 71 ++++---- 4 files changed, 131 insertions(+), 120 deletions(-) diff --git a/episodes/01-intro-to-r.Rmd b/episodes/01-intro-to-r.Rmd index 3cae81eb..1c23e5a8 100644 --- a/episodes/01-intro-to-r.Rmd +++ b/episodes/01-intro-to-r.Rmd @@ -848,7 +848,7 @@ multiple <- data.frame("operator" = c("&", "|", "!", "any", "all") , "function" = c("boolean AND", "boolean OR", "boolean NOT", "ANY true", "ALL true") , stringsAsFactors = F) -kable(multiple) %>% +kable(multiple) |> kable_styling(bootstrap_options = c("striped", "hover", "condensed", "responsive")) ``` @@ -873,7 +873,7 @@ assigning variables). operators <- tibble("operator" = c("<", ">", "==", "<=", ">=", "!=", "%in%", "is.na", "!is.na"), "function" = c("Less Than", "Greater Than", "Equal To", "Less Than or Equal To", "Greater Than or Equal To", "Not Equal To", "Has A Match In", "Is NA", "Is Not NA")) -kable(operators) %>% +kable(operators) |> kable_styling(bootstrap_options = c("striped", "hover", "condensed", "responsive")) ``` diff --git a/episodes/03-data-cleaning-and-transformation.Rmd b/episodes/03-data-cleaning-and-transformation.Rmd index 77921a65..f042687d 100644 --- a/episodes/03-data-cleaning-and-transformation.Rmd +++ b/episodes/03-data-cleaning-and-transformation.Rmd @@ -9,8 +9,8 @@ source: Rmd - Describe the functions available in the **`dplyr`** and **`tidyr`** packages. - Recognise and use the following functions: `select()`, `filter()`, `rename()`, -`recode()`, `mutate()` and `arrange()`. -- Combine one or more functions using the 'pipe' operator `%>%`. +`case_match()`, `mutate()` and `arrange()`. +- Combine one or more functions using the 'pipe' operator `|>`. - Use the split-apply-combine concept for data analysis. - Export a data frame to a csv file. @@ -51,7 +51,7 @@ page by loading the `tidyverse` and the `books` dataset we downloaded earlier. We're going to learn some of the most common **`dplyr`** functions: - `rename()`: rename columns -- `recode()`: recode values in a column +- `case_match()`: recode values in a column - `select()`: subset columns - `filter()`: subset rows on conditions - `mutate()`: create new columns by using information from other columns @@ -192,43 +192,49 @@ knitr::include_graphics("fig/BCODE1.png") knitr::include_graphics("fig/BCODE2.png") ``` -You can reassign names to the values easily using the `recode()` function -from the `dplyr` package. Unlike `rename()`, the old value comes first here. -Also notice that we are overwriting the `books$subCollection` variable. +You can reassign names to the values easily using the `case_match()` function +from the `dplyr` package. Unlike `rename()`, the old value comes first here, +followed by `~` and the new value. Also notice that we are overwriting the +`books$subCollection` variable, and that we set `.default` to the original +column: without it, any value not listed below would silently become `NA` +instead of staying as-is. ```{r, comment=FALSE} # first print to the console all of the unique values you will need to recode distinct(books, subCollection) -books$subCollection <- recode(books$subCollection, - "-" = "general collection", - u = "government documents", - r = "reference", - b = "k-12 materials", - j = "juvenile", - s = "special collections", - c = "computer files", - t = "theses", - a = "archives", - z = "reserves") +books$subCollection <- case_match(books$subCollection, + "-" ~ "general collection", + "u" ~ "government documents", + "r" ~ "reference", + "b" ~ "k-12 materials", + "j" ~ "juvenile", + "s" ~ "special collections", + "c" ~ "computer files", + "t" ~ "theses", + "a" ~ "archives", + "z" ~ "reserves", + .default = books$subCollection) books ``` -Do the same for the `format` column. Note that you must put `"5"` and `"4"` into -quotation marks for the function to operate correctly. +Do the same for the `format` column. Note that every value on the left of `~` +needs to be in quotation marks, including single letters like `a` or `e`, for +the function to operate correctly. ```{r, comment=FALSE, purl=FALSE} -books$format <- recode(books$format, - a = "book", - e = "serial", - w = "microform", - s = "e-gov doc", - o = "map", - n = "database", - k = "cd-rom", - m = "image", - "5" = "kit/object", - "4" = "online video") +books$format <- case_match(books$format, + "a" ~ "book", + "e" ~ "serial", + "w" ~ "microform", + "s" ~ "e-gov doc", + "o" ~ "map", + "n" ~ "database", + "k" ~ "cd-rom", + "m" ~ "image", + "5" ~ "kit/object", + "4" ~ "online video", + .default = books$format) ``` Once you have finished recoding the values for the two variables, examine @@ -380,9 +386,9 @@ We see the error message `NAs introduced by coercion`. This is because non-numer -## Putting it all together with %>% +## Putting it all together with |> -The [Pipe Operator](https://www.datacamp.com/community/tutorials/pipe-r-tutorial) `%>%` is +The [Pipe Operator](https://style.tidyverse.org/pipes.html) `|>` is loaded with the `tidyverse`. It takes the output of one statement and makes it the input of the next statement. You can think of it as "then" in natural language. So instead of making a bunch of intermediate data frames and @@ -396,16 +402,16 @@ columns are selected, and finally the data is rearranged from most to least checkouts. ```{r pipe, comment=NA} -myBooks <- books %>% - filter(format == "book") %>% - select(title, tot_chkout) %>% +myBooks <- books |> + filter(format == "book") |> + select(title, tot_chkout) |> arrange(desc(tot_chkout)) myBooks ``` ::::::::::::::::::::::::::::::::::::::: challenge -### Exercise: Playing with pipes `%>%` +### Exercise: Playing with pipes `|>` 1. Create a new data frame `booksKids` with these conditions: @@ -420,10 +426,10 @@ myBooks ### Solution ```{r, answer=TRUE} -booksKids <- books %>% +booksKids <- books |> filter(subCollection %in% c("juvenile", "k-12 materials"), - format == "book") %>% -select(title, callnumber, tot_chkout, pubyear) %>% + format == "book") |> +select(title, callnumber, tot_chkout, pubyear) |> arrange(desc(tot_chkout)) mean(booksKids$tot_chkout) ``` @@ -453,8 +459,8 @@ to calculate the summary statistics. So to compute the average checkouts by format: ```{r, comment=NA} -books %>% - group_by(format) %>% +books |> + group_by(format) |> summarize(mean_checkouts = mean(tot_chkout)) ``` @@ -463,12 +469,12 @@ Books and maps have the highest, and as we would expect, databases, online video Here is a more complex example: ```{r, comment=NA} -books %>% - filter(format == "book") %>% - mutate(call_class = str_sub(callnumber, 1, 1)) %>% - group_by(call_class) %>% +books |> + filter(format == "book") |> + mutate(call_class = str_sub(callnumber, 1, 1)) |> + group_by(call_class) |> summarize(count = n(), - sum_tot_chkout = sum(tot_chkout)) %>% + sum_tot_chkout = sum(tot_chkout)) |> arrange(desc(sum_tot_chkout)) ``` @@ -500,9 +506,9 @@ Note: If the final product of this data will be imported into an ILS, you may n Read more about [matching patterns with regular expressions](https://r4ds.had.co.nz/strings.html#matching-patterns-with-regular-expressions). ```{r} -books %>% - mutate(title_modified = str_remove(title, "/$")) %>% # remove the trailing slash - mutate(title_modified = str_replace(title_modified, "\\s:\\|", ": ")) %>% # replace ' :|' with ': ' +books |> + mutate(title_modified = str_remove(title, "/$")) |> # remove the trailing slash + mutate(title_modified = str_replace(title_modified, "\\s:\\|", ": ")) |> # replace ' :|' with ': ' select(title_modified, title) ``` @@ -527,7 +533,7 @@ In preparation for our next lesson on plotting, we are going to create a version of the dataset with most of the changes we made above. We will first read in the original, then make all the changes with pipes. ```{r, comment=NA, eval=FALSE} -books_reformatted <- read_csv("./data/books.csv") %>% +books_reformatted <- read_csv("./data/books.csv") |> rename(title = X245.ab, author = X245.c, callnumber = CALL...BIBLIO., @@ -539,35 +545,37 @@ books_reformatted <- read_csv("./data/books.csv") %>% tot_chkout = TOT.CHKOUT, loutdate = LOUTDATE, subject = SUBJECT, - callnumber2 = CALL...ITEM.) %>% + callnumber2 = CALL...ITEM.) |> mutate(pubyear = as.integer(pubyear), call_class = str_sub(callnumber, 1, 1), - subCollection = recode(subCollection, - "-" = "general collection", - u = "government documents", - r = "reference", - b = "k-12 materials", - j = "juvenile", - s = "special collections", - c = "computer files", - t = "theses", - a = "archives", - z = "reserves"), - format = recode(format, - a = "book", - e = "serial", - w = "microform", - s = "e-gov doc", - o = "map", - n = "database", - k = "cd-rom", - m = "image", - "5" = "kit/object", - "4" = "online video")) + subCollection = case_match(subCollection, + "-" ~ "general collection", + "u" ~ "government documents", + "r" ~ "reference", + "b" ~ "k-12 materials", + "j" ~ "juvenile", + "s" ~ "special collections", + "c" ~ "computer files", + "t" ~ "theses", + "a" ~ "archives", + "z" ~ "reserves", + .default = subCollection), + format = case_match(format, + "a" ~ "book", + "e" ~ "serial", + "w" ~ "microform", + "s" ~ "e-gov doc", + "o" ~ "map", + "n" ~ "database", + "k" ~ "cd-rom", + "m" ~ "image", + "5" ~ "kit/object", + "4" ~ "online video", + .default = format)) ``` This chunk of code read the CSV, renamed the variables, used `mutate()` in -combination with `recode()` to recode the `format` and `subCollection` values, +combination with `case_match()` to recode the `format` and `subCollection` values, used `mutate()` in combination with `as.integer()` to coerce `pubyear` to integer, and used `mutate()` in combination with `str_sub` to create the new varable `call_class`. @@ -592,11 +600,11 @@ write_csv(books_reformatted, "./data_output/books_reformatted.csv") - Use the `dplyr` package to manipulate dataframes. - Subset data frames using `select()` and `filter()`. - Rename variables in a data frame using `rename()`. -- Recode values in a data frame using `recode()`. +- Recode values in a data frame using `case_match`. - Use `mutate()` to create new variables. - Sort data using `arrange()`. - Use `group_by()` and `summarize()` to work with subsets of data. -- Use pipe (`%>%`) to combine multiple commands. +- Use pipe (`|>`) to combine multiple commands. :::::::::::::::::::::::::::::::::::::::::::::::::: diff --git a/episodes/04-data-viz-ggplot.Rmd b/episodes/04-data-viz-ggplot.Rmd index 014f0e5c..c3884fce 100644 --- a/episodes/04-data-viz-ggplot.Rmd +++ b/episodes/04-data-viz-ggplot.Rmd @@ -129,7 +129,7 @@ Let's create a `booksPlot` and limit our visualization to only items in `subColl ```{r, purl=FALSE} # create a new data frame -booksPlot <- books2 %>% +booksPlot <- books2 |> filter(subCollection == "general collection" | subCollection == "juvenile" | subCollection == "k-12 materials", @@ -289,7 +289,7 @@ it to `booksHighUsage`. ```{r} # filter booksPlot to include only items with over 10 checkouts -booksHighUsage <- booksPlot %>% +booksHighUsage <- booksPlot |> filter(!is.na(tot_chkout), tot_chkout > 10) ``` @@ -472,7 +472,7 @@ books per year. We will do this by calling `mutate()` to create a new variable `pubyear_ymd`. ```{r, purl=FALSE} -booksPlot <- booksPlot %>% +booksPlot <- booksPlot |> mutate(pubyear_ymd = ymd(pubyear, truncated = 2)) # convert pubyear to a Date object with ymd() class(booksPlot$pubyear) # integer @@ -486,9 +486,9 @@ that the date must fall between that range. We then need to group the data and count records within each group. ```{r, purl=FALSE} -yearly_counts <- booksPlot %>% +yearly_counts <- booksPlot |> filter(!is.na(pubyear_ymd), - pubyear_ymd > "1989-01-01" & pubyear_ymd < "2002-01-01") %>% + pubyear_ymd > "1989-01-01" & pubyear_ymd < "2002-01-01") |> count(pubyear_ymd, subCollection) ``` @@ -695,10 +695,10 @@ publication. Add one of the themes listed above. ## Solution ```{r} -yearly_checkouts <- booksPlot %>% +yearly_checkouts <- booksPlot |> filter(!is.na(pubyear_ymd), - pubyear_ymd > "1989-01-01" & pubyear_ymd < "2002-01-01") %>% - group_by(pubyear_ymd) %>% + pubyear_ymd > "1989-01-01" & pubyear_ymd < "2002-01-01") |> + group_by(pubyear_ymd) |> summarize(checkouts_sum = sum(tot_chkout)) ggplot(data = yearly_checkouts, mapping = aes(x = pubyear_ymd, y = checkouts_sum)) + diff --git a/episodes/05-reproducible-reports.Rmd b/episodes/05-reproducible-reports.Rmd index 80f3451f..43434505 100644 --- a/episodes/05-reproducible-reports.Rmd +++ b/episodes/05-reproducible-reports.Rmd @@ -23,7 +23,7 @@ source: Rmd source("files/download_data.R") library(tidyverse) # Read raw data and apply cleaning steps from previous episodes -books2 <- read_csv("data/books.csv") %>% +books2 <- read_csv("data/books.csv") |> rename( title = X245.ab, author = X245.c, @@ -37,32 +37,34 @@ books2 <- read_csv("data/books.csv") %>% loutdate = LOUTDATE, subject = SUBJECT, callnumber2 = CALL...ITEM. - ) %>% + ) |> mutate( pubyear = as.integer(pubyear), - subCollection = recode(subCollection, - "-" = "general collection", - u = "government documents", - r = "reference", - b = "k-12 materials", - j = "juvenile", - s = "special collections", - c = "computer files", - t = "theses", - a = "archives", - z = "reserves" + subCollection = case_match(subCollection, + "-" ~ "general collection", + "u" ~ "government documents", + "r" ~ "reference", + "b" ~ "k-12 materials", + "j" ~ "juvenile", + "s" ~ "special collections", + "c" ~ "computer files", + "t" ~ "theses", + "a" ~ "archives", + "z" ~ "reserves", + .default = subCollection ), - format = recode(format, - a = "book", - e = "serial", - w = "microform", - s = "e-gov doc", - o = "map", - n = "database", - k = "cd-rom", - m = "image", - "5" = "kit/object", - "4" = "online video" + format = case_match(format, + "a" ~ "book", + "e" ~ "serial", + "w" ~ "microform", + "s" ~ "e-gov doc", + "o" ~ "map", + "n" ~ "database", + "k" ~ "cd-rom", + "m" ~ "image", + "5" ~ "kit/object", + "4" ~ "online video", + .default = format ) ) ``` @@ -144,17 +146,18 @@ Let's clean up the example file and create a report using our `books` data. library(tidyverse) # Load data and rename columns for clarity -books2 <- read_csv("data/books.csv") %>% +books2 <- read_csv("data/books.csv") |> rename( subCollection = BCODE1, tot_chkout = TOT.CHKOUT, format = BCODE2 - ) %>% + ) |> mutate( - subCollection = recode(subCollection, - "-" = "general collection", - j = "juvenile", - b = "k-12 materials" + subCollection = case_match(subCollection, + "-" ~ "general collection", + "j" ~ "juvenile", + "b" ~ "k-12 materials", + .default = subCollection ) ) ``` @@ -182,7 +185,7 @@ Next, insert a new code chunk and paste the plotting code we developed in the pr #| echo: false # Filter for high usage -booksHighUsage <- books2 %>% +booksHighUsage <- books2 |> filter(!is.na(tot_chkout), tot_chkout > 10) @@ -235,9 +238,9 @@ The table below shows the average checkouts for each item format. ```{{r}} #| label: summary-table -books2 %>% - group_by(format) %>% - summarize(mean_checkouts = mean(tot_chkout, na.rm = TRUE)) %>% +books2 |> + group_by(format) |> + summarize(mean_checkouts = mean(tot_chkout, na.rm = TRUE)) |> arrange(desc(mean_checkouts)) ```