Week 7 · Thursday, Nov 5 · Lecture
| Time | What |
|---|---|
| 1:30 | DS 6: your questions |
| 1:45 | Mid-quarter check-ins: the schedule is on Ed |
| 1:50 | Groups: group_by() and summarize() |
| 2:25 | Joins: left_join(), inner_join(), anti_join() |
| 2:55 | Tuesday: DS 7, check-ins, combining data practice II |
Learning goals: answer “for each group” questions in one pipe, know when a data frame is still grouped and why that matters, and attach a lookup table with a join, counting rows (and checking keys) every time.
Still no AI. You need data/sites.csv and data/buildings.csv from last week.
⏱ 15 min
Not a check, and not graded. It’s open time for questions about DS 6 (Lesson 12: Writing Data, Lesson 13: dplyr I), or about anything from Tuesday’s pipes.
If nobody starts: when do you need <- at the start of a pipe? Why == and not = inside filter()? When does mutate() make a new column, and when does it overwrite one? Why did filter() drop the person with an NA commute?
Important
The check-in sign up is posted on Ed Discussion. Find a time and add your name today.
Check-ins are mandatory.
5% of your grade. If you can’t make your time, send me a private post on Ed before your slot, not after it.
Create week07/groups-joins.R:
You’ve already answered “for each group” questions twice:
One line per group doesn’t scale. With 142 countries, you’d need 142 lines.
group_by() + summarize()group_by(site) splits the rows into groups. Nothing looks different yetsummarize() collapses each group to one row: one column per summary you ask forn() counts the rows in each groupTwo things to notice before you trust this table. Look again.
NA: one person in north has no commute. mean() still needs na.rm = TRUE, and n() still counts that person (15 rows, 14 commutes)<NA> is a group. split() dropped the person with no site. group_by() keeps them. 11 + 15 + 18 + 1 = 45Tip
count(site) is a shortcut for group_by(site) |> summarize(n = n()). It’s the fastest way to see your groups, NA included.
⏱ 8 min
For 2007 only, one pipe that gives one row per continent with:
n_countries: how many countriesmedian_le: the median life expectancymax_gdp: the largest GDP per capitaBefore you run it: how many rows and columns?
gapminder |>
filter(year == 2007) |>
group_by(continent) |>
summarize(n_countries = n(), median_le = median(lifeExp), max_gdp = max(gdpPercap))
#> continent n_countries median_le max_gdp
#> 1 Africa 52 52.9 13206.
#> 2 Americas 25 72.9 42952.
#> 3 Asia 33 72.4 47307.
#> 4 Europe 30 78.6 49357.
#> 5 Oceania 2 80.7 34435.5 rows, 4 columns. 52 + 25 + 33 + 30 + 2 = 142, the number of countries. Oceania’s “median” is the average of 2 countries: always look at n next to a summary.
continentThe exact wording depends on your dplyr version. Either way it means “still grouped”.
Important
Done with groups? Say so: add ungroup() at the end of the pipe, or summarize(..., .groups = "drop").
Rank the continent-years by mean life expectancy, then look at 2007:
Every continent is #1. The rank was computed within each continent, across years.
No error, no warning, a plausible-looking column. That’s how a leftover group bites you.
group_by() changes what every verb after it does:
mutate() with groups keeps every row (142); summarize() collapses to one per group (5). Ask which one you want before you write it.
⏱ 7 min
For 2007:
pop_share: each country’s population as a share of its continent’s populationgapminder |>
filter(year == 2007) |>
group_by(continent) |>
mutate(pop_share = pop / sum(pop)) |>
slice_max(pop_share, n = 1) |>
ungroup() |>
select(continent, country, pop_share)
#> 1 Africa Nigeria 0.145
#> 2 Americas United States 0.335
#> 3 Asia China 0.346
#> 4 Europe Germany 0.141
#> 5 Oceania Australia 0.832Forget the group_by() and sum(pop) is the world: China becomes 0.21 and the shares stop meaning “of its continent”.
left_join(x, y) = merge(x, y, all.x = TRUE): every row of x, NA where y has no matchsurvey’s order (merge() re-sorted them by site), and it fits in a pipe| Keeps | survey + sites |
|
|---|---|---|
left_join(x, y) |
every row of x |
45 |
inner_join(x, y) |
rows of x with a match in y |
44 |
anti_join(x, y) |
rows of x with no match in y |
1 |
anti_join() doesn’t add anything. It’s for finding gaps, in both directions:
Order matters: anti_join(x, y) answers “what in x is missing from y?”
Tuesday’s buildings.csv has two rows for north:
merge() gave you 86 rows on Tuesday and said nothing. dplyr warns. Better: tell it what you expect, and it stops:
Tip
For “add information from a lookup table”: relationship = "many-to-one". Many survey rows per site, one row per site in the lookup.
⏱ 10 min
Read wave 2 again and fix its column name: wave2 <- read.csv("data/survey_wave2.csv") |> rename(commute_min = commute).
Write down the number you expect, then run:
left_join(wave2, sites, by = "site"): how many rows?inner_join() of the same: how many rows? Why the same as 1, when it wasn’t for survey?anti_join()s: what’s in each?wave2 with buildings, using relationship = "many-to-one". What happens, and what would you ask whoever made buildings.csv?4 stops with an error, and that’s the right outcome. “Which building is north?” is a question about the data, not the code.
group_by() + summarize(): one row per group. n() counts rows; check it next to every summarygroup_by() keeps NA as a group (split() dropped it). count() shows your groups fastmean(), median() and friends still need na.rm = TRUE inside summarize()summarize() with two grouping columns, the result is still grouped: ungroup() or .groups = "drop"mutate() keeps every row; grouped summarize() collapses. A leftover group changes results silentlyleft_join() keeps every row of x, inner_join() only matches, anti_join() finds the gapsrelationship = "many-to-one" turns a duplicated lookup key from a silent problem into an error9:50-10:55 am · DS 7 + check-ins
Lesson 14: dplyr II and Lesson 15: tidyr, alone, no AI. Two PDFs.
1:30-3:05 pm · Combining data practice II
Starts with the DS 7 check: swap laptops. Then both survey waves, by group and reshaped, and an average that looks right and isn’t.
Important
Check-ins are mandatory. Look up your time on the Ed schedule today. If it isn’t today, it’s in office hours. The DS is not check-in time.
Before Tuesday: read the Lesson 14 and 15 pages. Lesson 15 introduces pivot_longer() and pivot_wider(), which change the shape of a data frame. The DS is your first practice with them.
Next: Tuesday, Nov 10 · DS 7 in the morning, combining data practice II in the afternoon.

STAT 266A · Fall 2026 · UC Santa Cruz