Contents
Deep Breakdown
What it tells me (Situation Awareness): The normal equation is the linear system whose solution gives the coordinates of the orthogonal projection of onto the subspace spanned by the columns of . Solving it for and then computing gives , the closest point in the subspace to . It arises directly from the optimality condition: the residual must be orthogonal to every column of , which gives , i.e., . In DA/DS: this is the OLS normal equation , which every linear regression solves (either directly or via numerical methods).
Flowchart
Real-world use
Where it is used
OLS linear regression: gives the least squares coefficients. Ridge regression: (regularized normal equation). Any least-squares problem: fitting a polynomial, exponential, or any linear model. Solving projection problems numerically (preferred over explicitly computing ).
Problem it solves
Given basis and target , find the coordinates such that is closest to . This requires minimizing , which leads to the normal equation. It translates a geometric problem (closest point in a subspace) into a solvable linear system.
How it solves the problem
Expand: . This is quadratic in . Setting the gradient to zero: . If has full column rank, is positive definite and the system has a unique solution .
Example in a business
In OLS regression with design matrix (n×k) and response (n×1): the normal equation gives directly. Numerically, this is solved via np.linalg.solve(X.T @ X, X.T @ y) (more stable than explicitly inverting ) or via np.linalg.lstsq (which uses SVD and handles rank-deficient cases).
The formula, piece by piece
- : transpose of the basis matrix; "aggregates" information from all dimensions into equations
- : Gram matrix; measures pairwise inner products between basis vectors; invertible iff has full column rank
- : vector of unknown coordinates (coefficients in the basis)
- : vector; the right-hand side; projects onto each basis direction
- : a linear system; the solution minimizes
- Solution: (when is invertible)
- OLS form:
The formula in words
"Take each basis vector and require that the residual is orthogonal to it. This gives one equation per basis vector. Solving the resulting system gives the coordinates of the closest point." Tiny example: , . . . Normal equation: . Solution: . .
Critical Concepts
- Preferred over explicit inversion: solve
np.linalg.solve(B.T @ B, B.T @ x)rather thannp.linalg.inv(B.T @ B) @ B.T @ x. Numerically more stable. - Best practice: use
np.linalg.lstsq(B, x)which uses SVD and handles rank-deficient cases. - Ridge modification: adds to each diagonal of the Gram matrix, ensuring invertibility and shrinking the solution.
- Rank deficiency: if is singular (linearly dependent columns), the normal equation has infinitely many solutions; use the minimum-norm solution via pseudoinverse.
- Numerical conditioning: can be ill-conditioned when columns of are nearly parallel. QR decomposition or SVD are more numerically stable alternatives.
- OLS interpretation: = "feature cross-products," = "feature-response cross-products." The normal equation balances these to find .
Step-by-step calculation (tiny example)
Step 1: Set up the normal equation , . . . Normal equation: .
Step 2: Compute the projection .
Step 3: Verify residual orthogonality . . The residual is orthogonal to .
Code examples
Beginner example (Python)
import numpy as np # -- Solve normal equation B^T B lambda = B^T x ---
B = np.array([[1.0, 0.0], [0.0, 1.0], [1.0, 1.0]])
x = np.array([2.0, 3.0, 4.0])
gram_matrix = B.T @ B # B^T B (2x2)
rhs = B.T @ x # B^T x (2,) # Solve (preferred: avoid explicit inversion)
lambda_hat = np.linalg.solve(gram_matrix, rhs)
projection = B @ lambda_hat
residual = x - projection
print(f"B^T B:\n{gram_matrix}")
print(f"B^T x: {rhs}")
print(f"\nlambda_hat (solution): {lambda_hat.round(4)}")
print(f"Projection pi_U(x): {projection.round(4)}")
print(f"Residual: {residual.round(4)}")
print(f"B^T residual (check): {(B.T @ residual).round(10)}") # Alternative: lstsq handles rank-deficient cases
lambda_lstsq, _, _, _ = np.linalg.lstsq(B, x, rcond=None)
print(f"\nlstsq solution: {lambda_lstsq.round(4)} (matches: {np.allclose(lambda_hat, lambda_lstsq)})")Beginner example (R)
# -- Normal equation B^T B
lambda = B^T x ---
B <- matrix(c(1,0,1, 0,1,1), nrow=3, ncol=2)
x <- c(2, 3, 4)
gram <- t(B) %*% B
rhs <- t(B) %*% x
lambda <- solve(gram, rhs)
proj <- B %*% lambda
resid <- x - proj
cat("lambda_hat:", round(lambda, 4), "\n")
cat("projection:", round(proj, 4), "\n")
cat("B^T resid: ", round(t(B) %*% resid, 10), "\n")Professional example (Python)
import numpy as np
import pandas as pd
np.random.seed(42) # -- Business scenario: solve OLS normal equations, compare methods ---
# X^T X beta = X^T y (normal equation for linear regression)
N, K = 200, 4
X = np.column_stack([ np.ones(N), np.random.uniform(0, 10, N), np.random.uniform(0, 5, N), np.random.uniform(0, 3, N),
])
true_beta = np.array([1.5, 0.8, -0.5, 0.3])
y = X @ true_beta + np.random.randn(N) * 0.3
gram = X.T @ X # X^T X (normal equation left side)
rhs = X.T @ y # X^T y (normal equation right side) # Method 1: solve the normal equation directly
beta_solve = np.linalg.solve(gram, rhs) # Method 2: via explicit inversion (less stable)
beta_inv = np.linalg.inv(gram) @ rhs # Method 3: lstsq (uses SVD, most numerically stable)
beta_lstsq, _, _, _ = np.linalg.lstsq(X, y, rcond=None)
comparison = pd.DataFrame({ "parameter": [f"beta_{i}" for i in range(K)], "true_beta": true_beta, "solve": beta_solve.round(4), "inv": beta_inv.round(4), "lstsq": beta_lstsq.round(4),
})
print("OLS via normal equation (B^T B lambda = B^T x with B=X, x=y):")
print(comparison.to_string(index=False))
print(f"\nCondition number of X^T X: {np.linalg.cond(gram):.2f}")
print("Recommendation: use lstsq or solve, NOT explicit inversion.")Source: MML1