
Using one measurement to predict another (Whitlock & Schluter Ch. 17)
2026-11-10
Note
✅ Where today comes in
lm()summary() output — slope, p-value, R²lm() — fit the linesummary() — read the resultplot(model) — check the residuals7.660e-03 as 0.007660geom_smooth(method = "lm") — draw the line_model_plotWe will cover:
the question regression answers,
the equation \(\hat{Y} = a + bX\),
what a residual is,
what R² tells you,
and the assumptions.
No code in this chunk — just the idea.
Linear regression fits the best straight line through a cloud of points so you can predict one number (Y) from another number (X).
| Role | Variable | On the plot |
|---|---|---|
| Explanatory (X) | what you can measure easily | x-axis |
| Response (Y) | what you actually want to know | y-axis |
X is usually the easy measurement. Y is the expensive one. Regression gives you an equation that converts X into Y.
📖 W&S §17.1 p. 540–541
Regression vs. the t-test:
| Test | The question |
|---|---|
| t-test | Do two groups have different means? |
| Regression | Does X predict Y? |
Use regression when:

W&S Example 17.1 — “the lion’s nose.” 32 male lions of known age.
Aging a wild lion is hard — you have to have watched it since it was a cub.
But a male lion’s nose darkens as it ages, and you can photograph a nose from a truck.
Fit the line once on lions of known age, and from then on you can estimate a lion’s age from a photograph.
📖 W&S §17.1, Example 17.1
The problem: we want leaf area in cm², but we don’t have a leaf-area meter. We have a balance.
The fix — a calibration curve:
Why it works: paper from one stock has a constant area per gram, so mass and area move together almost perfectly.
| Known area (cm²) | Squares weighed |
|---|---|
| 1 | 25 |
| 4 | 32 |
| 16 | 24 |
| 36 | 3 |
| 64 | 7 |
| 100 | 14 |
| 225–567 | 13 |
118 paper squares in all.
Remember paper_mass_g in our leaf data? That is the tracing mass. By the end of today it becomes an area.
\[\hat{Y} = a + bX\]
| Symbol | Name | What it means |
|---|---|---|
| \(\hat{Y}\) | predicted Y | what the line says Y should be |
| a | intercept | Y when X = 0 — where the line crosses |
| b | slope | how much Y changes per 1 unit of X |
Ours will read:
\[\widehat{\text{area}} = a + b \times \text{mass}\]
📖 W&S §17.1 pp. 543–545
The slope is the useful one.
b = the extra cm² you get for each extra gram of paper
That is just the area density of the paper stock — and it is the number that does the converting.
The intercept is a bookkeeping number.
a = the predicted area of a piece of paper weighing nothing
Physically that should be 0. A small value near zero is fine; it is measurement noise, not biology.

Red = the residuals for the lion data.
A residual is one point’s miss:
\[\text{residual} = \text{observed} - \text{predicted}\]
The line R gives you is the one that makes those red segments as small as possible overall. That is all “least squares” means — and R does the work.
📖 W&S §17.1 p. 542

