Checkpoint: Statistical Modeling
Reviewed & published by Brayan K
This checkpoint consolidates your advanced R skills — matrix algebra, stringr, ANOVA, time series, data.table, Shiny, R Markdown, closures, and debugging — into one practical statistical-modeling exercise that fits and interprets a linear model.
Part of the free R course at LearnCodingFast — hands-on lessons with worked examples and the output they print, plus practice exercises and a quick quiz.
You'll review everything you've built, tackle a multi-step modeling challenge with a worked solution, and test yourself with a short quiz.
What This Checkpoint Covers
- Recap of nine advanced R lessons
- Fitting a linear model with lm()
- Reading coefficients, p-values, and R-squared
- Predicting from a fitted model
- Comparing nested models with anova()
- A self-check quiz to confirm mastery
📚 What You've Mastered
Before the challenge, here's the toolkit you've assembled across the advanced track. Each row is a skill you'll lean on when modeling real data.
🧩 Warm-Up: Clean, Test, Remember
Before the modeling challenge, here are three of the module's idioms on inputs small enough to check by eye: a stringr pipeline, a one-way ANOVA, and a closure that keeps state between calls.
# Three idioms this checkpoint leans on, each shown on a tiny input.
library(stringr)
# 1) stringr: every function takes the string FIRST, so it pipes cleanly
labels <- c(" Low", "high ", "LOW")
print(str_trim(labels) |> str_to_lower()) # the same three labels, trimmed and lower-cased
# 2) aov: a one-way ANOVA compares a numeric outcome across the levels of a factor
dat <- data.frame(group = factor(c("a", "a", "b", "b", "c", "c")),
y = c(5, 6, 9, 10, 5, 4))
print(summary(aov(y ~ group, data = dat))) # an ANOVA table: Df, Sum Sq, Mean Sq, F value, Pr(>F)
# 3) closure: the inner function remembers 'total' between calls via <<-
make_adder <- function() {
total <- 0
function(x) {
total <<- total + x # update the total that lives in the ENCLOSING environment
total
}
}
add <- make_adder()
print(add(5)) # the running total after adding 5
print(add(10)) # the running total after adding 10 more — it rememberedYour turn. Real data arrives with messy labels, so clean them with stringr first, then ask aov() whether the groups differ, and finish with a small closure that keeps count. Replace each ___ with the value in its hint, then run it and compare with the output panel.
# 🎯 YOUR TURN — replace each ___ then run it.
# Combines: stringr (clean labels), ANOVA (aov), and a closure (function factory).
library(stringr)
# Reaction times (ms) recorded under three UNTIDY condition labels
trial <- data.frame(
condition = c("Quiet ", "quiet", "NOISY", "noisy ", " Music", "music",
"QUIET", "Noisy", "music "),
rt = c(310, 295, 402, 388, 350, 341, 302, 395, 356)
)
# 1) Tidy the labels: trim stray spaces, lower-case them, then make a factor
trial$condition <- str_trim(trial$condition) |> ___() |> factor() # 👉 replace ___ with str_to_lower
print(table(condition = trial$condition))
# 2) One-way ANOVA: does mean reaction time differ between the conditions?
fit <- ___(rt ~ condition, data = trial) # 👉 replace ___ with aov
print(summary(fit))
# 3) A closure that counts how many models you have fitted this session
make_counter <- function() {
n <- 0
function() {
n ___ n + 1 # 👉 replace ___ with <<-
n
}
}
fitted_so_far <- make_counter()
print(fitted_so_far()) # first call
print(fitted_so_far()) # second call: one higher than the first
# ✅ Expected output:
# condition
# music noisy quiet
# 3 3 3
# Df Sum Sq Mean Sq F value Pr(>F)
# condition 2 12881 6440 119 1.49e-05 ***
# Residuals 6 325 54
# ---
# Signif. codes: 0 ‘***’ 0.001 ‘**’ 0.01 ‘*’ 0.05 ‘.’ 0.1 ‘ ’ 1
# [1] 1
# [1] 2Build Challenge: Fit & Interpret a Model
Work through the four steps on the inline study dataset: fit a linear model, read its summary, make a prediction, and compare it against a quadratic model with anova(). Attempt it yourself first, then expand the solution to compare your approach.
# 🎯 BUILD CHALLENGE: fit and interpret a linear model.
#
# A small inline dataset: hours studied vs. exam score.
study <- data.frame(
hours = c(1, 2, 3, 4, 5, 6, 7, 8),
score = c(52, 56, 63, 68, 72, 79, 81, 88)
)
# STEP 1: Fit a simple linear regression of score on hours.
# fit <- lm(score ~ hours, data = study)
# STEP 2: Read the model.
# summary(fit)
# -> the 'hours' coefficient (Estimate) is points gained per study hour
# -> its Pr(>|t|) tells you if that slope is significant
# -> Multiple R-squared is the fraction of variance explained
# STEP 3: Predict the score for 9 hours of study.
# predict(fit, newdata = data.frame(hours = 9))
# STEP 4: Compare against a model that adds a squared term.
# fit2 <- lm(score ~ hours + I(hours^2), data = study)
# anova(fit, fit2) # does the curve significantly beat the straight line?
# Write one sentence interpreting the slope and whether fit2 is worth it.
# your code here — then open the solution below to check your approachA complete, commented solution. The exact numbers depend on the fit, so run it to see them; the comments explain how to interpret each piece.
📝 Checkpoint Quiz
Answer each in your head, then expand to check. No peeking first.
It's the slope: the average change in score for each one-unit increase in hours. If it's about 5, each extra hour of study is associated with roughly 5 more points.
Yes. At the usual 0.05 threshold, 0.002 is well below it, so the slope is statistically distinguishable from zero — the predictor has a detectable effect.
Plain R-squared never decreases when you add predictors, even worthless ones. Adjusted R-squared penalizes extra terms, so it only rises when a new predictor genuinely improves the fit — making it fairer for comparing models of different sizes.
Keep the simpler fit. A p-value of 0.40 means the extra terms in fit2 don't significantly improve the fit, so the added complexity isn't justified.
Use predict(fit, newdata = data.frame(hours = 9)). The newdata frame must use the same column names as the predictors in the model.
Call traceback() immediately to see the call stack and find which function actually failed, then drop a browser() or use debugonce() there to inspect the inputs.
🎉 Checkpoint Complete!
- ✅ You've reviewed nine advanced R lessons in one place
- ✅ You fit a linear model with lm() and read its summary
- ✅ You interpreted slopes, p-values, and R-squared
- ✅ You predicted new values and compared models with anova()
- ✅ You passed the self-check quiz
Practice quiz
In lm(score ~ hours, data = study), what does the 'hours' coefficient (Estimate) represent?
- The predicted score at hours = 0
- The total variance explained
- The average change in score per one extra hour
- The p-value of the model
Answer: The average change in score per one extra hour. The coefficient is the slope: the average change in the outcome for each one-unit increase in that predictor.
A predictor's Pr(>|t|) is 0.002. At the usual 0.05 threshold, is its slope significant?
- Yes, 0.002 is well below 0.05
- No, 0.002 is too small to matter
- Only if R-squared is high
- It cannot be determined
Answer: Yes, 0.002 is well below 0.05. 0.002 is well below the 0.05 threshold, so the slope is statistically distinguishable from zero.
Why prefer ADJUSTED R-squared over Multiple R-squared when comparing models of different sizes?
- It is always larger
- It ignores the intercept
- It is faster to compute
- It penalizes extra predictors, so it only rises for genuinely useful terms
Answer: It penalizes extra predictors, so it only rises for genuinely useful terms. Plain R-squared never decreases when you add predictors, even useless ones. Adjusted R-squared penalizes extra terms.
anova(fit, fit2) gives Pr(>F) = 0.40. Which model do you keep?
- The more complex fit2
- The simpler fit
- Neither, refit both
- Whichever has higher R-squared
Answer: The simpler fit. A p-value of 0.40 means the extra terms in fit2 do not significantly improve the fit, so keep the simpler model.
How do you predict the outcome for 9 hours of study with a fitted model 'fit'?
- predict(fit, newdata = data.frame(hours = 9))
- fit(9)
- summary(fit, 9)
- fit$predict(9)
Answer: predict(fit, newdata = data.frame(hours = 9)). Use predict() with a newdata frame whose column names match the model's predictors.
Your lm() errors deep inside a helper function. What is the best first debugging move?
- Rewrite the whole script
- Increase the data size
- Call traceback() to see the call stack
- Remove the intercept
Answer: Call traceback() to see the call stack. traceback() shows the call stack so you can find which function actually failed, then use browser() or debugonce() there.
How do aov() and lm() relate?
- They fit completely different models
- They fit the same linear model but present results differently
- aov() only works on time series
- lm() cannot handle categorical predictors
Answer: They fit the same linear model but present results differently. Under the hood they fit the same linear model; lm() is framed for regression coefficients, aov() for an ANOVA table with an F-test.
Which stringr function tests whether a pattern is PRESENT in each string?
- str_replace()
- str_split()
- str_extract()
- str_detect()
Answer: str_detect(). str_detect() returns a logical vector of whether the pattern matches. str_extract() pulls out the matched text.
In data.table syntax dt[i, j, by], what does the 'by' argument do?
- Filters rows
- Groups rows for aggregation
- Selects columns
- Sorts the table
Answer: Groups rows for aggregation. i filters rows, j selects/computes columns, and by groups the rows so j is computed per group.
In a closure, what does the <<- operator do?
- Creates a local variable
- Defines a new function
- Assigns to a variable in the enclosing environment, persisting private state
- Compares two values
Answer: Assigns to a variable in the enclosing environment, persisting private state. <<- assigns up into the enclosing environment, which is how function factories keep persistent private state between calls.
Continue this course
- Previous: Debugging R (browser, traceback)
- Next: Capstone: A Data Analysis Report — Tie it all together: import, clean, analyse, visualise, and report
- Quick reference: R cheat sheet
Frequently asked questions
How do I read the coefficient table from summary(lm)?
Each row is a predictor. The Estimate is the slope — how much the outcome changes per one-unit increase in that predictor, holding others fixed. Std. Error measures the uncertainty of that estimate, the t value is Estimate divided by Std. Error, and Pr(>|t|) is the p-value testing whether the true slope is zero. A small p-value (typically < 0.05), often flagged with stars, means the predictor has a statistically detectable effect. The Intercept row is the predicted outcome when all predictors are zero.
What does R-squared tell me, and what doesn't it?
Multiple R-squared is the proportion of variation in the outcome explained by the model, from 0 to 1; higher means the model accounts for more of the spread. But it always rises when you add predictors, even useless ones, so use Adjusted R-squared to compare models of different sizes since it penalizes extra terms. Crucially, a high R-squared does not prove the model is correct, that relationships are causal, or that predictions will generalize — always check residual plots and out-of-sample performance too.
When should I prefer a simpler model over a more complex one?
Follow the principle of parsimony: prefer the simpler model unless the extra complexity earns its place. Concretely, when anova() comparing nested models gives a non-significant p-value, or when AIC/BIC favor the smaller model, keep the simpler one. Complex models risk overfitting — fitting noise rather than signal — which makes them look great on your data but predict poorly on new data. Simpler models are also easier to explain and trust.
What is the difference between aov() and lm() since both fit models?
Under the hood they fit the same linear model; they differ in how results are presented. lm() is framed for regression: summary() gives coefficients, slopes, and R-squared. aov() is framed for the analysis-of-variance view: summary() gives an ANOVA table with sums of squares and an F-test per term, which is natural when your predictors are categorical groups. You can even call anova() on an lm object to get the ANOVA-style table. Choose the lens that matches your question.