Prof. Marc Los Huertos • Pomona College
lm() and read the
summary output.
What this module covers. Simple linear regression describes the straight-line relationship between one continuous predictor and one continuous outcome. This tutorial builds the model from first principles, shows you how to fit and interpret
lm()output, diagnose assumption violations with residual plots, and draw valid inference from slope confidence intervals and F-tests.
Regression asks: if I know x, what is my best guess for y? The word simple means one predictor; linear means the relationship is a straight line.
Environmental applications are everywhere. Does PM2.5 increase as traffic volume rises? Does soil carbon decrease with distance from a riparian corridor? Does tree height predict biomass? In every case the method is the same: fit a line that minimizes prediction error, then ask how confident we are in its slope.
# Ozone vs. solar radiation from the built-in airquality dataset
data(airquality)
aq <- na.omit(airquality[, c("Ozone", "Solar.R", "Temp", "Wind")])
head(aq)
## Ozone Solar.R Temp Wind
## 1 41 190 67 7.4
## 2 36 118 72 8.0
## 3 12 149 74 12.6
## 4 18 313 62 11.5
## 7 23 299 65 8.6
## 8 19 99 59 13.8
Ozone concentration vs. temperature in New York, summer 1973. Each point is one day. The dashed line is a visual guess; the solid line is the least-squares fit.
The simple linear regression model is:
\[y_i = \beta_0 + \beta_1 x_i + \varepsilon_i\]
where:
The ordinary least-squares (OLS) estimator chooses \(\hat\beta_0\) and \(\hat\beta_1\) to minimize the sum of squared residuals:
\[\text{SSR} = \sum_{i=1}^n (y_i - \hat y_i)^2 \quad \text{where} \quad \hat y_i = \hat\beta_0 + \hat\beta_1 x_i\]
The closed-form solution is:
\[\hat\beta_1 = \frac{\sum(x_i - \bar x)(y_i - \bar y)}{\sum(x_i - \bar x)^2} = \frac{S_{xy}}{S_{xx}}\]
\[\hat\beta_0 = \bar y - \hat\beta_1 \bar x\]
Formula in words The slope is the covariance of x and y divided by the variance of x. The intercept is chosen so the line passes through (x̄, ȳ).
x <- aq$Temp
y <- aq$Ozone
b1 <- sum((x - mean(x)) * (y - mean(y))) / sum((x - mean(x))^2)
b0 <- mean(y) - b1 * mean(x)
knitr::kable(
data.frame(
Coefficient = c("Slope (β₁)", "Intercept (β₀)"),
Estimate = round(c(b1, b0), 3),
Interpretation = c("ppb per °F", "ppb when Temp = 0°F")
),
caption = "OLS estimates computed by hand."
)
| Coefficient | Estimate | Interpretation |
|---|---|---|
| Slope (β₁) | 2.439 | ppb per °F |
| Intercept (β₀) | -147.646 | ppb when Temp = 0°F |
lm()In practice, always use lm(). The formula syntax
y ~ x maps directly onto the statistical model \(y = \beta_0 + \beta_1 x +
\varepsilon\).
mod <- lm(Ozone ~ Temp, data = aq)
The fitted object mod contains everything: coefficients,
residuals, fitted values, the variance–covariance matrix of the
estimates, and more.
coef(mod) # intercept and slope
## (Intercept) Temp
## -147.64607 2.43911
fitted(mod)[1:6] # predicted values yhat
## 1 2 3 4 7 8
## 15.774291 27.969841 32.848061 3.578742 10.896071 -3.738588
residuals(mod)[1:6] # observed - predicted
## 1 2 3 4 7 8
## 25.225709 8.030159 -20.848061 14.421258 12.103929 22.738588
summary() Outputsummary(mod)
##
## Call:
## lm(formula = Ozone ~ Temp, data = aq)
##
## Residuals:
## Min 1Q Median 3Q Max
## -40.922 -17.459 -0.874 10.444 118.078
##
## Coefficients:
## Estimate Std. Error t value Pr(>|t|)
## (Intercept) -147.6461 18.7553 -7.872 2.76e-12 ***
## Temp 2.4391 0.2393 10.192 < 2e-16 ***
## ---
## Signif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
##
## Residual standard error: 23.92 on 109 degrees of freedom
## Multiple R-squared: 0.488, Adjusted R-squared: 0.4833
## F-statistic: 103.9 on 1 and 109 DF, p-value: < 2.2e-16
The output has four important sections:
Residuals. A five-number summary of the residuals. Ideally symmetric around zero. Large asymmetry suggests the normality assumption is violated.
Coefficients table. Each row is one model parameter.
| Column | Meaning |
|---|---|
Estimate |
\(\hat\beta\) — the OLS coefficient |
Std. Error |
SE(\(\hat\beta\)) — precision of the estimate |
t value |
\(\hat\beta\) / SE — the test statistic for \(H_0: \beta = 0\) |
Pr(>|t|) |
Two-tailed p-value for the t-test |
Residual standard error (RSE). \(\hat\sigma\) — typical size of a residual. Smaller is better; units match y.
R-squared. The proportion of variance in y explained by x. \(R^2 = 1 - \text{SS}_\text{res} / \text{SS}_\text{tot}\). Range 0–1; higher is better, but high \(R^2\) does not guarantee the model is correct.
F-statistic. Tests whether the overall model explains more variance than a model with no predictors (intercept only). For simple regression, \(F = t^2\).
📌 The slope is the key result In simple regression, the slope coefficient and its p-value are the primary result. A slope of 2.43 ppb/°F means: for each 1 °F rise in temperature, ozone increases by about 2.43 ppb on average. The p-value tests whether this relationship is distinguishable from zero given sampling variability.
Linear regression produces valid inference only when four assumptions hold. They are easiest to remember with the acronym LINE:
| Letter | Assumption | What “violation” looks like |
|---|---|---|
| L | Linearity | Curved pattern in residuals vs. fitted |
| I | Independence | Residuals correlated over time or space |
| N | Normality | Heavy-tailed or strongly skewed residuals |
| E | Equal variance (homoscedasticity) | Fan shape in residuals vs. fitted |
The most important are linearity and equal variance; the t-tests and confidence intervals are fairly robust to modest normality violations when n is large.
plot(mod) produces four standard diagnostic plots.
Always examine all four.
par(mfrow = c(2, 2), mar = c(4, 4, 3, 1))
plot(mod)
Four standard regression diagnostic plots for the ozone model. (1) Residuals vs. Fitted checks linearity and equal variance. (2) Q-Q plot checks normality. (3) Scale-Location is a second variance check. (4) Residuals vs. Leverage identifies influential points.
par(mfrow = c(1, 1))
Residuals vs. Fitted (top-left). Look for a horizontal band of points with no curve or funnel. A curved pattern indicates the relationship is not linear; a funnel indicates heteroscedasticity (unequal variance).
Normal Q-Q (top-right). Points should fall close to the diagonal line. Heavy tails (points curving upward at both ends) indicate the residuals are more extreme than a normal distribution predicts.
Scale-Location (bottom-left). The square root of the standardized residuals vs. fitted values. The fitted line should be roughly horizontal. An upward slope indicates that variance increases with the fitted value.
Residuals vs. Leverage (bottom-right). Identifies influential observations — points whose removal would substantially change the regression line. Points outside Cook’s distance contours (dashed lines) deserve scrutiny.
⚠️ High leverage vs. high influence A point has high leverage if its x-value is far from x̄ — it has the potential to pull the regression line. It has high influence if removing it actually changes the fit substantially. Both Cook’s distance and leverage are shown in plot 4.
# Cook's distance > 4/n is a common threshold
n <- nrow(aq)
cd <- cooks.distance(mod)
influential <- which(cd > 4 / n)
5 observation(s) have Cook’s D > 4/n (row indices: 23, 34, 63, 77, 80).
if (length(influential) > 0) {
knitr::kable(
aq[influential, ],
caption = paste0("Observations with Cook's distance > 4/", n,
" (potentially influential).")
)
}
| Ozone | Solar.R | Temp | Wind | |
|---|---|---|---|---|
| 30 | 115 | 223 | 79 | 5.7 |
| 62 | 135 | 269 | 84 | 4.1 |
| 99 | 122 | 255 | 89 | 4.0 |
| 117 | 168 | 238 | 81 | 3.4 |
| 121 | 118 | 225 | 94 | 2.3 |
confint(mod, level = 0.95)
## 2.5 % 97.5 %
## (Intercept) -184.818372 -110.473773
## Temp 1.964787 2.913433
The 95% CI for the slope is more informative than the p-value alone: it shows the range of plausible values for the true population slope. A narrow CI indicates the estimate is precise; a wide CI indicates high uncertainty.
new_x <- data.frame(Temp = seq(min(aq$Temp), max(aq$Temp), length.out = 200))
ci_fit <- predict(mod, newdata = new_x, interval = "confidence")
pi_fit <- predict(mod, newdata = new_x, interval = "prediction")
par(las = 1, mar = c(4.5, 4.5, 2, 1))
plot(Ozone ~ Temp, data = aq, pch = 20,
col = rgb(0.29, 0.49, 0.37, 0.55),
xlab = "Temperature (°F)", ylab = "Ozone (ppb)",
main = "Fitted regression with CI and PI")
# Prediction interval ribbon
polygon(c(new_x$Temp, rev(new_x$Temp)),
c(pi_fit[, "lwr"], rev(pi_fit[, "upr"])),
col = rgb(0.2, 0.35, 0.25, 0.08), border = NA)
# Confidence interval ribbon
polygon(c(new_x$Temp, rev(new_x$Temp)),
c(ci_fit[, "lwr"], rev(ci_fit[, "upr"])),
col = rgb(0.2, 0.35, 0.25, 0.20), border = NA)
lines(new_x$Temp, ci_fit[, "fit"], col = "#1e3a2f", lwd = 2)
lines(new_x$Temp, pi_fit[, "lwr"], col = "#4a7c5e", lwd = 1, lty = 2)
lines(new_x$Temp, pi_fit[, "upr"], col = "#4a7c5e", lwd = 1, lty = 2)
legend("topleft",
legend = c("Fitted line", "95% CI (mean)", "95% PI (individual)"),
col = c("#1e3a2f", rgb(0.2,.35,.25,.5), "#4a7c5e"),
lty = c(1, 1, 2), lwd = c(2, 6, 1), bty = "n", cex = 0.85)
Fitted regression line (dark) with 95% confidence band (grey) and 95% prediction interval (dashed). The CI captures uncertainty in the mean response; the PI captures uncertainty in a single future observation.
📌 CI vs. PI — a critical distinction The confidence interval captures uncertainty about the mean response at a given x (the population average). The prediction interval captures uncertainty about a single new observation. The PI is always wider because it must account for both the uncertainty in the mean and the random scatter of individual observations around that mean.
# Predict ozone at specific temperatures
predict_data <- data.frame(Temp = c(65, 75, 85, 95))
preds <- predict(mod, newdata = predict_data, interval = "confidence")
result <- cbind(predict_data, round(preds, 1))
names(result) <- c("Temp (°F)", "Predicted Ozone (ppb)", "95% CI Lower", "95% CI Upper")
knitr::kable(result, caption = "Predicted mean ozone at selected temperatures.")
| Temp (°F) | Predicted Ozone (ppb) | 95% CI Lower | 95% CI Upper |
|---|---|---|---|
| 65 | 10.9 | 3.3 | 18.5 |
| 75 | 35.3 | 30.6 | 40.0 |
| 85 | 59.7 | 54.0 | 65.3 |
| 95 | 84.1 | 74.7 | 93.4 |
⚠️ Do not extrapolate Predictions are only reliable within the range of the observed x values. The model has no information about the relationship outside that range — and the relationship may be nonlinear beyond the observed data.
# 1. Explore
plot(y ~ x, data = mydata)
# 2. Fit
mod <- lm(y ~ x, data = mydata)
# 3. Summarize
summary(mod)
confint(mod)
# 4. Diagnose
par(mfrow = c(2, 2)); plot(mod); par(mfrow = c(1, 1))
# 5. Predict
predict(mod, newdata = data.frame(x = new_values), interval = "confidence")
lm(y ~ x) fits the OLS line. The formula maps directly onto
\(y = \beta_0 + \beta_1 x +
\varepsilon\).
plot(mod) before reporting results. Curved residuals
invalidate the model; heteroscedasticity inflates standard errors.
predict() with interval = “confidence” for
the mean response and interval = “prediction” for
individual observations.
Using airquality, fit a linear model predicting
Ozone from Temp. (a) Report the slope, its 95%
CI, and the R². (b) Write one sentence interpreting the slope in plain
language, including units. (c) Is the intercept scientifically
meaningful here?
mod <- lm(Ozone ~ Temp, data = airquality)
summary(mod)
confint(mod)
Slope ≈ 2.4 ppb/°F: for each 1°F increase in temperature, predicted ozone rises by ~2.4 ppb (95% CI: ~2.0–2.8). R² ≈ 0.49 — temperature explains about half the variation in ozone. The intercept (~−147) is not meaningful: it predicts ozone at 0°F, which is impossible in this context.
Run par(mfrow=c(2,2)); plot(mod) on your model from
Exercise 1. Identify (a) any evidence of non-linearity in the Residuals
vs. Fitted plot, (b) any high-leverage points in the Scale–Location and
Cook’s distance plots, and (c) whether residuals appear approximately
normal in the Q–Q plot.
par(mfrow = c(2,2))
plot(mod)
The Residuals vs. Fitted plot shows a slight curve — ozone–temperature may have a non-linear component. Observations 117 and 62 appear repeatedly as potential outliers/leverage points. The Q–Q plot shows right-skew, consistent with ozone’s skewed distribution. A log transformation of Ozone may help.