4  Data Manipulation with dplyr

4.1 Learning Objectives

By the end of this chapter, you will be able to:

  1. Create and modify variables using dplyr::mutate()
  2. Sort data using dplyr::arrange() in ascending and descending order
  3. Calculate summary statistics using dplyr::summarise()
  4. Perform grouped operations with group_by() and summarise()
  5. Use helper functions: count(), rename(), and relocate()
  6. Apply conditional logic with if_else() and case_when()
  7. Work across multiple columns efficiently using across()
  8. Handle missing data with is.na(), drop_na(), replace_na(), and coalesce()
  9. Use window functions: lag(), lead(), cumsum(), and row_number()
  10. Chain multiple dplyr operations into complex data transformation pipelines

4.2 Introduction to dplyr Verbs

In the previous chapter, you learned select() and filter() for choosing columns and rows. This chapter covers the remaining core dplyr verbs that complete your data manipulation toolkit.

4.2.1 The Five Main dplyr Verbs

flowchart TD
    A[dplyr Verbs] --> B[select<br/>Pick columns]
    A --> C[filter<br/>Pick rows]
    A --> D[mutate<br/>Create/modify columns]
    A --> E[arrange<br/>Sort rows]
    A --> F[summarise<br/>Calculate summaries]

    B --> B1["select(animal_id, weight)"]
    C --> C1["filter(weight > 450)"]
    D --> D1["mutate(bmi = weight/height)"]
    E --> E1["arrange(weight)"]
    F --> F1["summarise(mean_weight = mean(weight))"]

This chapter focuses on: mutate(), arrange(), summarise(), and group_by()

NoteReview: Data Used in This Chapter

We’ll use animal science datasets throughout. Make sure you have these packages loaded:

library(tidyverse)  # Includes dplyr, ggplot2, tidyr, etc.
library(lubridate)  # For dates

4.3 Creating and Modifying Variables with mutate()

mutate() creates new columns or modifies existing columns in your data frame.

4.3.1 Basic Usage

# Create example cattle data
cattle <- tibble(
  animal_id = c("H001", "H002", "H003", "J001", "J002"),
  breed = c("Holstein", "Holstein", "Holstein", "Jersey", "Jersey"),
  weight_kg = c(600, 625, 580, 450, 470),
  height_cm = c(145, 148, 142, 130, 133),
  age_months = c(30, 36, 28, 30, 32)
)

cattle
# A tibble: 5 × 5
  animal_id breed    weight_kg height_cm age_months
  <chr>     <chr>        <dbl>     <dbl>      <dbl>
1 H001      Holstein       600       145         30
2 H002      Holstein       625       148         36
3 H003      Holstein       580       142         28
4 J001      Jersey         450       130         30
5 J002      Jersey         470       133         32
# Create new column: weight in pounds
cattle %>%
  mutate(weight_lb = weight_kg * 2.20462)
# A tibble: 5 × 6
  animal_id breed    weight_kg height_cm age_months weight_lb
  <chr>     <chr>        <dbl>     <dbl>      <dbl>     <dbl>
1 H001      Holstein       600       145         30     1323.
2 H002      Holstein       625       148         36     1378.
3 H003      Holstein       580       142         28     1279.
4 J001      Jersey         450       130         30      992.
5 J002      Jersey         470       133         32     1036.
# Create multiple new columns
cattle %>%
  mutate(
    weight_lb = weight_kg * 2.20462,
    height_m = height_cm / 100,
    bmi = weight_kg / (height_m^2)
  )
# A tibble: 5 × 8
  animal_id breed    weight_kg height_cm age_months weight_lb height_m   bmi
  <chr>     <chr>        <dbl>     <dbl>      <dbl>     <dbl>    <dbl> <dbl>
1 H001      Holstein       600       145         30     1323.     1.45  285.
2 H002      Holstein       625       148         36     1378.     1.48  285.
3 H003      Holstein       580       142         28     1279.     1.42  288.
4 J001      Jersey         450       130         30      992.     1.3   266.
5 J002      Jersey         470       133         32     1036.     1.33  266.
Importantmutate() Adds Columns, Doesn’t Replace Data

mutate() returns a new data frame. To keep changes, assign the result:

# ❌ Changes are lost
cattle %>% mutate(weight_lb = weight_kg * 2.20462)

# ✅ Save the result
cattle <- cattle %>% mutate(weight_lb = weight_kg * 2.20462)
# OR
cattle_with_lb <- cattle %>% mutate(weight_lb = weight_kg * 2.20462)

4.3.2 Overwriting Existing Columns

# Modify existing column: convert breed to uppercase
cattle %>%
  mutate(breed = str_to_upper(breed))
# A tibble: 5 × 5
  animal_id breed    weight_kg height_cm age_months
  <chr>     <chr>        <dbl>     <dbl>      <dbl>
1 H001      HOLSTEIN       600       145         30
2 H002      HOLSTEIN       625       148         36
3 H003      HOLSTEIN       580       142         28
4 J001      JERSEY         450       130         30
5 J002      JERSEY         470       133         32
# Round weight to nearest 10 kg
cattle %>%
  mutate(weight_kg = round(weight_kg / 10) * 10)
# A tibble: 5 × 5
  animal_id breed    weight_kg height_cm age_months
  <chr>     <chr>        <dbl>     <dbl>      <dbl>
1 H001      Holstein       600       145         30
2 H002      Holstein       620       148         36
3 H003      Holstein       580       142         28
4 J001      Jersey         450       130         30
5 J002      Jersey         470       133         32

4.3.3 Using New Columns Immediately

You can reference newly created columns within the same mutate():

cattle %>%
  mutate(
    height_m = height_cm / 100,       # Create height_m
    bmi = weight_kg / (height_m^2)    # Use height_m immediately!
  )
# A tibble: 5 × 7
  animal_id breed    weight_kg height_cm age_months height_m   bmi
  <chr>     <chr>        <dbl>     <dbl>      <dbl>    <dbl> <dbl>
1 H001      Holstein       600       145         30     1.45  285.
2 H002      Holstein       625       148         36     1.48  285.
3 H003      Holstein       580       142         28     1.42  288.
4 J001      Jersey         450       130         30     1.3   266.
5 J002      Jersey         470       133         32     1.33  266.

4.3.4 Useful Functions Inside mutate()

Common operations you’ll use with mutate():

Operation Function Example
Math +, -, *, /, ^ weight_kg * 2.20462
Rounding round(), floor(), ceiling() round(weight, 1)
Logarithms log(), log10(), exp() log(concentration)
Ranking min_rank(), dense_rank() min_rank(desc(weight))
String functions str_*() str_to_upper(breed)
Date functions year(), month(), day() year(birth_date)
Conditional if_else(), case_when() if_else(weight > 500, "Heavy", "Light")

4.4 Conditional Operations

4.4.1 Simple Conditions with if_else()

if_else() creates values based on a TRUE/FALSE condition:

Syntax: if_else(condition, value_if_true, value_if_false)

# Classify animals as heavy or light
cattle %>%
  mutate(
    weight_class = if_else(weight_kg > 500, "Heavy", "Light")
  )
# A tibble: 5 × 6
  animal_id breed    weight_kg height_cm age_months weight_class
  <chr>     <chr>        <dbl>     <dbl>      <dbl> <chr>       
