Foundations of
Data Science

Spring 2026 | Data 2 (399)
Jeffrey M. Girard | Lecture 14a

Roadmap

  • 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()

Comparisons &
Boolean Logic

Logical Vectors

  • Before we can build conditional statements, we need to understand logicals
  • Logicals have three possible values:
    • TRUE
    • FALSE
    • NA (missing)
  • We rarely type these by hand. Instead, we generate them using comparisons.

Relational Operators

Numerical only comparisons:

  • x > 1 (is x greater than 1?)
  • x >= 1 (is x greater than or equal to 1?)
  • x < 1 (is x less than 1?)
  • x <= 1 (is x less than or equal to 1?)

Numerical or character comparisons:

  • x == 1 (is x equal to 1?)
  • x != 1 (is x not equal to 1?)
  • x %in% c(1, 2, 3) (is x equal to 1, 2, or 3?)

Comparisons in Action

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

# Create a logical vector asking: is highway mpg greater than 30?
mpg |> 
  mutate(good_mileage = hwy > 30) |> 
  select(manufacturer, model, year, trans, hwy, good_mileage)
# A tibble: 234 × 6
  manufacturer model  year trans        hwy good_mileage
  <chr>        <chr> <int> <chr>      <int> <lgl>       
1 audi         a4     1999 auto(l5)      29 FALSE       
2 audi         a4     1999 manual(m5)    29 FALSE       
3 audi         a4     2008 manual(m6)    31 TRUE        
4 audi         a4     2008 auto(av)      30 FALSE       
5 audi         a4     1999 auto(l5)      26 FALSE       
6 audi         a4     1999 manual(m5)    26 FALSE       
# ℹ 228 more rows

Boolean Algebra

  • We can test multiple conditions at once
  • Boolean algebra allows us to combine multiple logical vectors
    • & (AND):
      TRUE only if both conditions are TRUE
    • | (OR):
      TRUE if at least one condition is TRUE
    • ! (NOT):
      Inverts the logical (TRUE \(\leftrightarrow\) FALSE)

Boolean Algebra in Action

# Find cars that have great highway mileage AND are compact
mpg |> 
  filter(hwy > 30 & class == "compact") |> 
  select(manufacturer, model, year, trans, hwy, class)
# A tibble: 8 × 6
  manufacturer model         year trans        hwy class  
  <chr>        <chr>        <int> <chr>      <int> <chr>  
1 audi         a4            2008 manual(m6)    31 compact
2 toyota       camry solara  2008 manual(m5)    31 compact
3 toyota       camry solara  2008 auto(s5)      31 compact
4 toyota       corolla       1999 auto(l4)      33 compact
5 toyota       corolla       1999 manual(m5)    35 compact
6 toyota       corolla       2008 manual(m5)    37 compact
# ℹ 2 more rows

Logical Summaries

Summarizing Logicals

  • Logical vectors are essentially numbers under the hood
    • TRUE becomes 1 and FALSE becomes 0
  • This allows us to easily use numeric summary functions on them:
    • sum() tells us the count of TRUE values
    • mean() tells us the proportion of TRUE values
mpg |> 
  summarize(
    n_efficient = sum(hwy > 30),
    p_efficient = mean(hwy > 30)
  )
# A tibble: 1 × 2
  n_efficient p_efficient
        <int>       <dbl>
1          22      0.0940

any() and all()

  • Does a condition exist at all? Does it apply universally?
  • any() returns TRUE if at least one value is TRUE
  • all() returns TRUE if every value is TRUE
mpg |> 
  summarize(
    .by = drv,
    any_efficient = any(hwy > 30),
    all_efficient = all(hwy > 30)
  )
# A tibble: 3 × 3
  drv   any_efficient all_efficient
  <chr> <lgl>         <lgl>        
1 f     TRUE          FALSE        
2 4     FALSE         FALSE        
3 r     FALSE         FALSE        

If-Else

What is Control Flow?

  • Control Flow allows for conditional outcomes, i.e., the output changes depending on the input
  • Think of it like a fork in the road. You ask a question (a condition), and depending on the answer, you take a different path.
  • The tidyverse provides if_else() for two-way forks and case_when() for many

The if_else() Function

