Week 14: Program

[14a] Conditionals

Topics

  • Evaluate comparisons and combine multiple conditions using Boolean logic
  • Summarize logical vectors to count occurrences and calculate proportions
  • Control data transformations conditionally using if_else() and case_when()

Readings

Slides

*Note that you can click the three-line (hamburger) icon on the bottom-left of the slides to access a navigation menu. You can click inside the slides region and press the left and right arrow keys on your keyboard to advance and reverse the slides and animations.

Practice

1) Categorizing City Mileage

Using the mpg dataset (from {ggplot2}), let’s use if_else() to create a new categorical column based on city fuel efficiency.

  • Use mutate() and if_else() to create a new column called efficiency.
  • If cty is greater than or equal to 20, label it “High”. Otherwise, label it “Low”.
  • Select the manufacturer, model, cty, and efficiency columns to view your results.

Answer key

library(tidyverse)
data("mpg", package = "ggplot2")

mpg |> 
  mutate(
    efficiency = if_else(
      condition = cty >= 20, 
      true = "High", 
      false = "Low"
    )
  ) |> 
  select(manufacturer, model, cty, efficiency)
## # A tibble: 234 × 4
##    manufacturer model        cty efficiency
##    <chr>        <chr>      <int> <chr>     
##  1 audi         a4            18 Low       
##  2 audi         a4            21 High      
##  3 audi         a4            20 High      
##  4 audi         a4            21 High      
##  5 audi         a4            16 Low       
##  6 audi         a4            18 Low       
##  7 audi         a4            18 Low       
##  8 audi         a4 quattro    18 Low       
##  9 audi         a4 quattro    16 Low       
## 10 audi         a4 quattro    20 High      
## # ℹ 224 more rows

2) Sleep Summaries

Let’s use the msleep dataset (from {ggplot2}) to practice summarizing logical conditions across groups.

  • Use summarize() to calculate two new metrics.
  • First, calculate the proportion of animals that sleep more than 12 hours (sleep_total > 12) and name it prop_heavy_sleepers.
  • Second, check if any animal in the group has a body weight greater than 500 (bodywt > 500) and name it any_giants.
  • Group the summary by the vore column using the .by argument.

Answer key

library(tidyverse)
data("msleep", package = "ggplot2")

msleep |> 
  summarize(
    prop_heavy_sleepers = mean(sleep_total > 12),
    any_giants = any(bodywt > 500),
    .by = vore
  )
## # A tibble: 5 × 3
##   vore    prop_heavy_sleepers any_giants
##   <chr>                 <dbl> <lgl>     
## 1 carni                 0.421 TRUE      
## 2 omni                  0.2   FALSE     
## 3 herbi                 0.438 TRUE      
## 4 <NA>                  0.286 FALSE     
## 5 insecti               0.6   FALSE

3) Sizing Up Starwars Characters

The starwars dataset (from {dplyr}) contains physical attributes of characters. Let’s practice handling multiple conditions using case_when().

  • Use mutate() and case_when() to create a new column called size_category based on the mass column.
  • If mass is less than 50, label it “Light”.
  • If mass is less than 100, label it “Medium”.
  • If mass is 100 or greater, label it “Heavy”.
  • Use the .default argument to label any remaining cases (like missing values) as “Unknown”.
  • Select the name, mass, and size_category columns to verify your logic worked correctly. Note: Remember that case_when() evaluates from top to bottom!

Answer key

library(tidyverse)
data("starwars", package = "dplyr")

starwars |> 
  mutate(
    size_category = case_when(
      mass < 50 ~ "Light",
      mass < 100 ~ "Medium",
      mass >= 100 ~ "Heavy",
      .default = "Unknown"
    )
  ) |> 
  select(name, mass, size_category)
## # A tibble: 87 × 3
##    name                mass size_category
##    <chr>              <dbl> <chr>        
##  1 Luke Skywalker        77 Medium       
##  2 C-3PO                 75 Medium       
##  3 R2-D2                 32 Light        
##  4 Darth Vader          136 Heavy        
##  5 Leia Organa           49 Light        
##  6 Owen Lars            120 Heavy        
##  7 Beru Whitesun Lars    75 Medium       
##  8 R5-D4                 32 Light        
##  9 Biggs Darklighter     84 Medium       
## 10 Obi-Wan Kenobi        77 Medium       
## # ℹ 77 more rows

