🎯 Section 3 · Model Evaluation Mastery 🟡 Intermediate MODULE 15

Regression Metrics & Residual Analysis

⏱️ 26 min read
📖 MAE, MSE, RMSE, R² & Residual Plots
🧩 4 Quiz Questions
🏗️ 1 Challenge · 3 tasks
Your progress in Section 375%
🎯 What you'll learn: Lessons 13 and 14 covered CLASSIFICATION metrics — precision, recall, F1. This lesson does the same job for REGRESSION: MAE, MSE, RMSE, and , computed with real sklearn.metrics functions. You'll see exactly why MAE resists outliers while MSE and RMSE punish them harder, why a single number is never the whole story, and how to read a residual plot — predicted vs actual, and residuals vs predicted — to catch problems like heteroscedasticity and non-linearity that no single metric can show you.

One Number Is Never the Whole Story

Lesson 12's capstone used RMSE and R² to compare LinearRegression against GradientBoostingRegressor — but only briefly explained what those numbers actually meant. This lesson slows down and covers the full regression-metrics toolbox properly: four metrics that each answer a slightly different question about the same set of prediction errors.

Every metric in this lesson starts from the same raw ingredient: the residual — the gap between what actually happened and what the model predicted.

The residual, for a single prediction i residual_i  =  y_i  -  ŷ_i y_i is the true value, ŷ_i ("y-hat") is what the model predicted. Positive residual = model UNDER-predicted. Negative = model OVER-predicted.

MAE, MSE, RMSE, and R² are all different ways of summarizing a whole array of residuals — one per test-set row — into a single number. The differences between them come down to HOW they combine those residuals.

📝
Regression metrics vs. classification metrics
Classification metrics (Lessons 13–14) count DISCRETE outcomes — right class or wrong class. Regression metrics measure CONTINUOUS distance — how far off in actual numeric terms. There's no confusion matrix here; instead, every metric below is computed directly from y_test and y_pred, both arrays of real numbers.

Mean Absolute Error (MAE) — Robust to Outliers

MAE takes the absolute value of every residual (so a $10,000 over-prediction and a $10,000 under-prediction count equally), then averages them. It answers a simple, very interpretable question: "on average, how many dollars off is a typical prediction?"

Mean Absolute Error MAE  =  (1/n) · Σ |y_i - ŷ_i| Same units as the target (e.g. dollars). Every error contributes proportionally — a $40,000 miss counts exactly 4x a $10,000 miss, no more.
mae_example.py
PYTHON
from sklearn.metrics import mean_absolute_error

mae = mean_absolute_error(y_test, y_pred)
print(f"MAE: ${mae:,.0f}")
# MAE: $22,410   -> on average, predictions are off by about $22,410
Why "robust to outliers" specifically means THIS
Because MAE takes the absolute value rather than squaring, one single catastrophic miss (say, a $400,000 error on one unusual row) contributes to MAE only in proportion to its own size — it doesn't get amplified. Compare that to MSE and RMSE below, where the SAME $400,000 miss gets squared before averaging, and can dominate the whole metric almost by itself.

Mean Squared Error (MSE) & RMSE — Punishing Big Misses Harder

MSE squares every residual before averaging. Squaring does two things at once: it makes every error positive (like abs() did for MAE), and it makes LARGE errors count disproportionately more than small ones.

Mean Squared Error MSE  =  (1/n) · Σ (y_i - ŷ_i)² Units are SQUARED (e.g. dollars²) — which is exactly why RMSE exists: to undo the squaring and get back to interpretable units.
Root Mean Squared Error RMSE  =  √MSE Same units as the target again (e.g. dollars) — the square root undoes MSE's squaring, making it directly comparable to MAE.
mse_rmse_example.py
PYTHON
import numpy as np
from sklearn.metrics import mean_squared_error

mse = mean_squared_error(y_test, y_pred)
rmse = np.sqrt(mse)

print(f"MSE:  {mse:,.0f}")
print(f"RMSE: ${rmse:,.0f}")
# MSE:  1,142,650,000
# RMSE: $33,803
📝
Newer scikit-learn also ships root_mean_squared_error directly
Recent scikit-learn versions add sklearn.metrics.root_mean_squared_error(y_test, y_pred) as a dedicated function, computing the exact same value as np.sqrt(mean_squared_error(...)) above without the extra step. Either approach is correct — this lesson uses the explicit np.sqrt version, matching Lesson 12's capstone, since it works across scikit-learn versions without depending on which one is installed.

Notice RMSE ($33,803) came out higher than MAE ($22,410) on the exact same predictions. That gap is not a coincidence — it happens whenever the errors aren't all the same size, and it grows larger the more a few big misses stand out from the rest.

⚠️
RMSE ≥ MAE is a mathematical guarantee, not a fluke
RMSE is always greater than or equal to MAE for the same set of predictions. They're equal only in the unusual case where every single residual has the exact same absolute size. The bigger the gap between RMSE and MAE, the more the errors are concentrated in a few large misses rather than spread evenly — a useful diagnostic on its own, before even looking at a residual plot.

