Date: October 15, 2026
One predictor was never the plan.
https://bsky.app/profile/andrew.heiss.phd/post/3ly6hqr3mtk2z
| Week | Model | What was new |
|---|---|---|
| 5 | \(Y = b_0 + b_1X\) | one continuous predictor |
| 6 | \(Y = b_0 + b_1D\) | the predictor can be a group |
| 7 | \(SS_{total} = SS_{model} + SS_{res}\) | how well the line fits |
Everything so far has been one predictor at a time. That is almost never the model you want.
Overall, we are providing a model to give us a “best guess” on predicting our outcome
\[ Y_i = b_0 + b_1X_i + e_i \]
Once we have a model, we will be able to interpret the coefficients. For a bivariate regression (simple regression), this was fairly straightforward
We would look to the beta (effect size) and interpret it as: for every 1 unit increase in our X variable, there will be beta units increase in our Y variable.
\[Y_i = b_0 + b_1X_{1i} + b_2X_{2i} + \dots + b_kX_{ki} + e_i\]
Important
That phrase — holding the other predictors constant — is the entire week. It is not decoration. It changes what the number means.
The fire department wants to know what drives the cost of a fire.
They hand you 300 fires: how many trucks were dispatched, how many acres burned, and the damage in thousands of dollars.
This relationship can also be represented by a correlation, or a regression
Pearson's product-moment correlation
data: fire_data$trucks and fire_data$damage
t = 24.728, df = 298, p-value < 0.00000000000000022
alternative hypothesis: true correlation is not equal to 0
95 percent confidence interval:
0.7790614 0.8539156
sample estimates:
cor
0.8199638
Call:
lm(formula = damage ~ trucks, data = fire_data)
Residuals:
Min 1Q Median 3Q Max
-73.661 -16.168 1.857 15.619 76.910
Coefficients:
Estimate Std. Error t value Pr(>|t|)
(Intercept) 8.8397 3.2784 2.696 0.00741 **
trucks 12.7299 0.5148 24.728 < 0.0000000000000002 ***
---
Signif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
Residual standard error: 25.26 on 298 degrees of freedom
Multiple R-squared: 0.6723, Adjusted R-squared: 0.6712
F-statistic: 611.5 on 1 and 298 DF, p-value: < 0.00000000000000022
Based on the visualization, the correlation and the regression, what would we conclude from this analysis?
A linear regression was used to examine the impact of number of trucks on the scene and the amount of damage that was done by a fire. The number of trucks positively impacted the damage done by the fire (b = 11.67, p < .001), suggesting that as more trucks are present, the fire does more damage.
“Trucks strongly predict damage. Send fewer trucks.”
Warning
Every number in that sentence is correct. The recommendation would get people killed.
Before you fit anything, sketch what you think is going on. Arrows for “causes.”
trucks → damage?A variable that causes both your predictor and your outcome is a confound. Leave it out and your slope absorbs its effect.
These sketches have a name. Remember when we talked about Directed Acyclic Graphs (DAG’s)?
Big fires get more trucks and do more damage. The slope we estimated was mostly fire size wearing a truck costume.
Call:
lm(formula = damage ~ trucks + acres, data = fire_data)
Residuals:
Min 1Q Median 3Q Max
-34.190 -9.226 0.103 8.002 40.010
Coefficients:
Estimate Std. Error t value Pr(>|t|)
(Intercept) 10.9421 1.6520 6.624 0.000000000164 ***
trucks -2.6306 0.5794 -4.541 0.000008163552 ***
acres 4.0861 0.1378 29.645 < 0.0000000000000002 ***
---
Signif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
Residual standard error: 12.72 on 297 degrees of freedom
Multiple R-squared: 0.9172, Adjusted R-squared: 0.9167
F-statistic: 1646 on 2 and 297 DF, p-value: < 0.00000000000000022
trucks: +12.73 → −2.63. The sign flipped.acres: +4.09 per acreEach additional truck is associated with $2,630 less damage — among fires of the same size.
Before
We compared a 2-truck fire to a 9-truck fire. Those are not the same fire. One was a kitchen; one was a neighborhood.
After
We compare 2-truck and 3-truck fires of the same acreage. Now the extra truck can only help.
“Holding constant” is a comparison, not a magic word
\(b_1\) answers: among cases that are identical on every other predictor in the model, what is the difference in \(Y\) per unit of \(X_1\)?
Identical on the predictors you included. Nothing else.
“The current set of analyses sought to examine the influence of the number of firetrucks on the scene and the amount of damage that a fire caused, controlling for the size of the fire. The damage amount (in \$1,000s) was regressed on the number of firetrucks present and the size of the fire (in acres burned). The overall multiple regression was statistically significant ( \(R^2\) = .92, p < .001), meaning the two variables (Number of Trucks and Size of the Fire) accounted for 92% of the variance in damage done. Each of the two independent variables also had a statistically significant effect on damage (p’s < .001). The number of trucks was negatively related to the amount of damage done (b = -2.63), meaning that for each additional truck that was on the scene, there was a $2,630 reduction in damage, controlling for the size of the fire. Additionally, the size of the fire increased the amount of damage by approximately $4,000 for each additional acre burned.”
When you leave out a variable that is related to both \(X\) and \(Y\), its effect does not vanish — it gets folded into the coefficients you did estimate.
You cannot tell which from the output. You can only tell from what you chose to measure.
Models that are underfit, overfit or just-right
Note
This example will focus mostly on the model comparisons
# Step 1: Simulate the Data
n <- 150 # Number of students
# Simulate total daily computer use in hours
computer_use <- rnorm(n, mean = 6, sd = 1.5)
# Simulate hours spent on stats homework, now as a whole number
stats_homework <- round(0.7 * computer_use + rnorm(n, 0, 1))
# Ensure no negative values after rounding
stats_homework[stats_homework < 0] <- 0
stats_homework[stats_homework > computer_use] <- 1
# Simulate frustration level (driven mostly by stats homework)
# Scale of 1-100
frustration <- 10 + (2 * computer_use) + (8 * stats_homework) + rnorm(n, 0, 10)
# Create a dataframe and clean up any impossible values
student_data <- tibble(
frustration = frustration,
computer_use = computer_use,
stats_homework = stats_homework
) %>%
filter(frustration > 0, computer_use > 0)
# Bin the 'stats_homework' variable
student_data <- student_data %>%
mutate(
stats_homework_bin = cut(stats_homework,
breaks = quantile(stats_homework, probs = seq(0, 1, by = 1/3)),
include.lowest = TRUE,
labels = c("Low", "Medium", "High"))
)We have collected data on graduate students and their weekly frustration levels. Data that were also collected included the amount of time they were spending on their computer as well as the amount of statistics homework that they had.
Taking a bivariate look at things, we might have an initial model like this:
\[ Frustration=b_0 + b_1Comp.Hours + e_i \]
p1 <- student_data %>%
ggplot(aes(x = computer_use, y = frustration)) +
geom_point(alpha = 0.5) +
geom_smooth(method = "lm",
se = FALSE,
color = "darkorange") +
labs(
title = "Frustration & Computer Use",
subtitle = "Linear Fit",
x = "Daily Computer Use (Hours)",
y = "Frustration Level (1-100)"
) +
theme_minimal()
p2 <- student_data %>%
ggplot(aes(x = computer_use, y = frustration)) +
geom_point(alpha = 0.5) +
geom_smooth(method = "loess",
se = FALSE,
color = "darkorange") +
labs(
title = "Frustration & Computer Use",
subtitle = "Loess Fit",
x = "Daily Computer Use (Hours)",
y = "Frustration Level (1-100)"
) +
theme_minimal()
p1 + p2https://strengejacke.github.io/sjPlot/reference/tab_corr.html
| Frustration | Computer Use | Statistics Homework | |
|---|---|---|---|
| Frustration | |||
| Computer Use | 0.717*** | ||
| Statistics Homework | 0.845*** | 0.751*** | |
| Computed correlation used pearson-method with listwise-deletion. | |||
All of these are checked when using check_model() within the easystats library
Call:
lm(formula = frustration ~ computer_use, data = student_data)
Residuals:
Min 1Q Median 3Q Max
-30.562 -8.140 1.604 7.439 33.613
Coefficients:
Estimate Std. Error t value Pr(>|t|)
(Intercept) -1.1840 4.5220 -0.262 0.794
computer_use 9.2552 0.7427 12.462 <0.0000000000000002 ***
---
Signif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
Residual standard error: 13.14 on 147 degrees of freedom
Multiple R-squared: 0.5137, Adjusted R-squared: 0.5104
F-statistic: 155.3 on 1 and 147 DF, p-value: < 0.00000000000000022
student_data %>%
ggplot(aes(x = computer_use, y = frustration,
color = stats_homework_bin)) +
geom_point(alpha = 0.5) +
# Fit a line FOR EACH GROUP to show the true relationship
geom_smooth(method = "lm", se = FALSE) +
labs(
title = "Frustration & Computer Use by Stats",
x = "Daily Computer Use (Hours)",
y = "Frustration Level (1-100)",
color = "Stats HW (Hours)"
) +
theme_bw()
Call:
lm(formula = frustration ~ computer_use + stats_homework, data = student_data)
Residuals:
Min 1Q Median 3Q Max
-19.5242 -7.7787 0.5553 6.0824 23.6036
Coefficients:
Estimate Std. Error t value Pr(>|t|)
(Intercept) 6.0628 3.4473 1.759 0.08073 .
computer_use 2.4187 0.8416 2.874 0.00466 **
stats_homework 8.3376 0.7712 10.811 < 0.0000000000000002 ***
---
Signif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
Residual standard error: 9.828 on 146 degrees of freedom
Multiple R-squared: 0.7299, Adjusted R-squared: 0.7262
F-statistic: 197.3 on 2 and 146 DF, p-value: < 0.00000000000000022
Maybe we would like to test to see if including the Stats Homework variable improved the model
Since the first regression is nested within the second, we can run an ANOVA
Analysis of Variance Table
Model 1: frustration ~ computer_use
Model 2: frustration ~ computer_use + stats_homework
Res.Df RSS Df Sum of Sq F Pr(>F)
1 147 25391
2 146 14102 1 11289 116.88 < 0.00000000000000022 ***
---
Signif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
| Statistic | Meaning | Interpretation |
|---|---|---|
Res.Df (Residual Degrees of Freedom) |
Remaining degrees of freedom after estimating parameters | 148 for Model 1, 147 for Model 2 (one more parameter estimated) |
RSS (Residual Sum of Squares) |
Total unexplained variance by the model | Decreases substantially from 24,468 → 13,620 with the new predictor, showing improved fit |
Df (Difference in df) |
Number of parameters added | 1 (adding stats_homework) |
Sum of Sq (Difference in RSS) |
Variance explained by the added predictor | 10,848 units of variance explained by stats_homework |
F |
F-statistic testing whether this variance reduction is significant | 117.07 — very large, meaning substantial improvement |
Pr(>F) |
p-value associated with F |
Backward elimination
Start with the full model (all potential predictors)
Predictors eliminated one-at-a-time from the model until we cannot improve the model any further
Forward selection
Start with nothing in the model
Continue to add predictors one-at-a-time until we cannot find any predictors that improve the model any further
ALWAYS start with theory and previous work
You should NEVER just go into data with just a lot of predictors and backward selection in mind. 🧨
520 students, one survey 😵💫
A student-stress survey: everything rated 1–5. The column names are, charitably, a disaster — so we rename by position.
vars n mean sd median min max range skew kurtosis se
sleep_quality 1 520 3.12 1.10 3.0 1 5 4 0.01 -0.66 0.05
headaches 2 520 2.18 1.25 2.0 1 5 4 0.75 -0.48 0.05
academic_perf 3 520 3.33 1.06 3.0 1 5 4 -0.34 -0.28 0.05
study_load 4 520 2.75 1.37 2.5 1 5 4 0.23 -1.25 0.06
extracurricular 5 520 2.68 1.47 3.0 1 5 4 0.32 -1.26 0.06
stress 6 520 2.88 1.36 3.0 1 5 4 0.09 -1.18 0.06
| Sleep Quality | Headaches | Academic Performance | Study Load | Extracurricular | Stress | |
|---|---|---|---|---|---|---|
| Sleep Quality | ||||||
| Headaches | -0.059 | |||||
| Academic Performance | 0.254*** | -0.205*** | ||||
| Study Load | 0.065 | -0.007 | 0.096* | |||
| Extracurricular | 0.001 | -0.073 | 0.153*** | 0.175*** | ||
| Stress | 0.165*** | -0.072 | 0.055 | 0.392*** | 0.052 | |
| Computed correlation used pearson-method with listwise-deletion. | ||||||
study_load is the obvious candidate: r = .39sleep_quality is next: r = .17
Call:
lm(formula = stress ~ study_load, data = stress)
Residuals:
Min 1Q Median 3Q Max
-2.7468 -0.9719 -0.1969 1.0844 2.8031
Coefficients:
Estimate Std. Error t value Pr(>|t|)
(Intercept) 1.8095 0.1229 14.722 <0.0000000000000002 ***
study_load 0.3875 0.0400 9.687 <0.0000000000000002 ***
---
Signif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
Residual standard error: 1.251 on 518 degrees of freedom
Multiple R-squared: 0.1534, Adjusted R-squared: 0.1517
F-statistic: 93.84 on 1 and 518 DF, p-value: < 0.00000000000000022
Each 1-point increase in study load → 0.39 points more stress. \(R^2\) = .153.
# A tibble: 3 × 5
term estimate std.error statistic p.value
<chr> <dbl> <dbl> <dbl> <dbl>
1 (Intercept) 1.29 0.191 6.76 3.78e-11
2 study_load 0.378 0.0397 9.54 5.55e-20
3 sleep_quality 0.173 0.0495 3.50 5.00e- 4
| Predictor | Alone | Together |
|---|---|---|
study_load |
0.388 | 0.378 |
sleep_quality |
— | 0.174 |
The study-load slope moved by 0.01. Essentially nothing.
A coefficient only changes when the new predictor is related to both the outcome and the old predictor.
Pearson's product-moment correlation
data: stress$study_load and stress$sleep_quality
t = 1.4933, df = 518, p-value = 0.136
alternative hypothesis: true correlation is not equal to 0
95 percent confidence interval:
-0.0206320 0.1506088
sample estimates:
cor
0.0654704
Tip
“I added a predictor and the first one didn’t budge” is a real result, and it is the common one. It means your two predictors are not competing for the same variance. Say that, instead of apologizing for it.
\[\widehat{Stress} = 1.29 + 0.378(StudyLoad) + 0.174(SleepQuality)\]
Better sleep quality predicts more stress (b = +0.174, p < .001), holding study load constant.
Three possibilities, and you cannot choose between them from this output:
Important
This is the midterm’s lesson arriving in a lecture. Check the direction of every coefficient against what you expected. When it disagrees, the first suspect is always scoring — not the universe.
# A tibble: 1 × 3
r.squared adj.r.squared sigma
<dbl> <dbl> <dbl>
1 0.153 0.152 1.25
# A tibble: 1 × 3
r.squared adj.r.squared sigma
<dbl> <dbl> <dbl>
1 0.173 0.170 1.24
| Model | \(R^2\) | adj. \(R^2\) | RSE |
|---|---|---|---|
| study load | .1534 | .1517 | 1.2506 |
| + sleep quality | .1730 | .1698 | 1.2372 |
Model 1 is nested inside Model 2 — same data, same outcome, Model 1 is Model 2 with one coefficient forced to zero. That lets us test the difference directly.
| Column | What it is | Here |
|---|---|---|
Res.Df |
df left after estimating | 518 → 517 |
RSS |
unexplained SS | 810.12 → 791.34 |
Df |
predictors added | 1 |
Sum of Sq |
SS the new predictor picked up | 18.78 |
F |
is that drop bigger than noise? | 12.27 |
Pr(>F) |
the p-value | .0005 |
With one added predictor, this \(F\) is just the square of its \(t\): \(3.503^2 = 12.27\). Same test, different table. ✅
# A tibble: 6 × 5
term estimate std.error statistic p.value
<chr> <dbl> <dbl> <dbl> <dbl>
1 (Intercept) 1.59 0.275 5.80 1.13e- 8
2 study_load 0.383 0.0404 9.50 8.00e-20
3 sleep_quality 0.178 0.0512 3.47 5.68e- 4
4 extracurricular -0.0148 0.0380 -0.389 6.97e- 1
5 headaches -0.0740 0.0445 -1.66 9.73e- 2
6 academic_perf -0.0384 0.0547 -0.703 4.83e- 1
Extracurriculars (p = .70), headaches (p = .10), academic performance (p = .48).
| Model | \(R^2\) | adj. \(R^2\) | RSE |
|---|---|---|---|
| 2 predictors | .1730 | .1698 | 1.2372 |
| 5 predictors | .1779 ⬆️ | .1699 ➡️ | 1.2371 |
Important
Week 7’s warning, now with consequences. \(R^2\) cannot tell you whether a predictor belongs in the model. Adjusted \(R^2\), the nested \(F\), and your theory can.
# A tibble: 3 × 2
term estimate
<chr> <dbl>
1 (Intercept) 1.26e-16
2 study_load 3.82e- 1
3 sleep_quality 1.40e- 1
Report b when
Report β when
Here: β = .38 vs .14. Study load matters roughly 2.7× as much as sleep quality for stress.
If two predictors are nearly the same variable, the model cannot tell them apart. Coefficients get unstable and standard errors balloon.
VIF (variance inflation factor) measures it: \(VIF_j = \frac{1}{1 - R^2_j}\), where \(R^2_j\) is from regressing predictor \(j\) on all the others.
With two predictors you can compute it by hand — regress one predictor on the other:
[1] 1.004305
[1] 4.997326
trucks rose from 0.515 to 0.579 as a resultRules of thumb: > 5 is worth a sentence, > 10 is a problem. Note that the fire model still worked — high VIF widens your SEs, it does not invalidate the model.
There are a lot of ways to take a look at the model
One of the easiest ways is to use performance and their check_model() function
Stress was regressed on self-reported study load and sleep quality in a sample of 520 students. The overall model was significant, F(2, 517) = 54.07, p < .001, accounting for 17.3% of the variance in stress (adj. R² = .170). Holding sleep quality constant, each one-point increase in study load was associated with a 0.38-point increase in stress (b = 0.38, SE = 0.04, t = 9.54, p < .001). Holding study load constant, each one-point increase in rated sleep quality was associated with a 0.17-point increase in stress (b = 0.17, SE = 0.05, t = 3.50, p < .001). Adding sleep quality improved fit over study load alone, ΔR² = .020, F(1, 517) = 12.27, p < .001. The positive coefficient for sleep quality was contrary to expectation and may reflect the direction in which that item was scored.
Tip
Everything a reader needs: the overall test, each coefficient with its holding-constant clause, the model comparison, and an honest flag on the odd result.
Tip
Week 8 Exercises · ~45 min · Pairs
1,763 films, 1970–2013: budget, international gross, year, and whether the film passes the Bechdel test.
Finish these at the bottom of your activity file:
📂 Midterm Project — due Tuesday 10/27 at 11:59pm
Part 6 is now unlocked. You have everything you need: the two-predictor model, the “holding constant” interpretation, \(\Delta R^2\), and the model-comparison table.
Target for this weekend: Parts 1–5 done, Part 6 drafted. That leaves next week for writing instead of panicking.
No lab is due this week or next — the midterm is the assignment.
📖 Chapter 14.3 — ST · Chapter 8.1 — MSR
Next week: what happens when the groups from Week 6 meet the predictors from this week — and what to do when a slope is different for different groups.