Simple Linear Regression
What you’ll learn
- the model , and what each parameter means in the units of your data
- why we square the errors instead of taking absolute values or plain sums
- how to derive the least-squares slope and intercept by setting two derivatives to zero
- how to compute the whole fit by hand on five points, then check it against scikit-learn
- how to read a residual plot, and what a bad one looks like
- the four assumptions the model makes, and which pitfalls follow when they break
Intuition
You spend money on advertising and you record sales. Plot the pairs and they trend upward, but not perfectly — a straight line through the cloud would miss every point by a little.
Simple linear regression asks one question: of all the straight lines you could draw, which one misses by the least? Once you have that line, two numbers summarise the whole relationship — how steeply sales rise per dollar spent, and where the line starts.
flowchart LR X["Feature x
(ad spend)"] --> M["Model
ŷ = wx + b"] M --> P["Prediction ŷ"] P --> E["Residual
e = y - ŷ"] Y["Observed y
(sales)"] --> E E --> L["Loss
sum of e squared"] L -->|"adjust w, b"| M
The loop at the bottom is the whole of supervised learning in miniature: predict, measure the miss, adjust. Simple linear regression is special only because the adjustment has a formula — you can jump straight to the answer instead of iterating.
The math
The model
One feature, two parameters:
- is the slope — the change in for a one-unit increase in . Its units are units of y per unit of x.
- is the intercept — the prediction when . Sometimes meaningful, often not.
The loss
For a dataset of pairs , the residual of point is . We minimise the sum of squared errors:
Why squared? Three reasons, in order of importance:
- Signs would cancel. Plain is zero for infinitely many terrible lines.
- It is differentiable everywhere. has a corner at zero, so there is no clean closed-form solution.
- Big misses hurt more. Squaring penalises a residual of 4 sixteen times as hard as a residual of 1, which is usually what you want — and occasionally exactly what you don’t (see Pitfalls).
Deriving the solution
is a convex paraboloid in , so its single minimum is where both partial derivatives vanish. Start with the intercept:
Divide by and rearrange:
That is a useful fact on its own: the least-squares line always passes through the centroid of the data. Now the slope:
Substitute and solve for :
The slope is the covariance of the two variables divided by the variance of the input. Everything below is arithmetic on that one formula.
Worked example by hand
Five months of ad spend against sales, in thousands:
| (spend) | (sales) | |||||
|---|---|---|---|---|---|---|
| 1 | 1 | 2 | −2 | −2 | 4 | 4 |
| 2 | 2 | 4 | −1 | 0 | 0 | 1 |
| 3 | 3 | 5 | 0 | 1 | 0 | 0 |
| 4 | 4 | 4 | 1 | 0 | 0 | 1 |
| 5 | 5 | 5 | 2 | 1 | 2 | 4 |
| 6 | 10 |
Step 1 — the means. , .
Step 2 — the slope. Divide the two column totals:
Step 3 — the intercept. Push the line through the centroid:
So the fitted line is . Every extra thousand dollars of spend buys 600 more units of sales.
Step 4 — predictions and residuals.
| 1 | 2 | 2.8 | −0.8 | 0.64 |
| 2 | 4 | 3.4 | +0.6 | 0.36 |
| 3 | 5 | 4.0 | +1.0 | 1.00 |
| 4 | 4 | 4.6 | −0.6 | 0.36 |
| 5 | 5 | 5.2 | −0.2 | 0.04 |
| 0.0 | 2.40 |
The residuals sum to exactly zero. That is not luck — it is the first normal equation, , which is what fixing guarantees.
Step 5 — the error numbers.
Step 6 — how much did we explain? Compare against the dumbest possible model, which always predicts :
The line removes 60% of the variance that predicting the mean would have left. Metrics R-Squared and Adjusted R-Squared takes that number apart properly.
Why no other line beats it
Two plausible alternatives, scored on the same five points:
See it move
Below, the line starts flat and walks toward the least-squares solution by repeatedly nudging and downhill. The closed-form answer and the iterative one land in the same place; the iterative route is the one that keeps working when there are a million features.
From scratch
The two formulas, transcribed directly. No optimiser, no library model:
import numpy as np
def fit_simple_linear(x, y):
"""Return (w, b) for the least-squares line through 1-D data."""
x, y = np.asarray(x, dtype=float), np.asarray(y, dtype=float)
x_mean, y_mean = x.mean(), y.mean()
# w = Cov(x, y) / Var(x), written out as the two sums from the derivation
numerator = ((x - x_mean) * (y - y_mean)).sum()
denominator = ((x - x_mean) ** 2).sum()
if denominator == 0:
raise ValueError("x has zero variance — no line is determined")
w = numerator / denominator
b = y_mean - w * x_mean
return w, b
x = [1, 2, 3, 4, 5]
y = [2, 4, 5, 4, 5]
w, b = fit_simple_linear(x, y)
pred = w * np.array(x) + b
resid = np.array(y) - pred
print(f"w = {w}") # w = 0.6
print(f"b = {b}") # b = 2.2
print(f"residuals = {resid}") # residuals = [-0.8 0.6 1. -0.6 -0.2]
print(f"SSE = {(resid ** 2).sum()}") # SSE = 2.4import numpy as np
def fit_simple_linear(x, y):
"""Return (w, b) for the least-squares line through 1-D data."""
x, y = np.asarray(x, dtype=float), np.asarray(y, dtype=float)
x_mean, y_mean = x.mean(), y.mean()
# w = Cov(x, y) / Var(x), written out as the two sums from the derivation
numerator = ((x - x_mean) * (y - y_mean)).sum()
denominator = ((x - x_mean) ** 2).sum()
if denominator == 0:
raise ValueError("x has zero variance — no line is determined")
w = numerator / denominator
b = y_mean - w * x_mean
return w, b
x = [1, 2, 3, 4, 5]
y = [2, 4, 5, 4, 5]
w, b = fit_simple_linear(x, y)
pred = w * np.array(x) + b
resid = np.array(y) - pred
print(f"w = {w}") # w = 0.6
print(f"b = {b}") # b = 2.2
print(f"residuals = {resid}") # residuals = [-0.8 0.6 1. -0.6 -0.2]
print(f"SSE = {(resid ** 2).sum()}") # SSE = 2.4Every number matches the hand calculation, which is the point of doing both.
With scikit-learn
The same fit through the estimator API. Note the reshape(-1, 1)reshape(-1, 1) — scikit-learn always wants
XX two-dimensional, one row per sample and one column per feature, even when there is only one
feature:
import numpy as np
from sklearn.linear_model import LinearRegression
from sklearn.metrics import mean_squared_error, r2_score
X = np.array([1, 2, 3, 4, 5]).reshape(-1, 1) # (5, 1) — 5 samples, 1 feature
y = np.array([2, 4, 5, 4, 5])
model = LinearRegression()
model.fit(X, y)
print(f"w = {model.coef_[0]:.4f}") # w = 0.6000
print(f"b = {model.intercept_:.4f}") # b = 2.2000
pred = model.predict(X)
print(f"MSE = {mean_squared_error(y, pred):.4f}") # MSE = 0.4800
print(f"R^2 = {r2_score(y, pred):.4f}") # R^2 = 0.6000
# Predicting for a new spend level
print(f"x=6 -> {model.predict([[6]])[0]:.2f}") # x=6 -> 5.80import numpy as np
from sklearn.linear_model import LinearRegression
from sklearn.metrics import mean_squared_error, r2_score
X = np.array([1, 2, 3, 4, 5]).reshape(-1, 1) # (5, 1) — 5 samples, 1 feature
y = np.array([2, 4, 5, 4, 5])
model = LinearRegression()
model.fit(X, y)
print(f"w = {model.coef_[0]:.4f}") # w = 0.6000
print(f"b = {model.intercept_:.4f}") # b = 2.2000
pred = model.predict(X)
print(f"MSE = {mean_squared_error(y, pred):.4f}") # MSE = 0.4800
print(f"R^2 = {r2_score(y, pred):.4f}") # R^2 = 0.6000
# Predicting for a new spend level
print(f"x=6 -> {model.predict([[6]])[0]:.2f}") # x=6 -> 5.80On real data
Five tidy points are a teaching device. Here is the same estimator on 442 real patients from the
load_diabetesload_diabetes dataset, predicting one-year disease progression from body mass index alone:
from sklearn.datasets import load_diabetes
from sklearn.linear_model import LinearRegression
data = load_diabetes()
bmi = data.data[:, 2].reshape(-1, 1) # column 2 is the standardised BMI
y = data.target
model = LinearRegression().fit(bmi, y)
print(f"w = {model.coef_[0]:.2f}") # w = 949.44
print(f"b = {model.intercept_:.2f}") # b = 152.13
print(f"R^2 = {model.score(bmi, y):.4f}") # R^2 = 0.3439from sklearn.datasets import load_diabetes
from sklearn.linear_model import LinearRegression
data = load_diabetes()
bmi = data.data[:, 2].reshape(-1, 1) # column 2 is the standardised BMI
y = data.target
model = LinearRegression().fit(bmi, y)
print(f"w = {model.coef_[0]:.2f}") # w = 949.44
print(f"b = {model.intercept_:.2f}") # b = 152.13
print(f"R^2 = {model.score(bmi, y):.4f}") # R^2 = 0.3439Reading the plot
Three things to take from that figure, in the order you should check them:
- The direction is real. The cloud tilts upward consistently, not just at the edges. A positive slope here is a genuine signal, not an artefact of two outliers.
- The spread grows with . Points on the right scatter further from the line than points on the left. That is heteroscedasticity — it does not bias the slope, but it does make the usual confidence intervals too optimistic.
- is not a failure. For a single biological predictor it is a strong result. It is a signal that more features would help, which is exactly what the next page does.
The residual plot is where you confirm all three at a glance. For the five-point example it looks like this — no curve, no fan, no runaway point:
The four assumptions
Least squares is always computable. It is only trustworthy when these hold:
| Assumption | What it means | How to check | If it breaks |
|---|---|---|---|
| Linearity | The true relationship is a straight line | Residual plot shows no curve | Add polynomial terms, or transform |
| Independence | Observations do not influence each other | Know your sampling; check time ordering | Use time-series or mixed models |
| Normality | Residuals are roughly normal | Q-Q plot of residuals | Slope is still fine; intervals are not |
| Equal variance | Residual spread is constant across | Residual plot shows no fan | Transform , or use weighted least squares |
Only linearity affects the point predictions. The other three affect how much you should believe the confidence intervals around them.
APIsklearn.linear_model.LinearRegression
Assumes
- The relationship between x and y is linear
- Observations are independent of one another
- Residual variance is constant across the range of x
- Residuals are approximately normal (needed for intervals, not for the fit)
Cost
- train
O(n)- predict
O(1)- memory
O(1)
n = samples; the one-feature case needs only running sums
Hyperparameters that matter
fit_interceptdefault TrueSet False only when the data is already centred, or theory forces the line through the origin.positivedefault FalseConstrains the slope to be non-negative — useful when a negative coefficient would be physically impossible.
Reach for it when
- You need a baseline before trying anything complicated
- Interpretability matters more than the last point of accuracy
- You must explain the effect of one variable in its own units
- The scatter plot genuinely looks like a line
Look elsewhere when
- The scatter plot curves — polynomial or tree models will beat it easily
- Outliers dominate and cannot be removed on principle
- You have many correlated features (use Ridge or Lasso instead)
- You need a prediction outside the observed range of x
Pitfalls
Compare
| Model | Handles curves | Interpretable | Robust to outliers | Needs scaling | First reach for |
|---|---|---|---|---|---|
| Simple linear (OLS) | No | Very | No | No | A baseline, and one-variable stories |
| Polynomial regression | Yes | Moderate | No | Yes (high degree) | Visible curvature |
| Ridge / Lasso | No | Good | No | Yes | Many correlated features |
| Huber regression | No | Very | Yes | No | Data with genuine outliers |
| KNN regressor | Yes | Poor | Moderate | Yes | Local structure, no global shape |
| Decision tree | Yes | Moderate | Yes | No | Interactions and thresholds |
The least-squares line is guaranteed to pass through which point?
Setting the derivative with respect to b to zero gives b = y-bar minus w times x-bar, which is exactly the statement that the line passes through the centroid.
Show answer
B — The centroid (mean of x, mean of y) — Setting the derivative with respect to b to zero gives b = y-bar minus w times x-bar, which is exactly the statement that the line passes through the centroid.
Your residual plot shows a clear U-shape. What has gone wrong?
A systematic curve in the residuals means the model missed structure in the data. Add polynomial terms or transform the feature.
Show answer
C — The true relationship is curved, so a straight line underfits — A systematic curve in the residuals means the model missed structure in the data. Add polynomial terms or transform the feature.
Why square the residuals rather than sum them directly?
Signs cancel. Squaring also keeps the loss differentiable everywhere, which is what makes a closed-form solution possible — but it makes the fit less robust to outliers, not more.
Show answer
B — Positive and negative residuals would cancel, so the plain sum is zero for many bad lines — Signs cancel. Squaring also keeps the loss differentiable everywhere, which is what makes a closed-form solution possible — but it makes the fit less robust to outliers, not more.
A model fit on ad spend from 1 to 5 thousand is asked to predict for 50 thousand. What should you do?
The model has no evidence about that region. Real relationships usually saturate, while a straight line keeps climbing forever.
Show answer
B — Refuse: the prediction is extrapolation far outside the observed range — The model has no evidence about that region. Real relationships usually saturate, while a straight line keeps climbing forever.
🧪 Try It Yourself
Exercise 1 – Compute the slope by hand
Exercise 2 – Push the line through the centroid
Exercise 3 – Residuals must sum to zero
Exercise 4 – Reproduce it with scikit-learn
Exercise 5 – Watch one outlier wreck the fit
Recap
- The model is ; the loss is .
- Setting both partial derivatives to zero gives and — no iteration required.
- The fitted line always passes through , and residuals always sum to zero.
- compares your SSE against the SSE of predicting the mean; 0.60 on the worked example, 0.34 on real diabetes data.
- Squaring the errors buys differentiability and a closed form, and costs robustness to outliers.
- Check linearity and equal variance in the residual plot before believing anything else.
Exercise 6 – Check the three identities a least-squares fit must satisfy
Next
Continue to Multiple Linear Regression — the same idea with many features at once, where the two scalar formulas become one matrix equation and interpreting a coefficient gets considerably subtler.
If this helped you, consider buying me a coffee ☕
Buy me a coffeeWas this page helpful?
Let us know how we did
