
Predictive Regression
Predictive Regression
Many predictors, few cases, and a number to predict
Chapter 13
Polla Fattah
By the end of today you can
- measure numeric predictions with RMSE, MAE, and R², against a baseline;
- explain why ordinary regression overfits with many predictors;
- fit and tune ridge, lasso, and elastic net regression;
- read which predictors the lasso keeps, and what that does not mean;
- explain how boosting builds a model from small trees, and tune Gradient Boosting;
- choose a model by cross-validation, and report the final test.
Many predictors, few cases
A questionnaire, several rounds of measurement, and a background survey can supply 50 predictors for 60 people.
Ordinary regression estimates a coefficient for each, fits its own data almost perfectly, and predicts new cases badly.
Correlated predictors, like questionnaire items, make it worse.
The question: final GPA from year one
The university’s advisers meet students at the end of year one.
| May use | May not use |
|---|---|
| background | anything after year one |
| all 22 questionnaire items | the workshop’s later effects |
| semester 1 and 2 records |
The outcome, GPA in semester 4, is a number: a regression problem.
One row per student, wide
year_one = s12.pivot(
index='student_id',
columns='semester',
values=['gpa', 'sleep_hours', 'study_hours', 'exercise_days',
'caffeine_mg', 'supervisor_meetings', 'wellbeing']
)
year_one.columns = [f"{col[0]}_s{col[1]}" for col in year_one.columns]Column names become gpa_s1, gpa_s2, sleep_hours_s1, etc.
Only students who stayed have a final GPA
gpa_data = (
students
.merge(questionnaire, on='student_id')
.merge(year_one, on='student_id')
.merge(final_gpa, on='student_id')
.drop(columns=['student_id', 'supervisor_id', 'considering_dropout'])
)merge(final_gpa, on='student_id') keeps students with a final GPA: 37 students who left are excluded.
The model predicts only for students who stay, and must not be used to judge the leavers.
Split, folds, and recipe
X_train, X_test, y_train, y_test = train_test_split(
X, y, test_size=0.25, random_state=2026
)
gpa_folds = KFold(n_splits=10, shuffle=True, random_state=2026)
gpa_recipe = ColumnTransformer([
('num', Pipeline([
('impute', SimpleImputer(strategy='median')),
('scale', StandardScaler())
]), numeric_cols),
('cat', Pipeline([
('impute', SimpleImputer(strategy='most_frequent')),
('dummy', OneHotEncoder(drop='first', sparse_output=False, handle_unknown='ignore'))
]), categorical_cols)
])After one-hot encoding categorical variables: 51 predictors.
Errors of a numeric prediction
| Actual | Predicted | Error |
|---|---|---|
| 3.2 | 3.0 | 0.2 |
| 2.8 | 2.9 | −0.1 |
| 3.6 | 3.3 | 0.3 |
| 3.0 | 3.1 | −0.1 |
| 2.5 | 2.9 | −0.4 |
Each error is actual minus predicted (\(y - \hat{y}\)).
Three summaries of the errors
| Measure | Definition | Here |
|---|---|---|
| MAE | average size of the errors (\(\frac{1}{n}\sum |y - \hat{y}|\)) | 0.22 |
| RMSE | square root of average squared error (\(\sqrt{\frac{1}{n}\sum (y - \hat{y})^2}\)) | larger; punishes big misses |
| R² | variance explained (\(1 - \frac{SS_{\text{res}}}{SS_{\text{tot}}}\)) | share captured, 0 to 1 |
MAE and RMSE are in GPA points, lower is better: “typically off by about 0.2”.
Always compare with a baseline
Predict the training mean for everyone: RMSE 0.32.
Any useful model must do clearly better.
Ordinary regression with every predictor
Cross-validated RMSE 0.220: much better than the baseline.
With 422 students and 51 predictors, it works reasonably. Theses often have 60.
The same model on 60 students
| Evaluated on | RMSE |
|---|---|
| its own 60 students | 0.09 |
| new students | 0.60 |
| baseline (the mean) | 0.32 |
Excellent on its own data, worse than predicting the average on new data.
Multicollinearity
Six stress items, two semesters of GPA: many predictors are strongly correlated.
Regression struggles to divide credit between them.
Coefficients become large, unstable, even of opposite signs, cancelling out on training data but not on new data.
Regularisation penalises large coefficients
\[ \text{sum of squared errors} + \alpha \times \text{size of coefficients} \]
| Penalty \(\alpha\) | Effect |
|---|---|
| 0 | ordinary regression |
| larger | coefficients shrunk towards zero |
A little shrinkage costs almost no training fit and buys substantial stability. Predictors must be standardised.
Ridge, lasso, and elastic net
| Method | Penalty | Effect |
|---|---|---|
| ridge | \(\sum \beta_j^2\) (\(L_2\)) | shrinks all; keeps every predictor |
| lasso | \(\sum |\beta_j|\) (\(L_1\)) | shrinks, and sets some to exactly zero |
| elastic net | \(\rho \sum |\beta_j| + (1-\rho) \sum \beta_j^2\) | balance between ridge and lasso |
In scikit-learn: Ridge(alpha=...), Lasso(alpha=...), ElasticNet(alpha=..., l1_ratio=...).
A lasso on the same 60 students
lasso_small = Pipeline([
('prep', gpa_recipe),
('model', Lasso(alpha=0.03, random_state=2026))
]).fit(X_small, y_small)| Model on 60 students | RMSE on new students |
|---|---|
| ordinary regression | 0.60 |
| lasso | 0.22 |
The penalty kept the model from chasing noise.
Shrinkage, coefficient by coefficient
The lasso keeps 11 of 51 predictors and shrinks even those.
Coefficients that make no sense
| Predictor | Ordinary regression | Lasso |
|---|---|---|
| semester 1 GPA | 0.22 | 0.09 |
| semester 2 GPA | 0.02 | 0.11 |
| semester 2 caffeine | -0.16 | 0.00 |
With 60 students, ordinary regression fits chance patterns. Shrunken coefficients cannot chase one sample’s noise.
Tuning penalty \(\alpha\)
alphas = np.logspace(-4, 0, 20)
lasso_grid = GridSearchCV(
Pipeline([('prep', gpa_recipe), ('model', Lasso(random_state=2026, max_iter=5000))]),
param_grid={'model__alpha': alphas},
cv=gpa_folds, scoring='neg_root_mean_squared_error'
).fit(X_train, y_train)20 penalties on a log scale, for ridge, elastic net, and lasso.
Too little, just right, too much

