0Pricing
Machine Learning Academy · Lesson

Regression Metrics: MAE, MSE, RMSE, and R-Squared

Learners will calculate error metrics on a regression output, understand scale-dependence of MAE/RMSE, and use R-squared as a standardised goodness-of-fit.

Regression Metrics: MAE, MSE, RMSE, and R-Squared is a free Machine Learning Academy lesson on CoddyKit — lesson 3 of 4. You can read the complete lesson below for free — then practise it hands-on in the browser with a built-in code editor and a 24/7 AI tutor. It is part of the Machine Learning Academy learning path, one of 4 lessons in the course, and your progress syncs across the web and the CoddyKit app.

Why Regression Needs Different Metrics

Regression models predict continuous values — house prices, temperatures, sales volumes. Unlike classification, where a prediction is either correct or wrong, regression predictions are almost never exactly right. The question is: how far off is the prediction? Regression metrics quantify the difference between predicted values and actual values. Different metrics weight errors differently: some penalise large errors more than small ones, some are robust to outliers, and some are normalised to be interpretable as a fraction of variance explained. Choosing the right metric depends on the problem's error cost structure.

import numpy as np

y_true = np.array([100, 150, 200, 250, 300], dtype=float)
y_pred = np.array([110, 140, 210, 240, 320], dtype=float)

errors = y_pred - y_true
print('Actual:   ', y_true)
print('Predicted:', y_pred)
print('Errors:   ', errors)  # Some positive, some negative
print('We need metrics that summarise these errors into a single number.')

MAE: Mean Absolute Error

MAE = mean(|y_true - y_pred|). Mean Absolute Error computes the average of the absolute differences between predictions and actual values. It is in the same units as the target variable — a house price MAE of $15,000 means predictions are off by $15,000 on average. MAE treats all errors linearly: a prediction that is twice as wrong costs exactly twice as much. MAE is robust to outliers because it does not square errors. It is the recommended metric when the distribution of errors is roughly symmetric and outlier errors should not dominate the aggregate.

import numpy as np
from sklearn.metrics import mean_absolute_error

y_true = np.array([100, 150, 200, 250, 300], dtype=float)
y_pred = np.array([110, 140, 210, 240, 320], dtype=float)

# Manual calculation
mae_manual = np.mean(np.abs(y_true - y_pred))
print('Manual MAE:', mae_manual)  # |10|+|10|+|10|+|10|+|20| / 5 = 12

# Sklearn
mae = mean_absolute_error(y_true, y_pred)
print('Sklearn MAE:', mae)
print(f'Interpretation: predictions are off by {mae:.1f} units on average')

MSE: Mean Squared Error

MSE = mean((y_true - y_pred)^2). Mean Squared Error squares each error before averaging. This makes MSE sensitive to outliers: a single prediction with error 10 contributes 100 to MSE, while a prediction with error 1 contributes only 1 — outlier errors are amplified quadratically. MSE is the natural loss function for linear regression (minimising MSE gives the ordinary least squares solution). Its units are the square of the target variable (dollars^2 for house prices), making it less interpretable than MAE but mathematically convenient for optimisation.

import numpy as np
from sklearn.metrics import mean_squared_error

y_true = np.array([100, 150, 200, 250, 300], dtype=float)
y_pred = np.array([110, 140, 210, 240, 320], dtype=float)

# Squaring emphasises large errors
errors = y_true - y_pred
print('Squared errors:', errors**2)  # 100, 100, 100, 100, 400

mse = mean_squared_error(y_true, y_pred)
print(f'MSE: {mse}')  # 160.0
print(f'Units: (target units)^2 -- less interpretable than MAE')
print(f'The large error at last sample contributes 400/5=80 to MSE alone')

RMSE: Root Mean Squared Error

