Regression Metrics & Residual Analysis
MAE, MSE, RMSE, and R², 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.
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.
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?"
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
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.
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
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.
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.
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
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.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.
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
| Metric | Value | What it tells you |
|---|---|---|
| MAE | $22,410 | Typical error, outlier-resistant |
| MSE | 1,142,650,000 | Squared units — rarely reported alone |
| RMSE | $33,803 | Typical error, punishes big misses more |
| R² | 0.884 | 88.4% of variance explained vs. mean baseline |
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:
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()
LinearRegression) is missing a real curved relationship a more flexible model, or an engineered feature, could capture.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
sklearn.metrics: mean_absolute_error, mean_squared_error, r2_score (plus np.sqrt for RMSE).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.
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.
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?
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.metricsimport in older versions — usenp.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.
LinearRegressionon 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.