R² (Coefficient of Determination) — Proportion of Variance Explained

MAE and RMSE tell you the error in absolute units — but "$22,410 off" is hard to judge without context. Is that good? It depends entirely on whether the target ranges from $100,000 to $800,000, or from $20,000 to $30,000. R² instead expresses the model's performance RELATIVE to the simplest possible baseline: just predicting the mean every time.

R² — Coefficient of Determination R²  =  1  -  SS_res / SS_tot SS_res = Σ(y_i - ŷ_i)² — the model's actual squared errors. SS_tot = Σ(y_i - ȳ)² — squared errors of a "dumb" model that always predicts the mean ȳ.
r2_example.py
PYTHON
from sklearn.metrics import r2_score

r2 = r2_score(y_test, y_pred)
print(f"R2: {r2:.3f}")
# R2: 0.884   -> the model explains about 88.4% of price's variance
🎯
R² = 1.0
A perfect model — every prediction exactly matches the true value. SS_res = 0.
⚖️
R² = 0.0
No better than always guessing the mean. SS_res equals SS_tot exactly.
📉
R² < 0
Worse than the mean-guessing baseline — a real, legal outcome, not a bug.
⚠️
R² can go negative — and that's the metric working correctly
Unlike accuracy (Lesson 13), which is bounded between 0 and 1, r2_score has no floor. If a model's SS_res is LARGER than SS_tot — meaning its predictions are, on average, worse than simply guessing the training mean every time — R² comes out negative. This happens most often when a model is evaluated on data very different from what it was trained on, or when it badly underfits.
R² lets you compare across different targets; RMSE/MAE don't
An RMSE of $30,000 sounds enormous for predicting a $50,000 used car price, but tiny for predicting an $800,000 house price. Because R² is a RATIO relative to that same target's own variance, an R² of 0.88 means roughly the same thing — "explains 88% of the variance" — regardless of whether the target is measured in dollars, years, or degrees Celsius.

Computing All Four Together

In practice, a real project prints all four side by side rather than picking just one — each one flags a different kind of problem.

all_regression_metrics.py
PYTHON
import numpy as np
from sklearn.metrics import mean_absolute_error, mean_squared_error, r2_score

def regression_report(y_true, y_pred, name="model"):
    mae = mean_absolute_error(y_true, y_pred)
    mse = mean_squared_error(y_true, y_pred)
    rmse = np.sqrt(mse)
    r2 = r2_score(y_true, y_pred)
    print(f"{name} -> MAE=${mae:,.0f}  RMSE=${rmse:,.0f}  R2={r2:.3f}")
    return {"mae": mae, "rmse": rmse, "r2": r2}

regression_report(y_test, y_pred, "LinearRegression")
# LinearRegression -> MAE=$22,410  RMSE=$33,803  R2=0.884
Illustrative side-by-side comparison, same toy predictions
MetricValueWhat it tells you
MAE$22,410Typical error, outlier-resistant
MSE1,142,650,000Squared units — rarely reported alone
RMSE$33,803Typical error, punishes big misses more
0.88488.4% of variance explained vs. mean baseline
📝
Choosing which one to optimize for
If a few very large errors are especially costly in the real-world use case (e.g. a pricing tool that occasionally recommends a wildly wrong price), favor a model with lower RMSE — it's the more error-sensitive metric. If large and small errors should count equally and a handful of outlier rows shouldn't dominate the comparison, favor MAE. R² is best used as a normalized, comparable headline number rather than an optimization target on its own.

Residual Plots — Seeing What the Metrics Can't

MAE, RMSE, and R² each compress an entire array of residuals into one number — which is exactly their weakness. Two very different-looking sets of errors can produce the identical RMSE. A residual plot shows every individual error, so patterns invisible to any single metric become visible at a glance.

Two residual plots are standard, and they answer different questions:

🎯
Predicted vs. Actual
Points should hug the diagonal y=x line. Systematic drift away from it reveals bias.
📊
Residuals vs. Predicted
Points should form a random, flat cloud centered on zero — no shape, no trend.
residual_plots.py
PYTHON
import matplotlib.pyplot as plt

y_pred = model.predict(X_test)
residuals = y_test - y_pred

fig, axes = plt.subplots(1, 2, figsize=(12, 5))

# Plot 1: Predicted vs Actual — points should hug the diagonal
axes[0].scatter(y_test, y_pred, alpha=0.5)
axes[0].plot(
    [y_test.min(), y_test.max()],
    [y_test.min(), y_test.max()],
    "r--", label="Perfect prediction"
)
axes[0].set_xlabel("Actual")
axes[0].set_ylabel("Predicted")
axes[0].set_title("Predicted vs Actual")
axes[0].legend()

