Section 1 of 9

1 Before you start

Before you begin, take a few minutes to check what you already know and how confident you feel. You will see the same questions again at the end of the module — this helps both you and us measure what you have learned. Click an option for every question and confidence rating, then click Next to continue.

Pre-test

A fitted model gives an age slope of 0.05 with a 95% confidence interval of 0.02 to 0.08. What does that interval tell you?

Pre-test

A 95% confidence interval for a regression slope runs from -0.01 to 0.12. What can you conclude at the 5% level?

Pre-test

You fit lm(hba1c ~ age) on patients aged 45 to 75, then call predict() for a 95-year-old. Why should you be cautious?

Pre-test

To predict HbA1c for a new 68-year-old, which call is correct given a model fitted with predictor age?

Pre-test

In a residuals-vs-fitted plot, what pattern suggests a straight line was the right shape?

Pre-test

On a Normal Q-Q plot of a model's residuals, the points curve away from the diagonal at both ends. What does this suggest?

Pre-test

What makes a variable a confounder of the relationship between a predictor and an outcome?

Pre-test

In lm(hba1c ~ bmi + age + sex), how do you interpret the bmi slope?

Pre-confidence

I can attach a 95% confidence interval to a regression slope with confint(), predict the outcome for a new patient with predict(), and explain when a prediction is unsafe extrapolation.

Not at all confident
Fully confident
Pre-confidence

I can read the residuals-vs-fitted and Normal Q-Q plots from plot(model) to judge whether a linear model's assumptions hold before I trust its slope.

Not at all confident
Fully confident
Pre-confidence

I can explain what a confounder is, fit an adjusted multiple regression by adding covariates with +, and compare the raw and adjusted slopes to see how the estimate moves.

Not at all confident
Fully confident
Section 2 of 9

2 Introduction

In Part I you fitted a straight line through a cloud of clinical measurements with lm() and read its slope, intercept, and R-squared. A slope on its own is only half the story: it is one estimate from one sample, it assumes a shape that may not fit, and it can be an artefact of a variable you never measured. This part shows you how to put a range around a slope, use the line to predict a new patient, check whether the model's assumptions actually hold, and see how an estimate moves once you account for a confounder.

This part of the module covers four steps that turn a fitted line into a result you can defend:

  • Uncertainty and prediction — a confidence interval for the slope with confint(), a prediction for a new patient with predict(), and the fitted line drawn with geom_smooth(method = "lm").
  • Diagnostic plots — what plot(model) is really testing, reading the residuals-vs-fitted plot for the wrong shape and the Q-Q plot for non-Normal residuals.
  • Confounding — how a third variable linked to both predictor and outcome can distort a slope, and why a significant raw slope is never proof of cause.
  • Adjusting with multiple regression — adding covariates with + to estimate each predictor's effect with the others held fixed, and comparing raw and adjusted estimates with broom::tidy().
Once a line is fitted, you attach a confidence interval and a prediction, check its assumptions with diagnostic plots, and adjust for confounders before you trust the slope as a real effect.
Once a line is fitted, you attach a confidence interval and a prediction, check its assumptions with diagnostic plots, and adjust for confounders before you trust the slope as a real effect.

By the end of this part you will be able to attach a 95% confidence interval to a slope and say what it means, predict the outcome for a new patient and know when a prediction is unsafe, read the first two diagnostic plots to judge whether the linearity and Normality assumptions hold, and fit a small adjusted model to watch a slope move once you account for a confounder.

Try every snippet in the R Scratchpad on the right. This part needs no data file — you will build a small cohort with set.seed(), rnorm(), and data.frame() so everyone sees the same numbers. Use the native pipe |> if you reach for a pipe, and call library() explicitly for ggplot2 and broom when you need them.

Section 3 of 9

3 Uncertainty, prediction, and drawing the line

A slope is an estimate from one sample, so it comes with uncertainty. Now you put a range around it, use the line to predict a new patient, and draw the whole thing so you can see what the model is claiming.

