Skip to content

Regression Evaluation

Every regression metric answers a slightly different question about your model's errors. MAE asks "how far off are we on average?" RMSE asks "are there catastrophic misses?" R² asks "how much of the variation in the target does the model explain?" Choosing the wrong one means optimising for the wrong thing — and you will not know until the model is in production and someone is asking why the forecast was so far off.

Learning Objectives

  • Calculate and interpret MAE, MSE, RMSE, R², Adjusted R², and MAPE
  • Choose the right metric based on business cost of errors
  • Read a residual plot and identify systematic problems
  • Compare models fairly using baseline-relative metrics

The Metrics and What They Measure

MAE — Mean Absolute Error

MAE is the average of the absolute differences between predictions and actual values. It is in the same units as the target, which makes it immediately interpretable.

Formula: MAE = mean(|y_actual - y_predicted|)

import numpy as np
from sklearn.metrics import mean_absolute_error
from sklearn.ensemble import RandomForestRegressor
from sklearn.datasets import fetch_california_housing
from sklearn.model_selection import train_test_split
from sklearn.preprocessing import StandardScaler
from sklearn.pipeline import Pipeline

housing = fetch_california_housing()
X_housing, y_housing = housing.data, housing.target  # target is median house value in $100k

X_train, X_test, y_train, y_test = train_test_split(
    X_housing, y_housing, test_size=0.2, random_state=42
)

rf_pipeline = Pipeline([
    ("scaler", StandardScaler()),
    ("rf", RandomForestRegressor(n_estimators=100, random_state=42))
])
rf_pipeline.fit(X_train, y_train)
y_pred = rf_pipeline.predict(X_test)

mae = mean_absolute_error(y_test, y_pred)
print(f"MAE: {mae:.3f} (${mae * 100_000:,.0f})")
# Output: MAE: 0.329 ($32,900)
# Interpretation: on average, our prediction is off by $32,900

When to use MAE: when all errors cost roughly the same. A forecast that is $10k wrong is five times as bad as one that is $2k wrong — linear, proportional. MAE is also robust to outliers because it does not square the errors.

MSE — Mean Squared Error

MSE squares each error before averaging. This makes large errors much more expensive than small ones.

Formula: MSE = mean((y_actual - y_predicted)²)

from sklearn.metrics import mean_squared_error

mse = mean_squared_error(y_test, y_pred)
print(f"MSE: {mse:.4f}")
# Output: MSE: 0.2551

MSE is hard to interpret on its own because the units are squared. It is most useful as a loss function during training (it is differentiable, which gradient-based methods need) and for comparing models against each other.

When to use MSE: as a training objective, or when comparing models internally. Do not report MSE to stakeholders — the units are unintuitive.

RMSE — Root Mean Squared Error

RMSE takes the square root of MSE, bringing the metric back to the original units of the target.

Formula: RMSE = sqrt(MSE)

rmse = np.sqrt(mse)
print(f"RMSE: {rmse:.3f} (${rmse * 100_000:,.0f})")
# Output: RMSE: 0.505 ($50,500)

Compare MAE ($32,900) to RMSE ($50,500) on the same predictions. RMSE is larger because it penalises the large errors more heavily. The gap between MAE and RMSE tells you something: a large gap means your model has occasional large misses that MAE is hiding.

Tip

If RMSE is substantially larger than MAE, your model is producing some very large errors on specific cases. Investigate which rows have the largest residuals — that gap is telling you where the model breaks down.

When to use RMSE: as the primary reporting metric when large errors are disproportionately costly. It is the most common regression metric for reporting.

R² — Coefficient of Determination

R² measures what fraction of the variance in the target the model explains. It is unitless and always relative to a baseline (predicting the mean).

Formula: R² = 1 - (SS_residual / SS_total) where SS_total = mean((y - y_mean)²)

from sklearn.metrics import r2_score