RMSE = sqrt(MSE). Root Mean Squared Error brings MSE back to the original units of the target variable (dollars, not dollars^2) by taking the square root. It is the most widely reported regression metric because it is interpretable in the same units as the target while still penalising large errors more than small ones. An RMSE of $20,000 for house prices means the typical error magnitude is $20,000, though outlier errors are weighted more heavily than in MAE. RMSE is always >= MAE for the same predictions; the gap between them increases when outlier errors are present.

import numpy as np
from sklearn.metrics import mean_squared_error

y_true = np.array([100, 150, 200, 250, 300], dtype=float)
y_pred = np.array([110, 140, 210, 240, 320], dtype=float)

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

print(f'MSE:  {mse:.2f} (units: target^2)')
print(f'RMSE: {rmse:.2f} (units: same as target)')

# RMSE always >= MAE
from sklearn.metrics import mean_absolute_error
mae = mean_absolute_error(y_true, y_pred)
print(f'MAE:  {mae:.2f}')
print(f'MAE <= RMSE: {mae <= rmse}')

MAE vs RMSE: When to Use Which

Choose MAE when: outliers are expected and you do not want them to dominate the metric; you want a metric that is easily interpretable as the average error; errors of different sizes are equally important proportionally. Choose RMSE when: large errors are much more costly than small ones (e.g., predicting power demand — being off by 10x is far more costly than being off by 1x); the loss function used in training is MSE and you want evaluation to match training; variance in predictions matters more than median error. In practice, always report both MAE and RMSE — if RMSE >> MAE, outlier errors dominate; if RMSE ≈ MAE, errors are uniformly distributed.

import numpy as np
from sklearn.metrics import mean_absolute_error, mean_squared_error

# Two prediction sets with same MAE but different RMSE
y_true = np.array([100.0, 200.0, 300.0, 400.0, 500.0])

# Set 1: uniform errors of 10
pred1 = y_true + 10

# Set 2: mostly 0 error, one huge error of 50
pred2 = y_true.copy()
pred2[-1] += 50
pred2[0:4] += 0

for name, pred in [('Uniform errors', pred1), ('One outlier error', pred2)]:
    mae  = mean_absolute_error(y_true, pred)
    rmse = np.sqrt(mean_squared_error(y_true, pred))
    print(f'{name}: MAE={mae:.2f}, RMSE={rmse:.2f}')

R-Squared: Coefficient of Determination

R^2 = 1 - MSE(model) / Var(y_true). R-squared measures the fraction of variance in the target explained by the model. R^2 = 1.0 means perfect predictions (zero residual variance). R^2 = 0.0 means the model is no better than predicting the mean of y for every sample. R^2 can be negative if the model is worse than the mean baseline. R^2 is scale-independent — a value of 0.85 means the same thing whether the target is house prices or temperatures. This makes it the primary metric for comparing regression models across different problems or scales.

import numpy as np
from sklearn.metrics import r2_score

y_true = np.array([100, 150, 200, 250, 300], dtype=float)
y_pred = np.array([110, 140, 210, 240, 320], dtype=float)
y_mean = np.mean(y_true)

# Manual R^2
ss_res = np.sum((y_true - y_pred)**2)   # Residual sum of squares
ss_tot = np.sum((y_true - y_mean)**2)   # Total sum of squares
r2_manual = 1 - ss_res / ss_tot

print(f'SS_res (model): {ss_res}')
print(f'SS_tot (mean):  {ss_tot}')
print(f'R^2 manual: {r2_manual:.4f}')
print(f'R^2 sklearn: {r2_score(y_true, y_pred):.4f}')

Adjusted R-Squared: Penalising Extra Features

Adding more features to a regression model always increases R^2 (or keeps it the same) on training data — even if those features are random noise. Adjusted R^2 corrects for this by penalising the number of features: it decreases when you add a feature that does not improve the model enough to justify the additional complexity. Formula: Adj R^2 = 1 - (1 - R^2) * (n-1) / (n-p-1) where n = samples, p = features. Use adjusted R^2 when comparing models with different numbers of features to avoid selecting overly complex models.

import numpy as np

