Note: it’s a good idea to load libraries in a separate code chunk, so you don’t have to mess with the setup chunk again.
## ── Attaching core tidyverse packages ──────────────────────── tidyverse 2.0.0 ──
## ✔ dplyr 1.2.1 ✔ readr 2.1.5
## ✔ forcats 1.0.1 ✔ stringr 1.6.0
## ✔ ggplot2 4.0.3 ✔ tibble 3.3.0
## ✔ lubridate 1.9.5 ✔ tidyr 1.3.2
## ✔ purrr 1.1.0
## ── Conflicts ────────────────────────────────────────── tidyverse_conflicts() ──
## ✖ dplyr::filter() masks stats::filter()
## ✖ dplyr::lag() masks stats::lag()
## ℹ Use the conflicted package (<http://conflicted.r-lib.org/>) to force all conflicts to become errors
simulate_plant_growth <- function(n = 500,
alpha = 2,
beta1 = 20,
rse = 8,
moisture_range = c(0.1, 5.0)) {
soil_moisture <- runif(n, min = moisture_range[1], max = moisture_range[2])
error <- rnorm(n, mean = 8, sd = rse)
growth_rate <- alpha + beta1 * soil_moisture + error
data.frame(
soil_moisture = soil_moisture,
growth_rate = growth_rate
)
}
# Run simulation
set.seed(123)
data_single <- simulate_plant_growth()
head(data_single)## soil_moisture growth_rate
## 1 1.5091298 37.17777
## 2 3.9626952 84.75889
## 3 2.1039869 49.32840
## 4 4.4267853 99.25968
## 5 4.7082897 116.95386
## 6 0.3232268 15.75602
fit_single <- lm(growth_rate ~ soil_moisture, data = data_single)
# Extract fitted values and residuals
data_single <- data_single %>%
mutate(
fitted = predict(fit_single),
resid_model = growth_rate - fitted, # Residuals from line (SSE)
resid_mean = growth_rate - mean(growth_rate) # Deviations from Y_bar (SST)
)# Sums of squares
SST <- sum(data_single$resid_mean^2) # Total variance
SSE <- sum(data_single$resid_model^2) # Residual variance
R2_manual <- 1 - (SSE / SST)
R2_lm <- summary(fit_single)$r.squared
cat(sprintf("Manual R²: %.5f\nlm() R²: %.5f\n", R2_manual, R2_lm))## Manual R²: 0.92365
## lm() R²: 0.92365
Visualizing Residuals from the Regression Line
ggplot(data_single, aes(x = soil_moisture, y = growth_rate)) +
geom_point(alpha = 0.6) +
geom_smooth(method = "lm", color = "darkblue", se = FALSE) +
geom_segment(aes(xend = soil_moisture, yend = fitted), alpha = 0.3, color = "red") +
theme_classic() +
labs(
title = "Model Residuals (Vertical Distance to Regression Line)",
x = "Soil Moisture Content",
y = "Growth Rate"
)## `geom_smooth()` using formula = 'y ~ x'
simulate_two_populations <- function(n_total = 200, moisture_range = c(0.1, 2.0)) {
n_sub <- n_total / 2
# Define group-level parameters
params <- list(
A = list(alpha = 2.348, beta = -8, rse = 8.019),
B = list(alpha = 6.500, beta = 20.750, rse = 5.000)
)
# Generate categorical factor and predictor values
pop <- rep(c("A", "B"), each = n_sub)
moisture <- runif(n_total, min = moisture_range[1], max = moisture_range[2])
# Vectorized parameter mapping
alpha <- ifelse(pop == "A", params$A$alpha, params$B$alpha)
beta <- ifelse(pop == "A", params$A$beta, params$B$beta)
rse <- ifelse(pop == "A", params$A$rse, params$B$rse)
growth <- alpha + beta * moisture + rnorm(n_total, mean = 0, sd = rse)
data.frame(
population = factor(pop),
soil_moisture = moisture,
growth_rate = growth
)
}
set.seed(42)
data_two <- simulate_two_populations()
head(data_two)## population soil_moisture growth_rate
## 1 A 1.8381315 -2.726511
## 2 A 1.8804433 -4.317687
## 3 A 0.6436651 -10.846051
## 4 A 1.6778505 3.748172
## 5 A 1.3193165 -13.553388
## 6 A 1.0862823 -5.496143
m0 <- lm(growth_rate ~ 1, data = data_two)
# Model 1: Ignores population (pooled model)
m1 <- lm(growth_rate ~ soil_moisture, data = data_two)
# Model 2: Population main effect (different intercepts / additive model / ANCOVA)
m2 <- lm(growth_rate ~ soil_moisture + population, data = data_two)
# Model 3: Population interaction (different intercepts + different slopes)
m3 <- lm(growth_rate ~ soil_moisture * population, data = data_two)
# Model comparison table
data.frame(
Model = c("Soil Moisture Only", "+ Population (Separate Intercepts)", "* Population (Interaction)"),
R_Squared = c(summary(m1)$r.squared, summary(m2)$r.squared, summary(m3)$r.squared),
Adj_R_Squared = c(summary(m1)$adj.r.squared, summary(m2)$adj.r.squared, summary(m3)$adj.r.squared)
)## Model R_Squared Adj_R_Squared
## 1 Soil Moisture Only 0.01862786 0.01367144
## 2 + Population (Separate Intercepts) 0.76192562 0.75950862
## 3 * Population (Interaction) 0.91197041 0.91062302
## Analysis of Variance Table
##
## Model 1: growth_rate ~ 1
## Model 2: growth_rate ~ soil_moisture
## Model 3: growth_rate ~ soil_moisture + population
## Model 4: growth_rate ~ soil_moisture * population
## Res.Df RSS Df Sum of Sq F Pr(>F)
## 1 199 88119
## 2 198 86477 1 1641 41.475 9.012e-10 ***
## 3 197 20979 1 65499 1654.970 < 2.2e-16 ***
## 4 196 7757 1 13222 334.078 < 2.2e-16 ***
## ---
## Signif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
What could tell us Residual Sum of Squares (RSS or SSE) and Sum of Squares (Sum of Sq)?
\(R^2 = SSR/SST\) or \(R^2 = 1 - SSE/SST\)
SSE_m1 <- sum(residuals(m1)^2) # or deviance(m1)
SSE_m2 <- sum(residuals(m2)^2) # or deviance(m2)
SSE_m3 <- sum(residuals(m3)^2) # or deviance(m3)
# Calculate SSR for each model (SSR = SST - SSE)
SSR_m1 <- SST - SSE_m1
SSR_m2 <- SST - SSE_m2
SSR_m3 <- SST - SSE_m3
# Verify R^2 manually (matches summary()$r.squared exactly)
R2_m1 <- SSR_m1 / SST
R2_m2 <- SSR_m2 / SST
R2_m3 <- SSR_m3 / SST
R2_m1## [1] 0.7954379
# Visualizing regression lines per population
data_two <- data_two %>%
mutate(fitted = predict(m3))
# 3. Plot points, fitted parallel lines, and residual segments
ggplot(data_two, aes(x = soil_moisture, y = growth_rate, color = population)) +
geom_point(alpha = 0.6) +
# Draw fitted lines for model m2 (parallel slopes)
geom_line(aes(y = fitted), linewidth = 1) +
# Draw residual line segments connecting each point to its fitted line
geom_segment(aes(xend = soil_moisture, yend = fitted), alpha = 0.3) +
theme_minimal() +
labs(
title = "",
x = "Soil Moisture Content",
y = "Growth Rate",
color = "Population"
)Key Discussion Points & ConceptsWhat does the slope (\(\beta_1\)) represent?
It represents the expected average change in plant growth rate per unit increase in soil moisture content.
Why does \(R^2\) increase when adding population?\(R^2\) increases (from \(\approx 0.708\) to \(\approx 0.733\)) because adding population accounts for baseline group differences (intercepts), reducing the total unexplained variance (\(\text{SSE}\)).
Why does the interaction term (soil.moisture:populationB) not improve \(R^2\)?
The simulated data follow a simple linear regression model:
\[ Y_i = \alpha + \beta X_i + \varepsilon_i \]
where:
| Parameter | Meaning | What it controls | Effect on the plot |
|---|---|---|---|
n_total |
Sample size | Total number of observations (\(N\)). | Number of points displayed. |
alpha (a) |
Intercept | Expected growth when moisture = 0. | Moves the regression line up or down. |
beta (b) |
Slope | Change in expected growth for each unit increase in moisture. | Changes the slope and direction of the line. |
rse (s) |
Error standard deviation (\(\sigma\)) | Amount of random variation around the regression line. | Controls how spread out the points are. |
| Variable | Type | Description | Effect on the plot |
|---|---|---|---|
pop |
Categorical | Population group (A or B). | Determines colour or grouping. |
moisture |
Numeric (X) | Soil moisture content (predictor). | Determines horizontal position. |
growth |
Numeric (Y) | Plant growth rate (response). | Determines vertical position. |
pop): Identifies groups;
it only affects the fitted model if explicitly included as a
predictor.