r2 = r2_score(y_test, y_pred)
print(f"R²: {r2:.3f}")
# Output: R²: 0.805
# Interpretation: the model explains 80.5% of the variance in house prices

Interpreting R²: - R² = 1.0 — perfect predictions - R² = 0.0 — the model does no better than predicting the mean for every row - R² < 0.0 — the model is worse than predicting the mean (this happens; it is a sign of serious problems)

Warning

R² always increases when you add more features to a linear model, even if those features are random noise. Use Adjusted R² when comparing models with different numbers of features. Never trust R² alone for model selection.

Adjusted R²

Adjusted R² penalises adding features that do not contribute meaningfully to the fit.

Formula: Adj R² = 1 - (1 - R²) * (n - 1) / (n - k - 1) where n is sample size and k is number of features.

def adjusted_r2(r_squared, n_samples, n_features):
    return 1 - (1 - r_squared) * (n_samples - 1) / (n_samples - n_features - 1)

n_samples = X_test.shape[0]
n_features = X_test.shape[1]

adj_r2 = adjusted_r2(r2, n_samples, n_features)
print(f"R²:          {r2:.3f}")
print(f"Adjusted R²: {adj_r2:.3f}")
# Output: R²:          0.805
# Output: Adjusted R²: 0.804
# Small difference here — expected with many samples and few features

Info

Adjusted R² is primarily useful for linear models. For tree-based models or neural networks, it is less meaningful — use cross-validated RMSE or MAE for model comparison instead.

MAPE — Mean Absolute Percentage Error

MAPE expresses error as a percentage of the actual value, making it intuitive for business communication: "our forecast is off by 8% on average."

Formula: MAPE = mean(|y_actual - y_predicted| / |y_actual|) * 100

def mean_absolute_percentage_error(y_actual, y_predicted):
    y_actual = np.array(y_actual)
    y_predicted = np.array(y_predicted)
    # Avoid division by zero
    mask = y_actual != 0
    return np.mean(np.abs((y_actual[mask] - y_predicted[mask]) / y_actual[mask])) * 100

mape = mean_absolute_percentage_error(y_test, y_pred)
print(f"MAPE: {mape:.1f}%")
# Output: MAPE: 16.8%
# Interpretation: on average, predictions are 16.8% off from actual values

Warning

MAPE is undefined when any actual value is zero — division by zero. It also gives disproportionate weight to cases where the actual value is very small (a $1 actual with a $0.50 error = 50% MAPE, but a $10,000 actual with a $5,000 error = 50% MAPE — the latter is far more important). Use MAPE cautiously, and never when your target can be zero.


Putting It Together — Compare All Metrics

from sklearn.dummy import DummyRegressor
from sklearn.linear_model import LinearRegression

# Baseline: predict the mean
baseline = DummyRegressor(strategy="mean")
baseline.fit(X_train, y_train)
y_baseline = baseline.predict(X_test)

# Simple linear model
linear_model = Pipeline([
    ("scaler", StandardScaler()),
    ("lr", LinearRegression())
])
linear_model.fit(X_train, y_train)
y_linear = linear_model.predict(X_test)

models = {
    "Baseline (mean)": y_baseline,
    "Linear Regression": y_linear,
    "Random Forest":    y_pred,
}

print(f"{'Model':<25} {'MAE':>8} {'RMSE':>8} {'R²':>8}")
print("-" * 55)
for model_name, predictions in models.items():
    m_mae  = mean_absolute_error(y_test, predictions)
    m_rmse = np.sqrt(mean_squared_error(y_test, predictions))
    m_r2   = r2_score(y_test, predictions)
    print(f"{model_name:<25} {m_mae:>8.3f} {m_rmse:>8.3f} {m_r2:>8.3f}")

# Output:
# Model                      MAE     RMSE       R²
# -------------------------------------------------------
# Baseline (mean)          0.534    0.736    0.000
# Linear Regression        0.530    0.726    0.030   (barely better than baseline on R²)
# Random Forest            0.329    0.505    0.805