[14b] Functions

Topics

  • Package repetitive code into custom functions using the DRY principle
  • Control function behavior using arguments, defaults, and branching logic
  • Apply custom vector functions inside tidyverse data pipelines

Readings

Slides

*Note that you can click the three-line (hamburger) icon on the bottom-left of the slides to access a navigation menu. You can click inside the slides region and press the left and right arrow keys on your keyboard to advance and reverse the slides and animations.

Practice

1) Building an Outlier Flag

Let’s build a function to identify unusually high values in a dataset. We will start with the basic math.

  • Create a custom function named flag_outlier() with a single argument x.
  • Inside the function, calculate a cutoff value: the mean() of x plus 2 times the sd() of x. (Be sure to use na.rm = TRUE for both!).
  • Return a logical value indicating whether x is greater than your cutoff.
  • Test your function on the Ozone column of the airquality dataset (from the {datasets} package).

Answer key

library(tidyverse)
data("airquality", package = "datasets")

flag_outlier <- function(x) {
  cutoff <- mean(x, na.rm = TRUE) + (2 * sd(x, na.rm = TRUE))
  x > cutoff
}

flag_outlier(airquality$Ozone)
##   [1] FALSE FALSE FALSE FALSE    NA FALSE FALSE FALSE FALSE    NA FALSE FALSE
##  [13] FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
##  [25]    NA    NA    NA FALSE FALSE  TRUE FALSE    NA    NA    NA    NA    NA
##  [37]    NA FALSE    NA FALSE FALSE    NA    NA FALSE    NA    NA FALSE FALSE
##  [49] FALSE FALSE FALSE    NA    NA    NA    NA    NA    NA    NA    NA    NA
##  [61]    NA  TRUE FALSE FALSE    NA FALSE FALSE FALSE FALSE FALSE FALSE    NA
##  [73] FALSE FALSE    NA FALSE FALSE FALSE FALSE FALSE FALSE FALSE    NA    NA
##  [85] FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
##  [97] FALSE FALSE  TRUE FALSE  TRUE    NA    NA FALSE FALSE FALSE    NA FALSE
## [109] FALSE FALSE FALSE FALSE FALSE FALSE    NA FALSE  TRUE FALSE    NA FALSE
## [121]  TRUE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
## [133] FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
## [145] FALSE FALSE FALSE FALSE FALSE    NA FALSE FALSE FALSE

2) Adding Customization

Let’s make the function more flexible by allowing the user to decide exactly how extreme an outlier needs to be before it gets flagged.

  • Redefine your function and add a second argument called multiplier. Set its default value to 2.
  • Update your cutoff calculation. Instead of hardcoding a number, multiply the standard deviation by your new multiplier argument.
  • Test your upgraded function by passing airquality$Ozone to it and setting the multiplier to 3.
  • Pipe the result of that test directly into the any() function to quickly check if there are any extreme outliers in the vector.

Answer key

flag_outlier <- function(x, multiplier = 2) {
  
  cutoff <- mean(x, na.rm = TRUE) + (multiplier * sd(x, na.rm = TRUE))
  
  x > cutoff
}

flag_outlier(airquality$Ozone, multiplier = 3) |> any()
## [1] TRUE

3) Functions in Pipelines

Because your function takes a vector and returns a logical vector of the exact same length, it drops perfectly into your data wrangling workflows.

  • Open a tidyverse pipeline using the airquality dataset.
  • Immediately pipe it to the as_tibble() function to make it a tibble.
  • Use mutate() to create a new column called is_high_ozone using your flag_outlier() function.
  • Use select() to view only the Month, Day, Ozone, and is_high_ozone columns.

Answer key

airquality |> 
  as_tibble() |> 
  mutate(is_high_ozone = flag_outlier(Ozone)) |> 
  select(Month, Day, Ozone, is_high_ozone)