1 H001      Holstein       600       145         30 Heavy       
2 H002      Holstein       625       148         36 Heavy       
3 H003      Holstein       580       142         28 Heavy       
4 J001      Jersey         450       130         30 Light       
5 J002      Jersey         470       133         32 Light       
# Create logical column
cattle %>%
  mutate(
    is_mature = if_else(age_months >= 30, TRUE, FALSE)
  )
# A tibble: 5 × 6
  animal_id breed    weight_kg height_cm age_months is_mature
  <chr>     <chr>        <dbl>     <dbl>      <dbl> <lgl>    
1 H001      Holstein       600       145         30 TRUE     
2 H002      Holstein       625       148         36 TRUE     
3 H003      Holstein       580       142         28 FALSE    
4 J001      Jersey         450       130         30 TRUE     
5 J002      Jersey         470       133         32 TRUE     
# Can use existing values
cattle %>%
  mutate(
    adjusted_weight = if_else(breed == "Jersey",
                              weight_kg * 1.1,  # Boost Jersey weights by 10%
                              weight_kg)        # Keep others same
  )
# A tibble: 5 × 6
  animal_id breed    weight_kg height_cm age_months adjusted_weight
  <chr>     <chr>        <dbl>     <dbl>      <dbl>           <dbl>
1 H001      Holstein       600       145         30             600
2 H002      Holstein       625       148         36             625
3 H003      Holstein       580       142         28             580
4 J001      Jersey         450       130         30             495
5 J002      Jersey         470       133         32             517
Tipif_else() vs base R ifelse()

dplyr’s if_else() is stricter and safer than base R’s ifelse():

  • Type safety: Both true and false values must be the same type
  • NA handling: Explicit missing argument for NA values
  • Speed: Faster for large datasets
# ✅ Good (both character)
if_else(condition, "yes", "no")

# ❌ Error (different types)
if_else(condition, "yes", 0)

# ✅ With NAs
if_else(condition, "yes", "no", missing = "unknown")

4.4.2 Multiple Conditions with case_when()

For multiple conditions, use case_when():

# Classify into three weight categories
cattle %>%
  mutate(
    weight_category = case_when(
      weight_kg < 500 ~ "Light",
      weight_kg < 600 ~ "Medium",
      weight_kg >= 600 ~ "Heavy"
    )
  )
# A tibble: 5 × 6
  animal_id breed    weight_kg height_cm age_months weight_category
  <chr>     <chr>        <dbl>     <dbl>      <dbl> <chr>          
1 H001      Holstein       600       145         30 Heavy          
2 H002      Holstein       625       148         36 Heavy          
3 H003      Holstein       580       142         28 Medium         
4 J001      Jersey         450       130         30 Light          
5 J002      Jersey         470       133         32 Light          
# Multiple factors
cattle %>%
  mutate(
    category = case_when(
      breed == "Jersey" & weight_kg > 450 ~ "Large Jersey",
      breed == "Jersey" & weight_kg <= 450 ~ "Small Jersey",
      breed == "Holstein" & weight_kg > 600 ~ "Large Holstein",
      breed == "Holstein" & weight_kg <= 600 ~ "Small Holstein",
      TRUE ~ "Other"  # Catch-all (like "else")
    )
  )
# A tibble: 5 × 6
  animal_id breed    weight_kg height_cm age_months category      
  <chr>     <chr>        <dbl>     <dbl>      <dbl> <chr>         
1 H001      Holstein       600       145         30 Small Holstein
2 H002      Holstein       625       148         36 Large Holstein
3 H003      Holstein       580       142         28 Small Holstein
4 J001      Jersey         450       130         30 Small Jersey  
5 J002      Jersey         470       133         32 Large Jersey  

How case_when() works: 1. Evaluates conditions in order from top to bottom 2. Returns the value (~ right side) for the first TRUE condition 3. Stops checking once a match is found 4. Use TRUE ~ value as a catch-all for everything else

WarningOrder Matters in case_when()!
# ❌ WRONG: Everything becomes "Heavy"
cattle %>%
  mutate(
    wrong = case_when(
      weight_kg > 0 ~ "Heavy",      # This matches EVERYTHING!
      weight_kg > 500 ~ "Medium",   # Never reached
      TRUE ~ "Light"                # Never reached
    )
  ) %>%
  select(animal_id, weight_kg, wrong)
# A tibble: 5 × 3
  animal_id weight_kg wrong
  <chr>         <dbl> <chr>
1 H001            600 Heavy
2 H002            625 Heavy
3 H003            580 Heavy
4 J001            450 Heavy
5 J002            470 Heavy
# ✅ CORRECT: Specific conditions first
cattle %>%
  mutate(
    correct = case_when(
      weight_kg > 600 ~ "Heavy",
      weight_kg > 500 ~ "Medium",
      TRUE ~ "Light"
    )
  ) %>%
  select(animal_id, weight_kg, correct)
# A tibble: 5 × 3
  animal_id weight_kg correct
  <chr>         <dbl> <chr>  
1 H001            600 Medium 
2 H002            625 Heavy  
3 H003            580 Medium 
4 J001            450 Light  
5 J002            470 Light  

Rule: Put specific conditions before general ones!

4.4.3 Real-World Example: Creating Treatment Groups

# Create feed trial data
feed_trial <- tibble(
  animal_id = sprintf("A%03d", 1:10),
  baseline_weight = c(450, 480, 445, 490, 455, 470, 460, 475, 465, 485),
  age_months = c(24, 30, 22, 32, 26, 28, 24, 30, 26, 31),
  sex = c("F", "F", "M", "F", "M", "F", "F", "M", "F", "M")
)

feed_trial
# A tibble: 10 × 4
   animal_id baseline_weight age_months sex  
   <chr>               <dbl>      <dbl> <chr>
 1 A001                  450         24 F    
 2 A002                  480         30 F    
 3 A003                  445         22 M    
 4 A004                  490         32 F    
 5 A005                  455         26 M    
 6 A006                  470         28 F    
 7 A007                  460         24 F    
 8 A008                  475         30 M    
 9 A009                  465         26 F    
10 A010                  485         31 M    
# Assign animals to treatment groups based on multiple factors
feed_trial_assigned <- feed_trial %>%
  mutate(
    # Age category
    age_group = case_when(
      age_months < 26 ~ "Young",
      age_months <= 30 ~ "Adult",
      TRUE ~ "Mature"
    ),
    # Weight category
    weight_group = if_else(baseline_weight > 470, "Heavy", "Light"),
    # Treatment assignment (balanced by sex and weight)
    treatment = case_when(
      sex == "F" & baseline_weight > 470 ~ "A",
      sex == "F" & baseline_weight <= 470 ~ "B",
      sex == "M" & baseline_weight > 470 ~ "B",
      sex == "M" & baseline_weight <= 470 ~ "A"
    )
  )

feed_trial_assigned
# A tibble: 10 × 7
   animal_id baseline_weight age_months sex   age_group weight_group treatment
   <chr>               <dbl>      <dbl> <chr> <chr>     <chr>        <chr>    
 1 A001                  450         24 F     Young     Light        B        
 2 A002                  480         30 F     Adult     Heavy        A        
 3 A003                  445         22 M     Young     Light        A        
 4 A004                  490         32 F     Mature    Heavy        A        
 5 A005                  455         26 M     Adult     Light        A        
 6 A006                  470         28 F     Adult     Light        B        
 7 A007                  460         24 F     Young     Light        B        
 8 A008                  475         30 M     Adult     Heavy        B        
 9 A009                  465         26 F     Adult     Light        B        