A confidence interval for the slope is the plausible range for the TRUE slope in the wider population, given your sample. You get it with confint, which returns the lower and upper bound for every coefficient. If the age slope is 0.05 with a 95% interval of 0.02 to 0.08, the data are consistent with anything in that band.

Try it out

Try this snippet in the R Scratchpad on the right.

Try this snippet
set.seed(123)
age <- round(rnorm(40, mean = 60, sd = 8))
hba1c <- 4 + 0.05 * age + rnorm(40, mean = 0, sd = 0.4)
clinic <- data.frame(age, hba1c)
model <- lm(hba1c ~ age, data = clinic)
confint(model)
A slope from one sample is uncertain, so its 95% confidence interval is the range of true slopes the data support, and that range is what the line carries forward when it predicts a new patient.
A slope from one sample is uncertain, so its 95% confidence interval is the range of true slopes the data support, and that range is what the line carries forward when it predicts a new patient.

If a slope's confidence interval does NOT cross zero, the predictor is statistically associated with the outcome at the 5% level. But a slope being statistically significant still does not make it causal — significance and causation are separate questions, and Section 6 shows why.

To predict the outcome for a new patient, hand predict the model and a small data frame holding the new predictor value. The column name must match the predictor in the model exactly. Here you predict HbA1c for a 68-year-old.

Try it out

Try this snippet in the R Scratchpad on the right.

Try this snippet
set.seed(123)
age <- round(rnorm(40, mean = 60, sd = 8))
hba1c <- 4 + 0.05 * age + rnorm(40, mean = 0, sd = 0.4)
clinic <- data.frame(age, hba1c)
model <- lm(hba1c ~ age, data = clinic)
new_patient <- data.frame(age = 68)
predict(model, newdata = new_patient, interval = "confidence")
A fitted regression line converts a new patient's age into an estimated HbA1c, and its confidence interval reports how precise that single estimate is.
A fitted regression line converts a new patient's age into an estimated HbA1c, and its confidence interval reports how precise that single estimate is.

Beware prediction beyond your data. Predicting far outside the range of ages you actually observed — extrapolation — is unreliable, because the straight line may not hold out there. A model built on patients aged 45 to 75 says little about a 95-year-old.

Section 3.1 of 9

3.1 Drawing the fitted line with ggplot2

Numbers are easier to trust once you see the line on the data. With ggplot2 you draw the scatter of points with geom_point(), then overlay the regression line with geom_smooth(method = "lm"). The method = "lm" tells ggplot to fit the same least-squares line and draw its confidence band.

A ggplot is built in layers, and overlaying the least-squares line with its confidence band turns a vague scatter of points into a trend you can read and a range you can trust.
A ggplot is built in layers, and overlaying the least-squares line with its confidence band turns a vague scatter of points into a trend you can read and a range you can trust.

A ggplot only appears when it is printed at the top level, so end the snippet with the plot object itself. Run this and read the line against the cloud of points.

Try it out

Try this snippet in the R Scratchpad on the right.

Try this snippet
library(ggplot2)
set.seed(123)
age <- round(rnorm(40, mean = 60, sd = 8))
hba1c <- 4 + 0.05 * age + rnorm(40, mean = 0, sd = 0.4)
clinic <- data.frame(age, hba1c)
ggplot(clinic, aes(x = age, y = hba1c)) +
  geom_point() +
  geom_smooth(method = "lm")
A ggplot is assembled one layer at a time, and the lm line fitted through the scatter shows that HbA1c tends to rise with age.
A ggplot is assembled one layer at a time, and the lm line fitted through the scatter shows that HbA1c tends to rise with age.

We can practice using the example below to put an interval on the slope.

Worked example · An interval for the slope

Work through this example in three stages. You unlock each stage only after the tutor confirms the previous one. Each stage removes more of the scaffolding — by the end you are writing it yourself.

