STAT 204 – Introduction to Statistical Data Analysis
15 Oct 2026
Programming is a systematic way to think about a problem — turning a fuzzy question into precise definitions and a chain of answers. Learning R is how we build that mental model.
There’s no point using AI if you’re trying to build that mental model yourself.
By the end of today’s class, you will be able to:
|> and the tidyverse pipe %>%, and know where they differggplot2patchwork and save them with ggsave()Week 3: a ggplot is data + aesthetics + geoms, plus labels, facets and a theme.
Today: getting the data into the right shape before ggplot(), and the layers that come after the geom.
Mean body mass of Gentoo penguins, in kg:
[1] 5.08
[1] 5.08
[1] 5.08
x |> f(y) means f(x, y): the left side becomes the first argument of the function on the right.
|> vs tidyverse %>%|> (base R, since 4.1)
_, and it must be a named argument: data = _x |> head()%>% (magrittr, loaded by the tidyverse)
library(tidyverse) or library(magrittr)., anywhere: lm(y ~ x, .)x %>% head also worksThe placeholder matters when the data is not the first argument, as in lm():
(Intercept) flipper_length_mm
-5780.83136 49.68557
(Intercept) flipper_length_mm
-5780.83136 49.68557
Either is fine in this course. Pick one and use it consistently within a document.
|> before, + afterData steps are joined with |>. Plot layers are joined with +. Mixing them up is one of the most common errors:
| tidyverse | What it does | Base R |
|---|---|---|
filter() |
keep rows | subset(), [rows, ] |
select() |
keep columns | [ , cols] |
mutate() |
add or change columns | transform(), $<- |
group_by() + summarise() |
one row per group | aggregate(), tapply() |
count() |
rows per group | table() |
arrange() |
sort rows | order() |
pivot_longer() |
wide → long | stack(), reshape() |
All of them take the data as the first argument, which is why they chain well.
species sex body_mass_g
1 Adelie female 3368.836
2 Chinstrap female 3527.206
3 Gentoo female 4679.741
4 Adelie male 4043.493
5 Chinstrap male 3938.971
6 Gentoo male 5484.836
# A tibble: 6 × 3
species sex mean_mass
<fct> <fct> <dbl>
1 Adelie female 3369.
2 Adelie male 4043.
3 Chinstrap female 3527.
4 Chinstrap male 3939.
5 Gentoo female 4680.
6 Gentoo male 5485.
geom_col() plots values you already computed. geom_bar() counts the rows for you.
ggplot wants one row per mark. To show several measurements side by side, pivot them into one column first:
scales = "free" gives each panel its own axes. Use it when the panels have different ranges. Don’t use it when the point is to compare across panels.
Partner work (10 minutes)
penguins: drop rows with missing sex, compute the median flipper length for each species and sex, and plot it with geom_col().subset(), aggregate(), |>).Discuss: Which version would you rather read in six weeks?
Each layer can use its own data. Show the points and the group means:
Or let ggplot compute it: stat_summary(fun = mean, geom = "point", colour = "red", size = 4). A bar of means alone hides how many points there are and how spread out they are.
With 54,000 diamonds, plain points only show the outline of the cloud, not where most of the data are.
Fixes: transparency (alpha), smaller points, 2D bins, or geom_jitter() when values are rounded.
Every aesthetic (x, y, colour, fill, size…) has a scale that turns data values into positions or colours. Change it with scale_<aesthetic>_<type>():
On log scales the relationship is close to a straight line. Keep that in mind for Tuesday.
scale_colour_brewer(palette = "Dark2") or scale_colour_manual()scale_fill_viridis_c()scale_fill_gradient2(midpoint = 0)About 1 in 12 men has some colour-vision deficiency. Avoid red/green pairs, and let shape or a label carry the information too:
These look like they do the same thing. They don’t.
Scale limits delete the points outside the range before anything is computed, so the fitted line changes. coord_cartesian() only zooms the view.
The default order is alphabetical, which is rarely what you want:
From forcats (loaded by the tidyverse): fct_reorder() sorts by another variable, fct_infreq() by frequency, and fct_relevel() by hand.
A legend makes the reader look back and forth. Put the labels on the plot:
centres <- penguins |>
group_by(species) |>
summarise(across(c(flipper_length_mm, body_mass_g), \(x) mean(x, na.rm = TRUE)))
ggplot(penguins, aes(flipper_length_mm, body_mass_g, colour = species)) +
geom_point(alpha = 0.4) +
geom_label(data = centres, aes(label = species), fontface = "bold") +
annotate("text", x = 172, y = 6000, label = "Each point is one penguin", hjust = 0) +
theme(legend.position = "none")When labels overlap, ggrepel::geom_text_repel() moves them apart.
theme_*() sets the overall look. theme() changes single elements.
base_size is the one people forget: slides and posters need 14 or more. Set it once per document with theme_set(theme_minimal(base_size = 14)).
+ or | puts plots side by side, / stacks them, and parentheses group them. Use facets for the same plot on different subsets, and patchwork for different plots.
width and height, otherwise you get whatever size your plot pane happens to beggsave(). Use the chunk options fig-width, fig-height, fig-cap and fig-alt insteadPartner work (15 minutes)
This plot runs, but every choice in it is a default:
class by median hwy: fct_reorder(class, hwy, median)base_size = 14. Flip the axes if the labels collideggsave()From last week (Yu & Barter): exploratory plots are for you. Make many of them, quickly, and don’t polish. Explanatory plots are for a reader, with one message per figure.
An explanatory figure:
Cleveland & McGill (1984) measured how accurately people read values from different encodings. From most to least accurate:
So: dot plots and bar charts rather than pie charts, facets with shared axes rather than separate plots, and colour for groups, not for precise values.
This is the stability idea from Week 3: if a different bin width, axis range or smoother would change your conclusion, say so.
Partner work (15 minutes). Use the coffee ratings data from HW3:
You have been adding this line since Week 3. What is it, how is it computed, what is the grey band, and when should you believe it? That is linear regression.
By the end of today’s class, you will be able to:
lm() in Rsummary() outputToday we’ll work with the famous Palmer Penguins dataset!
Research Question:
Can we describe penguin body mass using other measurements?
Simple Linear Regression: \[Y_i = \beta_0 + \beta_1 X_i + \epsilon_i\]
where:
Multiple Linear Regression: \[Y_i = \beta_0 + \beta_1 X_{i1} + \beta_2 X_{i2} + \cdots + \beta_p X_{ip} + \epsilon_i\]
Or in matrix notation: \[\mathbf{Y} = \mathbf{X}\boldsymbol{\beta} + \boldsymbol{\epsilon}\]
where \(\mathbf{X}\) is the design matrix and \(\boldsymbol{\beta}\) is the coefficient vector.
Before we dive into estimation, what do we assume?
We write this as: \(\epsilon_i \overset{iid}{\sim} N(0, \sigma^2)\)
We want to find \(\hat{\beta}_0\) and \(\hat{\beta}_1\) that minimize the Sum of Squared Residuals (SSR):
\[\text{SSR} = \sum_{i=1}^{n} (Y_i - \hat{Y}_i)^2 = \sum_{i=1}^{n} (Y_i - \hat{\beta}_0 - \hat{\beta}_1 X_i)^2\]
This is called the Ordinary Least Squares (OLS) criterion.
Solving the normal equations gives:
\[\hat{\beta}_1 = \frac{\sum_{i=1}^{n}(X_i - \bar{X})(Y_i - \bar{Y})}{\sum_{i=1}^{n}(X_i - \bar{X})^2} = \frac{S_{XY}}{S_{XX}}\]
\[\hat{\beta}_0 = \bar{Y} - \hat{\beta}_1\bar{X}\]
Key insight: The regression line always passes through \((\bar{X}, \bar{Y})\)!
In multiple regression, the OLS estimator is:
\[\hat{\boldsymbol{\beta}} = (\mathbf{X}^T\mathbf{X})^{-1}\mathbf{X}^T\mathbf{Y}\]
This elegant formula gives us all coefficient estimates at once!
Condition: \(\mathbf{X}^T\mathbf{X}\) must be invertible (no perfect multicollinearity).
Deriving the normal equations, computing \(\hat{\boldsymbol{\beta}}\) with linear algebra, and the properties and variance of the OLS estimators are not covered live. See the Self-Guided Practice section at the end of these slides.
The lm() function does all the matrix algebra for us!
Example: predicting body mass from flipper length: \[\hat{\text{Body Mass}} = -5780.83 + 49.69 \times \text{Flipper Length}\]
Intercept (\(\hat{\beta}_0 = -5780.83\) g): the expected value of \(Y\) when \(X = 0\), i.e. the expected body mass of a penguin with 0 mm flippers. It has no practical interpretation here: no penguin has 0 mm flippers.
Slope (\(\hat{\beta}_1 = 49.69\) g/mm): the expected change in \(Y\) for a one-unit increase in \(X\). For every 1 mm increase in flipper length, body mass increases by about 49.69 grams on average.
Units matter! Always interpret in context with units.
Each coefficient is interpreted as the expected change in \(Y\) for a one-unit change in that predictor, holding all other predictors constant.
# Fit a multiple regression model with 2-3 predictors
my_model <- lm(body_mass_g ~ flipper_length_mm + bill_length_mm,
data = penguins)
summary(my_model)
# Questions:
# 1. How do you interpret each coefficient?
# 2. How do they differ from simple regression?
# 3. Which predictor seems most important?We estimate the error variance using the Mean Squared Error (MSE):
\[\hat{\sigma}^2 = \text{MSE} = \frac{\sum_{i=1}^{n}(Y_i - \hat{Y}_i)^2}{n - p - 1} = \frac{\text{SSR}}{n - p - 1}\]
where \(p\) is the number of predictors.
The denominator \((n - p - 1)\) gives us degrees of freedom.
R-squared (\(R^2\)) measures the proportion of variance in \(Y\) explained by the model:
\[R^2 = 1 - \frac{\text{SSR}}{\text{SST}} = 1 - \frac{\sum(Y_i - \hat{Y}_i)^2}{\sum(Y_i - \bar{Y})^2}\]
Problem: \(R^2\) always increases when we add predictors!
Solution: Adjusted R-squared penalizes model complexity:
\[R^2_{\text{adj}} = 1 - \frac{\text{SSR}/(n-p-1)}{\text{SST}/(n-1)} = 1 - \frac{n-1}{n-p-1}(1-R^2)\]
Use \(R^2_{\text{adj}}\) when comparing models with different numbers of predictors.
We test: \(H_0: \beta_j = 0\) vs. \(H_a: \beta_j \neq 0\)
Test statistic: \[t = \frac{\hat{\beta}_j}{\text{SE}(\hat{\beta}_j)} \sim t_{n-p-1}\]
p-value: Probability of observing a test statistic of this value or more extreme given that \(H_0\) is true.
Reference: Wasserstein, R. L., & Lazar, N. A. (2016). The ASA Statement on p-Values: Context, Process, and Purpose. The American Statistician, 70(2), 129-133.
ASA Principle #1
“P-values can indicate how incompatible the data are with a specified statistical model.”
A p-value tells us: The probability of obtaining results at least as extreme as observed, assuming the null hypothesis is true
A p-value does NOT tell us:
ASA Principles #2 & #3
P-values do not measure the probability that the studied hypothesis is true. Scientific conclusions should not be based solely on whether a p-value passes a specific threshold.
Problems with p < 0.05:
ASA Recommendations
Proper inference requires full reporting, transparency, and consideration of multiple factors beyond p-values.
Key practices:
Bottom line: Accept uncertainty, be thoughtful, be open, and be modest in your claims.
A \((1-\alpha)100\%\) confidence interval for \(\beta_j\):
\[\hat{\beta}_j \pm t_{\alpha/2, n-p-1} \times \text{SE}(\hat{\beta}_j)\]
Interpretation: We are \((1-\alpha)100\%\) confident that the true \(\beta_j\) lies in this interval.
# 95% confidence intervals for coefficients
confint(model2, level = 0.95)
# For predictions
new_penguin <- data.frame(flipper_length_mm = 200,
bill_length_mm = 45,
bill_depth_mm = 15)
predict(model2, newdata = new_penguin,
interval = "confidence")
predict(model2, newdata = new_penguin,
interval = "prediction")Tests: \(H_0\): All slope coefficients are zero (\(\beta_1 = \beta_2 = \cdots = \beta_p = 0\))
Test statistic: \[F = \frac{(\text{SST} - \text{SSR})/p}{\text{SSR}/(n-p-1)} = \frac{\text{MSR}}{\text{MSE}} \sim F_{p, n-p-1}\]
This appears at the bottom of summary() output.
model1 <- lm(body_mass_g ~ flipper_length_mm, data = penguins)
model2 <- lm(body_mass_g ~ flipper_length_mm + bill_length_mm + bill_depth_mm,
data = penguins)
summary(model2)
confint(model2)
# Questions:
# 1. What percentage of the variance in body mass does model2 explain?
# Compare summary(model1)$r.squared, summary(model2)$r.squared
# and summary(model2)$adj.r.squared. Did adding predictors help?
# 2. Which coefficients are statistically significant? Do any
# confidence intervals contain zero?
# 3. What are the F-statistic and its p-value? What do they tell you?
# 4. Can individual predictors be non-significant while the overall
# model is significant?library(ggplot2)
# Add predictions to data
penguins_clean <- penguins %>%
drop_na(any_of(c("body_mass_g","bill_length_mm",
"bill_depth_mm")))%>%
mutate(fitted = fitted(model2),
residuals = residuals(model2))
# Plot actual vs fitted
ggplot(penguins_clean, aes(x = fitted, y = body_mass_g)) +
geom_point(alpha = 0.5) +
geom_abline(slope = 1, intercept = 0, color = "blue") +
labs(title = "Actual vs Fitted Values", x = "Fitted Body Mass (g)",y = "Actual Body Mass (g)")R automatically creates dummy variables for factors:
The reference category (Adelie) is absorbed into the intercept.
Test whether the effect of one predictor depends on another:
Key concepts covered:
lm(), with categorical predictors and interactionsNext week: checking the assumptions (diagnostics) and choosing between models.
📚 Recommended:
💻 R packages:
The following slides are not covered live. Work through them on your own, or bring questions to the Wednesday self-guided session.
Take partial derivatives and set to zero:
\[\frac{\partial \text{SSR}}{\partial \beta_0} = -2\sum_{i=1}^{n}(Y_i - \hat{\beta}_0 - \hat{\beta}_1 X_i) = 0\]
\[\frac{\partial \text{SSR}}{\partial \beta_1} = -2\sum_{i=1}^{n}X_i(Y_i - \hat{\beta}_0 - \hat{\beta}_1 X_i) = 0\]
These are the normal equations.
# Fit a simple linear regression model using LA
# Extract the response variable (y)
y <- penguins$body_mass_g
# Create the design matrix (X)
# Include intercept (column of 1s) and predictor variable
X <- cbind(1, penguins$flipper_length_mm)
# Remove rows with missing values (to match what lm() does)
complete_cases <- complete.cases(y, X)
y <- y[complete_cases]
X <- X[complete_cases, ]
# Calculate coefficients using linear algebra: β = (X'X)^(-1)X'y
beta_hat <- ## complete the code
# Display the results
beta_hat
# Compare with lm() results
model1 <- lm(body_mass_g ~ flipper_length_mm, data = penguins)
coef(model1)
# Give names to our manually calculated coefficients
rownames(beta_hat) <- c("(Intercept)", "flipper_length_mm")
beta_hatUnder the standard assumptions, OLS estimators are:
BLUE means: Among all linear unbiased estimators, OLS has the smallest variance!
For simple linear regression:
\[\text{Var}(\hat{\beta}_1) = \frac{\sigma^2}{\sum_{i=1}^{n}(X_i - \bar{X})^2} = \frac{\sigma^2}{S_{XX}}\]
\[\text{Var}(\hat{\beta}_0) = \sigma^2\left(\frac{1}{n} + \frac{\bar{X}^2}{S_{XX}}\right)\]
Key insight: More spread in \(X\) (larger \(S_{XX}\)) gives more precise estimates!
When predictors are on different scales, standardized coefficients help compare importance:
Now coefficients represent change in \(Y\) for a 1 SD change in \(X\).
![]()
STAT 204 – Intro to Statistical Data Analysis