10 A010                  485         31 M     Mature    Heavy        B        

4.5 Sorting Data with arrange()

arrange() sorts rows by one or more columns.

4.5.1 Basic Sorting

# Sort by weight (ascending, lightest first)
cattle %>%
  arrange(weight_kg)
# A tibble: 5 × 5
  animal_id breed    weight_kg height_cm age_months
  <chr>     <chr>        <dbl>     <dbl>      <dbl>
1 J001      Jersey         450       130         30
2 J002      Jersey         470       133         32
3 H003      Holstein       580       142         28
4 H001      Holstein       600       145         30
5 H002      Holstein       625       148         36
# Sort by weight (descending, heaviest first)
cattle %>%
  arrange(desc(weight_kg))
# A tibble: 5 × 5
  animal_id breed    weight_kg height_cm age_months
  <chr>     <chr>        <dbl>     <dbl>      <dbl>
1 H002      Holstein       625       148         36
2 H001      Holstein       600       145         30
3 H003      Holstein       580       142         28
4 J002      Jersey         470       133         32
5 J001      Jersey         450       130         30
# Sort by breed, then by weight within breed
cattle %>%
  arrange(breed, weight_kg)
# A tibble: 5 × 5
  animal_id breed    weight_kg height_cm age_months
  <chr>     <chr>        <dbl>     <dbl>      <dbl>
1 H003      Holstein       580       142         28
2 H001      Holstein       600       145         30
3 H002      Holstein       625       148         36
4 J001      Jersey         450       130         30
5 J002      Jersey         470       133         32
# Sort by breed (ascending), weight (descending)
cattle %>%
  arrange(breed, desc(weight_kg))
# A tibble: 5 × 5
  animal_id breed    weight_kg height_cm age_months
  <chr>     <chr>        <dbl>     <dbl>      <dbl>
1 H002      Holstein       625       148         36
2 H001      Holstein       600       145         30
3 H003      Holstein       580       142         28
4 J002      Jersey         470       133         32
5 J001      Jersey         450       130         30

4.5.2 Sorting with Missing Values

# Data with missing weights
cattle_na <- tibble(
  animal_id = c("H001", "H002", "H003", "H004", "H005"),
  weight_kg = c(600, NA, 580, 625, NA)
)

cattle_na
# A tibble: 5 × 2
  animal_id weight_kg
  <chr>         <dbl>
1 H001            600
2 H002             NA
3 H003            580
4 H004            625
5 H005             NA
# By default, NAs go to the end
cattle_na %>%
  arrange(weight_kg)
# A tibble: 5 × 2
  animal_id weight_kg
  <chr>         <dbl>
1 H003            580
2 H001            600
3 H004            625
4 H002             NA
5 H005             NA
# Descending: NAs still at the end
cattle_na %>%
  arrange(desc(weight_kg))
# A tibble: 5 × 2
  animal_id weight_kg
  <chr>         <dbl>
1 H004            625
2 H001            600
3 H003            580
4 H002             NA
5 H005             NA
TipUse arrange() to Check Your Work

When creating or modifying columns, sort to verify results:

# Did the weight classification work correctly?
cattle %>%
  mutate(weight_class = if_else(weight_kg > 500, "Heavy", "Light")) %>%
  arrange(weight_kg) %>%
  select(animal_id, weight_kg, weight_class)
# A tibble: 5 × 3
  animal_id weight_kg weight_class
  <chr>         <dbl> <chr>       
1 J001            450 Light       
2 J002            470 Light       
3 H003            580 Heavy       
4 H001            600 Heavy       
5 H002            625 Heavy       

Sorting makes it easy to spot errors!


4.6 Grouped Operations with group_by() and summarise()

The real power of dplyr comes from grouped operations: calculating summaries for each group separately.

4.6.1 Understanding group_by()

group_by() doesn’t change your data—it adds invisible grouping information:

# Create farm data with multiple breeds and farms
farm_data <- tibble(
  animal_id = sprintf("A%03d", 1:12),
  breed = rep(c("Holstein", "Jersey", "Angus"), each = 4),
  farm = rep(c("North", "South"), 6),
  weight_kg = c(600, 620, 590, 610, 450, 470, 455, 465,
                550, 570, 540, 560)
)

farm_data
# A tibble: 12 × 4
   animal_id breed    farm  weight_kg
   <chr>     <chr>    <chr>     <dbl>
 1 A001      Holstein North       600
 2 A002      Holstein South       620
 3 A003      Holstein North       590
 4 A004      Holstein South       610
 5 A005      Jersey   North       450
 6 A006      Jersey   South       470
 7 A007      Jersey   North       455
 8 A008      Jersey   South       465
 9 A009      Angus    North       550
10 A010      Angus    South       570
11 A011      Angus    North       540
12 A012      Angus    South       560
# Group by breed
farm_data %>%
  group_by(breed)
# A tibble: 12 × 4
# Groups:   breed [3]
   animal_id breed    farm  weight_kg
   <chr>     <chr>    <chr>     <dbl>
 1 A001      Holstein North       600
 2 A002      Holstein South       620
 3 A003      Holstein North       590
 4 A004      Holstein South       610
 5 A005      Jersey   North       450
 6 A006      Jersey   South       470
 7 A007      Jersey   North       455
 8 A008      Jersey   South       465
 9 A009      Angus    North       550
10 A010      Angus    South       570
11 A011      Angus    North       540
12 A012      Angus    South       560

Notice: “Groups: breed [3]” in the output. The data looks the same, but it’s now grouped!

4.6.2 Summarizing Data with summarise()

summarise() (or summarize()) calculates summary statistics:

# Overall mean weight (no grouping)
farm_data %>%
  summarise(
    mean_weight = mean(weight_kg),
    sd_weight = sd(weight_kg),
    n = n()  # n() counts rows
  )
# A tibble: 1 × 3
  mean_weight sd_weight     n
        <dbl>     <dbl> <int>
1         540      63.7    12
# Mean weight BY BREED
farm_data %>%
  group_by(breed) %>%
  summarise(
    mean_weight = mean(weight_kg),
    sd_weight = sd(weight_kg),
    n = n()
  )
# A tibble: 3 × 4
  breed    mean_weight sd_weight     n
  <chr>          <dbl>     <dbl> <int>
1 Angus            555     12.9      4
2 Holstein         605     12.9      4
3 Jersey           460      9.13     4
ImportantThe group_by() + summarise() Pattern

This is one of the most powerful patterns in data analysis:

data %>%
  group_by(category_column) %>%
  summarise(
    summary_name = summary_function(numeric_column)
  )

Read as: “For each category, calculate the summary”

4.6.3 Common Summary Functions

Function What it does Example
mean() Average mean(weight)
median() Median (50th percentile) median(weight)
sd() Standard deviation sd(weight)
var() Variance var(weight)
min() Minimum value min(weight)
max() Maximum value max(weight)
sum() Sum of all values sum(milk_yield)
n() Count of rows n()
n_distinct() Count unique values n_distinct(animal_id)
first() First value first(weight)
last() Last value last(weight)

