Monday, June 1, 2026
Today: Monday June 1
Before class
Plan for today
summarize() and group_by()na.rm trapn() in summariesHelpful links
For two weeks we’ve been making plots. But that assumed the data was already in the right shape, with one row per glyph and one column per aesthetic.
Tip
Real data almost never arrives glyph-ready. This week is about getting it there.
Today we will build the mental model that every wrangling function hangs off of.
We’ve learned |>. Read it as the word “then.”
Note
The DC reading writes the pipe as %>%. |> and %>% behave identically, so read them as the same word. We’ll use |>.
DC §7.1 gives the single most useful habit in this whole unit:
Before any coding, sketch the glyph-ready table you need, then plan the steps to get there in plain English.
For “how does average body mass differ by species?”, the table you wish you had is:
| species | avg_mass |
|---|---|
| Adelie | … |
| Chinstrap | … |
| Gentoo | … |
Here we have one row per species and one column for the number you want. The case is a species, not a penguin. That shift in what one row represents is from summarize() and group_by().
DC’s smoking example plans the wrangling as four English sentences before any code:
Each sentence maps to one operation. Today we learn the vocabulary for sentences like #3 (split into groups) and #4 (for each group, count / average).
DC §7.2 boils all of wrangling down to three shapes of data…
and three families of functions, sorted by what goes in and what comes out:
| Family | Input → Output | Examples |
|---|---|---|
| Reduction | variable → scalar | mean(), sum(), n(), median(), sd(), n_distinct() |
| Transformation | variable(s) → variable | x / y, log10(), round(), ifelse(), <, ==, %in% |
| Data verb | data frame → data frame | summarize(), group_by(), filter(), mutate(), … |
A reduction takes a whole column and crushes it down to a single number (DC §7.3).
We’ve used many of these before: mean(), sum(), median(), sd(), min(), max(). Here are two new ones:
n() — how many cases (takes no arguments)n_distinct() — how many different valuesNote
n() doesn’t look at any column, it simply counts rows. It only works inside a data verb like summarize(), not on its own.
A transformation runs once per row and hands back a column the same length as the input (DC §7.4).
Comparisons are transformations too, they return a column of TRUE/FALSE:
Tip
Reduction vs. transformation in one question: does it give back one number, or one per row? mean() to one. round() to one per row.
A data verb takes a whole data frame and returns a whole (new) data frame (DC §7.5). Here are three rules every dplyr verb obeys:
Warning
Because verbs never modify their input, penguins |> summarize(...) on its own just prints. To keep a result, assign it: out <- penguins |> summarize(...).
Each step in data wrangling involves a data verb and one or more reduction or transformation functions.
The verb is the action on the table; the reduction/transformation is the detail of what to compute.
summarize() turns many rows into one row, using reduction functions.
# A tibble: 1 × 3
n avg_mass max_mass
<int> <dbl> <int>
1 344 4202. 6300
Things to notice:
na.rm trapWatch what happens without na.rm = TRUE:
One missing value anywhere in the column makes the whole answer NA. The reduction can’t average around a hole it doesn’t know how to fill.
Warning
This is the AI failure we dissected on Friday: code that runs, looks fine, and quietly returns NA. The fix is na.rm = TRUE, but the skill is noticing the NA in the first place.
group_by() doesn’t compute anything itself. It tags the data frame so the next verb runs once per group.
# A tibble: 3 × 3
species n avg_mass
<fct> <int> <dbl>
1 Adelie 152 3701.
2 Chinstrap 68 3733.
3 Gentoo 124 5076.
Same summarize() as before, now one row per species instead of one row total.
Note
group_by() should (almost) always be followed by another verb. On its own it just attaches an invisible label, the work happens in the verb after it.
List several columns to get one row per combination:
# A tibble: 8 × 4
# Groups: species [3]
species sex n avg_mass
<fct> <fct> <int> <dbl>
1 Adelie female 73 3369.
2 Adelie male 73 4043.
3 Adelie <NA> 6 3540
4 Chinstrap female 34 3527.
5 Chinstrap male 34 3939.
6 Gentoo female 58 4680.
7 Gentoo male 61 5485.
8 Gentoo <NA> 5 4588.
Two things worth a pause:
NA row for sex indicating some penguins have missing sex. group_by() keeps them as their own group. (Dropping them needs filter().).groups = "drop" to silence it.group_by() changes the caseWrangling changes what one row means.
| Pipeline | One row = |
|---|---|
penguins |
one penguin |
... |> summarize(n = n()) |
the whole dataset |
... |> group_by(species) |> summarize(...) |
one species |
... |> group_by(species, sex) |> summarize(...) |
one species × sex combo |
Tip
Before you run a group_by() |> summarize(), predict how many rows the result has. It’s the number of distinct group combinations. If your prediction is wrong, you’ve learned something about your data.
n()Average mass by the finest grouping:
# A tibble: 13 × 5
species island sex n avg_mass
<fct> <fct> <fct> <int> <dbl>
1 Adelie Biscoe female 22 3369.
2 Adelie Biscoe male 22 4050
3 Adelie Dream female 27 3344.
4 Adelie Dream male 28 4046.
5 Adelie Dream <NA> 1 2975
6 Adelie Torgersen female 24 3396.
7 Adelie Torgersen male 23 4035.
8 Adelie Torgersen <NA> 5 3681.
9 Chinstrap Dream female 34 3527.
10 Chinstrap Dream male 34 3939.
11 Gentoo Biscoe female 58 4680.
12 Gentoo Biscoe male 61 5485.
13 Gentoo Biscoe <NA> 5 4588.
Some groups have a handful of birds, others have dozens. A mean from a group of 3 is not the same kind of fact as a mean from a group of 60.
Warning
Whenever you compute a group summary, add n = n(). A surprising-looking average is very often just a tiny group. Without the count, you can’t tell a real effect from noise.
This is also a verification move, exactly like Friday: the number that looks like an answer might be resting on almost no data.
Today: the framework + summarize() + group_by(). That’s already enough to answer a lot of “how many / what’s the average, by group” questions.
Tomorrow (DC Ch. 10) we add the four verbs:
filter() — keep some rows (e.g. drop the NA sexes)select() — keep some columnsmutate() — add a new column (this is where transformations live)arrange() — reorder rowsWith the two we covered today, these handle “take a messy CSV and produce a useful summary.”
summarize(): many rows to one; named args become columns; output is a data framena.rm = TRUE: one NA poisons a whole reduction; spotting it is the skillgroup_by(): tags the frame so the next verb runs per group; changes what a “case” meansn(): include it in every summary so you never trust a mean from three rowsRoughly 25 minutes. Individually or with a neighbor.
You’ll classify functions into the three families, build up summarize() and group_by() pipelines on penguins, deliberately trip the na.rm trap, and practice predicting the shape of a grouped result before you run it.
To turn in: save your .qmd and rendered HTML.
Tomorrow (Tuesday): the row and column verbs, filter, select, mutate, arrange, the rest of the dplyr toolkit.
Upcoming Deadlines
Reach out
Stuck? Office hours, or email me (cgc5478@psu.edu) or Xinyue (xpw5228@psu.edu).
Reference
Syllabus · AI policy · Schedule · R4DS Ch. 3 · DC Ch. 7 · dplyr reference