R Squared and Adjusted R Squared
R squared (R²) tells you what fraction of the variation in the target your model explains. Adjusted R² does the same, but penalises you for adding features that don't really help.
Your Path Through This Lesson
0 of 5 done
- Explain R² as the share of variation a model explains.
- Compute R² and adjusted R² by hand and in code.
- Say why adjusted R² is fairer when comparing models.
The Question R² Answers
We have our line, . Is it any good? The cost was 24, but "24" means nothing on its own. Is that good? Bad? It depends on the units, and it changes if you measure marks out of 100 or out of 1,000.
R² fixes that by comparing your model with the laziest possible model: one that ignores completely and always predicts the average, .
- If your model is no better than predicting the average, R² = 0.
- If your model is perfect, R² = 1.
- Everything in between tells you how much of the "spread" in your model accounts for.
Two Kinds of Error
Total sum of squares (SST) is the error of the lazy, average-only model:
Residual sum of squares (SSE) is the error that's left over after your model:
Then:
Read it as: "the fraction of the original error that the model got rid of."
Working It Out by Hand
Our five students again, with and the line :
| 1 | 20 | −20 | 400 | 28 | −8 | 64 |
| 2 | 40 | 0 | 0 | 34 | 6 | 36 |
| 3 | 50 | 10 | 100 | 40 | 10 | 100 |
| 4 | 40 | 0 | 0 | 46 | −6 | 36 |
| 5 | 50 | 10 | 100 | 52 | −2 | 4 |
| SST | 600 | SSE | 240 |
Our line explains 60% of the variation in marks. The other 40% comes from things the line doesn't know about: sleep, how hard the test was, luck, and so on.
Checking It in Code
import numpy as np
from sklearn.linear_model import LinearRegression
from sklearn.metrics import r2_score
X = np.array([1, 2, 3, 4, 5]).reshape(-1, 1)
y = np.array([20, 40, 50, 40, 50])
model = LinearRegression().fit(X, y)
y_pred = model.predict(X)
print("R2 :", round(r2_score(y, y_pred), 4))
print("R2 :", round(model.score(X, y), 4)) # same thing, built into the modelR2 : 0.6
R2 : 0.6The argument order is r2_score(y_true, y_pred): the real answers first. Swap them and you get a different, wrong number with no error message.
Reading the Value
| R² | What it means |
|---|---|
| 1.0 | Perfect: every point is exactly on the line. On real data, this usually means a bug or leakage. |
| 0.6 | The model explains 60% of the variation. |
| 0.0 | No better than always predicting the average. |
| below 0 | Worse than always predicting the average. |
Yes, R² can be negative. That surprises a lot of people because of the "squared" in the name. A model that always predicts 0 marks would have , so . On the test set, a badly overfitted model can easily score below zero.
What counts as a "good" R² depends on the field. In physics experiments you might expect 0.99. For predicting human behaviour, like sales or exam marks, 0.5 can be genuinely useful. Always compare against a sensible baseline rather than a magic number.
The Problem With R²
Here's the catch: R² never goes down when you add a feature. Even a completely useless one, like each student's roll number or a column of random numbers, will nudge R² up a little, because the model can always use it to fit the training data slightly better by chance.
So if you judge only by R², you'll keep adding junk features and think you're improving.
Adjusted R²
Adjusted R² adds a penalty for every extra feature:
- = number of examples (rows)
- = number of features (columns)
For our data, and :
It's lower than 0.6 because with only five students, one feature is already "expensive". Adjusted R² rises only if a new feature improves the fit more than you'd expect by chance. If you add a junk feature, it goes down.
n, p = X.shape
r2 = r2_score(y, y_pred)
adj_r2 = 1 - (1 - r2) * (n - 1) / (n - p - 1)
print("Adjusted R2:", round(adj_r2, 4))Adjusted R2: 0.4667scikit-learn doesn't have a built-in function for adjusted R², so this one-liner is how everyone computes it.
R² vs Adjusted R²
| R² | Adjusted R² | |
|---|---|---|
| Adding a useless feature | Goes up (slightly) | Goes down |
| Adding a useful feature | Goes up | Goes up |
| Can be compared across models with different numbers of features? | Not fairly | Yes |
| Always ≤ R²? | Yes |
- Add a column of random numbers as a second feature:
X2 = np.column_stack([X, np.random.rand(5)]). Fit again. Did R² go up? Compute adjusted R² with . What happened? - Compute R² on a model that always predicts 40 (the mean). You should get exactly 0.
- Why can't you compute adjusted R² when ?
R² = 1 − SSE / SST. Adjusted R² adds a penalty for every extra feature.
Summary
- : the share of variation the model explains.
- For our line, .
- R² can be negative, and it never falls when you add features, even useless ones.
- Adjusted R² penalises extra features: for us, about 0.4667.
- Next up in this part: MSE, MAE and RMSE, the error metrics that keep the target's units.