4.6.4 Grouping by Multiple Variables

# Mean weight by breed AND farm
farm_data %>%
  group_by(breed, farm) %>%
  summarise(
    mean_weight = mean(weight_kg),
    n = n(),
    .groups = "drop"  # Remove grouping after summarise
  )
# A tibble: 6 × 4
  breed    farm  mean_weight     n
  <chr>    <chr>       <dbl> <int>
1 Angus    North        545      2
2 Angus    South        565      2
3 Holstein North        595      2
4 Holstein South        615      2
5 Jersey   North        452.     2
6 Jersey   South        468.     2
NoteThe .groups Argument

After summarise(), data remains grouped by all but the last grouping variable. This can cause unexpected behavior!

# Recommended: explicitly drop groups
data %>%
  group_by(var1, var2) %>%
  summarise(mean_x = mean(x), .groups = "drop")

Options: - .groups = "drop": Remove all grouping (recommended) - .groups = "keep": Keep all grouping - .groups = "drop_last": Drop last grouping variable (default)

4.6.5 Multiple Summaries at Once

# Comprehensive summary by breed
farm_data %>%
  group_by(breed) %>%
  summarise(
    n = n(),
    mean_weight = mean(weight_kg),
    sd_weight = sd(weight_kg),
    min_weight = min(weight_kg),
    max_weight = max(weight_kg),
    median_weight = median(weight_kg),
    .groups = "drop"
  )
# A tibble: 3 × 7
  breed        n mean_weight sd_weight min_weight max_weight median_weight
  <chr>    <int>       <dbl>     <dbl>      <dbl>      <dbl>         <dbl>
1 Angus        4         555     12.9         540        570           555
2 Holstein     4         605     12.9         590        620           605
3 Jersey       4         460      9.13        450        470           460

4.6.6 Filtering on Grouped Data

filter() works with groups too:

# Keep only animals heavier than the breed average
farm_data %>%
  group_by(breed) %>%
  filter(weight_kg > mean(weight_kg)) %>%
  ungroup()  # Remove grouping when done
# A tibble: 6 × 4
  animal_id breed    farm  weight_kg
  <chr>     <chr>    <chr>     <dbl>
1 A002      Holstein South       620
2 A004      Holstein South       610
3 A006      Jersey   South       470
4 A008      Jersey   South       465
5 A010      Angus    South       570
6 A012      Angus    South       560

4.6.7 Mutating on Grouped Data

mutate() calculates within groups:

# Calculate deviation from breed mean
farm_data %>%
  group_by(breed) %>%
  mutate(
    breed_mean = mean(weight_kg),
    deviation = weight_kg - breed_mean
  ) %>%
  ungroup() %>%
  arrange(breed, animal_id)
# A tibble: 12 × 6
   animal_id breed    farm  weight_kg breed_mean deviation
   <chr>     <chr>    <chr>     <dbl>      <dbl>     <dbl>
 1 A009      Angus    North       550        555        -5
 2 A010      Angus    South       570        555        15
 3 A011      Angus    North       540        555       -15
 4 A012      Angus    South       560        555         5
 5 A001      Holstein North       600        605        -5
 6 A002      Holstein South       620        605        15
 7 A003      Holstein North       590        605       -15
 8 A004      Holstein South       610        605         5
 9 A005      Jersey   North       450        460       -10
10 A006      Jersey   South       470        460        10
11 A007      Jersey   North       455        460        -5
12 A008      Jersey   South       465        460         5

4.7 Helper Functions

4.7.1 Counting with count()

count() is a shortcut for group_by() + summarise() + n():

# How many animals per breed?
# Long way
farm_data %>%
  group_by(breed) %>%
  summarise(n = n(), .groups = "drop")
# A tibble: 3 × 2
  breed        n
  <chr>    <int>
1 Angus        4
2 Holstein     4
3 Jersey       4
# Short way with count()
farm_data %>%
  count(breed)
# A tibble: 3 × 2
  breed        n
  <chr>    <int>
1 Angus        4
2 Holstein     4
3 Jersey       4
# Count by multiple variables
farm_data %>%
  count(breed, farm)
# A tibble: 6 × 3
  breed    farm      n
  <chr>    <chr> <int>
1 Angus    North     2
2 Angus    South     2
3 Holstein North     2
4 Holstein South     2
5 Jersey   North     2
6 Jersey   South     2
# Sort by count (most common first)
farm_data %>%
  count(breed, sort = TRUE)
# A tibble: 3 × 2
  breed        n
  <chr>    <int>
1 Angus        4
2 Holstein     4
3 Jersey       4
Tipcount() Pro Tips
# Rename the count column
farm_data %>%
  count(breed, name = "number_of_animals")
# A tibble: 3 × 2
  breed    number_of_animals
  <chr>                <int>
1 Angus                    4
2 Holstein                 4
3 Jersey                   4
# Add a total row (using add_tally)
farm_data %>%
  count(breed) %>%
  mutate(proportion = n / sum(n))
# A tibble: 3 × 3
  breed        n proportion
  <chr>    <int>      <dbl>
1 Angus        4      0.333
2 Holstein     4      0.333
3 Jersey       4      0.333

4.7.2 Renaming Columns with rename()

rename() changes column names:

Syntax: rename(new_name = old_name)

# Rename columns
cattle %>%
  rename(
    id = animal_id,
    weight = weight_kg,
    height = height_cm
  )
# A tibble: 5 × 5
  id    breed    weight height age_months
  <chr> <chr>     <dbl>  <dbl>      <dbl>
1 H001  Holstein    600    145         30
2 H002  Holstein    625    148         36
3 H003  Holstein    580    142         28
4 J001  Jersey      450    130         30
5 J002  Jersey      470    133         32
# Rename with a function
cattle %>%
  rename_with(str_to_upper)  # All columns to uppercase
# A tibble: 5 × 5
  ANIMAL_ID BREED    WEIGHT_KG HEIGHT_CM AGE_MONTHS
  <chr>     <chr>        <dbl>     <dbl>      <dbl>
1 H001      Holstein       600       145         30
2 H002      Holstein       625       148         36
3 H003      Holstein       580       142         28
4 J001      Jersey         450       130         30
5 J002      Jersey         470       133         32
# Rename specific columns with a function
cattle %>%
  rename_with(str_to_upper, starts_with("weight"))
# A tibble: 5 × 5
  animal_id breed    WEIGHT_KG height_cm age_months
  <chr>     <chr>        <dbl>     <dbl>      <dbl>
1 H001      Holstein       600       145         30
2 H002      Holstein       625       148         36
3 H003      Holstein       580       142         28
4 J001      Jersey         450       130         30
5 J002      Jersey         470       133         32

4.7.3 Reordering Columns with relocate()

relocate() moves columns to different positions:

# Move breed to the front
cattle %>%
  relocate(breed)
# A tibble: 5 × 5
  breed    animal_id weight_kg height_cm age_months
  <chr>    <chr>         <dbl>     <dbl>      <dbl>
1 Holstein H001            600       145         30
2 Holstein H002            625       148         36
3 Holstein H003            580       142         28
4 Jersey   J001            450       130         30
5 Jersey   J002            470       133         32
# Move weight_kg to the end
cattle %>%
  relocate(weight_kg, .after = last_col())