def adjusted_r2(y_true, y_pred, n_features):
    n = len(y_true)
    r2 = r2_score(y_true, y_pred)
    return 1 - (1 - r2) * (n - 1) / (n - n_features - 1)

from sklearn.metrics import r2_score
from sklearn.linear_model import LinearRegression
import numpy as np

np.random.seed(42)
n = 100
X_2 = np.random.randn(n, 2)
X_10 = np.random.randn(n, 10)  # 8 noise features added
y = X_2[:, 0] * 2 + X_2[:, 1] + np.random.randn(n) * 0.5

for X, p in [(X_2, 2), (X_10, 10)]:
    pred = LinearRegression().fit(X, y).predict(X)
    print(f'p={p}: R^2={r2_score(y,pred):.3f}, Adj R^2={adjusted_r2(y,pred,p):.3f}')

MAPE: Mean Absolute Percentage Error

MAPE = 100 * mean(|y_true - y_pred| / |y_true|). Mean Absolute Percentage Error expresses the error as a percentage of the actual value, making it scale-independent and easily interpretable: a MAPE of 5% means predictions are off by 5% of the true value on average. Limitations: MAPE is undefined when actual values are zero (division by zero) and is asymmetric — underpredicting gives higher error than overpredicting by the same amount. Use MAPE for forecasting tasks (sales, demand) where stakeholders understand percentage deviations naturally.

import numpy as np

def mape(y_true, y_pred):
    y_true = np.array(y_true, dtype=float)
    # Avoid division by zero
    mask = y_true != 0
    return 100 * np.mean(np.abs((y_true[mask] - np.array(y_pred)[mask]) / y_true[mask]))

y_true = [100, 200, 300, 400, 500]
y_pred = [110, 190, 315, 380, 510]

print(f'MAPE: {mape(y_true, y_pred):.2f}%')

# Asymmetry example
print(f'Over by 50: MAPE = {mape([100],[150]):.1f}%')
print(f'Under by 50: MAPE = {mape([100],[50]):.1f}%')
# Underprediction by 50% gives 50% error; overprediction by 50% also 50% (same amount, asymmetric impact)

Computing Regression Metrics with scikit-learn

Scikit-learn provides all major regression metrics in sklearn.metrics. When using cross_val_score, pass scoring='neg_mean_squared_error' (negative because higher is better by convention) and negate the result, or use 'neg_root_mean_squared_error' for direct RMSE. For R^2, use scoring='r2'. Always report multiple metrics: MAE for average absolute error, RMSE to show outlier sensitivity, and R^2 for explained variance. This gives a complete picture of model performance.

from sklearn.linear_model import LinearRegression
from sklearn.datasets import fetch_california_housing
from sklearn.model_selection import cross_val_score
from sklearn.metrics import mean_absolute_error, mean_squared_error, r2_score
import numpy as np

X, y = fetch_california_housing(return_X_y=True)

for metric in ['neg_mean_absolute_error', 'neg_root_mean_squared_error', 'r2']:
    scores = cross_val_score(LinearRegression(), X, y, cv=5, scoring=metric)
    mean_val = scores.mean()
    if 'neg' in metric:
        mean_val = -mean_val
    print(f'{metric.replace("neg_",""):30}: {mean_val:.4f}')

Residual Analysis: Diagnosing Model Quality

Beyond summary metrics, residual analysis reveals whether model assumptions are met. Plot residuals (y_true - y_pred) vs predicted values. A good model shows: (1) residuals randomly scattered around zero with no pattern (homoscedastic), (2) no relationship between residual magnitude and predicted value (no heteroscedasticity). Warning signs: a funnel shape (residuals increase with predicted value) suggests the model needs log-transformation; a curved pattern suggests the relationship is non-linear and the model is misspecified. Always do residual analysis before deploying any regression model.

import matplotlib.pyplot as plt
import numpy as np
from sklearn.linear_model import LinearRegression
from sklearn.datasets import fetch_california_housing
from sklearn.model_selection import train_test_split