Problem: Fit HbA1c on age for the eight patients and report the 95% confidence interval for every coefficient with confint(), rounded to three decimals.

Stage 1 · Study the solved example
Fully solved solution
age <- c(45, 52, 61, 58, 70, 49, 65, 55)
hba1c <- c(6.4, 6.9, 7.8, 7.5, 8.6, 6.7, 8.1, 7.2)
model <- lm(hba1c ~ age)
round(confint(model), 3)
Walk-through
  1. fit the model
  2. confint() returns the lower and upper bound for each coefficient
  3. a slope interval that excludes 0 is significant at the 5% level
A regression slope is reported with a 95% confidence interval from confint(), and when that whole interval lies above zero the effect of age on HbA1c is significant at the 5% level.
A regression slope is reported with a 95% confidence interval from confint(), and when that whole interval lies above zero the effect of age on HbA1c is significant at the 5% level.
Section 4 of 9

4 Checking the fit with diagnostic plots

You can fit a line to any data, even data where a straight line makes no sense. So before you trust a slope, you check whether the model's assumptions actually hold — and R gives you the plots to do it in one call.

plot(model) draws four diagnostic plots; the first two matter most here. They test the assumptions a linear model rests on, using the residuals — the leftover gaps between each point and the line you met in Section 2.

The residuals-vs-fitted plot checks whether a straight line was the right shape. You want the points scattered randomly around the horizontal zero line with no pattern. A clear curve or funnel shape is a warning that the relationship is not linear, or that the spread of the outcome changes across its range.

The Q-Q plot (quantile-quantile) checks whether the residuals are roughly Normal, which the usual p-values and intervals assume. You want the points to sit close to the diagonal line. Points curving away at the ends flag skew or heavy tails in the residuals.

A straight line can be fitted to any data, so before trusting the slope you read the residuals vs fitted and Normal Q-Q plots to check that the linearity and normality assumptions actually hold.
A straight line can be fitted to any data, so before trusting the slope you read the residuals vs fitted and Normal Q-Q plots to check that the linearity and normality assumptions actually hold.

A base-R plot appears as soon as you call it — no printing needed. Run this and read the first two panels.

Try it out

Try this snippet in the R Scratchpad on the right.

Try this snippet
set.seed(123)
age <- round(rnorm(40, mean = 60, sd = 8))
hba1c <- 4 + 0.05 * age + rnorm(40, mean = 0, sd = 0.4)
clinic <- data.frame(age, hba1c)
model <- lm(hba1c ~ age, data = clinic)
plot(model, which = 1)
A residual is the vertical distance from each point to the fitted line, and Residuals vs Fitted re-plots those distances against the predictions so a flat, patternless band signals that linearity and constant variance hold.
A residual is the vertical distance from each point to the fitted line, and Residuals vs Fitted re-plots those distances against the predictions so a flat, patternless band signals that linearity and constant variance hold.

The second panel is the Q-Q plot of the residuals. Swap which = 1 for which = 2 to see whether the residuals track the diagonal — a quick read on the Normality the p-values assume.

Try it out

Try this snippet in the R Scratchpad on the right.

Try this snippet
set.seed(123)
age <- round(rnorm(40, mean = 60, sd = 8))
hba1c <- 4 + 0.05 * age + rnorm(40, mean = 0, sd = 0.4)
clinic <- data.frame(age, hba1c)
model <- lm(hba1c ~ age, data = clinic)
plot(model, which = 2)
Standardised residuals that sit on the diagonal of a Q-Q plot are behaving Normally, which is the assumption a regression's p-values rely on, while points that peel away at the tails are a sign to treat those p-values with caution.
Standardised residuals that sit on the diagonal of a Q-Q plot are behaving Normally, which is the assumption a regression's p-values rely on, while points that peel away at the tails are a sign to treat those p-values with caution.

The mistake to avoid: skipping the diagnostic plots and reporting a slope from a model whose assumptions are clearly broken. A curved residual plot means the straight-line slope is misleading, no matter how small its p-value. Look before you report.