# A tibble: 5 × 5
  animal_id breed    height_cm age_months weight_kg
  <chr>     <chr>        <dbl>      <dbl>     <dbl>
1 H001      Holstein       145         30       600
2 H002      Holstein       148         36       625
3 H003      Holstein       142         28       580
4 J001      Jersey         130         30       450
5 J002      Jersey         133         32       470
# Move height_cm before breed
cattle %>%
  relocate(height_cm, .before = breed)
# A tibble: 5 × 5
  animal_id height_cm breed    weight_kg age_months
  <chr>         <dbl> <chr>        <dbl>      <dbl>
1 H001            145 Holstein       600         30
2 H002            148 Holstein       625         36
3 H003            142 Holstein       580         28
4 J001            130 Jersey         450         30
5 J002            133 Jersey         470         32
# Move all numeric columns to the front
cattle %>%
  relocate(where(is.numeric))
# A tibble: 5 × 5
  weight_kg height_cm age_months animal_id breed   
      <dbl>     <dbl>      <dbl> <chr>     <chr>   
1       600       145         30 H001      Holstein
2       625       148         36 H002      Holstein
3       580       142         28 H003      Holstein
4       450       130         30 J001      Jersey  
5       470       133         32 J002      Jersey  

4.8 Working Across Multiple Columns with across()

across() applies the same operation to multiple columns at once.

4.8.1 Basic Usage

# Create test data
test_data <- tibble(
  animal_id = c("A001", "A002", "A003"),
  weight_kg = c(600.123, 450.456, 550.789),
  height_cm = c(145.678, 130.234, 140.567),
  age_months = c(30.5, 28.3, 32.1)
)

test_data
# A tibble: 3 × 4
  animal_id weight_kg height_cm age_months
  <chr>         <dbl>     <dbl>      <dbl>
1 A001           600.      146.       30.5
2 A002           450.      130.       28.3
3 A003           551.      141.       32.1
# Round all numeric columns to 1 decimal place
test_data %>%
  mutate(across(where(is.numeric), round, 1))
# A tibble: 3 × 4
  animal_id weight_kg height_cm age_months
  <chr>         <dbl>     <dbl>      <dbl>
1 A001           600.      146.       30.5
2 A002           450.      130.       28.3
3 A003           551.      141.       32.1
# Convert all character columns to uppercase
farm_data %>%
  mutate(across(where(is.character), str_to_upper)) %>%
  head(3)
# A tibble: 3 × 4
  animal_id breed    farm  weight_kg
  <chr>     <chr>    <chr>     <dbl>
1 A001      HOLSTEIN NORTH       600
2 A002      HOLSTEIN SOUTH       620
3 A003      HOLSTEIN NORTH       590

4.8.2 Selecting Columns for across()

# Apply to specific columns
test_data %>%
  mutate(across(c(weight_kg, height_cm), round, 1))
# A tibble: 3 × 4
  animal_id weight_kg height_cm age_months
  <chr>         <dbl>     <dbl>      <dbl>
1 A001           600.      146.       30.5
2 A002           450.      130.       28.3
3 A003           551.      141.       32.1
# Apply to columns matching a pattern
test_data %>%
  mutate(across(ends_with("_kg"), ~ . * 2.20462))  # Convert kg to lb
# A tibble: 3 × 4
  animal_id weight_kg height_cm age_months
  <chr>         <dbl>     <dbl>      <dbl>
1 A001          1323.      146.       30.5
2 A002           993.      130.       28.3
3 A003          1214.      141.       32.1
# Apply to all except some columns
test_data %>%
  mutate(across(-animal_id, round, 0))
# A tibble: 3 × 4
  animal_id weight_kg height_cm age_months
  <chr>         <dbl>     <dbl>      <dbl>
1 A001            600       146         30
2 A002            450       130         28
3 A003            551       141         32
NoteAnonymous Functions with across()

The ~ creates an anonymous function, and . represents each column:

# These are equivalent:
across(cols, ~ round(., 1))
across(cols, round, 1)
across(cols, function(x) round(x, 1))

# Use ~ when you need more complex operations:
across(cols, ~ . * 2 + 1)
across(cols, ~ if_else(. > 100, ., . * 2))

4.8.3 Multiple Summaries with across()

# Calculate multiple summaries for multiple columns
farm_data %>%
  group_by(breed) %>%
  summarise(
    across(
      weight_kg,
      list(
        mean = mean,
        sd = sd,
        min = min,
        max = max
      )
    ),
    n = n(),
    .groups = "drop"
  )
# A tibble: 3 × 6
  breed    weight_kg_mean weight_kg_sd weight_kg_min weight_kg_max     n
  <chr>             <dbl>        <dbl>         <dbl>         <dbl> <int>
1 Angus               555        12.9            540           570     4
2 Holstein            605        12.9            590           620     4
3 Jersey              460         9.13           450           470     4
# Cleaner column names
farm_data %>%
  group_by(breed) %>%
  summarise(
    across(
      weight_kg,
      list(
        mean = mean,
        sd = sd
      ),
      .names = "{.col}_{.fn}"  # Creates: weight_kg_mean, weight_kg_sd
    ),
    .groups = "drop"
  )
# A tibble: 3 × 3
  breed    weight_kg_mean weight_kg_sd
  <chr>             <dbl>        <dbl>
1 Angus               555        12.9 
2 Holstein            605        12.9 
3 Jersey              460         9.13

4.9 Handling Missing Data

Missing data (NA) is common in real datasets. dplyr provides several tools to handle it.

4.9.1 Detecting Missing Values

# Create data with missing values
cattle_missing <- tibble(
  animal_id = c("H001", "H002", "H003", "H004", "H005"),
  breed = c("Holstein", "Jersey", NA, "Angus", "Holstein"),
  weight_kg = c(600, NA, 580, 625, NA),
  age_months = c(30, 28, NA, 32, 26)
)

cattle_missing
# A tibble: 5 × 4
  animal_id breed    weight_kg age_months
  <chr>     <chr>        <dbl>      <dbl>
1 H001      Holstein       600         30
2 H002      Jersey          NA         28
3 H003      <NA>           580         NA
4 H004      Angus          625         32
5 H005      Holstein        NA         26
# Check for missing values
cattle_missing %>%
  mutate(
    breed_is_missing = is.na(breed),
    weight_is_missing = is.na(weight_kg)
  )
# A tibble: 5 × 6
  animal_id breed    weight_kg age_months breed_is_missing weight_is_missing
  <chr>     <chr>        <dbl>      <dbl> <lgl>            <lgl>            
1 H001      Holstein       600         30 FALSE            FALSE            
2 H002      Jersey          NA         28 FALSE            TRUE             
3 H003      <NA>           580         NA TRUE             FALSE            
4 H004      Angus          625         32 FALSE            FALSE            
5 H005      Holstein        NA         26 FALSE            TRUE             
# Count missing values per column
cattle_missing %>%
  summarise(
    across(everything(), ~ sum(is.na(.)))
  )
# A tibble: 1 × 4
  animal_id breed weight_kg age_months
      <int> <int>     <int>      <int>
1         0     1         2          1

4.9.2 Removing Missing Values with drop_na()

# Remove rows with ANY missing values
cattle_missing %>%
  drop_na()