X, y = fetch_california_housing(return_X_y=True)
X_tr, X_te, y_tr, y_te = train_test_split(X, y, random_state=42)

model = LinearRegression()
model.fit(X_tr, y_tr)
y_pred = model.predict(X_te)

residuals = y_te - y_pred

plt.figure(figsize=(8, 4))
plt.scatter(y_pred, residuals, alpha=0.3, s=10)
plt.axhline(0, color='red', linestyle='--')
plt.xlabel('Predicted Value')
plt.ylabel('Residual (Actual - Predicted)')
plt.title('Residual Plot')
plt.show()

Choosing the Right Regression Metric

A practical guide: use MAE when you want to minimise average absolute error and outliers should not be overemphasised — robust choice for general reporting. Use RMSE when your model minimises MSE during training and you want evaluation to match, or when large errors are particularly costly (demand forecasting, energy prediction). Use R^2 to compare models on the same dataset or explain model quality to non-technical stakeholders as 'percentage of variance explained'. Use MAPE for business forecasting where percentage deviations from actual values are the natural language of stakeholders.

# Metric selection guide
def recommend_metric(has_outliers, need_interpretable_units, compare_across_scales):
    if compare_across_scales:
        return 'R^2 (scale-independent, 0-1 range)'
    elif need_interpretable_units and has_outliers:
        return 'MAE (same units as target, robust to outliers)'
    elif need_interpretable_units:
        return 'RMSE (same units, penalises large errors more)'
    else:
        return 'MSE (convenient for optimisation, same loss as linear regression)'

print(recommend_metric(True, True, False))   # MAE
print(recommend_metric(False, True, False))  # RMSE
print(recommend_metric(False, False, True))  # R^2

Quick Check

Test your understanding of Machine Learning with Python concepts from this lesson.

Lesson Recap

In this lesson you learned: MAE averages absolute errors and is robust to outliers, RMSE squares errors and penalises large deviations more heavily, R^2 measures the fraction of variance explained (0 = mean baseline, 1 = perfect), and the gap between MAE and RMSE reveals outlier error concentration. Next up we explore choosing the right metric for your specific business problem.

Frequently asked questions

Is the “Regression Metrics: MAE, MSE, RMSE, and R-Squared” lesson free?

Yes — the full text of “Regression Metrics: MAE, MSE, RMSE, and R-Squared” is free to read here on the web, and the Machine Learning Academy course includes 4 lessons in total. To practise it interactively (a built-in code editor and a 24/7 AI tutor) and unlock the rest of the Machine Learning Academy course, upgrade to CoddyKit PRO.

What will I learn in “Regression Metrics: MAE, MSE, RMSE, and R-Squared”?

Learners will calculate error metrics on a regression output, understand scale-dependence of MAE/RMSE, and use R-squared as a standardised goodness-of-fit. You practise Machine Learning Academy with hands-on code you run directly in the browser, and a 24/7 AI tutor answers your questions as you work through the lesson.

Do I need any experience to start Machine Learning Academy?

No prior experience is required. Machine Learning Academy on CoddyKit is structured for beginners through advanced learners; this is — lesson 3 of 4, so you can start here or from the beginning and move at your own pace.

How long does the “Regression Metrics: MAE, MSE, RMSE, and R-Squared” lesson take?

Most CoddyKit lessons take about 5–10 minutes. Each one is bite-sized and interactive, so you make steady progress and pick up exactly where you left off across the web and the app.

Can I write and run code in this Machine Learning Academy lesson?

Yes. Every Machine Learning Academy lesson includes a built-in code editor, so you write and run real code right in your browser and get instant AI feedback — no local setup required.

All lessons in this course

  1. Classification Metrics: Accuracy, Precision, Recall, F1
  2. ROC Curves and AUC-ROC
  3. Regression Metrics: MAE, MSE, RMSE, and R-Squared
  4. Choosing the Right Metric for Your Business Problem
← Back to Machine Learning Academy