
Suppose we wish to learn a linear model. Our estimate (denoted by hats) is
\[\hat Y = \hat \beta_0 + \hat \beta_1 X_1 + \dots + \hat \beta_p X_p\]
Why would we wish to select a subset of the \(p\) variables?
Start with just null model \(\mathcal{M}_0\) that has no predictors
Fit every possible 1 variable model, save the one with the best results
Fit every possible 2 variable model, save the one with the “best” results
Repeat this process for all subset sizes \(p\)
Pick the single best model among \(\mathcal{M}_0, \dots, \mathcal{M}_P\)
This is not typically used in research!
Only practical with a smaller number of variables (10 variables = \(2^{10} = 1024\) models)
Arbitrary way of defining best and ignores prior knowledge about potential predictors
Forward stepwise selection
Start with the null model, containing no predictors
Fit all one-predictor models: select the predictor whose addition to the model produces the smallest p-value.
Add that predictor to the model
From the remaining predictors, fit all models obtained by adding one additional predictor to the current model. Select the predictor whose addition gives the smallest p-value.
Repeat this process until a stopping criterion is met (e.g. the smallest p-value among the remaining candidate predictors exceeds 0.05, the improvement in AIC/BIC is no longer sufficient)
Backward stepwise selection has a similar process, you just start with all predictors in the model.
Both algorithms are greedy: make short-sighted, localized decisions that maximize the model’s immediate fit, without considering how that choice will impact the final model in later stages.
Variable selection is a difficult problem! Like much of statistics, there is no one right answer
Idea: use your brain instead of algorithms
Justify which predictors used in modeling based on:
A confounding variable (or lurking variable) is one that is influences both the treatment/predictor of interest (\(X\)) and the outcome (\(Y\))
When a confounder is present, we may observe a spurious correlation between the \(X\) and \(Y\), even if no direct causal relationship exists
So, it is important to stop and think about whether there are confounding variables which could be explaining the association instead