At the far right, lasso zeros every coefficient: back to baseline. Best: \(\alpha \approx\) 0.013.
What the lasso keeps
Of 51 predictors, the lasso keeps 9. Year-one GPA dominates.
One SD higher semester 2 GPA: about 0.15 points higher final GPA.
The lasso path

As the penalty falls, predictors enter one by one. The two GPA measures enter first.
Selected is not the same as causal
Sleep affects GPA (Chapter 8), yet the lasso drops it.
Its effect is already visible in year-one GPA, so it adds little to the prediction.
Among correlated predictors the lasso keeps one, and which one can change between samples. Predict with the lasso; explain with Chapters 8 and 10.
Boosting
flowchart LR
A["Start:<br>the average"] --> B["Fit a small tree<br>to the errors"] --> C["Add it,<br>scaled down"]
C -->|repeat hundreds of times| B
Each tree learns what the model so far missed. The learning rate keeps each step small.
Many weak trees add up to a strong model.
Boosting with one predictor

One step, a staircase, then a curve that follows the data.
Forests and boosting
| Random forest | Boosting | |
|---|---|---|
| trees grown | independently | in sequence |
| combined by | averaging | adding corrections |
| main settings | trees, max_features |
trees, learning_rate, max_depth |
GradientBoostingRegressor fits trees sequentially to correct the ensemble’s residual errors.
Tuning Gradient Boosting
gb_grid = GridSearchCV(
Pipeline([('prep', gpa_recipe), ('model', GradientBoostingRegressor(n_estimators=500, random_state=2026))]),
param_grid={'model__learning_rate': [0.01, 0.03, 0.1], 'model__max_depth': [1, 2, 4]},
cv=gpa_folds, scoring='neg_root_mean_squared_error'
).fit(X_train, y_train)Best: depth 1, RMSE 0.211.
Depth-1 trees add separate effects with no interactions. Shallow winning means few interactions to find.
Comparing the models
| Model | CV RMSE | Std. Error | |
|---|---|---|---|
| 0 | lasso | 0.204 | 0.008 |
| 1 | elastic net | 0.204 | 0.008 |
| 2 | Gradient Boosting | 0.211 | 0.009 |
| 3 | ridge | 0.220 | 0.009 |
| 4 | linear regression | 0.220 | 0.009 |
| 5 | baseline (mean) | 0.322 | 0.010 |
Lasso and elastic net on top, Gradient Boosting and ridge close behind, all better than ordinary regression. The lasso is chosen for using only 9 predictors.
The final test
| MAE | RMSE | R² |
|---|---|---|
| 0.16 | 0.19 | 0.66 |
Off by 0.16 GPA points on average, capturing 66% of the variation.
Predicted against actual