# A tibble: 2 × 4
  animal_id breed    weight_kg age_months
  <chr>     <chr>        <dbl>      <dbl>
1 H001      Holstein       600         30
2 H004      Angus          625         32
# Remove rows with missing values in specific columns
cattle_missing %>%
  drop_na(weight_kg)
# A tibble: 3 × 4
  animal_id breed    weight_kg age_months
  <chr>     <chr>        <dbl>      <dbl>
1 H001      Holstein       600         30
2 H003      <NA>           580         NA
3 H004      Angus          625         32
# Remove rows missing BOTH weight and age
cattle_missing %>%
  drop_na(weight_kg, age_months)
# A tibble: 2 × 4
  animal_id breed    weight_kg age_months
  <chr>     <chr>        <dbl>      <dbl>
1 H001      Holstein       600         30
2 H004      Angus          625         32
WarningBe Careful When Dropping NAs!

Removing missing data can: - Reduce sample size significantly - Introduce bias if missingness isn’t random - Lose valuable information in other columns

Better approach: Understand WHY data is missing, then decide how to handle it.

4.9.3 Replacing Missing Values with replace_na()

# Replace NAs with specific values
cattle_missing %>%
  mutate(
    breed = replace_na(breed, "Unknown"),
    weight_kg = replace_na(weight_kg, 0)
  )
# A tibble: 5 × 4
  animal_id breed    weight_kg age_months
  <chr>     <chr>        <dbl>      <dbl>
1 H001      Holstein       600         30
2 H002      Jersey           0         28
3 H003      Unknown        580         NA
4 H004      Angus          625         32
5 H005      Holstein         0         26
# Replace NAs in multiple columns
cattle_missing %>%
  mutate(
    across(
      where(is.numeric),
      ~ replace_na(., mean(., na.rm = TRUE))  # Replace with column mean
    )
  )
# A tibble: 5 × 4
  animal_id breed    weight_kg age_months
  <chr>     <chr>        <dbl>      <dbl>
1 H001      Holstein      600          30
2 H002      Jersey        602.         28
3 H003      <NA>          580          29
4 H004      Angus         625          32
5 H005      Holstein      602.         26

4.9.4 Filling Missing Values with coalesce()

coalesce() returns the first non-missing value:

# Multiple weight measurements, use first available
weights <- tibble(
  animal_id = c("A001", "A002", "A003", "A004"),
  weight_measurement1 = c(600, NA, 580, NA),
  weight_measurement2 = c(605, 450, NA, 625),
  weight_measurement3 = c(NA, 455, 585, 630)
)

weights
# A tibble: 4 × 4
  animal_id weight_measurement1 weight_measurement2 weight_measurement3
  <chr>                   <dbl>               <dbl>               <dbl>
1 A001                      600                 605                  NA
2 A002                       NA                 450                 455
3 A003                      580                  NA                 585
4 A004                       NA                 625                 630
# Use first non-missing weight
weights %>%
  mutate(
    weight_final = coalesce(weight_measurement1,
                           weight_measurement2,
                           weight_measurement3)
  )
# A tibble: 4 × 5
  animal_id weight_measurement1 weight_measurement2 weight_measurement3
  <chr>                   <dbl>               <dbl>               <dbl>
1 A001                      600                 605                  NA
2 A002                       NA                 450                 455
3 A003                      580                  NA                 585
4 A004                       NA                 625                 630
# ℹ 1 more variable: weight_final <dbl>

4.9.5 Handling NAs in Calculations

Most R functions return NA if any input is NA:

# Mean returns NA if any value is NA
mean(c(1, 2, NA, 4))
[1] NA
# Use na.rm = TRUE to remove NAs before calculating
mean(c(1, 2, NA, 4), na.rm = TRUE)
[1] 2.333333
# In a summarise
cattle_missing %>%
  summarise(
    mean_weight_with_na = mean(weight_kg),           # Returns NA
    mean_weight_removed = mean(weight_kg, na.rm = TRUE)  # Calculates mean
  )
# A tibble: 1 × 2
  mean_weight_with_na mean_weight_removed
                <dbl>               <dbl>
1                  NA                602.

4.10 Window Functions

Window functions operate on groups of rows and return a value for each row (unlike summarise() which returns one value per group).

4.10.1 Ranking Functions

# Create competition data
competition <- tibble(
  animal_id = sprintf("A%03d", 1:8),
  breed = rep(c("Holstein", "Jersey"), each = 4),
  score = c(92, 88, 88, 85, 78, 76, 76, 74)
)

competition
# A tibble: 8 × 3
  animal_id breed    score
  <chr>     <chr>    <dbl>
1 A001      Holstein    92
2 A002      Holstein    88
3 A003      Holstein    88
4 A004      Holstein    85
5 A005      Jersey      78
6 A006      Jersey      76
7 A007      Jersey      76
8 A008      Jersey      74
# Rank animals by score
competition %>%
  mutate(
    rank = min_rank(desc(score)),        # Rank (ties get same rank, gaps after)
    dense = dense_rank(desc(score)),     # Dense rank (ties, no gaps)
    row_num = row_number(desc(score)),   # Row number (no ties, arbitrary order)
    percentile = percent_rank(desc(score))  # Percentile (0 to 1)
  )
# A tibble: 8 × 7
  animal_id breed    score  rank dense row_num percentile
  <chr>     <chr>    <dbl> <int> <int>   <int>      <dbl>
1 A001      Holstein    92     1     1       1      0    
2 A002      Holstein    88     2     2       2      0.143
3 A003      Holstein    88     2     2       3      0.143
4 A004      Holstein    85     4     3       4      0.429
5 A005      Jersey      78     5     4       5      0.571
6 A006      Jersey      76     6     5       6      0.714
7 A007      Jersey      76     6     5       7      0.714
8 A008      Jersey      74     8     6       8      1    
# Rank within breed
competition %>%
  group_by(breed) %>%
  mutate(
    breed_rank = min_rank(desc(score))
  ) %>%
  ungroup() %>%
  arrange(breed, breed_rank)
# A tibble: 8 × 4
  animal_id breed    score breed_rank
  <chr>     <chr>    <dbl>      <int>
1 A001      Holstein    92          1
2 A002      Holstein    88          2
3 A003      Holstein    88          2
4 A004      Holstein    85          4
5 A005      Jersey      78          1
6 A006      Jersey      76          2
7 A007      Jersey      76          2
8 A008      Jersey      74          4

4.10.2 Offset Functions: lag() and lead()

lag() and lead() access previous or next values:

# Weight measurements over time
growth <- tibble(
  animal_id = rep("A001", 5),
  week = 1:5,
  weight_kg = c(450, 465, 478, 492, 505)
)

growth
# A tibble: 5 × 3
  animal_id  week weight_kg
  <chr>     <int>     <dbl>
1 A001          1       450
2 A001          2       465
3 A001          3       478
4 A001          4       492
5 A001          5       505
# Calculate weight change from previous week
growth %>%
  mutate(
    previous_weight = lag(weight_kg),           # Previous row
    weight_gain = weight_kg - lag(weight_kg),  # Change from previous
    next_weight = lead(weight_kg)              # Next row
  )
# A tibble: 5 × 6
  animal_id  week weight_kg previous_weight weight_gain next_weight
  <chr>     <int>     <dbl>           <dbl>       <dbl>       <dbl>