Success

Always include the baseline in your comparison table. A model with R² = 0.03 looks like it's doing something — until you see that the baseline has R² = 0.00 and the improvement is 3 percentage points. Context changes everything.


Residual Analysis

An aggregate metric tells you how big the errors are on average. A residual plot tells you whether those errors are random or systematic. Random errors are acceptable — they are the irreducible noise in your data. Systematic errors mean the model is missing something.

import matplotlib.pyplot as plt
import numpy as np

residuals = y_test - y_pred

fig, axes = plt.subplots(1, 3, figsize=(15, 4))

# Plot 1: Residuals vs Predicted
axes[0].scatter(y_pred, residuals, alpha=0.3, s=10, color="steelblue")
axes[0].axhline(y=0, color="red", linestyle="--", linewidth=1)
axes[0].set_xlabel("Predicted values")
axes[0].set_ylabel("Residuals")
axes[0].set_title("Residuals vs Predicted")

# Plot 2: Residual distribution
axes[1].hist(residuals, bins=50, color="steelblue", edgecolor="white")
axes[1].axvline(x=0, color="red", linestyle="--", linewidth=1)
axes[1].set_xlabel("Residual")
axes[1].set_ylabel("Count")
axes[1].set_title("Residual Distribution")

# Plot 3: Actual vs Predicted
axes[2].scatter(y_test, y_pred, alpha=0.3, s=10, color="steelblue")
axes[2].plot([y_test.min(), y_test.max()],
             [y_test.min(), y_test.max()],
             color="red", linestyle="--", linewidth=1)
axes[2].set_xlabel("Actual values")
axes[2].set_ylabel("Predicted values")
axes[2].set_title("Actual vs Predicted")

plt.tight_layout()
plt.savefig("residual_plots.png", dpi=150, bbox_inches="tight")
plt.show()

What to look for in the residuals vs predicted plot:

Pattern What it means
Random scatter around zero Model is working well — errors are noise
Fan shape (variance increases with predictions) Heteroscedasticity — consider log-transforming the target
Curved pattern The relationship is non-linear and your model is not capturing it
Cluster of large errors at high predictions The model systematically underestimates high-value cases
Non-zero mean residuals Systematic bias — model consistently over- or under-predicts

Tip

The residual distribution should be approximately symmetric around zero. A strong left or right skew in residuals means the model is systematically wrong in one direction. Check whether a log transformation of the target fixes this.


Choosing Your Metric

Situation Recommended metric
All errors cost equally MAE
Large errors are especially costly RMSE
Need to explain to business stakeholders MAPE (if no zero targets)
Comparing models with different feature counts Adjusted R²
General model quality assessment R² + RMSE
Time series forecasting MAE or MAPE

Info

In practice, report at least two metrics: one that is absolute (MAE or RMSE, in the units of the target) and one that is relative (R² or MAPE). The absolute metric tells the business how far off predictions are. The relative metric tells you how much of the problem the model solves.



What's Next

You've covered MAE, MSE, RMSE, MAPE, R², and adjusted R² with their formulas and failure modes, the residuals-vs-predicted diagnostic plot for detecting heteroscedasticity and non-linearity, baseline comparison with DummyRegressor, and the metric selection guide for different business contexts. Next up: 04-classification-evaluation — where you'll learn confusion matrices, precision, recall, F1-score, ROC-AUC, PR-AUC, and threshold selection — the evaluation toolkit for every classification problem where the default 0.5 threshold is almost never the right choice.

Optional Deep Dive

Read "Regression Diagnostics: An Introduction" by John Fox (Sage Publications, Quantitative Applications in the Social Sciences series) — it covers the full set of regression diagnostic tools including influence measures, Cook's distance, and partial regression plots that reveal which individual observations are driving your model's errors.

Previous: Cross-Validation | Next: Classification Evaluation