Always check the model before you trust it. Try the example below.

Worked example · Check the fit

Work through this example in three stages. You unlock each stage only after the tutor confirms the previous one. Each stage removes more of the scaffolding — by the end you are writing it yourself.

Problem: Fit HbA1c on age for the eight patients and draw the residuals-vs-fitted diagnostic plot.

Stage 1 · Study the solved example
Fully solved solution
age <- c(45, 52, 61, 58, 70, 49, 65, 55)
hba1c <- c(6.4, 6.9, 7.8, 7.5, 8.6, 6.7, 8.1, 7.2)
model <- lm(hba1c ~ age)
plot(model, which = 1)
Walk-through
  1. fit the model
  2. plot(model, which = 1) draws residuals against fitted values
  3. a random cloud around zero means a straight line fits
A curved residuals-vs-fitted plot signals that a straight-line model is wrong and its slope cannot be trusted, while a patternless cloud around zero signals the linear fit is sound.
A curved residuals-vs-fitted plot signals that a straight-line model is wrong and its slope cannot be trusted, while a patternless cloud around zero signals the linear fit is sound.
Section 5 of 9

5 Confounding and adjusting for other variables

A single-predictor slope answers one question in isolation, but patients are not isolated. The last and most important idea in this part is that another variable can lurk behind your slope and change its whole meaning.

A confounder is a third variable that is linked to BOTH your predictor and your outcome, and so distorts the relationship between them. Age is the classic one: it relates to almost everything in medicine, so an unadjusted slope often partly reflects age rather than the predictor you care about.

A confounder such as age, by being linked to both the predictor and the outcome, can manufacture a slope that nearly vanishes once you compare like with like.
A confounder such as age, by being linked to both the predictor and the outcome, can manufacture a slope that nearly vanishes once you compare like with like.

Make it concrete. Suppose you regress HbA1c on body-mass index and find a clear positive slope. But older patients tend to have both higher BMI and higher HbA1c. Some of that BMI slope may really be age in disguise. To see the BMI effect on its own, you adjust for age.

You adjust by adding more predictors to the formula with +. lm(hba1c ~ bmi + age + sex) fits one model where each slope is the effect of THAT predictor while holding the others fixed. The BMI slope now answers: among patients of the same age and sex, how does HbA1c change per unit of BMI? This is a multiple linear regression.

djusting for a confounder such as age isolates a predictor's own effect, which is exactly what each slope in a multiple linear regression estimates while the other predictors are held fixed.
djusting for a confounder such as age isolates a predictor's own effect, which is exactly what each slope in a multiple linear regression estimates while the other predictors are held fixed.

Build a cohort where age genuinely drives both BMI and HbA1c, fit the unadjusted model, then add age and sex, and watch the BMI slope move.

Try it out

Try this snippet in the R Scratchpad on the right.

Try this snippet
set.seed(42)
age <- round(rnorm(60, mean = 60, sd = 9))
sex <- factor(sample(c("F", "M"), 60, replace = TRUE))
bmi <- 0.15 * age + rnorm(60, mean = 15, sd = 2)
hba1c <- 3 + 0.05 * age + 0.02 * bmi + rnorm(60, mean = 0, sd = 0.4)
clinic <- data.frame(age, sex, bmi, hba1c)
unadjusted <- lm(hba1c ~ bmi, data = clinic)
coef(unadjusted)
A predictor can look strongly associated with an outcome only because a common cause inflates it, so its coefficient shrinks once you adjust for that cause.
A predictor can look strongly associated with an outcome only because a common cause inflates it, so its coefficient shrinks once you adjust for that cause.
Try it out

Try this snippet in the R Scratchpad on the right.

