Activity 05 - Wrangling data

filter, select, mutate, arrange — and chaining them into a pipeline

tidyverse

Hands-on companion to the wrangling lecture. Chain filter(), select(), mutate(), and arrange() into a pipeline on the leaf data.

Author

Bill Perry

Published

September 10, 2026

Wrangling Our Leaf Data

Recap from Activity 04 (GGPlot II)

  • Loaded the leaf data with read_excel() and clean_names()
  • Built faceted plots, mean ± SE plots with stat_summary(), and custom colors with scale_color_manual()
  • Adjusted axis limits with coord_cartesian() and picked a theme
  • Used the pipe %>% to chain steps

Today’s Objectives

  1. Use filter(), select(), mutate(), and arrange() to wrangle data
  2. Chain all four verbs into a single pipeline

How this activity works

You are building one R script this whole class, and you turn it in. Create it now: scripts/05_wrangling.R.

  • Every line you run goes in that script — not in the Console, not typed into this page. Type it, don’t paste it.
  • Start every code chunk in your script with a short # comment that says what it does. The comments are part of the grade.
  • The top of your script, in this order: a title comment, then your library() calls, then the line that loads the data into leaf_df.
  • Code marked ▶ Run this is typed into your script exactly as shown. Code marked ✏️ Your turn is a change you make in that same script and run.

🔮 Predict before you run

Before you run any ▶ Run this block, predict what R will print — how many rows, how many columns, which value comes out on top. Then run it and compare. Predicting exposes what you don’t actually know yet, and typing (not pasting) is what trains your eye to catch the = vs == typo before R does.


Part 1 · Load libraries and data

Tip📂 Get the data

Same leaf file you have used since Activity 02 — it should already be in your project’s data/ folder. If not: 2026_09_03_data_sci_leaf_area.xlsx → put it in data/.

▶ Type this at the very top of scripts/05_wrangling.R:

# ---- Activity 05: Wrangling data -------------------------
# your name, today's date

# ---- Libraries -------------------------------------------
library(readxl)      # read Excel files
library(tidyverse)   # dplyr for wrangling + ggplot2
library(janitor)     # clean_names()
# ---- Load data ------------------------------------------
leaf_df <- read_excel("data/2026_09_03_data_sci_leaf_area.xlsx") %>%
  clean_names()

glimpse(leaf_df)     # look at the data right after loading

⚠️ Watch out! If R says “could not find function read_excel you forgot library(readxl). You must reload libraries every time you restart R.


Part 2 · Inspect the data

▶ Run this:

# glimpse() is the one to reach for: one row per column, with type + first values
glimpse(leaf_df)

✏️ Your turn: Fill in the table below from the glimpse() output.

Column name    | Data type (<chr> / <dbl>)  | Example value
---------------|----------------------------|---------------
shade          |                            |
mass_g         |                            |
petiole_mm     |                            |
thickness_mm   |                            |
paper_mass_g   |                            |

✏️ Your turn — in your script: How many leaves are from the sunny side and how many from the shady side? Use table(leaf_df$shade).

Sunny leaves:
Shady leaves:
Is the design balanced (equal n per group)?  Y / N

Part 3 · The core tidyverse verbs — filter, select, mutate, arrange

These four functions do most of the work in data wrangling. All take a data frame and return a data frame.

filter() — keeping rows you want

🔮 Predict first: Before running, guess how many rows filter(shade == "sunny") will return. Write your number, then check.

▶ Run this:

# filter() keeps rows where the condition is TRUE

# keep only sunny leaves
leaf_df %>% filter(shade == "sunny")

# keep only leaves heavier than 0.5 g
leaf_df %>% filter(mass_g > 0.5)

# combine conditions (comma = AND = both must be true)
leaf_df %>% filter(shade == "shady", mass_g > 0.5)

⚠️ Watch out! = assigns a value. == tests equality. Always use == inside filter().

✏️ Your turn — in your script: Use filter() to find all leaves where thickness_mm is greater than 0.16.

Number of leaves with thickness_mm > 0.16:
Are they mostly sunny or shady?

select() — keeping columns you want

▶ Run this:

# select() keeps (or drops) the columns you name

# keep only shade and mass
leaf_df %>% select(shade, mass_g)

# drop the paper_mass_g column (minus sign = drop)
leaf_df %>% select(-paper_mass_g)

# keep shade plus every column whose name ends in "_mm"
leaf_df %>% select(shade, ends_with("_mm"))

✏️ Your turn — in your script: Create a data frame called lean_df that has only shade and thickness_mm. Then run glimpse(lean_df) to verify.

How many columns does lean_df have?
What are they?

mutate() — adding new columns

mutate() adds new columns (or changes existing ones) without removing anything.