In reality, there is no causal relationship between ice cream sales and shark attacks: changing one will not cause a change in the other. Temperature influences both variables simultaneously.
Does a pitcher with a high groundball rate inherently prevent more runs?
Including quality of infield defense in the model may help to reduce bias in estimating the relevent effect of groundball rate.
Covariance is a measure of the linear dependence between two variables
Measures how two variables change together, indicating the direction of their relationship
Independence implies zero covariance (knowing the value of one tells you nothing about the probability distribution of the other)
Zero covariance does not guarantee independence: only means there is no linear correlation between the variables
Correlation is a normalized form of covariance, ranges from -1 to 1
-1 means one variable linearly decreases absolutely in value while the other increases in value
0 means no linear dependence
1 means one variable linearly increases absolutely while the other increases
A subset of batting statistics for all MLB players with at least 500 plate appearances in 2025. Obtained from Baseball Savant.
Interested in relationship between batting statistics with wOBA
View the correlation matrix with ggcorrplot
Let’s say we are interested in understanding the relationship between quality of contact and wOBA. We might measure quality of contact in two ways:
hard_hit_percent: ball struck at least 95 mphbarrel_batted_rate: ball struck at least 98 mph with a launch angle between 26-30 degrees. As exit velocity increases, the launch angle range widens.
| Characteristic | Beta | 95% CI | p-value |
|---|---|---|---|
| barrel_batted_rate | 0.004 | 0.003, 0.006 | <0.001 |
| hard_hit_percent | 0.000 | -0.001, 0.001 | 0.5 |
| Abbreviation: CI = Confidence Interval | |||
After accounting for Barrel%, Hard Hit% provides almost no additional information about wOBA.
Inference: if we are trying to understand does the quality of contact affect wOBA? we can roughly consider Barrel% and Hard Hit% as two measures of the same underlying construct
Fair to remove one in this scenario, as
“holding Barrel% fixed while changing Hard Hit%” is not practical in baseball
Hard Hit% is not necessarily a confounding variable, rather, Barrel% is a subset of Hard Hit%
Prediction: if we are trying to build a linear regression model to predict a new player’s wOBA from their Hard Hit% and Barrel%, it could go either way
Collinearity is a problem insofar as it creates high prediction variance
But maybe, given Barrel%, Hard Hit% still explains a enough variance in the outcome to counteract this
One option is to use cross validation to see!
Courtesy of Alex Reinhart’s regression notes:
Be very careful with the interpretation of your coefficients
\(\beta_1\) is the change in the mean of \(Y\) when increasing \(X_1\) while holding \(X_2\) fixed.
If \(X_1\) and \(X_2\) have a strong correlation, that may be very different than the change in the mean of \(Y\) when increasing \(X_1\) without holding \(X_2\) fixed.
The question to ask when we have collinearity is “Have we chosen the correct predictors for the research question?”
If we have, there is little to be done; if we have not, we can reconsider our choice of predictors and perhaps eliminate the collinear ones.
In other words, if you have a confounding variable that is collinear with your variable of interest, leave it in the model and move on.
For each fold \(k=1,2,\dots,K\)
Fit the model on data excluding observations in fold \(k\)
Obtain predictions for fold \(k\)
Compute performance measure (e.g. MSE) for fold \(k\)
Aggregate performance measure across folds
We can justify model choice (e.g. model selection/comparison, variable selection, tuning parameter selection) using any performance measure we want with CV
Why do we typically choose \(K=5\) or \(K=10\)? Think about: What’s the smallest and largest \(K\) could be? And what’s the trade-off?
Perform 5-fold cross-validation to assess model performance
Create a column of test fold assignments to our dataset with the sample() function
Always remember to set.seed() prior to performing cross-validation
Function to perform cross-validation
batting_cv <- function(x) {
# get test and training data
test_data <- batting_2025 |> filter(fold == x)
train_data <- batting_2025 |> filter(fold != x)
# fit models to training data
barrel_fit <- lm(woba ~ barrel_batted_rate, data = train_data)
combined_fit <- lm(woba ~ barrel_batted_rate + hard_hit_percent, data = train_data)
# return test results
out <- tibble(
barrel_pred = predict(barrel_fit, newdata = test_data),
combined_pred = predict(combined_fit, newdata = test_data),
test_actual = test_data$woba,
test_fold = x
)
return(out)
}Compute RMSE across folds with standard error intervals
batting_test_summary <- batting_test_preds |>
pivot_longer(barrel_pred:combined_pred, names_to = "model", values_to = "test_pred") |>
group_by(model, test_fold) |>
summarize(rmse = sqrt(mean((test_actual - test_pred)^2)))
batting_test_summary |>
group_by(model) |>
summarize(avg_cv_rmse = mean(rmse),
sd_rmse = sd(rmse),
k = n()) |>
mutate(se_rmse = sd_rmse / sqrt(k),
lower_rmse = avg_cv_rmse - se_rmse,
upper_rmse = avg_cv_rmse + se_rmse) |>
select(model, avg_cv_rmse, lower_rmse, upper_rmse)# A tibble: 2 × 4
model avg_cv_rmse lower_rmse upper_rmse
<chr> <dbl> <dbl> <dbl>
1 barrel_pred 0.0244 0.0227 0.0261
2 combined_pred 0.0245 0.0229 0.0262
Whether or not you include Hard Hit%, model performance is essentially the same
If anything, performance is slightly better without Hard Hit%
We will discuss more about prediction and correlated covariates tomorrow!
This data analysis workflow is adapted from Valérie Ventura’s 36-617 class.
Formulate a question. Some of you have very well-defined projects, some not quite as much. This step can take a while!
Determine what sort of analysis is needed: are we doing inference? Prediction?
Identify the data you’d want to have to answer your question: this will not only help you determine the analysis you want to do, but also the potential limitations of your data.
Understand the data you do have.
Identify covariate(s) of main interest.
Identify covariates that also (partially) cause the response variable.
Do you anticipate having to create new covariates? What about an interaction in your model?
Think about limitations: will your analysis be limited in any way because the data is insufficient, maybe because it contains the wrong variables, it is missing variables, etc.?
Why is your model(s) appropriate for the problem?
For simplicity and explanability purpose? (Why do you prefer a simpler model over a black box?)
For complexity and flexibility purpose? (Why do you prefer a black box over a simpler model?)
How do you evaluate your model?
Do you compare it with other models? Do you have a baseline model?
Do you compare it with alternative specifications (e.g. different set of predictors)?
One-dimensional EDA: create plots and summaries of all relevant variables, look for missing data and outliers.
Two-dimensional EDA: plot all important variables against each other.
Create any new covariates you previously identified.
Try to answer your question of interest through plots. This will help inform and justify your modeling choices.
You are now ready to start modeling!
Don’t just say “we find a statistically significant association between Y and X…”
What do those coefficient estimates tell you? Interpret in context
What’s the margin of error?
Did the model also adjust for other potential meaningful predictors?
This also applies to (\(k\)-fold) cross-validation results
Report the average of the performance measure (e.g. MSE, accuracy, etc.) across the \(k\) test set along with standard errors
In some cases, all \(k\) folds yield similar results, but in other cases, the results are vastly different across the folds
Without some indication of uncertainty, no one will have a clue of how reliable your results are
| Term | Estimate | SE | t | p-value |
|---|---|---|---|---|
| (Intercept) | 40.46 | 1.35 | 29.89 | 0.00 |
| RottenTomatoes | 0.39 | 0.02 | 24.69 | 0.00 |
| TheatersOpenWeek | 0.00 | 0.00 | −5.44 | 0.00 |
| OpeningWeekend | −0.11 | 0.04 | −3.16 | 0.00 |
| Budget | 0.02 | 0.01 | 1.45 | 0.15 |
| DomesticGross | 0.07 | 0.01 | 5.52 | 0.00 |
| ForeignGross | 0.00 | 0.00 | 0.84 | 0.40 |
| Characteristic | Beta | 95% CI | p-value |
|---|---|---|---|
| RottenTomatoes | 0.39 | 0.36, 0.43 | <0.001 |
| TheatersOpenWeek | 0.00 | 0.00, 0.00 | <0.001 |
| OpeningWeekend | -0.11 | -0.19, -0.04 | 0.002 |
| Budget | 0.02 | -0.01, 0.04 | 0.15 |
| DomesticGross | 0.07 | 0.04, 0.09 | <0.001 |
| ForeignGross | 0.00 | -0.01, 0.01 | 0.4 |
| Abbreviation: CI = Confidence Interval | |||
# https://bradcongelio.com/nfl-analytics-with-r-book/04-nfl-analytics-visualization.html
cpoe <- read_csv("http://nfl-book.bradcongelio.com/pure-cpoe")
cpoe_gt <- cpoe |>
select(passer, season, total_attempts, mean_cpoe) |>
gt(rowname_col = "passer") |>
fmt_number(columns = c(mean_cpoe), decimals = 2) |>
data_color(columns = c(mean_cpoe),
fn = scales::col_numeric(palette = c("#FEE0D2", "#67000D"), domain = NULL)) |>
cols_align(align = "center", columns = c("season", "total_attempts")) |>
tab_stubhead(label = "Quarterback") |>
cols_label(season = "Season",
total_attempts = "Attempts",
mean_cpoe = "Mean CPOE") |>
tab_header(title = md("**Average CPOE in Pure Passing Situations**"),
subtitle = md("*For seasons between 2010 and 2022*")) |>
tab_source_note(source_note = md("Example adapted from the book<br>*An Introduction to NFL Analytics with R*")) |>
gtExtras::gt_theme_espn()
# gtsave(cpoe_gt, "cpoe_gt.png")| Average CPOE in Pure Passing Situations | |||
| For seasons between 2010 and 2022 | |||
| Quarterback | Season | Attempts | Mean CPOE |
|---|---|---|---|
| P.Rivers | 2013 | 291 | 10.89 |
| P.Mahomes | 2018 | 261 | 9.29 |
| A.Rodgers | 2020 | 242 | 8.57 |
| R.Wilson | 2013 | 231 | 8.47 |
| R.Wilson | 2018 | 277 | 8.45 |
| D.Brees | 2011 | 311 | 8.29 |
| M.Ryan | 2018 | 315 | 8.23 |
| M.Ryan | 2012 | 303 | 8.11 |
| R.Wilson | 2015 | 279 | 7.52 |
| J.Burrow | 2021 | 343 | 7.12 |
| Example adapted from the book An Introduction to NFL Analytics with R |
|||