# Plot 2: Residuals vs Predicted — should be a random cloud around 0
axes[1].scatter(y_pred, residuals, alpha=0.5)
axes[1].axhline(y=0, color="r", linestyle="--")
axes[1].set_xlabel("Predicted")
axes[1].set_ylabel("Residuals")
axes[1].set_title("Residuals vs Predicted")

plt.tight_layout()
plt.show()
Illustrative — a healthy residuals-vs-predicted plot
Random scatter, roughly constant spread across the whole x-axis, centered on zero. No visible shape.
Illustrative — heteroscedasticity (a "funnel" or "cone" shape)
Residuals stay small and tight at low predicted values, then fan out wider as predicted values grow. Variance is NOT constant.
Illustrative — non-linearity (a curved / systematic pattern)
Residuals dip below zero, then rise above it, then dip again — a smooth curve rather than random noise. The model missed a non-linear relationship.
📐
Heteroscedasticity
The spread of errors changes as the predicted value changes — often growing wider at higher values. Common in prices, where a $50,000 miss matters more, and happens more often, for a $2M home than a $150,000 one.
〰️
Non-linearity
Residuals trace a curve instead of random noise — the model (often LinearRegression) is missing a real curved relationship a more flexible model, or an engineered feature, could capture.
⚠️
Two models can share an RMSE and still fail differently
A model whose residuals form a clean, healthy random cloud, and a model whose residuals form a heteroscedastic funnel, can land on nearly identical RMSE values — RMSE only measures overall magnitude, not the SHAPE of where the errors occur. This is exactly why residual plots are a required companion to the metrics above, not an optional extra step.
What to actually do about a bad-looking residual plot
Heteroscedasticity often improves with a log-transform of a skewed target (e.g. modeling np.log(price) instead of raw price). Non-linearity often improves by switching from LinearRegression to a tree-based model like GradientBoostingRegressor (Lesson 10) that doesn't assume a straight-line relationship, or by engineering polynomial/interaction features for the linear model.

Lesson Summary

MAE averages absolute errors — outlier-resistant, in the target's own units.
MSE/RMSE square errors first, punishing large misses disproportionately harder; RMSE ≥ MAE always.
measures variance explained relative to a mean-guessing baseline — comparable across different targets, and CAN go negative.
All four come from sklearn.metrics: mean_absolute_error, mean_squared_error, r2_score (plus np.sqrt for RMSE).
Residual plots — predicted vs actual, and residuals vs predicted — reveal shape problems like heteroscedasticity and non-linearity that no single number can show.
🧩 Knowledge Check — Lesson 15
4 questions on MAE, MSE/RMSE, R², and residual plots.
1. Which metric is most robust to a few very large outlier errors, and why?
2. Why does RMSE end up penalizing large errors more heavily than MAE does?
3. A model scores R² = -0.15 on the test set. What does that mean?
4. A residuals-vs-predicted plot shows a "funnel" shape — tight near zero, fanning out much wider at higher predicted values. What does this indicate?
💪
Try It Yourself — Lesson 15
Compute every regression metric and read its residual plot · Intermediate Level

Use any regression model and dataset from this course — Lesson 12's house-price pipeline works well, or any other regression problem you've built.

Task 1: Print the full metric report 📊

Write a function like regression_report() from Section 5 that computes and prints MAE, MSE, RMSE, and R² together using sklearn.metrics. Run it on your model's test-set predictions.
Task 2: Draw both residual plots 📈

Using matplotlib, draw the predicted-vs-actual scatter plot AND the residuals-vs-predicted scatter plot from Section 6's code. Do the points hug the diagonal? Is the residual cloud flat and centered on zero, or does it show a shape?
Task 3: Break it on purpose 🔨

Fit a plain LinearRegression on a target you suspect has a non-linear or skewed relationship to its features (or add a handful of extreme outlier rows to your training data). Recompute MAE and RMSE — which one moved more? Redraw the residuals-vs-predicted plot — what shape shows up, and which pattern from Section 6 (heteroscedasticity or non-linearity) does it most resemble? Write 2–3 sentences describing what you saw.
💡 Show hints if you're stuck
  • Task 1: Remember RMSE isn't a direct sklearn.metrics import in older versions — use np.sqrt(mean_squared_error(...)) as shown in Section 3.
  • Task 2: If your model already looks good, try re-running the same plots on an intentionally under-trained model (e.g. LinearRegression on data with an obvious curve) to see a bad-looking plot for comparison.
  • Task 3: A handful of extreme outliers should move RMSE noticeably more than MAE, since RMSE squares errors before averaging — that's the exact mechanism from Section 3.
Finished this lesson?
Mark it complete to track your progress.
🎉

Lesson 15 Complete!

You can now compute and interpret MAE, MSE, RMSE, and R² correctly, and read a residual plot for heteroscedasticity and non-linearity — the same tools a working data scientist uses to judge every regression model. One lesson left in Section 3: the capstone project, comparing multiple models honestly with cross-validation.

Module 15 of 24 Section 3 — Model Evaluation Mastery