Skip to main content

Activity 11 — Linear Regression

Calibrate paper, convert tracings to leaf area, then test sunny against shady

tidyverse
descriptive-stats

Hands-on companion to the Regression lecture: fit a calibration line with lm(), check the residuals with plot(model), convert tracing masses into leaf areas, average away the pseudoreplication, and finish with paired t-tests on area and on mass. Students hand in the finished script with their answers.

Author

Bill Perry

Published

November 10, 2026

In-class Activity 11: Linear Regression

Today’s Objectives

  1. Fit a calibration line with lm(Y ~ X, data) — the measured variable on the left
  2. Check the regression assumptions with plot(model)
  3. Use the intercept and slope to turn a tracing mass into a leaf area
  4. Average to one value per team per side, so there is no pseudoreplication
  5. Finish with paired t-tests on leaf area and leaf mass
Tip📂 Files for today
ImportantWhat you hand in
  • All the code is written for you. Run it chunk by chunk and watch what comes out.
  • You answer four questions: the question of the study, the hypotheses, whether the regression assumptions are met, and what the t-tests mean.
  • Hand in scripts/11_regression.R. Run it top to bottom (Ctrl/Cmd + Shift + Enter) first — it must run with no errors.

Keep the plots plain today — no labels, colors, or themes. The goal is to read them.


Part 1 · The study

We picked a tree and took leaves from the sunny side and from the shady side. Each team traced their leaves onto paper, cut out the tracings, and weighed them.

A tracing mass is not an area. So we also weighed pieces of the same paper cut to areas we already knew. That is what the regression is for.

✏️ Q1: What is the question this study is asking? One sentence.

✏️ Q2: What are the hypotheses — the null and the alternative, for leaf area and for leaf mass?


Part 2 · Read the calibration data

▶ Run this:

paper_df <- read_excel("data/paper_area_weights.xlsx") %>%
  clean_names()

paper_df

118 pieces of paper. area_cm2 is what we cut it to; mass_g is what the balance said.


Part 3 · The regression

We chose the areas and measured the masses. So area goes on x and mass goes on y.

The variable you measured, with the error in it, always goes on the y axis. The one you set goes on x.

▶ Run this:

paper_df %>%
  ggplot(aes(x = area_cm2, y = mass_g)) +
  geom_point() +
  geom_smooth(method = "lm")

paper_model <- lm(mass_g ~ area_cm2, data = paper_df)

summary(paper_model)

These numbers are tiny, so R prints them in scientific notation:

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

7.660e-03 means 7.660 × 10⁻³ — move the decimal point three places left to get 0.007660. Same for the intercept: -2.058e-03 is -0.002058.

NoteThe slope is a real property of the paper

The area_cm2 slope is 0.007660 — the mass, in grams, of 1 cm² of this paper.

Multiply by 10,000 (the number of cm² in a m²) and you get 76.6 g/m².

That is the paper’s grammage — the number printers put on the side of the ream. Standard copy paper is sold at 75–80 g/m², so your regression just recovered a property of the paper that you can go and check against the label.

This is why the measured variable goes on y. Fit it the other way round and the slope is 130.5 cm²/g, which is correct but not a number you can look up anywhere.

TipOptional — if you’d rather not read scientific notation
options(scipen = 999)   # scientific notation off
options(scipen = 0)     # and back on again

We leave it on in this activity. With it off, the slope’s p-value prints as <0.0000000000000002 instead of <2e-16, which is worse, not better.


Part 4 · Are the assumptions met?

▶ Run this:

# All four residual plots in one call
par(mfrow = c(2, 2))
plot(paper_model)

# Put the plotting window back to one panel
par(mfrow = c(1, 1))
Panel Name What it checks
1 Residuals vs Fitted straight line? equal scatter?
2 Normal Q-Q are the misses bell-shaped?
3 Scale-Location is the scatter constant?
4 Residuals vs Leverage is any one point in charge?

✏️ Q3: Looking at the four plots, are the assumptions of the regression met? Say what you see in each plot, not what you hope to see. If something looks wrong, say which plot and what it means.


Part 5 · Turn every tracing mass into a leaf area

The model you just fitted says

\[\text{mass} = \text{intercept} + \text{slope} \times \text{area}\]

We have the mass of each tracing and we want its area, so rearrange it:

\[\text{area} = \frac{\text{mass} - \text{intercept}}{\text{slope}}\]

▶ Run this — type the two numbers in from the Coefficients table in Part 3, written out of scientific notation:

# Straight off the summary() output above
intercept <- -0.002058    # from -2.058e-03
slope     <-  0.007660    # from  7.660e-03