R² runs from 0 to 1. It is the fraction of the variation in Y that the line accounts for.
| R² | Reads as |
|---|---|
| 1.00 | points sit on the line |
| 0.62 | X explains 62% of the variation |
| 0.00 | the line tells you nothing |
Both of these are good regressions. Nose blackness really does predict a lion’s age — just with scatter, because lions vary. Paper is manufactured to be uniform, so it has almost none.
In biology, R² of 0.3–0.7 is normal and useful. Don’t expect 0.99.
📖 W&S §17.4 p. 555
| The question it answers | |
|---|---|
| p-value | Is there really a relationship, or could this slope be chance? |
| R² | How tightly do the points follow the line? |
You can have a significant slope with a low R² — a real relationship with a lot of scatter. That is most of ecology.
Important
The slope is the hypothesis test.
H₀: the slope is 0 — X does not predict Y
Hₐ: the slope is not 0 — X does predict Y
summary() gives you the p-value for exactly that test, and we will find it on the next few slides.
📖 W&S §17.3 pp. 551–553
Four things need to be roughly true for the p-value and the intervals to mean anything (W&S §17.5):
| # | Assumption | How you check it |
|---|---|---|
| 1 | Linear — Y really does follow a straight line | the scatter plot, then plot(model) panel 1 |
| 2 | Independent — one observation doesn’t depend on another | your study design |
| 3 | Equal variance — the scatter is about the same all the way along | plot(model) panels 1 and 3 |
| 4 | Normal residuals — the misses are bell-shaped | plot(model) panel 2 |
Important
Check the residuals, not the raw data.
Normality here is about the misses — the red segments — not about Y itself. Y is supposed to go up with X; of course it isn’t bell-shaped.
The good news: once you have a fitted model, plot() draws all of these for you. No extra code.
Same habit as the t-test: check → decide → report.
📖 W&S §17.5 pp. 557–560
We will cover: loading the paper data, the scatter plot, fitting with lm(), and reading the summary() output line by line.
🛑 After this chunk: Activity Parts 1–3 (the study, load, plot, fit, read the summary).
Two columns, 118 rows: area_cm2 is the known area of each square, mass_g is what it weighed.
Three plain lines. aes(x = , y = ) puts the variable you set on x and the one you measured on y, and geom_point() draws the dots.
What you are looking for:
Here: straight, tight, nothing strange. Fit the line.
Note
Never fit a regression you haven’t looked at. A curve, a funnel, or one wild point all change what you should do next.
lm()Nothing prints. lm() hands back a model object that is holding the answer — summary() is what shows it to you.
The formula is the whole trick:
~~ reads as “is predicted by”So mass_g ~ area_cm2 is “mass is predicted by area.”
Warning
⚠️ Which way round? We cut the areas and measured the masses, so mass is the response. lm(area_cm2 ~ mass_g) runs fine too — it just asks R to treat ten areas we chose on purpose as if they were the noisy measurement.
summary() — the Whole Output
Call:
lm(formula = mass_g ~ area_cm2, data = paper_df)
Residuals:
Min 1Q Median 3Q Max
-0.066548 -0.001774 0.000907 0.002267 0.056127
Coefficients:
Estimate Std. Error t value Pr(>|t|)
(Intercept) -2.058e-03 1.389e-03 -1.481 0.141
area_cm2 7.660e-03 8.646e-06 885.964 <2e-16 ***
---
Signif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
Residual standard error: 0.01351 on 116 degrees of freedom
Multiple R-squared: 0.9999, Adjusted R-squared: 0.9999
F-statistic: 7.849e+05 on 1 and 116 DF, p-value: < 2.2e-16
There are only four numbers in here that you need, and we will take them one at a time:
area_cm2 → Estimate)(Intercept) → Estimate)area_cm2 → Pr(>|t|))Multiple R-squared)Coefficients:
Estimate Std. Error t value Pr(>|t|)
(Intercept) -2.058e-03 1.389e-03 -1.481 0.141
area_cm2 7.660e-03 8.646e-06 885.964 <2e-16
The Estimate column is the equation.
(Intercept) = -0.002058 → that is aarea_cm2 = 0.007660 → that is b, the slope7.660e-03 means 7.660 × 10⁻³ — move the decimal point three places left.
Tip
Prefer plain decimals? options(scipen = 999) turns scientific notation off and options(scipen = 0) turns it back on. We leave it on — with it off, p prints as <0.0000000000000002.
So the fitted line is:
\[\widehat{\text{mass}} = -0.0021 + 0.00766 \times \text{area}\]
Say the slope out loud, with units:
Every extra 1 cm² of this paper weighs another 0.00766 g.
Note
×10,000 → 76.6 g/m² — the paper’s grammage, the number on the side of the ream. Copy paper is sold at 75–80 g/m², so the slope is a property you can go and check.
The other three columns, in one line each:
| Column | What it is |
|---|---|
Std. Error |
how uncertain that estimate is |
t value |
Estimate ÷ Std. Error |
Pr(>|t|) |
the p-value for that row |
📖 W&S §17.3 p. 551
(Intercept) -2.058e-03 ... 0.141
area_cm2 7.660e-03 ... <2e-16
The slope row is the one that matters. Pr(>|t|) for area_cm2 is <2e-16 — R’s way of writing “smaller than I can print.”
The intercept row: p = 0.141, so the intercept is not distinguishable from 0 — exactly what physics says it should be. A piece of paper with no area has no mass.
Tip
Which p-value do I report?
The slope’s. That is the biological question. Nobody reports the intercept’s p-value unless zero means something special.
Note
Never write p = 0. When R prints <2e-16, you write p < 0.001.
Residual standard error: 0.01351 on 116 degrees of freedom
Multiple R-squared: 0.9999, Adjusted R-squared: 0.9999
F-statistic: 7.849e+05 on 1 and 116 DF, p-value: < 2.2e-16
Where the df come from:
Use Adjusted R-squared once you have more than one X; with a single X, report Multiple R-squared.
Important
R² = 0.9999 does not mean “everything is fine.” It means the points are close to the line. Whether the assumptions hold is a separate question — and we check it two slides from now.
🛑 Do Activity Parts 1–3 now
Load the paper data, make the scatter plot, fit lm(mass_g ~ area_cm2), and read the slope, the p-value and R² off the summary output.
We will cover: drawing the fitted line, reading plot(model), converting the whole column of tracings, averaging away the pseudoreplication, and the paired t-tests.
🛑 After this chunk: Activity Parts 4–7 (check the residuals, convert, average, paired t-tests).
One extra line. geom_smooth(method = "lm") fits the same regression again, just for drawing, and adds:
The band is narrowest in the middle of the data and flares at the ends — the line is pinned down best where you have the most data. Here it is so narrow you can barely see it.
Note
geom_smooth() defaults to a wiggly curve. method = "lm" is what makes it a straight regression line.

