Problem
A house-price model, a delivery-time model and a demand forecast each produce a column of predictions next to a column of true values. To compare two models, tune hyperparameters or decide whether a model is good enough to ship, those two columns have to become one number. There are many ways to do that, and they disagree. A model can win on mean absolute error and lose on root mean squared error, look strong on R² for one test set and weak on another with the same errors, or score perfectly on explained variance while every prediction is off by the same amount.
The choice is not cosmetic. Each metric is minimised by a different prediction, so picking the metric picks what the model is pushed towards, the mean, the median or a quantile. The question to ask is what a miss costs the people who use the prediction.
Intuition
Every metric on this page starts from the residual r = y − ŷ and applies a penalty to it. Squaring makes a miss of 4 cost sixteen times a miss of 1, so one big miss can dominate the score. The absolute value treats every unit of error the same. The pinball loss charges the two sides differently, so under-predicting can be made nine times as costly as over-predicting. The penalty shape is also what makes each metric prefer a particular summary of the data, the same link the loss functions page draws for training.
Penalty per residual. Positive r means the prediction was too low. The pinball line is steep on the right and nearly flat on the left.
Mechanics
Error metrics in the target's units
MAE = (1/N) Σ |yᵢ − ŷᵢ| MSE = (1/N) Σ (yᵢ − ŷᵢ)² RMSE = √MSE
MedAE = median |yᵢ − ŷᵢ| max error = max |yᵢ − ŷᵢ|
MAE and RMSE are in the same units as the target, so "off by 2.9 on average" is something a stakeholder can read. RMSE is never smaller than MAE, and the gap between them grows with how uneven the errors are: equal-sized misses give RMSE = MAE, one large miss among small ones gives RMSE far above MAE. The median absolute error ignores the largest half of the misses entirely, and the max error looks only at the worst one. Lower is better for all of them, and none has a natural scale, so a value means something only next to a baseline.
R² and explained variance
R² = 1 − Σ (yᵢ − ŷᵢ)² / Σ (yᵢ − ȳ)² explained variance = 1 − Var(y − ŷ) / Var(y)
R², the coefficient of determination, compares the model's squared error with that of the best constant prediction, the mean of the test targets. A score of 1 is perfect, 0 means no better than predicting ȳ for every row, and anything worse than that is negative; there is no lower bound. Because the denominator is the spread of the test targets, the same errors give a lower R² on a test set whose targets vary less. R² is a property of the model and the data together, not of the model alone.
Explained variance subtracts the mean residual before squaring, so a constant offset costs nothing: a model that is always 5 too high but moves exactly with the target scores 1. When the mean residual is zero the two scores are equal, which is why they agree on most models.
Percentage errors
MAPE = (1/N) Σ |yᵢ − ŷᵢ| / max(|yᵢ|, ε)
MAPE makes errors comparable across series of different sizes, but it divides by the target, which has two side effects. Targets near zero blow it up (scikit-learn's ε is the float64 machine epsilon, so a zero target gives an enormous but finite term). And it is not symmetric: predicting 150 when the truth is 100 costs 50.0%, while predicting 100 when the truth is 150 costs 33.3%. For positive targets and predictions, an under-prediction can cost at most 100%, while an over-prediction has no limit, so a model tuned for MAPE learns to aim low. scikit-learn returns MAPE as a fraction, not multiplied by 100.
Mean squared log error, (1/N) Σ (log(1 + yᵢ) − log(1 + ŷᵢ))², is the other relative metric: it scores ratios rather than differences and is meant for targets that grow exponentially. scikit-learn computes it with log1p and raises a ValueError for any value at or below −1, where the log is undefined.
Pinball (quantile) loss
pinballα(y, ŷ) = α (y − ŷ) if y ≥ ŷ, otherwise (1 − α)(ŷ − y)
The pinball loss is the asymmetric absolute error behind quantile regression (Koenker and Bassett, 1978). With α = 0.5 it is half the absolute error. With α = 0.9 a miss below the target costs 0.9 per unit and a miss above costs 0.1, so the best prediction sits where about 90% of the targets are below it: the 0.9 quantile. It is the metric for a prediction that is meant to be an upper or lower bound, such as stock to hold so that demand is covered nine days in ten.
What each metric rewards
The cleanest way to see what a metric wants is to ask which single number c, predicted for every row, minimises it. Squared error is minimised by the mean, absolute error by any median, the pinball loss by the α-quantile. Gneiting (2011) makes the general point: a point forecast should be scored with a function that is consistent for the summary it is meant to report, the mean for squared error and quantiles for pinball losses. Scoring a median forecast with RMSE, or a mean forecast with MAE, penalises the model for doing what it was asked.
Best constant prediction per metric, found by a grid search in steps of 0.01 over the target range. The box on the axis is the whole flat minimum of MAE.
| Component | Shape/Params | Meaning | Source |
|---|---|---|---|
mean_absolute_error |
y_true, y_pred (n_samples,) or (n_samples, n_outputs); sample_weight, multioutput | Weighted mean of |y − ŷ| per output, then averaged across outputs. | _regression.py 225-308 |
mean_pinball_loss |
alpha=0.5 | α·diff where diff = y − ŷ ≥ 0, (α − 1)·diff otherwise. | _regression.py 320-408, formula 390-393 |
mean_absolute_percentage_error |
ε = float64 machine epsilon | |ŷ − y| / max(|y|, ε), returned as a fraction. | _regression.py 419-513, ε 494-497 |
mean_squared_error, root_mean_squared_error |
sample_weight, multioutput | MSE; RMSE takes the square root per output before averaging outputs. | _regression.py 524-605, 616-690 |
mean_squared_log_error |
values must be > −1 | MSE of log1p(y) against log1p(ŷ); ValueError for any value ≤ −1. | _regression.py 701-779, check 767 |
median_absolute_error |
multioutput | Median of |y − ŷ| per output. | _regression.py 869-944 |
_assemble_fraction_of_explained_deviance |
numerator, denominator, force_finite | 1 − num/den, with a zero numerator scored 1.0 and a zero denominator otherwise scored 0.0 instead of nan or −inf. | _regression.py 946-994 |
explained_variance_score |
force_finite=True | Numerator subtracts the mean residual before squaring, so a constant offset is free. | _regression.py 1009-1141, terms 1121-1130 |
r2_score |
force_finite=True | Σ w(y − ŷ)² over Σ w(y − ȳ)²; fewer than two samples warns and returns nan. | _regression.py 1157-1319, terms 1304-1308 |
max_error |
single output only | Largest |y − ŷ|. | _regression.py 1328-1363 |
nn.HuberLoss |
delta=1.0, reduction="mean" | The training-side loss between MSE and MAE, shown in the first figure. | loss.py 1081-1150 |
A model's R² on the test set is −0.3. What does that say?
Its squared error is 30% larger than that of predicting the test set's own mean for every row. That is worse than the simplest baseline, often a sign of a distribution shift between training and test data, a broken pipeline, or a model that extrapolates badly.
Worked example
The data. 12 targets, y = (12, 14, 15, 15, 17, 18, 20, 22, 25, 31, 38, 55), mean 23.5, median 19. Five prediction columns are scored against them:
- steady: every prediction off by 2 to 4, ŷ = (15, 12, 19, 12, 19, 14, 23, 20, 29, 28, 40, 52).
- spiky: eleven predictions within 0.5 and one miss of 20 on the largest target, ŷ = (12.5, 13.5, 15.0, 15.5, 16.5, 18.0, 20.5, 21.5, 25.0, 31.5, 37.5, 35.0).
- biased +5: the true value plus 5, every time.
- constant mean and constant median: 23.5 or 19 for every row.
| Model | MAE | RMSE | MedAE | Max | MAPE | R² | Expl. var. | Pinball α = 0.9 |
|---|---|---|---|---|---|---|---|---|
| steady | 2.917 | 3.014 | 3.00 | 4.0 | 15.0% | 0.936 | 0.936 | 1.425 |
| spiky | 2.000 | 5.788 | 0.50 | 20.0 | 4.8% | 0.766 | 0.785 | 1.667 |
| biased +5 | 5.000 | 5.000 | 5.00 | 5.0 | 25.6% | 0.825 | 1.000 | 0.500 |
| constant mean | 9.167 | 11.955 | 8.00 | 31.5 | 41.3% | 0.000 | 0.000 | 4.583 |
| constant median | 8.333 | 12.774 | 4.50 | 36.0 | 30.1% | −0.142 | 0.000 | 5.967 |
The same twelve targets. MAE prefers the spiky model, RMSE the steady one.
The ranking flips. The spiky model has the lower MAE (2.000 against 2.917) and a far lower median absolute error (0.50 against 3.00), but its one miss of 20 squares to 400, so its RMSE is 5.788 against 3.014 and its R² is 0.766 against 0.936. Which model is better depends on whether one large miss is worse than many small ones, which is a question about the application, not about the numbers.
R² of the baselines. Predicting the mean, 23.5, gives R² = 0.000 exactly, by definition. Predicting the median gives R² = −0.142: negative, although its MAE of 8.333 beats the mean's 9.167. Explained variance is 0.000 for both, since a constant prediction cannot move with the target.
Same errors, different R². Shrinking the targets halfway towards their mean, y′ = 23.5 + 0.5(y − 23.5) = (17.75, 18.75, 19.25, 19.25, 20.25, 20.75, 21.75, 22.75, 24.25, 27.25, 30.75, 39.25), and adding the steady model's exact errors, leaves MAE at 2.917 and RMSE at 3.014, but R² falls from 0.936 to 0.746 because the denominator shrank.
The offset that explained variance cannot see. The biased +5 model has explained variance 1.000 and R² 0.825. Its mean residual is −5.0, and only R² charges for it. The same model has the lowest pinball loss at α = 0.9, 0.500, because every miss is an over-prediction, charged at only 0.1 per unit.
Best constants. The grid search behind the second figure:
| Metric | Best constant | Loss there |
|---|---|---|
| squared error (MSE) | 23.50 | 142.917 |
| absolute error (MAE) | any c in [18.00, 20.00] | 8.333 |
| pinball, α = 0.9 | 38.00 | 2.867 |
| percentage error (MAPE) | 17.00 | 0.279 |
Squared error lands on the mean, absolute error on the whole interval between the 6th and 7th sorted targets, and pinball at α = 0.9 on 38, the 11th of the 12 targets. MAPE lands on 17, below the median of 19: the asymmetry from the Mechanics section, at work on real numbers.
Implementation
from sklearn.metrics import (
mean_absolute_error, root_mean_squared_error, median_absolute_error,
max_error, mean_absolute_percentage_error, r2_score,
explained_variance_score, mean_pinball_loss,
)
y_true = [12, 14, 15, 15, 17, 18, 20, 22, 25, 31, 38, 55]
y_pred = [12.5, 13.5, 15, 15.5, 16.5, 18, 20.5, 21.5, 25, 31.5, 37.5, 35]
for name, metric in [
("MAE", mean_absolute_error),
("RMSE", root_mean_squared_error),
("MedAE", median_absolute_error),
("max", max_error),
("MAPE", mean_absolute_percentage_error),
("R2", r2_score),
("explained var.", explained_variance_score),
]:
print(f"{name:15s}{metric(y_true, y_pred):.3f}")
print(mean_pinball_loss(y_true, y_pred, alpha=0.9))
# Use the same metric for model selection:
# GridSearchCV(..., scoring="neg_root_mean_squared_error")
The y_pred above is the spiky model, so the printed values match its row in the worked example. scikit-learn metrics take (y_true, y_pred) in that order, which matters for the asymmetric ones (MAPE, pinball, MSLE) and for R², whose denominator comes from y_true.
Scoring strings for search and cross-validation are negated where lower is better, so "higher is better" holds everywhere: neg_mean_absolute_error, neg_root_mean_squared_error, while r2 is used as is.
On the training side, the loss should match the metric where it can: nn.MSELoss for RMSE, nn.L1Loss for MAE, nn.HuberLoss in between, and the pinball loss (a two-line custom loss in PyTorch) for quantiles. Gradient boosting libraries offer squared, absolute and quantile objectives.
Tradeoffs
- MAE or RMSE. If cost grows with the square of the miss, or the mean is the quantity being forecast, use RMSE. If every unit of error costs the same, or outliers in the target are noise, use MAE. Reporting both shows how uneven the errors are.
- R² across datasets. R² depends on the test targets' variance, so it cannot compare models evaluated on different data. Compare errors in units, or against a fixed baseline, instead.
- R² on small or shifted test sets. R² measures against the test set's own mean, which a deployed model never knows. A "model" that predicted that mean would score 0 without having learned anything.
- Explained variance. It hides a systematic offset. Use it only together with the mean residual, or use R².
- Percentage errors. MAPE favours under-prediction and breaks near zero. For series compared across scales, MASE (Hyndman and Koehler, 2006; see time series) avoids both problems.
- Quantiles and intervals. A pinball loss at one α scores one quantile. Two quantile models (say 0.1 and 0.9) give an interval, judged by how often it covers the target and how wide it is.
- Validation. Any metric is only as good as the split it is computed on; see cross-validation.
A team trains with MSE loss but reports MAE to stakeholders. What can go wrong?
The model is pushed towards the conditional mean while the metric rewards the conditional median. On skewed targets these differ, so a model chosen for the lowest training loss may not have the lowest MAE, and an MAE-trained model may score better on the reported number. Match the training loss, the model-selection metric and the reported metric where you can.
Connections
Read first
- Loss functions intro
Read next
No page builds on this one yet.
Linear regression minimises the squared error behind RMSE and R². Classification metrics are the same question for labels instead of numbers.
References
- Koenker and Bassett. Regression Quantiles. Econometrica 46(1), 33–50, 1978. doi:10.2307/1913643
- Hyndman and Koehler. Another look at measures of forecast accuracy. International Journal of Forecasting 22(4), 679–688, 2006. doi:10.1016/j.ijforecast.2006.03.001
- Gneiting. Making and Evaluating Point Forecasts. Journal of the American Statistical Association, 746–762, 2011. doi:10.1198/jasa.2011.r10138
- scikit-learn 1.9.1:
sklearn/metrics/_regression.py. PyTorch v2.15.0:torch/nn/modules/loss.py.