# Paper is sold by mass per square METRE. 1 m2 = 10000 cm2
slope * 10000

leaf_df <- read_excel("data/2026_09_03_data_sci_leaf_area.xlsx") %>%
  clean_names() %>%
  mutate(area_cm2 = (paper_mass_g - intercept) / slope)

leaf_df

Typing the numbers in by hand is fine here, and it keeps the arithmetic in plain sight. Rounding to six decimals changes nothing — the t, df and p you get in Part 7 are identical either way.

Some leaves have NA for area — that team never weighed their tracings, and R keeps the gap instead of inventing a number.

TipOptional — let R hand you the numbers

You don’t have to type the coefficients. coef() pulls them out of the model:

coef(paper_model)

# One at a time, by position
coef(paper_model)[1]   # the intercept
coef(paper_model)[2]   # the slope

# Or by name, which is harder to get wrong
coef(paper_model)["(Intercept)"]
coef(paper_model)["area_cm2"]

# So Part 5 could be written with no typed numbers at all.
# A second copy, so the rest of the script keeps using
# the version you typed in
leaf_coef_df <- leaf_df %>%
  mutate(area_cm2 = (paper_mass_g - coef(paper_model)[1]) /
                     coef(paper_model)[2])

# Same answer either way
head(leaf_df$area_cm2, 3)
head(leaf_coef_df$area_cm2, 3)

Two things worth noticing: coef() prints plain decimals, so there is no scientific notation to convert — and you cannot mistype a digit. Typing the numbers in keeps the arithmetic visible. Either way is fine.


Part 6 · One value per team per side

Leaves from the same team on the same side of the tree are not independent — they came off the same branch of the same tree. Treating all 53 leaves as independent is pseudoreplication, and it makes your p-value look far better than the study earned.

The fix: average them, so each team gives one sunny value and one shady value.

▶ Run this:

team_df <- leaf_df %>%
  group_by(teams, shade) %>%
  summarize(area_cm2 = mean(area_cm2, na.rm = TRUE),
            mass_g   = mean(mass_g,   na.rm = TRUE),
            .groups  = "drop")

team_df

Part 7 · Paired t-tests

Each team measured both sides of the same tree, so sunny and shady are paired within a team. A paired test needs one row per team, so reshape first.

▶ Run this:

wide_df <- team_df %>%
  pivot_wider(names_from = shade,
              values_from = c(area_cm2, mass_g))

wide_df

# Paired t-test on leaf AREA
area_paired_model <- t.test(wide_df$area_cm2_shady,
                            wide_df$area_cm2_sunny,
                            paired = TRUE)

area_paired_model

# Paired t-test on leaf MASS
mass_paired_model <- t.test(wide_df$mass_g_shady,
                            wide_df$mass_g_sunny,
                            paired = TRUE)

mass_paired_model

Check the df on each. One team has no tracings at all, so the area test uses 4 teams and the mass test uses 5.

⚠️ Watch out! t.test(area_cm2 ~ shade, paired = TRUE) gives an error — R doesn’t allow paired with a formula. That is why we made the wide table.

✏️ Q4: What do you conclude from the two paired t-tests? For each one give the mean difference, t, df and p, then say in plain words what it means for sunny versus shady leaves. Do the two tests agree?


Homework — sugar maple

Write your own script. Hand it in with the four answers as # comments.

Do the same analysis on leaves I collected from sugar maple trees: data/2026_09_02_sugar_maple_leaf_area.xlsx.

The steps are the same as Parts 1–7, and most lines change only by a name. There is a bare, code-free guide at the bottom of the skeleton script to walk you through it.

One difference. These leaves went through a scanner, so the area is already in the file as total_area_cm2 — there is no tracing paper to convert. Your regression is therefore leaf mass against leaf area, which gives you the mass of 1 cm² of leaf instead of paper.

The columns you need are tree, side, total_area_cm2 and mass_g.

Answer the same four questions for sugar maple:

  1. What is the question of the study?
  2. What are the hypotheses?
  3. Are the regression assumptions met? Say what you see in each of the four plots, and whether it is better or worse behaved than the paper.
  4. What do you conclude from the two paired t-tests? Which side has the bigger leaves, and is the result clearer or murkier than the class data? Why might that be?
TipTwo things to think about
  • Each tree has a sunny side and a shady side, so the tree is the pair — just as the team was in class.
  • Two leaves from the same side of the same tree are not independent either. Average them first, exactly as in Part 6.

End of Activity 11. Next: real climate data, where the slope of a regression on time becomes a rate of change per year.