The if_else() function takes three main arguments:

  1. condition: A logical test (e.g., x > 10)
  2. true: The output if the condition is TRUE
  3. false: The output if the condition is FALSE
x <- 6
if_else(
  condition = x > 10,
  true = "large",
  false = "small"
)
[1] "small"

Vectorized if_else()

# if_else() is vectorized, meaning it works on an entire series of data at once
x <- c(6, 16, 3, 20)

if_else(x > 10, "large", "small")
[1] "small" "large" "small" "large"

Creating our Site Data

# Let's create a dataset of site temperatures
sitetemps <- 
  tibble(
    site = c(2, 1, 2, 3, 1),
    temp = c(-2.1, 38.6, 4.6, -0.2, 37.6)
  ) |> 
  print()
# A tibble: 5 × 2
   site  temp
  <dbl> <dbl>
1     2  -2.1
2     1  38.6
3     2   4.6
4     3  -0.2
5     1  37.6

Using in a pipeline

# Let's assign a temperature metric based on the site number
sitetemps |> 
  mutate(
    metric = if_else(
      condition = site == 1,
      true = "F",
      false = "C"
    )
  )
# A tibble: 5 × 3
   site  temp metric
  <dbl> <dbl> <chr> 
1     2  -2.1 C     
2     1  38.6 F     
3     2   4.6 C     
4     3  -0.2 C     
5     1  37.6 F     

Case-When

Handling Complex Logic

  • if_else() is great for binary choices
  • But what if we have three, four, or ten different possibilities?
  • Nesting if_else() statements inside each other gets messy quickly.
  • case_when() is designed specifically for complex, multi-condition logic.

The case_when() Function

  • case_when() evaluates a sequence of formulas
  • The left side of the ~ is the condition
  • The right side of the ~ is the output
  • It evaluates from top to bottom.
  • The first TRUE condition wins!
x <- c(6, 16, 3)
case_when(
  x > 10 ~ "large",
  x > 5  ~ "medium",
  x <= 5 ~ "small"
)
[1] "medium" "large"  "small" 

The .default Argument

# We can use `.default` to catch anything that doesn't match our specific rules
x <- c(6, 16, 3)

case_when(
  x > 10 ~ "large",
  x > 5  ~ "medium",
  .default = "small"
)
[1] "medium" "large"  "small" 

Pitfall: Ordering Matters!

x <- c(15)

# Watch what happens if we put the broader condition first:
case_when(
  x > 5  ~ "medium", 
  x > 10 ~ "large",   # 15 is > 10, but it already triggered "medium"!
  .default = "small"
)
[1] "medium"

Using in a pipeline

# Assigning nations based on multiple site codes
sitetemps |> 
  mutate(
    nation = case_when(
      site == 1 ~ "US",
      site == 2 ~ "DE",
      site == 3 ~ "FR",
      .default = "Unknown"
    )
  )
# A tibble: 5 × 3
   site  temp nation
  <dbl> <dbl> <chr> 
1     2  -2.1 DE    
2     1  38.6 US    
3     2   4.6 DE    
4     3  -0.2 FR    
5     1  37.6 US    

Complex Transformations

sitetemps |> 
  mutate(
    metric = if_else(site == 1, "F", "C"),
    temp_c = if_else(metric == "F", (temp - 32) * (5 / 9), temp),
    state = case_when(
      temp_c < 0 ~ "solid",
      temp_c < 100 ~ "liquid",
      temp_c >= 100 ~ "gas"
    )
  )
# A tibble: 5 × 5
   site  temp metric temp_c state 
  <dbl> <dbl> <chr>   <dbl> <chr> 
1     2  -2.1 C       -2.1  solid 
2     1  38.6 F        3.67 liquid
3     2   4.6 C        4.6  liquid
4     3  -0.2 C       -0.2  solid 
5     1  37.6 F        3.11 liquid

Summary

  • Comparisons: Evaluate relationships using >, <, ==, and !=
  • Boolean Algebra: Combine conditions using &, |, and !
  • Summaries: Use sum() to count TRUEs, mean() for proportions, and any()/all() for group-level checks
  • Control Flow: Handle two-way branches seamlessly with if_else()
  • Complex Cases: Manage multiple condition mapping safely with case_when()