Mean, spread, counting correctly, and group summaries
Last time you:
read_csv(), read_excel())filter(), select(), mutate(), arrange()distinct()write_csv()Note
✅ Key idea from Wrangling Your Data
You can reshape a dataset and hand off a clean file. Today we start describing it — the step that comes before any formal test.
Note
Today’s roadmap
length() trap — and the fixskimr — a fast full-dataset overviewgroup_by() + summarize()Before we can formally test that with statistics, we need to describe what we actually measured.
Note
Plan for this unit
\[\bar{x} = \frac{\sum_{i=1}^{n} x_i}{n}\]
Example: Values 5, 6, 7, 8, 50 → mean = 15.2.
Is that a fair summary?
Not really — the outlier dominates.
Note
✅ Why it matters
The mean is the most commonly reported statistic
— and it’s the number every t-test, ANOVA, and regression is ultimately built around.
[1] 20 21 23 25 21 16 19 18 20 23 21 18 20 21 23 25 21 16 19 18 20 23 21 18
[1] 20.41667
Tip
📖 New pattern
filter() picks rows. pull() extracts one column as a plain vector, not a data frame.
The middle value once the data are sorted
| Sorted data | Median |
|---|---|
| 5, 6, 7, 8, 50 | 7 |
| 5, 6, 7, 8, 50, 51 | (7 + 8) / 2 = 7.5 |
\[s^2 = \frac{\sum_{i=1}^{n}(x_i - \bar{x})^2}{n - 1} \qquad s = \sqrt{s^2}\]
Larger spread = larger variance and SD.
Note
[1] 16 25
[1] 9
0% 25% 50% 75% 100%
16.00 18.75 20.50 21.50 25.00
[1] 2.75
Note
✅ Key idea
The IQR is the range of the middle 50% of the data — it ignores the most extreme quarter on each end, which makes it robust to outliers the same way the median is.
Tip
📖 Preview
The box in a boxplot is the IQR — its bottom and top edges are the 25th and 75th percentiles you just computed. The boxplots you’ll build in Graphing Effectively are these five numbers, drawn.
length() Traplength() TrapImportant
🔮 Predict first: A vector has 5 slots, but 2 are NA. What does length() return? What should your sample size be for a mean or SE calculation?
Note
📖 New words
is.na(x) — TRUE where a value is missing!is.na(x) — the flip: TRUE where a value is realsum(!is.na(x)) — counts the real values = the correct nlength() Trap — Why It MattersWarning
⚠️ Watch out!
R will not warn you. length() happily returns 5 when only 3 values are real — a silent, common error.
sum(!is.na())[1] FALSE FALSE TRUE FALSE TRUE
[1] TRUE TRUE FALSE TRUE FALSE
[1] 3
Tip
Best practice: use sum(!is.na(x)) for n every time you compute an SE — even when you’re sure there are no missing values. Field data rarely stays that clean.
How it works, step by step:
is.na(x) → TRUE for each missing value!is.na(x) → flips it to TRUE for real valuessum(...) → adds up the TRUEs (TRUE = 1)📖 R4DS §18 — Missing Values
Tip
Even when you’re confident a column has zero missing values, run it through sum(!is.na()) anyway — it costs nothing and field data rarely stays clean for long.
\[SE = \frac{s}{\sqrt{n}}\]
| Describes | |
|---|---|
| SD | spread of individual data points |
| SE | precision of the sample mean |
We report SE whenever we’re making a claim about a group mean.
n_l <- sum(!is.na(shady_lengths))
mean_l <- mean(shady_lengths, na.rm = TRUE)
med_l <- median(shady_lengths, na.rm = TRUE)
sd_l <- sd(shady_lengths, na.rm = TRUE)
se_l <- sd_l / sqrt(n_l)
cat("--- Shady side ---\n",
"n =", n_l, "\n",
"Mean =", round(mean_l, 2), "\n",
"Median =", round(med_l, 2), "\n",
"SD =", round(sd_l, 2), "\n",
"SE =", round(se_l, 2), "\n")--- Shady side ---
n = 24
Mean = 20.42
Median = 20.5
SD = 2.45
SE = 0.5
na.rm = TRUEcat() prints clean, labeled outputNote
✅ Key idea
Copy-pasting this block for every group is exactly the repetition group_by() + summarize() exists to remove.
skimr — a Fast Full-Dataset Overviewskimr — Every Column, One Command| Name | pine_df |
| Number of rows | 48 |
| Number of columns | 6 |
| _______________________ | |
| Column type frequency: | |
| character | 4 |
| numeric | 2 |
| ________________________ | |
| Group variables | None |
Variable type: character
| skim_variable | n_missing | complete_rate | min | max | empty | n_unique | whitespace |
|---|---|---|---|---|---|---|---|
| date | 0 | 1 | 7 | 7 | 0 | 1 | 0 |
| group | 0 | 1 | 5 | 11 | 0 | 4 | 0 |
| n_s | 0 | 1 | 1 | 1 | 0 | 2 | 0 |
| sun | 0 | 1 | 5 | 5 | 0 | 2 | 0 |
Variable type: numeric
| skim_variable | n_missing | complete_rate | mean | sd | p0 | p25 | p50 | p75 | p100 | hist |
|---|---|---|---|---|---|---|---|---|---|---|
| tree_no | 0 | 1 | 2.50 | 1.13 | 1 | 1.75 | 2.5 | 3.25 | 4 | ▇▇▁▇▇ |
| length_mm | 0 | 1 | 17.67 | 3.53 | 12 | 15.00 | 17.5 | 20.25 | 25 | ▆▇▅▆▃ |
Note
✅ Key idea
skim() gives you n, missing count, mean, SD, and a tiny inline histogram for every numeric column — and counts for every character column — in one call. It’s the fastest way to sanity-check a dataset the moment you load it.
skimr — Doesn’t Replace, Just Speeds Up| Name | select(pine_df, length_mm… |
| Number of rows | 48 |
| Number of columns | 2 |
| _______________________ | |
| Column type frequency: | |
| numeric | 2 |
| ________________________ | |
| Group variables | None |
Variable type: numeric
| skim_variable | n_missing | complete_rate | mean | sd | p0 | p25 | p50 | p75 | p100 | hist |
|---|---|---|---|---|---|---|---|---|---|---|
| length_mm | 0 | 1 | 17.67 | 3.53 | 12 | 15.00 | 17.5 | 20.25 | 25 | ▆▇▅▆▃ |
| tree_no | 0 | 1 | 2.50 | 1.13 | 1 | 1.75 | 2.5 | 3.25 | 4 | ▇▇▁▇▇ |
Warning
⚠️ Watch out!
skim() is a first look, not a final answer.length_mm should be split by n_s
group_by() + summarize(), next.group_by() + summarize()group_by() + summarize()Note
🔮 Predict first: How many rows will the result have? (Hint: how many values does n_s take?)
# A tibble: 2 × 6
n_s n mean_mm med_mm sd_mm se_mm
<chr> <int> <dbl> <dbl> <dbl> <dbl>
1 n 24 20.4 20.5 2.45 0.5
2 s 24 14.9 15 1.91 0.39
group_by(n_s) splits the data by sidesummarize() collapses each group to one row of statssd_mm) can feed the next one (se_mm)📖 R4DS §3.5 — summarize()
# A tibble: 8 × 5
group n_s n mean_mm se_mm
<chr> <chr> <int> <dbl> <dbl>
1 cephalopods n 6 21 1.24
2 cephalopods s 6 15 0.58
3 crayfish n 6 21 1.24
4 crayfish s 6 15 0.58
5 salmon n 6 19.8 0.79
6 salmon s 6 12.8 0.4
7 snail n 6 19.8 0.79
8 snail s 6 16.8 0.6
group_by(group, n_s) splits by field team, then side — one row per combination.groups = "drop" turns off the grouping once you’re done — good habit, avoids surprises laterTip
🖐 Notice
Four teams × two sides = 8 rows. Does every team show the same shady-vs-sunny pattern, or do some disagree?
case_when() from Wrangling Your DataNote
🔮 Predict first: We’re grouping by the size_class bins ("short"/"medium"/"long") from Wrangling Your Data’s case_when(). How many rows will the summary have?
size_class_stats_df <- pine_df |>
mutate(
size_class = case_when(
length_mm < 16 ~ "short",
length_mm < 20 ~ "medium",
TRUE ~ "long"
)
) |>
group_by(size_class) |>
summarize(
n = sum(!is.na(length_mm)),
mean_mm = round(mean(length_mm, na.rm = TRUE), 2),
se_mm = round(sd(length_mm, na.rm = TRUE) / sqrt(n), 2)
)
size_class_stats_df# A tibble: 3 × 4
size_class n mean_mm se_mm
<chr> <int> <dbl> <dbl>
1 long 16 21.8 0.42
2 medium 17 17.3 0.28
3 short 15 13.7 0.3
Note
✅ Key idea
mutate() and group_by() + summarize() chain together like any other verbs — you don’t need to save the case_when() column separately first. This is the wrangling → describing pipeline in one block.
Today you:
length() trap and fixed it with sum(!is.na())skimr::skim()group_by() + summarize() to get every group’s stats in one table — including the case_when() categories from Wrangling Your DataTip
🖐 Before next class
Finish the worksheet: compute n, mean, SD, and SE for length_mm grouped by n_s, and again grouped by group. Save the summary table with write_csv().
Note
Up next — Graphing Effectively
stat_summary()ggsave() for a publication-ready figureWhen code breaks — and it will, that is normal:
library(tidyverse) loaded? Spelling? Did you forget na.rm = TRUE??function_name opens the help pageNote
✅ Key idea
Every working scientist googles error messages daily. Getting stuck is not failing — it is the job.