1 A001          1       450              NA          NA         465
2 A001          2       465             450          15         478
3 A001          3       478             465          13         492
4 A001          4       492             478          14         505
5 A001          5       505             492          13          NA
# Lag by multiple rows
growth %>%
  mutate(
    two_weeks_ago = lag(weight_kg, n = 2)
  )
# A tibble: 5 × 4
  animal_id  week weight_kg two_weeks_ago
  <chr>     <int>     <dbl>         <dbl>
1 A001          1       450            NA
2 A001          2       465            NA
3 A001          3       478           450
4 A001          4       492           465
5 A001          5       505           478

4.10.3 Cumulative Functions

# Running totals
milk_production <- tibble(
  day = 1:7,
  milk_liters = c(25, 28, 26, 29, 27, 30, 28)
)

milk_production
# A tibble: 7 × 2
    day milk_liters
  <int>       <dbl>
1     1          25
2     2          28
3     3          26
4     4          29
5     5          27
6     6          30
7     7          28
# Cumulative statistics
milk_production %>%
  mutate(
    cumulative_milk = cumsum(milk_liters),      # Running total
    running_mean = cummean(milk_liters),        # Running mean
    running_min = cummin(milk_liters),          # Running minimum
    running_max = cummax(milk_liters)           # Running maximum
  )
# A tibble: 7 × 6
    day milk_liters cumulative_milk running_mean running_min running_max
  <int>       <dbl>           <dbl>        <dbl>       <dbl>       <dbl>
1     1          25              25         25            25          25
2     2          28              53         26.5          25          28
3     3          26              79         26.3          25          28
4     4          29             108         27            25          29
5     5          27             135         27            25          29
6     6          30             165         27.5          25          30
7     7          28             193         27.6          25          30

4.10.4 Real-World Example: Growth Rates

# Multiple animals, multiple measurements
growth_data <- tibble(
  animal_id = rep(c("A001", "A002", "A003"), each = 4),
  week = rep(c(0, 4, 8, 12), 3),
  weight_kg = c(
    # A001
    450, 485, 518, 548,
    # A002
    445, 475, 502, 530,
    # A003
    455, 492, 525, 556
  )
)

growth_data
# A tibble: 12 × 3
   animal_id  week weight_kg
   <chr>     <dbl>     <dbl>
 1 A001          0       450
 2 A001          4       485
 3 A001          8       518
 4 A001         12       548
 5 A002          0       445
 6 A002          4       475
 7 A002          8       502
 8 A002         12       530
 9 A003          0       455
10 A003          4       492
11 A003          8       525
12 A003         12       556
# Calculate growth metrics for each animal
growth_summary <- growth_data %>%
  group_by(animal_id) %>%
  mutate(
    previous_weight = lag(weight_kg),
    weight_gain = weight_kg - lag(weight_kg),
    weeks_elapsed = week - lag(week),
    daily_gain = weight_gain / (weeks_elapsed * 7),
    total_gain = weight_kg - first(weight_kg),
    cumulative_gain = cumsum(replace_na(weight_gain, 0))
  ) %>%
  ungroup()

growth_summary
# A tibble: 12 × 9
   animal_id  week weight_kg previous_weight weight_gain weeks_elapsed
   <chr>     <dbl>     <dbl>           <dbl>       <dbl>         <dbl>
 1 A001          0       450              NA          NA            NA
 2 A001          4       485             450          35             4
 3 A001          8       518             485          33             4
 4 A001         12       548             518          30             4
 5 A002          0       445              NA          NA            NA
 6 A002          4       475             445          30             4
 7 A002          8       502             475          27             4
 8 A002         12       530             502          28             4
 9 A003          0       455              NA          NA            NA
10 A003          4       492             455          37             4
11 A003          8       525             492          33             4
12 A003         12       556             525          31             4
# ℹ 3 more variables: daily_gain <dbl>, total_gain <dbl>, cumulative_gain <dbl>

4.11 Putting It All Together: Complex Pipelines

Real data analysis often requires chaining many operations. Here’s a comprehensive example:

# Create realistic farm data
set.seed(123)
farm_complete <- tibble(
  animal_id = sprintf("F%04d", 1:100),
  farm = sample(c("North", "South", "East", "West"), 100, replace = TRUE),
  breed = sample(c("Holstein", "Jersey", "Angus", "Hereford"), 100,
                 replace = TRUE, prob = c(0.4, 0.3, 0.2, 0.1)),
  sex = sample(c("M", "F"), 100, replace = TRUE),
  birth_date = as.Date("2022-01-01") + sample(0:365, 100, replace = TRUE),
  weight_kg = rnorm(100, mean = 500, sd = 80),
  health_status = sample(c("Good", "Fair", "Poor", NA), 100,
                        replace = TRUE, prob = c(0.7, 0.2, 0.05, 0.05))
)

# Preview
head(farm_complete, 3)
# A tibble: 3 × 7
  animal_id farm  breed    sex   birth_date weight_kg health_status
  <chr>     <chr> <chr>    <chr> <date>         <dbl> <chr>        
1 F0001     East  Jersey   F     2022-08-06      397. Fair         
2 F0002     East  Holstein F     2022-04-16      454. Good         
3 F0003     East  Jersey   F     2022-07-05      549. Good         
# Complex analysis pipeline
analysis_result <- farm_complete %>%
  # Data cleaning
  drop_na(health_status) %>%                    # Remove missing health status
  filter(health_status != "Poor") %>%           # Exclude poor health
  mutate(
    # Calculate age
    age_days = as.numeric(Sys.Date() - birth_date),
    age_months = age_days / 30.44,
    # Standardize breed names
    breed = str_to_title(breed),
    # Weight categories
    weight_class = case_when(
      weight_kg < 450 ~ "Light",
      weight_kg < 550 ~ "Medium",
      TRUE ~ "Heavy"
    ),
    # Round weight
    weight_kg = round(weight_kg, 1)
  ) %>%
  # Filter to mature animals only
  filter(age_months >= 10) %>%
  # Group analysis
  group_by(farm, breed) %>%
  summarise(
    n = n(),
    mean_weight = round(mean(weight_kg), 1),
    sd_weight = round(sd(weight_kg), 1),
    min_weight = min(weight_kg),
    max_weight = max(weight_kg),
    prop_heavy = mean(weight_class == "Heavy"),
    .groups = "drop"
  ) %>%
  # Filter to groups with at least 3 animals
  filter(n >= 3) %>%
  # Sort by mean weight
  arrange(desc(mean_weight)) %>%
  # Add ranking
  mutate(rank = row_number())

analysis_result
# A tibble: 13 × 9
   farm  breed        n mean_weight sd_weight min_weight max_weight prop_heavy
   <chr> <chr>    <int>       <dbl>     <dbl>      <dbl>      <dbl>      <dbl>
 1 North Hereford     4        549.      47.1       485.       599.     0.75  
 2 North Jersey       6        538.      64.3       438.       628.     0.5   
 3 South Holstein     8        538.      89.1       443.       663      0.375 
 4 North Holstein    13        532.      72.7       409        683.     0.385 
 5 East  Angus        6        529.      70.5       440.       604      0.5   
 6 East  Jersey      10        500       99.9       339.       649.     0.3   
 7 West  Holstein     6        492.     105.        367.       652.     0.333 
 8 South Jersey      12        482.      71.9       362.       660.     0.0833
 9 West  Jersey       5        481.      60.4       395.       559.     0.2   
