Building a calibration curve to predict leaf area from paper tracing mass
2026-07-05
.qmd file holds your writing and your code and renders to Word, HTML, or PowerPoint`r `lm() and read every line of summary()predict() with confidence and prediction intervalsHow to use this worksheet
- Work through each part in order. Type the code into a new R script in Positron and run it line by line.
Code blocks marked ▶ Run this should be executed as written.
Blocks marked ✏️ Your turn ask you to write, modify, or interpret.
The Going further section is optional.
🔮 Predict before you run — and type, don’t paste
Before each ▶ Run this block, cover the output and predict what R will print. Then type the code yourself. Two reasons:
lm() formula, a wrong newdata column name) that you would otherwise spend real time hunting.📂 The same analysis, two ways — download both and compare
Today you get to feel the difference the Quarto intro talked about. These two files do the exact same regression — one as a plain script, one as a report:
regression_analysis_script.R — the analysis as a classic R script. Great for running the numbers. To share results you would copy them into a document by hand.regression_analysis_report.qmd — the analysis as a Quarto report. Same code, but wrapped in writing, with figures and numbers that render straight into a Word document.Open them side by side in Positron. As you work through this worksheet, notice which one you would rather hand in.
🧩 Chunk 1 — Set up the data (after lecture Chunk 1)
Parts 1–3: load the paper data, inspect it, and make the scatter plot.
▶ Run this at the top of your script:
The file paper_area_weights.xlsx contains 118 paper squares of known area (1–567 cm²) with their measured masses in grams. We will fit a regression to convert mass → area, then apply it to leaf tracings.
▶ Run this:
✏️ Your turn: Run dim(paper_df). How many rows and columns does the dataset have?
Rows:
Columns:
Column names and their roles:
area_cm2 → (response / explanatory / circle one)
mass_g → (response / explanatory / circle one)
▶ Run this:
✏️ Your turn: Fill in the table from the output above:
area_cm2 mean mass (g)
-------- -------------
1
4
16
64
100
400
✏️ Your turn: Is there a clear pattern — as area increases, what happens to mass? Is this what you expected from your knowledge of paper?
Your answer:
▶ Run this:
✏️ Your turn: Describe the relationship. Is it linear? Is the variance (spread of points) roughly constant across the range of mass values?
Shape of relationship (linear / curved):
Variance (constant / increases / decreases with X):
Any obvious outliers? Y / N
✏️ Your turn: In this study, which variable is the explanatory (X) and which is the response (Y)? Why does that direction make sense for our goal of predicting leaf area?
Explanatory (X):
Response (Y):
Reason for this direction:
💡 Key idea: In regression, X is what you measure (mass — easy with a balance), and Y is what you want to know (area — harder to measure on a complex leaf shape). We measure X to predict Y.
🧩 Chunk 2 — Fit & read the model (after lecture Chunk 2)
Parts 4–7: fit
lm(), decodesummary(), pull the equation and R².
lm()🔮 Predict first: area rises with mass. Before you read
summary()— will the slope be positive or negative, and roughly how many cm² per gram?
▶ Run this:
✏️ Your turn: Copy the Coefficients table output here:
Paste the Coefficients section of the summary() output:
summary() output line by line✏️ Your turn: Match each piece of output to its meaning. Fill in the blanks:
(Intercept) estimate = _____
→ This is "a" in Ŷ = a + bX.
It is the predicted area when mass = _____.
Does this make physical sense? (a piece of paper with zero mass has zero area) Y / N
mass_g estimate = _____
→ This is "b" (the slope).
It means: for every additional 1 gram of paper, area increases by _____ cm².
Biologically, 1 gram of this paper stock has _____ cm² of surface area.
Std. Error for mass_g = _____
→ The uncertainty in the slope estimate.
t value for mass_g = _____
→ slope / SE. This tests H₀: β = _____.
Pr(>|t|) for mass_g = _____
→ The p-value for the slope test. Is it significant at α = 0.05? Y / N
Multiple R-squared = _____
→ _____ % of the variation in area is explained by mass.
F-statistic = _____ on _____ and _____ df
→ Overall test of the model. p = _____
📖 Whitlock & Schluter §17.3 p. 551–553: The slope is tested with a t-statistic: t = b / SE_b, with df = n − 2. If H₀: β = 0 is rejected, mass significantly predicts area.
▶ Run this:
✏️ Your turn: Write the calibration equation for predicting area from mass:
area_cm2 = _____ × mass_g + _____
✏️ Your turn: Use the equation by hand to predict the area of a paper tracing that weighs 0.120 g. Show your work:
area = _____ × 0.120 + _____
= _____ cm²
🔮 Predict first: For uniform paper, predict R² — near 0, 0.5, or 1? Write your guess before you run it.
▶ Run this:
✏️ Your turn: What does R² = 0.9999 mean in plain language for our calibration?
Your answer:
✏️ Your turn: In ecology, you might see regression results with R² = 0.45. Is that a useful result? What does it mean?
Your answer:
📖 Whitlock & Schluter §17.4 p. 555: “R² measures the fraction of variation in Y that is explained by X.” A high R² indicates a tight fit; a low R² indicates high scatter around the line — but a low R² does not mean the relationship is not real or useful.
🧩 Chunk 3 — Visualize & check assumptions (after lecture Chunk 3)
Parts 8–10: plot the curve, then check the residual and QQ/Shapiro assumptions.
▶ Run this:
# Regression line with 95% confidence band --------------
calibration_plot <- paper_df %>%
ggplot(aes(x = mass_g, y = area_cm2)) +
geom_point(alpha = 0.5, size = 1.8) +
geom_smooth(method = "lm", se = TRUE,
color = "steelblue", fill = "lightblue") +
labs(
title = "Paper Calibration Curve",
subtitle = paste0("area = ", round(b_slope, 2),
" × mass + ", round(a_intercept, 3),
" | R² = ", round(r2_val, 4)),
x = "Paper Mass (g)",
y = "Area (cm²)"
) +
theme_minimal()
calibration_plot✏️ Your turn: Where is the shaded confidence band narrowest? Where is it widest? Why does the width vary?
Narrowest at:
Widest at:
Why it varies:
▶ Run this:
🔮 Predict first: If the line fits well, what should the residuals-vs-fitted plot look like — a flat random cloud, a curve, or a funnel? Predict, then run.
▶ Run this:
# Residuals vs Fitted — checks linearity and equal variance
resid_09_plot <- paper_resid_df %>%
ggplot(aes(x = fitted, y = residuals)) +
geom_point(alpha = 0.5) +
geom_hline(yintercept = 0, linetype = "dashed", color = "red") +
labs(
title = "Residuals vs Fitted Values",
x = "Fitted Values (cm²)",
y = "Residuals (cm²)"
) +
theme_minimal()
resid_09_plot✏️ Your turn: Describe the residual plot. Do the points form a random cloud around zero, or is there a pattern?
Pattern or random cloud?:
Any evidence of non-linearity (curved pattern)? Y / N
Any evidence of unequal variance (funnel shape)? Y / N
⚠️ Watch out! A funnel shape (residuals getting larger at higher fitted values) means variance is not constant — a violation of the equal variance assumption. A curved pattern means the relationship is not linear.
📖 Whitlock & Schluter §17.5 p. 559 (Figure 17.5-4): “A residual plot should show a roughly symmetric cloud of points with no pattern.”
▶ Run this:
# QQ plot of residuals — checks normality assumption ---
qq_resid_09_plot <- paper_resid_df %>%
ggplot(aes(sample = residuals)) +
stat_qq() +
stat_qq_line(color = "red", linewidth = 0.8) +
labs(
title = "Normal QQ Plot of Residuals",
x = "Theoretical Quantiles",
y = "Sample Quantiles"
) +
theme_minimal()
qq_resid_09_plot✏️ Your turn: Record the Shapiro-Wilk result:
W = _____ p = _____
Decision (normal / not normal):
✏️ Your turn: An important distinction — in regression, normality is checked on the residuals, not on Y or X directly. Why does that matter?
Your answer:
🧩 Chunk 4 — Predict & report (after lecture Chunk 4)
Parts 11–12: predict leaf area from tracing mass, then write the results paragraph.
🔮 Predict first: sunny tracing 0.092 g, shady 0.138 g. Which predicts the larger area? Does that match “shady leaves are bigger”? Predict before running.
▶ Run this:
# Direct calculation using the calibration equation ----
mass_sunny <- 0.092 # g — example sunny leaf tracing
mass_shady <- 0.138 # g — example shady leaf tracing
area_sunny <- b_slope * mass_sunny + a_intercept
area_shady <- b_slope * mass_shady + a_intercept
cat("Sunny tracing:", mass_sunny, "g →",
round(area_sunny, 2), "cm²\n")
cat("Shady tracing:", mass_shady, "g →",
round(area_shady, 2), "cm²\n")✏️ Your turn: Does the shady leaf have a larger predicted area? In Worksheet 04 we found shady leaves were significantly heavier. Does a larger predicted area match that finding? What does this tell you about the relationship between leaf weight and leaf area?
Sunny predicted area:
Shady predicted area:
Larger side:
Does larger area match the heavier weight from Worksheet 04? Y / N
What does this tell you about the relationship between weight and area in leaves?
predict() with intervals▶ Run this:
✏️ Your turn: The prediction interval is wider than the confidence interval. In your own words, explain why — what additional source of uncertainty does a prediction interval capture?
Your answer:
📖 Whitlock & Schluter §17.2 p. 549: “Confidence bands measure the precision of the predicted mean Y for each value of X. Prediction intervals measure the precision of the predicted single Y-values for each X.”
✏️ Your turn: Write a complete results paragraph using the information below. Follow the format from Lecture 05.
Information to include: - What the regression tested - F-statistic, df₁, df₂, and p-value (from the F-statistic line in summary()) - R² - The calibration equation (slope and intercept) - 95% confidence interval for the slope (use confint(paper_lm_model)) - Brief mention of assumption checks
Write your results paragraph here:
✍️ Now put it in a report
Open regression_analysis_report.qmd. Its Discussion section already pulls the slope, R², and p-value into the sentences with inline code. Paste your results paragraph in, press Render, and you have a Word document — no numbers copied by hand.
At this point you should be able to:
✏️ Your turn — before you move on: Run your entire script with Ctrl/Cmd + Shift + Enter. Does it run from top to bottom without errors?
Ran cleanly? Y / N
If not, what error appeared:
This section is optional — work through it if you finish early or want to push deeper.
▶ Try this:
✏️ Your turn: Are the residuals larger at bigger area values? What does that pattern (or lack of it) tell you about the equal-variance assumption?
Your observation:
Suppose you traced a leaf onto the same type of paper used in the calibration, cut it out, and weighed it. The tracing weighs 0.175 g.
▶ Try this:
✏️ Your turn: What is the predicted leaf area? What is the 95% prediction interval? How confident are you in this prediction?
Predicted area:
95% prediction interval: _____ to _____ cm²
Confidence in prediction:
The calibration data ranges from 1 to 567 cm². Suppose a very large leaf tracing weighs 5.2 g.
✏️ Your turn: Would you trust a prediction for a tracing mass of 5.2 g? Why or why not? (Hint: what is the largest mass in the calibration data?)
Max mass in calibration data:
Is 5.2 g within the calibration range? Y / N
Should you predict at 5.2 g? Y / N
Why:
📖 Whitlock & Schluter §17.2 p. 550: “Extrapolation is the prediction of Y at values of X beyond the range of X-values in the data. Extrapolation is problematic because there is no way to ensure the relationship between X and Y continues to be linear.”
In Worksheet 04 you ran a Welch’s t-test comparing leaf weight (grams) between sunny and shady sides and found a significant difference (p < 0.001). Now you have a calibration curve that converts tracing weight to area (cm²) — a more biologically meaningful measurement. You could use the regression equation as a mutate() step to add predicted area to a leaf data frame, then redo the t-test on area rather than weight.
▶ Sketch the code (do not necessarily run it — you would need actual tracing masses):
# How you WOULD apply the calibration to the leaf data
# (assuming trace_df has columns: side, tracing_mass_g)
# trace_df <- trace_df %>%
# mutate(predicted_area_cm2 = b_slope * tracing_mass_g + a_intercept)
#
# t.test(predicted_area_cm2 ~ side, data = trace_df,
# var.equal = FALSE, alternative = "two.sided")✏️ Your turn: What is the advantage of working in units of area (cm²) rather than raw weight (g) when comparing sunny vs. shady leaves?
Your answer:
figures/ folder should contain after this worksheetfigures/
├── paper_calibration_curve.png ← from Part 8
lm() error: the formula must be lm(Y ~ X, data) — response on the left, predictor on the right. Check column names with names(paper_df).coef() gives two values: coef(model)[1] is the intercept, coef(model)[2] is the slope for the first predictor.residuals() vs raw data: always apply shapiro.test() and ggplot(aes(sample = residuals)) to residuals(model), not to the raw Y column.predict() needs a tibble: newdata must be a data frame or tibble with the exact same column name as the predictor (mass_g). A typo here is the most common error.geom_smooth(se = TRUE) shows confidence band, not prediction interval. For prediction intervals, use predict() manually.💡 Key idea: The five-step logic of regression (plot → fit → check assumptions → interpret → predict) is the same framework you’ll use for multiple regression, ANOVA, and every other linear model in your career.
End of Worksheet 05. Next: Worksheet 06 — applying the calibration curve to measured leaf tracings and comparing sunny vs. shady leaf areas with a t-test.