Predictions are less spread than reality: extreme students are pulled towards the average.
How much does year one add to past grades?
gpa_only_pipe = Pipeline([
('impute', SimpleImputer(strategy='mean')),
('model', LinearRegression())
]).fit(X_train[['gpa_s1', 'gpa_s2']], y_train)| Model | Test RMSE |
|---|---|
| lasso with everything | 0.192 |
| two year-one GPAs only | 0.191 |
Past grades are by far the best predictor. Sleep, stress, and support matter (Chapter 8), but their influence is already in the grades.
In your field: engineering
c_lm = LinearRegression().fit(cX_train, cy_train)
c_gb = GradientBoostingRegressor(
n_estimators=500, learning_rate=0.05, max_depth=4, random_state=1
).fit(cX_train, cy_train)Concrete strength from its recipe and age: curves and interactions.
Boosting cuts RMSE from 10.90 to 4.55 MPa. Flexible models win when the data has structure for them.
Practical lab: the Chapter 13 playground
Work through the playground exercises in your browser, with hints and solutions.
The browser version uses Pyodide to run scikit-learn, NumPy, and pandas right in your browser.
Practical exercises 1–3: errors and shrinkage
- MAE and RMSE by hand; change one prediction by 1.0 and compare.
- Repeat the 60-student experiment with 150 students.
- Ridge against lasso coefficients: how many are exactly zero?
Practical exercises 4–6: design and communication
- Scale scores instead of 22 items: does the lasso’s RMSE change?
- Gradient Boosting with 100, 500, and 1000 trees at learning rate 0.03.
- Explain an RMSE of 0.2 to academic advisers in two sentences.
Try this yourself
Take a prediction problem from your own field:
- count your cases and your predictors;
- name a baseline prediction;
- decide whether a lasso, a ridge, or boosting suits the structure;
- plan how you would report the final test.
Troubleshooting guide (Part 1)
| Symptom | Likely cause |
|---|---|
| near-perfect training fit, poor new predictions | too many predictors for the cases |
| coefficients huge and of opposite signs | multicollinearity |
| regularisation changes nothing | predictors not standardised |
| the lasso predicts the mean for everyone | penalty \(\alpha\) too large |
Troubleshooting guide (Part 2)
| Symptom | Likely cause |
|---|---|
| a different lasso selection each sample | correlated predictors; selection is unstable |
| the lasso “drops” a known cause | its effect is carried by another predictor |
| RMSE reported without a baseline | the number cannot be judged |
| extremes predicted too close to the mean | normal for imperfect models |
Completion checklist
Misconceptions to leave behind (Part 1)
| Misconception | Better mental model |
|---|---|
| more predictors always help | each is another chance to fit noise |
| the lasso’s predictors are what matters | they help prediction, chosen partly by chance |
Misconceptions to leave behind (Part 2)
| Misconception | Better mental model |
|---|---|
| a low RMSE means every prediction is accurate | extremes are pulled to the average |
| boosting always beats regression | only when curves and interactions exist |
The chapter in one sentence
With many predictors, shrink the coefficients or build from small trees, compare against a baseline by cross-validation, and never read a selected predictor as a cause.
Next: Chapter 14
The next chapter revisits clustering:
- what counts as a group;
- Gaussian mixture models and soft membership;
- clusters as dense regions: DBSCAN;
- evaluating a clustering;
- reporting the choices behind the clusters.
Questions
How many predictors, and how many cases, would your model have?
What is the simplest prediction your model must beat?