10 West  Hereford     3        463.      50         419.       517      0     
11 South Angus        3        461.      17.1       441.       471.     0     
12 West  Angus        3        457.      59.1       392.       507.     0     
13 East  Holstein     9        452.      77.7       360.       577      0.111 
# ℹ 1 more variable: rank <int>
TipComplex Pipeline Best Practices
  1. Comment your steps: Explain what each section does
  2. One operation per line: Easier to read and debug
  3. Use intermediate results: Break very long pipes into steps
  4. Check intermediate output: Run the pipe up to a certain point to verify
  5. Consistent indentation: 2 spaces after pipe
# Good structure
result <- data %>%
  # Step 1: Clean data
  filter(!is.na(important_var)) %>%
  mutate(clean_var = str_trim(var)) %>%
  # Step 2: Calculate new variables
  mutate(
    var1 = calculation1,
    var2 = calculation2
  ) %>%
  # Step 3: Summarize by group
  group_by(category) %>%
  summarise(
    mean_value = mean(value),
    .groups = "drop"
  )

4.12 Summary

This chapter covered powerful dplyr verbs for data manipulation:

  • mutate() creates and modifies columns, works with functions and calculations
  • if_else() applies simple conditional logic (if/then/else)
  • case_when() handles multiple conditions elegantly (replaces nested if_else)
  • arrange() sorts data by one or more columns in ascending or descending order
  • summarise() calculates summary statistics (mean, sd, min, max, count, etc.)
  • group_by() + summarise() performs grouped operations (summaries by category)
  • count() quickly counts observations per group
  • rename() changes column names; relocate() reorders columns
  • across() applies functions to multiple columns efficiently
  • Missing data can be detected (is.na()), removed (drop_na()), or replaced (replace_na(), coalesce())
  • Window functions (lag(), lead(), cumsum(), ranking) operate within groups
  • Complex pipelines chain many operations together for complete data transformations

Key Principle: Start simple, build complexity gradually, and always check your work!

Next chapter: Data visualization with ggplot2!


4.13 Homework Assignment

4.13.1 Assignment: Data Transformation and Analysis

Due: Before Week 5

4.13.1.1 Part 1: Creating Variables (25 points)

You will receive a dataset called pig_growth.csv with the following columns: - pig_id: Pig identifier - birth_date: Date of birth (YYYY-MM-DD) - breed: Pig breed - sex: M or F - weight_day0: Birth weight (kg) - weight_day28: Weight at 28 days (kg) - weight_day56: Weight at 56 days (kg) - feed_type: Type of feed given

Tasks:

  1. Read the data and examine its structure
  2. Create new variables using mutate():
    • age_days: Calculate age in days from birth_date to today
    • gain_0_28: Weight gain from day 0 to day 28
    • gain_28_56: Weight gain from day 28 to day 56
    • adg_0_28: Average daily gain for first period (gain / 28)
    • adg_28_56: Average daily gain for second period (gain / 28)
  3. Create categorical variables:
    • birth_weight_class: “Low” (<1.2 kg), “Normal” (1.2-1.6 kg), “High” (>1.6 kg)
    • sex_label: Convert “M” to “Male”, “F” to “Female”
  4. Round all weight and gain variables to 2 decimal places

4.13.1.2 Part 2: Conditional Logic (25 points)

Using your data with new variables:

  1. Create a performance category using case_when():
    • “Excellent”: adg_28_56 > 0.50 kg/day AND weight_day56 > 18 kg
    • “Good”: adg_28_56 > 0.45 kg/day
    • “Fair”: adg_28_56 > 0.40 kg/day
    • “Poor”: everything else
  2. Create a treatment recommendation using conditional logic:
    • If performance is “Poor” AND sex is “M”: “Supplement + Monitor”
    • If performance is “Poor” AND sex is “F”: “Supplement”
    • If performance is “Fair”: “Monitor”
    • Otherwise: “Continue”
  3. Identify concerning cases:
    • Create a logical variable needs_attention that is TRUE if:
      • Weight gain in second period is LESS than first period, OR
      • Current weight is below 12 kg

4.13.1.3 Part 3: Grouped Summaries (30 points)

Calculate comprehensive summary statistics:

  1. Overall summaries (no grouping):
    • Number of pigs
    • Mean birth weight, day 28 weight, day 56 weight
    • Mean ADG for both periods
    • Number and proportion needing attention
  2. Summaries by breed:
    • Count of pigs per breed
    • Mean ADG (both periods) per breed
    • SD of ADG per breed
    • Min and max day 56 weight per breed
  3. Summaries by feed type AND sex:
    • Count per combination
    • Mean day 56 weight
    • Proportion in each performance category
  4. Summaries by performance category:
    • Count per category
    • Mean ADG in period 2
    • What proportion of each category is male vs female?

Hint: Use group_by() + summarise() for each question. You may need across() for multiple columns.

4.13.1.4 Part 4: Complex Pipeline (20 points)

Create ONE pipeline that:

  1. Starts with the raw data
  2. Removes any pigs with missing weight measurements
  3. Calculates all new variables from Part 1
  4. Creates categories from Part 2
  5. Filters to pigs with birth_weight_class == “Normal”
  6. Filters to feed_type “A” or “B” only
  7. Groups by feed_type and sex
  8. Calculates mean day 56 weight and ADG for period 2
  9. Arranges by mean day 56 weight (descending)
  10. Adds a rank column

Show: - The complete pipeline (with comments explaining each step) - The final output - How many rows in the final result?

4.13.3 Grading Rubric

  • Part 1: Creating Variables (25%):
    • All variables created correctly (15%)
    • Proper use of mutate() (5%)
    • Rounding applied correctly (5%)
  • Part 2: Conditional Logic (25%):
    • Performance category correct (10%)
    • Treatment recommendation correct (8%)
    • Needs attention logic correct (7%)
  • Part 3: Grouped Summaries (30%):
    • Overall summaries (8%)
    • Breed summaries (8%)
    • Feed/sex summaries (7%)
    • Performance category summaries (7%)
  • Part 4: Complex Pipeline (20%):
    • Pipeline executes correctly (12%)
    • All steps included (5%)
    • Clear comments and documentation (3%)

4.13.4 Bonus (10 points)

  1. Window functions:
    • For each pig, calculate the difference between their day 56 weight and the breed average day 56 weight
    • Rank pigs within their breed by day 56 weight
  2. Advanced grouping:
    • Identify the top 3 breeds by mean ADG in period 2
    • For just those breeds, create a detailed summary with all statistics

4.14 Additional Resources

4.14.1 Required Reading

4.14.2 Optional Reading

4.14.3 Videos

  • “Data Manipulation in R” by StatQuest with Josh Starmer
  • “dplyr Tutorial” by RStudio / Posit
  • “Tidy Data and tidyr” by RStudio / Posit
  • “grouped operations in dplyr” by Data Science Dojo

4.14.4 Cheat Sheets

4.14.5 Interactive Learning

4.14.6 Useful Websites


Next Chapter: Introduction to Data Visualization with ggplot2