## # A tibble: 153 × 4
##    Month   Day Ozone is_high_ozone
##    <int> <int> <int> <lgl>        
##  1     5     1    41 FALSE        
##  2     5     2    36 FALSE        
##  3     5     3    12 FALSE        
##  4     5     4    18 FALSE        
##  5     5     5    NA NA           
##  6     5     6    28 FALSE        
##  7     5     7    23 FALSE        
##  8     5     8    19 FALSE        
##  9     5     9     8 FALSE        
## 10     5    10    NA NA           
## # ℹ 143 more rows

[14c] Iteration

Topics

  • Iterate functions across multiple columns using across()
  • Use list objects to store complex, mixed-type data
  • Efficiently apply functions to vectors using map()

Readings

Slides

*Note that you can click the three-line (hamburger) icon on the bottom-left of the slides to access a navigation menu. You can click inside the slides region and press the left and right arrow keys on your keyboard to advance and reverse the slides and animations.

Practice

1) Summarizing Multiple Columns

We can use iteration to apply the same summary function to many columns at once without repetitive typing. Let’s use the iris dataset (from {datasets}) to find the median of all numeric columns for each species.

  • Use the appropriate summarizing and iteration functions to calculate the median for all numeric columns automatically.
  • Group your summary by the Species column.

Answer key

library(tidyverse)
data("iris", package = "datasets")

iris |> 
  summarize(
    .by = Species,
    across(.cols = where(is.numeric), .fns = median)
  )
##      Species Sepal.Length Sepal.Width Petal.Length Petal.Width
## 1     setosa          5.0         3.4         1.50         0.2
## 2 versicolor          5.9         2.8         4.35         1.3
## 3  virginica          6.5         3.0         5.55         2.0

2) Inline Operations with Anonymous Functions

Anonymous functions let us apply custom mathematical transformations without having to formally save and name a new function. Let’s use the penguins dataset (from {datasets})to convert several measurements from millimeters to centimeters.

  • Use the appropriate mutation and iteration functions to target the bill_len, bill_dep, and flipper_len columns.
  • Apply an anonymous function to these columns that divides their values by 10.
  • Select your modified columns along with the species column to view the results.

Answer key

library(tidyverse)
data("penguins", package = "datasets")

penguins |> 
  as_tibble() |> 
  mutate(
    across(
      .cols = c(bill_len, bill_dep, flipper_len),
      .fns = \(x) x / 10
    )
  ) |> 
  select(species, bill_len, bill_dep, flipper_len)
## # A tibble: 344 × 4
##    species bill_len bill_dep flipper_len
##    <fct>      <dbl>    <dbl>       <dbl>
##  1 Adelie      3.91     1.87        18.1
##  2 Adelie      3.95     1.74        18.6
##  3 Adelie      4.03     1.8         19.5
##  4 Adelie     NA       NA           NA  
##  5 Adelie      3.67     1.93        19.3
##  6 Adelie      3.93     2.06        19  
##  7 Adelie      3.89     1.78        18.1
##  8 Adelie      3.92     1.96        19.5
##  9 Adelie      3.41     1.81        19.3
## 10 Adelie      4.2      2.02        19  
## # ℹ 334 more rows

3) Iterating over Lists of Complex Objects

While across() is the best tool for iterating over columns inside a single dataset, map() shines when working with lists of complex objects like multiple data frames. Let’s practice by doing a quick sanity check on the size of several built-in datasets at once.

  • Create a list named my_datasets containing the mtcars, iris, and ToothGrowth datasets (all from {datasets}).
  • Use the set_names() function to name the elements of your list “mtcars”, “iris”, and “ToothGrowth” so your final output is easily readable.
  • Use map_int() to iterate over your list and return an integer vector.
  • Set the .f argument to the nrow function to extract the number of rows for each dataset.

Answer key

library(tidyverse)
data("mtcars", "iris", "ToothGrowth", package = "datasets")

my_datasets <- 
  list(mtcars, iris, ToothGrowth) |> 
  set_names(c("mtcars", "iris", "ToothGrowth"))

map_int(.x = my_datasets, .f = nrow)
##      mtcars        iris ToothGrowth 
##          32         150          60