🔮 Predict first: Will mutate(mass_mg = mass_g * 1000) replace mass_g or add a new column? How many columns will the result have — more, fewer, or the same?

▶ Run this:

# mutate() adds a new column to the data frame

# convert grams to milligrams
leaf_df %>%
  mutate(mass_mg = mass_g * 1000)

# add a size category based on mass
leaf_df %>%
  mutate(size_class = if_else(mass_g > 0.5, "large", "small"))

✏️ Your turn — in your script: Use mutate() to add a column called petiole_cm (petiole length in centimetres — divide petiole_mm by 10). What is the maximum petiole length in cm?

Column name you added:
Maximum petiole length in cm:

arrange() — sorting rows

▶ Run this:

# arrange() sorts rows by a column

# lightest leaves first (ascending — the default)
leaf_df %>% arrange(mass_g)

# heaviest leaves first (descending — wrap in desc())
leaf_df %>% arrange(desc(mass_g))

# sort by shade, then by mass within each shade
leaf_df %>% arrange(shade, desc(mass_g))

✏️ Your turn — in your script: Sort the data by thickness_mm descending. What is the shade of the thickest leaf?

Shade of the thickest leaf (sunny or shady):
Its thickness (mm):

The full pipeline — chaining all four verbs

▶ Run this:

# chain all four verbs — read it as a recipe, one %>% at a time
leaf_clean_df <- leaf_df %>%
  filter(mass_g > 0) %>%                      # drop any zeros
  select(shade, mass_g, thickness_mm) %>%     # keep three columns
  mutate(
    mass_mg    = mass_g * 1000,
    size_class = if_else(mass_g > 0.5, "large", "small")
  ) %>%
  arrange(shade, desc(mass_g))                # sort by shade, then mass

head(leaf_clean_df)

✏️ Your turn: Read the pipeline out loud, one %>% step at a time. Write in plain English what each step does:

Step 1 (filter):
Step 2 (select):
Step 3 (mutate):
Step 4 (arrange):

Part 4 · Review and checkpoint

At this point you should be able to:

✏️ Your turn — before you move on: Run your entire script top to bottom with Ctrl/Cmd + Shift + Enter (Source). Does it complete without errors?

Ran cleanly?  Y / N
If not, what error appeared:

Part 5 · Going further

This section is optional — work through it if you finish early.

Use mutate() to add a column, then plot it

▶ Try this:

# add size_class, then count large vs small leaves per shade
leaf_df %>%
  mutate(size_class = if_else(mass_g > 0.5, "large", "small")) %>%
  ggplot(aes(x = shade, fill = size_class)) +
  geom_bar(position = "dodge") +
  labs(title = "Large vs small leaves by shade",
       x = "Shade", y = "Count", fill = "Size class") +
  theme_minimal()

✏️ Your turn — in your script: Change position = "dodge" to position = "fill".

dodge does:
fill does:

Extension — out of class (~30–40 min)

Add this to the bottom of scripts/05_wrangling.R and turn it in with the rest. Put your written answers in # comments right under the code they go with. In class you split leaves into two groups at a fixed mass. Now you build three categories with cutoffs you pick.

E1 · Three size categories (4 pts)

Use mutate() with case_when() to sort every leaf into "small", "medium", "large" based on mass_g. You pick the two cutoff masses — look at the data first (summary(leaf_df$mass_g)) so they are reasonable, not 0 and Inf.

State your two cutoffs in a comment, then show count(shade, size_category) — how many leaves land in each category on each side.

E2 · Predict, then check (3 pts)

Before running the count(), write in comments:

  1. Which size category do you expect to hold the most leaves overall, and why?
  2. Do you expect sunny or shady leaves to skew toward "large"? One reason.

Then run it and write whether you were right and — if not — your best guess for why the data came out that way.

E3 · Explain it, with YOUR counts (3 pts)

  1. In plain language, what did your cutoff choices do to the data — why would different cutoffs have changed your counts? Use your actual numbers.
  2. case_when() and filter() can both split data into groups. In your own words, what is the actual difference in what each one does to the dataset?

Getting unstuck

  1. Read the error message out loud. R usually names the line and the problem.
  2. Check the usual suspects: Did you load library(readxl) and library(tidyverse)?
  3. Spelling? R is case-sensitive"Sunny""sunny". Check with names(leaf_df).
  4. File not found? Check with getwd() and confirm the data/ folder is inside your project.
  5. Cheat sheetshttps://posit.co/resources/cheatsheets/
  6. Bring the exact error (copy-paste it) to class, Canvas, or office hours.

💡 Key idea: an = where you meant ==, a typo’d column name — these are the errors this activity is built around, and reading the message before you panic will solve most of them.


End of the Wrangling activity. Next: summary statistics — mean, median, SD, SE, group_by() + summarize(), and skimr.