Try this snippet
set.seed(42)
age <- round(rnorm(60, mean = 60, sd = 9))
sex <- factor(sample(c("F", "M"), 60, replace = TRUE))
bmi <- 0.15 * age + rnorm(60, mean = 15, sd = 2)
hba1c <- 3 + 0.05 * age + 0.02 * bmi + rnorm(60, mean = 0, sd = 0.4)
clinic <- data.frame(age, sex, bmi, hba1c)
adjusted <- lm(hba1c ~ bmi + age + sex, data = clinic)
summary(adjusted)
An adjusted regression coefficient is the effect of one variable among patients who share the same values on the others, so controlling for age and sex strips out the apparent BMI effect that age was secretly inflating.
An adjusted regression coefficient is the effect of one variable among patients who share the same values on the others, so controlling for age and sex strips out the apparent BMI effect that age was secretly inflating.

Compare the BMI slope across the two models. The unadjusted slope can shrink, grow, or even flip sign once you adjust for a confounder — this is confounding in action. That is why a significant raw slope is never proof of cause: a confounder you left out could be producing it.

The broom package makes the comparison clean. tidy() turns a model into a one-row-per-coefficient tibble of estimates, standard errors, and p-values — easy to read side by side or drop into a report.

Try it out

Try this snippet in the R Scratchpad on the right.

Try this snippet
library(broom)
set.seed(42)
age <- round(rnorm(60, mean = 60, sd = 9))
sex <- factor(sample(c("F", "M"), 60, replace = TRUE))
bmi <- 0.15 * age + rnorm(60, mean = 15, sd = 2)
hba1c <- 3 + 0.05 * age + 0.02 * bmi + rnorm(60, mean = 0, sd = 0.4)
clinic <- data.frame(age, sex, bmi, hba1c)
adjusted <- lm(hba1c ~ bmi + age + sex, data = clinic)
tidy(adjusted)
broom::tidy() converts a fitted model into a tibble with one row per coefficient and tidy columns for the estimate, standard error, test statistic and p-value, so results are easy to read and report.
broom::tidy() converts a fitted model into a tibble with one row per coefficient and tidy columns for the estimate, standard error, test statistic and p-value, so results are easy to read and report.

Stay inside this part's scope. You now know how to add covariates to adjust for confounding. How predictors can interact, how to CHOOSE which variables to keep, and how to spot when two predictors are too tangled to separate (collinearity and the VIF) are all Course 2 topics. For now, adjust for an obvious confounder or two and report both the raw and adjusted estimate.

Put it together — watch a slope move once you adjust.

Worked example · Adjust for a confounder

Work through this example in three stages. You unlock each stage only after the tutor confirms the previous one. Each stage removes more of the scaffolding — by the end you are writing it yourself.

Problem: Eight patients have BMI 24, 27, 31, 29, 33, 26, 30, 28, ages 45, 52, 61, 58, 70, 49, 65, 55, and HbA1c 6.4, 6.9, 7.8, 7.5, 8.6, 6.7, 8.1, 7.2. Fit HbA1c on BMI alone, then adjust for age, and read the BMI slope each time.

Stage 1 · Study the solved example
Fully solved solution
bmi <- c(24, 27, 31, 29, 33, 26, 30, 28)
age <- c(45, 52, 61, 58, 70, 49, 65, 55)
hba1c <- c(6.4, 6.9, 7.8, 7.5, 8.6, 6.7, 8.1, 7.2)
unadjusted <- lm(hba1c ~ bmi)
adjusted <- lm(hba1c ~ bmi + age)
round(coef(unadjusted)["bmi"], 3)
round(coef(adjusted)["bmi"], 3)
Walk-through
  1. fit the simple model first
  2. add age with + to adjust
  3. compare the BMI slope — if it moves, age was confounding it
A predictor's slope can shrink toward zero once you adjust for a confounder, because the unadjusted slope was silently carrying the confounder's effect.
A predictor's slope can shrink toward zero once you adjust for a confounder, because the unadjusted slope was silently carrying the confounder's effect.
Section 6 of 9

6 Put it together