This is the version you’d put in a report — and it is only two additions to the plot you already made:
labs() — real axis labels with unitstheme_minimal() — drop the grey backgroundTip
Don’t type this today. Get the plain geom_point() + geom_smooth(method = "lm") working first. Labels and themes are the last thing you add, once you know the plot is right.
plot(model)par(mfrow = c(2, 2)) just says “put the next four plots in a 2×2 grid.”
| Panel | Name | Checks |
|---|---|---|
| 1 | Residuals vs Fitted | linear? equal scatter? |
| 2 | Normal Q-Q | residuals bell-shaped? |
| 3 | Scale-Location | equal scatter again |
| 4 | Residuals vs Leverage | any one point running the show |
Panel 1 is the one to look at first.
Look at panel 1 again. The points fan out — a 1 cm² square misses by hundredths of a cm², a 567 cm² square misses by several cm².
That is a funnel: assumption 3 (equal variance) does not hold.
# A tibble: 10 × 2
area_cm2 biggest_miss
<dbl> <dbl>
1 1 0
2 4 0
3 16 0
4 36 0.01
5 64 0.01
6 100 0.02
7 225 0.02
8 400 0.06
9 500 0.06
10 567 0.07
Important
A great R² and a bad residual plot can live together. R² says the points are near the line. The residual plot says the size of the miss depends on how big the square is — which it does, because a balance has percentage error, not fixed error.
So what do we do?
The model says
\[\text{mass} = a + b \times \text{area}\]
We have the mass and want the area, so rearrange:
\[\text{area} = \frac{\text{mass} - a}{b}\]
coef(paper_model), then [1] and [2] — and it prints plain decimalsNote
×10,000 → 76.6 g/m² — the paper’s grammage, the number on the side of the ream. Copy paper is sold at 75–80 g/m², so the slope is a property you can check against the label.
A tracing weighing 0.138 g is
\[\frac{0.138 - (-0.002058)}{0.007660} = 18.3 \text{ cm}^2\]
# Load our leaf data ----------------------------------
leaf_df <-
read_excel("data/2026_09_03_data_sci_leaf_area.xlsx") %>%
clean_names()
# Every tracing mass becomes an area ------------------
leaf_df <- leaf_df %>%
mutate(area_cm2 = (paper_mass_g - intercept) / slope)
leaf_df %>%
select(teams, shade, mass_g, paper_mass_g, area_cm2)# A tibble: 53 × 5
teams shade mass_g paper_mass_g area_cm2
<chr> <chr> <dbl> <dbl> <dbl>
1 12345 sunny 0.4 0.21 27.7
2 12345 sunny 0.48 0.21 27.7
3 12345 sunny 0.34 0.21 27.7
4 12345 sunny 0.65 0.24 31.6
5 12345 sunny 0.27 0.15 19.9
6 12345 sunny 0.43 0.34 44.7
7 12345 shady 0.39 0.29 38.1
8 12345 shady 0.45 0.25 32.9
9 12345 shady 0.6 0.23 30.3
10 12345 shady 0.57 0.28 36.8
# ℹ 43 more rows
This is the payoff. paper_mass_g was the mass of each leaf’s tracing — now every leaf has an area_cm2.
Note
The NAs are honest. One team never weighed their tracings, so those leaves have no area. R keeps the gap instead of inventing a number.
# A tibble: 10 × 4
teams shade area_cm2 mass_g
<chr> <chr> <dbl> <dbl>
1 12345 shady 34.0 0.448
2 12345 sunny 29.9 0.428
3 Oscar_Karl shady NaN 0.400
4 Oscar_Karl sunny NaN 0.339
5 fighting_mongooses shady 35.7 0.653
6 fighting_mongooses sunny 32.2 0.544
7 leaf_oglers shady 36.3 0.588
8 leaf_oglers sunny 37.4 0.599
9 still_loading shady 42.5 0.526
10 still_loading sunny 53.7 0.789
Note
You met this in T-Tests II. Same tree, same pairing — now in cm² instead of grams.
Each team measured both sides, so the team is the pair.
| mean diff | t | df | p | |
|---|---|---|---|---|
| area | −1.2 cm² | −0.34 | 3 | 0.76 |
| mass | −0.016 g | −0.25 | 4 | 0.81 |
Note
A real answer of “we could not detect a difference” is still an answer.
The template — fill in from your summary() output:
[What predicted what] (linear regression: F(df₁, df₂) = ___, p ___, R² = ___). The fitted equation was Y = ___ + ___ × X.
Ours:
Paper area was a very strong predictor of paper mass (linear regression: F(1, 116) = 784,900, p < 0.001, R² = 0.9999). The calibration equation was mass = −0.0021 + 0.00766 × area, equivalent to 76.6 g/m². Residual scatter increased with square size, so the largest squares are the least well described.
Where every number comes from:
| In the sentence | In summary() |
|---|---|
| F and both df | the F-statistic: line |
| p | that same line |
| R² | Multiple R-squared |
| intercept | (Intercept) → Estimate |
| slope | area_cm2 → Estimate |
Always include: the test name, F with both df, p, R², and the equation. Add a line about the assumptions you checked.
📖 W&S §17.3 p. 553
🛑 Do Activity Parts 4–7 now
plot(model) and say whether the assumptions are metpaper_mass_g column into areasThe ideas:
The code:
lm(Y ~ X, data = df) — fit itsummary(model) — slope, intercept, p, R²geom_smooth(method = "lm") — draw itplot(model) — check it7.660e-03 = 0.007660 — reading scientific notationscripts/11_regression.R — the code is given; your answers are graded📖 W&S Ch. 17:
Up next — Lecture 12:
GSODRlm() — temperature predicted by date