Linear regression and OLS
Set up linear regression, derive the OLS estimator from the normal equations, and read it as projection onto the column space.
Linear regression is the simplest supervised learner: model a target as a linear function of features plus noise, and fit the coefficients to minimize squared error. The closed form falls out of the least-squares lesson, but interpreting it as a projection is what makes the rest of the curriculum (ridge, LASSO, GLMs, calibration) click into place.
The model
Given examples with feature vectors and targets , the linear regression model assumes
where are the unknown coefficients and is zero-mean noise. Stacking rows, write for the design matrix and for the target vector; the model is
A column of ones in absorbs the intercept, so the formulas below cover the intercept case without extra notation.
The OLS objective
Ordinary least squares (OLS) picks the coefficients that minimize the sum of squared residuals:
Squared error is convex in (Hessian ), so the minimum is wherever the gradient vanishes.
The normal equations
Differentiating with respect to using matrix calculus,
Setting it to zero gives the normal equations
[1]When has full column rank, is invertible and
If columns of are collinear, is singular and the minimizer is not unique; that is the gap ridge regression will close.
Geometric reading: projection onto the column space
The fitted values are . Substituting,
where is the projection matrix onto the column space of (idempotent, , and symmetric)[2]. The residual lives in the orthogonal complement: (this is exactly the normal equations rewritten).
The geometric picture: is the closest point in to , and OLS is least-squares projection. The whole rest of the lesson chain is a refinement of this picture.
Numerical route
Forming squares the condition number; the conditioning lesson explains why. The numerically stable route uses the QR factorization :
solved by back-substitution. Production code uses QR (numpy's lstsq) or the
SVD, not the explicit inverse.
Check it on arrays
import numpy as np
rng = np.random.default_rng(0)
n, d = 200, 4
X = rng.normal(size=(n, d))
X[:, 0] = 1.0 # intercept
beta_true = np.array([2.0, -1.5, 0.7, 0.3])
y = X @ beta_true + 0.2 * rng.normal(size=n)
# Closed form via the normal equations
beta_normal = np.linalg.solve(X.T @ X, X.T @ y)
print("normal-eq beta:", np.round(beta_normal, 4))
# Stable route: QR factorization
Q, R = np.linalg.qr(X)
beta_qr = np.linalg.solve(R, Q.T @ y)
print("QR beta: ", np.round(beta_qr, 4))
# Reference: numpy's least-squares solver (SVD-based)
beta_lstsq, *_ = np.linalg.lstsq(X, y, rcond=None)
print("lstsq beta: ", np.round(beta_lstsq, 4))
print("agree?", np.allclose(beta_qr, beta_lstsq))
# Residuals are orthogonal to every column of X
y_hat = X @ beta_qr
r = y - y_hat
print("X^T r (should be ~0):", np.round(X.T @ r, 6))
# Projection matrix: P y == y_hat, and P^2 == P
P = X @ np.linalg.solve(X.T @ X, X.T)
print("P y == y_hat:", np.allclose(P @ y, y_hat))
print("P^2 == P: ", np.allclose(P @ P, P, atol=1e-10))The three routes recover the same , the residual is numerically orthogonal to every feature column, and is idempotent.
Exercises
Exercise 1. Fit OLS by hand for a single feature with no intercept. Given the three points and the model (scalar , design matrix ), solve the normal equation for .
Solution
Step 1. Compute the two scalars in the normal equation. With and ,
Step 2. Solve the scalar normal equation :
The fitted line minimizes the squared vertical gaps; no other slope through the origin does better on these three points.
Exercise 2. Prove that the OLS residual is orthogonal to the column space of , i.e. , directly from the normal equations.
Solution
Step 1. Start from the normal equations , which hold at the OLS minimizer by definition.
Step 2. Move both terms to one side and factor out :
Step 3. The left side is zero by the normal equations, so . Each entry is the inner product of feature column with the residual, so the residual is orthogonal to every column, hence to all of . This is the geometric statement that is the projection of .
Exercise 3 (code). A rank-deficient breaks the explicit inverse but not
the SVD-based solver. Build a design matrix whose third column is an exact copy
of its second, then show that np.linalg.solve(X.T @ X, ...) fails (singular
matrix) while np.linalg.lstsq still returns a valid least-squares fit, and
verify that its residual is orthogonal to the columns of .
Solution
import numpy as np
rng = np.random.default_rng(0)
n = 50
X = rng.normal(size=(n, 3))
X[:, 2] = X[:, 1] # column 3 duplicates column 2: rank 2
beta_true = np.array([1.0, 2.0, -0.5])
y = X @ beta_true + 0.1 * rng.normal(size=n)
# Explicit normal-equation solve fails: X^T X is singular.
singular = False
try:
np.linalg.solve(X.T @ X, X.T @ y)
except np.linalg.LinAlgError:
singular = True
assert singular # X^T X is not invertible
# lstsq (SVD) returns the minimum-norm least-squares solution regardless.
beta_hat, *_ = np.linalg.lstsq(X, y, rcond=None)
r = y - X @ beta_hat
assert np.allclose(X.T @ r, 0.0, atol=1e-8) # residual still orthogonal
print("residual orthogonal, max |X^T r| =", round(np.abs(X.T @ r).max(), 10))The collinear column makes singular, so the explicit solve raises
LinAlgError. The SVD-based lstsq sidesteps the inverse and still returns a
whose residual is orthogonal to the column space, the defining
property of a least-squares fit.
The next lesson asks where this estimator comes from probabilistically: it is the MLE under Gaussian noise.
References
- [1]Hastie, T., Tibshirani, R., & Friedman, J. (2009). The Elements of Statistical Learning (2nd ed.), §3.2. Springer. doi:10.1007/978-0-387-84858-7↩
- [2]James, G., Witten, D., Hastie, T., & Tibshirani, R. (2021). An Introduction to Statistical Learning (2nd ed.), §3.1. Springer. The projection/geometry reading of least squares.↩