Now you run the full arc: fit a simple regression, read its slope, intercept and R-squared, put an interval on the slope, then add one covariate and watch the estimate move. The worked example fades its support — study the full solution, then fill the gap, then solve a fresh one on your own.

Worked example · Fit, read, and then adjust a regression

Work through this example in three stages. You unlock each stage only after the tutor confirms the previous one. Each stage removes more of the scaffolding — by the end you are writing it yourself.

Problem: Using a simulated cohort, regress HbA1c on BMI. Read the BMI slope and the R-squared from summary(), get a 95% confidence interval for the slope with confint(), then refit adjusting for age and report whether the BMI slope changed.

Stage 1 · Study the solved example
Fully solved solution
set.seed(7)
age <- round(rnorm(50, mean = 60, sd = 8))
bmi <- 0.15 * age + rnorm(50, mean = 15, sd = 2)
hba1c <- 3 + 0.05 * age + 0.02 * bmi + rnorm(50, mean = 0, sd = 0.4)
clinic <- data.frame(age, bmi, hba1c)
simple <- lm(hba1c ~ bmi, data = clinic)
summary(simple)
confint(simple)
adjusted <- lm(hba1c ~ bmi + age, data = clinic)
coef(adjusted)
Walk-through
  1. Build the reproducible cohort with set.seed() so your numbers match
  2. Fit the simple model hba1c ~ bmi and read the BMI slope and R-squared from summary()
  3. Wrap the model in confint() to get the 95% interval for the slope
  4. Refit as hba1c ~ bmi + age and compare the BMI slope: if it moved, age was confounding it
A worked example teaches by removing its own scaffolding in stages, so you move from reading a full regression solution, to supplying the one missing step, to running the whole fit, read, interval and adjust arc unaided.
A worked example teaches by removing its own scaffolding in stages, so you move from reading a full regression solution, to supplying the one missing step, to running the whole fit, read, interval and adjust arc unaided.
Section 7 of 9

7 Check your understanding

You have reached the end of the module. Try the same questions again — your answers here, paired with your pre-test answers, are how we measure what the module taught you. Answer every question and confidence rating, then click Submit and see results to view your score.

Post-test

A fitted model gives an age slope of 0.05 with a 95% confidence interval of 0.02 to 0.08. What does that interval tell you?

Post-test

A 95% confidence interval for a regression slope runs from -0.01 to 0.12. What can you conclude at the 5% level?

Post-test

You fit lm(hba1c ~ age) on patients aged 45 to 75, then call predict() for a 95-year-old. Why should you be cautious?

Post-test

To predict HbA1c for a new 68-year-old, which call is correct given a model fitted with predictor age?

Post-test

In a residuals-vs-fitted plot, what pattern suggests a straight line was the right shape?

Post-test

On a Normal Q-Q plot of a model's residuals, the points curve away from the diagonal at both ends. What does this suggest?

Post-test

What makes a variable a confounder of the relationship between a predictor and an outcome?

Post-test

In lm(hba1c ~ bmi + age + sex), how do you interpret the bmi slope?

Post-confidence

I can attach a 95% confidence interval to a regression slope with confint(), predict the outcome for a new patient with predict(), and explain when a prediction is unsafe extrapolation.

Not at all confident
Fully confident
Post-confidence

I can read the residuals-vs-fitted and Normal Q-Q plots from plot(model) to judge whether a linear model's assumptions hold before I trust its slope.

Not at all confident
Fully confident
Post-confidence

I can explain what a confounder is, fit an adjusted multiple regression by adding covariates with +, and compare the raw and adjusted slopes to see how the estimate moves.

Not at all confident
Fully confident
Section 8 of 9

8 Your results

Here is how your post-test answers compare with your pre-test answers. The pre/post pairing is the most reliable way to see what this module actually taught you.

Your score

Submit the post-test to see your results.

Muddiest point

What is the one thing from this module that is still unclear to you?

Rate this module

Overall, how would you rate this module?

How likely are you to recommend this module to a peer? (0 = not at all, 10 = extremely likely)