Activity 05 - Wrangling data
filter, select, mutate, arrange — and chaining them into a pipeline
Hands-on companion to the wrangling lecture. Chain filter(), select(), mutate(), and arrange() into a pipeline on the leaf data.
Wrangling Our Leaf Data
Recap from Activity 04 (GGPlot II)
- Loaded the leaf data with
read_excel()andclean_names() - Built faceted plots, mean ± SE plots with
stat_summary(), and custom colors withscale_color_manual() - Adjusted axis limits with
coord_cartesian()and picked a theme - Used the pipe
%>%to chain steps
Today’s Objectives
- Use
filter(),select(),mutate(), andarrange()to wrangle data - 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 intoleaf_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
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 forgotlibrary(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==insidefilter().
✏️ 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)replacemass_gor 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:
- Which size category do you expect to hold the most leaves overall, and why?
- Do you expect
sunnyorshadyleaves 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)
- In plain language, what did your cutoff choices do to the data — why would different cutoffs have changed your counts? Use your actual numbers.
case_when()andfilter()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
- Read the error message out loud. R usually names the line and the problem.
- Check the usual suspects: Did you load
library(readxl)andlibrary(tidyverse)? - Spelling? R is case-sensitive —
"Sunny"≠"sunny". Check withnames(leaf_df). - File not found? Check with
getwd()and confirm thedata/folder is inside your project. - Cheat sheets — https://posit.co/resources/cheatsheets/
- 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.