Chapter 2: Linear Regression
Master the foundational algorithm of machine learning. Learn to predict continuous values with linear models, optimize using gradient descent, handle multiple features with matrix operations, and prevent overfitting with L1/L2 regularization.
Chapter Overview
Linear regression is arguably the most important algorithm to understand deeply. Not because it's the most powerful—it's not—but because it introduces nearly every concept you'll need for more complex models: loss functions, optimization, overfitting, regularization, and the bias-variance tradeoff.
The core idea is beautifully simple: model the relationship between inputs and outputs as a weighted sum plus a bias term. The weights tell us how much each feature contributes to the prediction. The challenge is finding the optimal weights—those that minimize the prediction errors.
For simple problems, we can solve for optimal weights analytically using calculus. But this approach doesn't scale. Gradient descent provides an iterative alternative: start with random weights, compute how the loss changes with small weight adjustments (the gradient), then nudge weights in the direction that decreases loss. Repeat until convergence.
Real problems have many features interacting in complex ways. Multiple regression extends the model to handle this, fitting a hyperplane in high-dimensional space. The normal equation gives a closed-form solution, but gradient descent is more practical for large datasets.
Regularization addresses a critical problem: models can fit training data too well, capturing noise rather than signal. By penalizing large weights, L2 (Ridge) regularization shrinks coefficients toward zero, while L1 (Lasso) can eliminate features entirely. This tradeoff between fitting the data and keeping the model simple is fundamental to all of machine learning.
This chapter covers:
- Simple Linear Regression: Fitting a line with MSE loss and the closed-form solution
- Gradient Descent: Iterative optimization, variants (batch, stochastic, mini-batch), and learning rate schedules
- Multiple Regression: Matrix formulation, feature scaling, and handling many features
- Polynomial Features: Capturing nonlinear relationships while staying in the linear regression framework
- Regularization: L1 (Lasso), L2 (Ridge), and Elastic Net for controlling complexity
- Model Assumptions & Diagnostics: When linear regression is appropriate and how to verify
Chapter Roadmap
Click any topic to jump in
Simple Linear Regression
The starting point — fit a line through data by minimizing squared errors with a closed-form solution.
Optimization for large data and extending to multiple features
Gradient Descent
Iterative optimization when closed-form solutions don't scale — follow the negative gradient to minimize loss.
Multiple Regression
Extend to many features with matrix notation, the normal equation, and feature scaling for stable training.
Nonlinear patterns and controlling model complexity
Polynomial Features
Capture nonlinear patterns while staying inside the linear regression framework via feature transformations.
Regularization
Prevent overfitting by penalizing large weights — L1 for sparsity, L2 for shrinkage, Elastic Net for both.
Assumptions & Diagnostics
When linear regression is appropriate and how to verify — residual analysis, influential points, and model checks.
You have two columns of numbers — house size and sale price, study hours and exam score — and you are asked what the second column would be for a value of the first that nobody recorded. Looking it up is impossible; you have to invent a rule that turns any input into a plausible output, and fit that rule to the data you do have.
That rule is a model: a formula containing a few unknown numbers called parameters. Choosing them from data is fitting, and "best fit" only means something once you define a loss — one number saying how wrong the model currently is. Chapter 1's Introduction to Machine Learning named these words; this is the first topic where you see all of them made concrete on a model you can solve by hand.
The arc follows the order you would actually derive it. We write down the straight-line model, define the residual (one point's error), average the squared residuals into MSE, defend the squaring, solve for the parameters exactly with calculus, and close with — the score that says whether fitting the line was worth it at all.
Definition
Simple linear regression models a numeric target as an affine function of one numeric input , , choosing the parameters and to minimise the mean squared error between predictions and observations. Because that objective is a convex quadratic, the minimiser is unique and available in closed form — no iterative search needed.
In this topic
Linear Model
Without a model you can only repeat values you have already observed. A model is a formula with unknown numbers. Here is the single input feature and — "y-hat" — is the predicted output, hatted to separate it from an observed . The parameters and are what we choose: the intercept is the prediction at , the slope is how far moves per one-unit increase in . This is chapter 0's in ML notation. Its assumption is strong: if the truth curves, no repairs it and the leftover errors are systematic, not random.
The model defines an affine mapping from input space to output space . The parameter is the rate of change , meaning each unit increase in shifts the prediction by exactly . The intercept anchors the line at . With data points, we have 2 free parameters and constraints — the system is overdetermined when , which is why we need a loss function to define "best fit" rather than exact interpolation.
Model: ŷ = 50 + 10x. What's the prediction when x=3?
Residuals
A model can only be fitted if you can measure how wrong it is at each point; the residual is that per-point measurement. For training example , is the observed target, is what the line predicts at that example's input, and is the signed vertical gap: positive means the model underpredicted, negative means it overpredicted. Residuals are not properties of the data: move the line and every changes, which is what fitting exploits. They measure vertical error only, so is assumed noise-free; residuals that curve or fan out warn that the linear model above is the wrong shape.
The residual measures the signed vertical distance from observation to the fitted line. For the OLS estimator, and — these are the normal equations. The first says residuals balance out on average; the second says residuals are uncorrelated with the predictor. Together, these two conditions uniquely determine and . Violations of these orthogonality conditions indicate the model has not converged or was computed incorrectly.
Actual y=100, predicted ŷ=85. What's the residual? Interpretation?
Mean Squared Error (MSE)
Residuals give numbers, but choosing parameters needs one number to minimise — and simply adding residuals fails, because positive and negative misses cancel and a terrible line can total zero. Squaring destroys the signs, so the mean of the squared residuals over the training examples is zero only for a perfect fit. That number is the loss. Dividing by makes it per-example, so datasets of different sizes stay comparable. Its units are the square of 's units, which is why people report (RMSE) instead. The price of squaring is outlier sensitivity: one point ten units off costs as much as a hundred points one unit off.
MSE is a convex, differentiable function of the weights. Squaring amplifies large errors: an error of 4 contributes 16 to the sum, while four errors of 1 contribute only 4. This makes MSE sensitive to outliers — a single point far from the line can dominate the entire loss. Statistically, minimizing MSE is equivalent to maximum likelihood estimation when the noise , connecting the geometric idea of "closest line" to the probabilistic idea of "most likely parameters."
Residuals: [2, -3, 1, -2]. Calculate MSE.
Why MSE?
Squaring residuals is a choice, not a law, with three defences. Probabilistic: assume every observation is the line plus independent noise , where is the noise variance. Each point's likelihood is proportional to , the dataset's likelihood is their product, and taking the logarithm turns that product into plus constants — so maximising likelihood is literally minimising MSE. Computational: is differentiable everywhere, while has a corner at zero. Behavioural: doubling an error quadruples its cost. That last is also the flaw — MSE estimates the conditional mean, so one outlier drags the line, whereas MAE estimates the median and shrugs.
The MSE loss surface is a paraboloid in parameter space — it has exactly one global minimum and no local minima. The Hessian matrix is positive semi-definite, guaranteeing convexity. The absolute error has a corner at zero where the derivative is undefined, making gradient-based optimization problematic. MSE's smoothness everywhere means the gradient exists at every point, providing a well-defined direction toward the optimum.
Why not use |error| (MAE) instead of error²?
Closed-Form Solution
Because MSE is a convex bowl in , its one flat point is the minimum: set both partial derivatives to zero and solve. Differentiating with respect to gives — residuals must sum to zero — which rearranges to , where and are sample means. Substituting back and differentiating with respect to produces the formula shown: the covariance over the variance of . It fails only when — every identical, so there is no slope to estimate; near-constant makes it numerically fragile.
Setting yields . This has a clean geometric interpretation: the slope equals the correlation coefficient times the ratio of standard deviations, . When features are perfectly correlated (), the line passes through every point. When , the slope is zero and the best prediction is just . The closed-form solution requires time for simple regression, making it efficient for small feature counts.
Cov(x,y) = 15, Var(x) = 5, x̄ = 10, ȳ = 25. Find the line.
R² (Coefficient of Determination)
MSE answers "how wrong?" in squared units of , so its value alone cannot say whether a fit is good. fixes that by comparing against the laziest model: ignore and always predict . The denominator is that baseline's squared error — the total variation in ; the numerator is what your line leaves. Their ratio is the fraction of baseline error still unexplained, so is the fraction removed: 0.8 means four fifths of the variation in is explained by . It goes negative when a model beats nothing, and never falls when you add a feature, even a useless one.
decomposes total variance into explained and unexplained parts: . For simple regression, — the square of the Pearson correlation. Adding features can only increase (or keep it the same), which is why adjusted penalizes model complexity by accounting for the number of parameters . A negative means the model fits worse than a horizontal line at .
SSres = 200, SStot = 1000. Calculate and interpret R².
Code Examples
Fitting by hand, and checking the two conditions the optimum must satisfy
import numpy as np
x = np.array([1.0, 2.0, 3.0, 4.0, 5.0])
y = np.array([2.0, 4.0, 5.0, 4.0, 5.0])
xb, yb = x.mean(), y.mean()
w1 = np.sum((x - xb) * (y - yb)) / np.sum((x - xb) ** 2)
w0 = yb - w1 * xb
e = y - (w0 + w1 * x)
print(f"w0 = {w0:.4f} w1 = {w1:.4f}")
print(f"sum of residuals = {e.sum():.2e}")
print(f"sum of x_i * e_i = {np.dot(x, e):.2e}")
mse = lambda a, b: np.mean((y - (a + b * x)) ** 2)
print(f"MSE at optimum = {mse(w0, w1):.6f}")
for d in (-0.2, 0.2):
print(f"MSE at w1 {d:+.1f} = {mse(w0, w1 + d):.6f}")The same five points as the theory exercise, so you can check the hand arithmetic: it prints w0 = 2.2000 and w1 = 0.6000. The two sums come out at the 1e-16 level — not merely small. They are the stationarity conditions from the derivation, exact up to floating point. MSE at the optimum is 0.480000, and nudging the slope by either -0.2 or +0.2 raises it to 0.920000: moving away from the minimum costs you in both directions and by the same amount, which is what a symmetric convex bowl looks like numerically.
Outliers, negative R², and why R² never goes down
import numpy as np
from sklearn.linear_model import LinearRegression
from sklearn.metrics import r2_score
rng = np.random.default_rng(1)
x = rng.uniform(0, 10, 60)
y = 2.0 * x + 1.0 + rng.normal(0, 1.0, 60)
X1 = x.reshape(-1, 1)
base = LinearRegression().fit(X1, y)
print(f"clean slope = {base.coef_[0]:.3f}")
y_out = y.copy(); y_out[x.argmax()] += 60.0 # corrupt the highest-x point
print(f"slope w/ 1 outlier = {LinearRegression().fit(X1, y_out).coef_[0]:.3f}")
X2 = np.column_stack([x, rng.normal(0, 1, 60)]) # add a pure-noise feature
fit2 = LinearRegression().fit(X2, y)
print(f"R2, 1 feature = {r2_score(y, base.predict(X1)):.6f}")
print(f"R2, + noise column = {r2_score(y, fit2.predict(X2)):.6f}")
print(f"R2 of always-zero = {r2_score(y, np.zeros_like(y)):.3f}")Three of the topic's claims, verified. The clean slope is 2.031, close to the true 2.0. Adding 60 to the single highest-x target drags it to 2.597 — one corrupted point in sixty, and the fit chases it. That is MSE's outlier sensitivity, and the point is corrupted at the edge of the x-range because leverage is highest there. Appending a column of pure random noise moves R² from 0.980120 to 0.980122: a rise in the sixth decimal, and never a fall, because the extra parameter can only reduce training error. That is exactly why later topics need adjusted R² and regularisation. The always-zero predictor scores -3.961, far below the mean baseline of 0.
Theory Exercise
Problem:
Given data points (1,2), (2,4), (3,5), (4,4), (5,5), calculate the best-fit line using the closed-form solution.
Hints:
- First calculate means: x̄ and ȳ
- Use the formula for w₁ involving covariance and variance
- Then calculate w₀ = ȳ - w₁x̄
Coding Exercise
Problem:
Generate noisy data from y = 2.5x + 1 with a fixed seed, then implement simple linear regression from scratch using the closed-form (least-squares) formulas for slope and intercept. Verify your weights against sklearn's LinearRegression and report the R² of your fit.
Hints:
- Slope = sum((x - x̄)(y - ȳ)) / sum((x - x̄)²); intercept = ȳ - slope·x̄.
- Compute predictions ŷ = slope·x + intercept, then use sklearn.metrics.r2_score(y, ŷ).
- Fit LinearRegression on x.reshape(-1, 1) and compare your slope to sk.coef_[0] and intercept to sk.intercept_.
Related Problems on PixelBank
The previous topic, Simple Linear Regression, handed you a closed-form answer: set the derivative of the mean squared error to zero, solve the normal equations, and the best-fitting line drops out. Inverting costs roughly and needs the whole design matrix in memory, so it stops being practical in the thousands of features -- and it exists at all only because linear regression is unusually well behaved. Swap in logistic regression or a neural network and there is no formula left to solve.
Gradient descent is the general answer, and it is the single most reused idea in this plan: the same loop trains the neural networks of chapter 9 and the CNNs of chapter 10. Rather than jumping to the minimum, you start anywhere, see which way the loss slopes, and take a small step downhill. All it needs is a loss and its derivative, both of which you already have from the MSE and the residuals of the previous topic.
We build it in order: the gradient geometrically, the update rule that consumes it, the MSE gradient derived for linear regression, the learning rate, the three batching strategies, and how to know when to stop.
Definition
Gradient descent is an iterative optimization algorithm that minimizes a differentiable loss by repeatedly stepping in the direction of the negative gradient, , until the loss stops improving. The gradient points along steepest ascent, so its negative is the locally fastest way downhill, and the learning rate decides how far to travel before measuring the slope again.
In this topic
Gradient
The loss at your current parameters says nothing about which way to move, and a blind grid search over values per axis costs evaluations. The gradient answers in one calculation. is the scalar loss, the parameter vector, and a vector shaped like whose -th entry is how fast loss rises per unit increase in , other parameters fixed. Assembled, those slopes point along steepest ascent -- precisely why descent subtracts them. It assumes is differentiable (mean absolute error is not, at zero), and on a flat plateau it sits near zero, offering no direction. These are chapter 0's partial derivatives doing real work.
The gradient points in the direction of steepest ascent in parameter space. Its magnitude tells you how steep the slope is. For MSE with linear regression, , which is a linear function of the residual vector. At the minimum, , which recovers the normal equations. The gradient exists because MSE is differentiable everywhere — this is not true for all loss functions (e.g., absolute error at zero).
Loss J(w) = w². At w=4, what's the gradient and which direction should we move?
Update Rule
A direction is not a move: the gradient is a rate of change, not a distance, and nothing stops you walking past the minimum. This rule supplies the missing scale. is the current parameter vector, the gradient at that point (recompute it every iteration), and the learning rate; the minus sign turns steepest ascent into descent. It is a first-order Taylor step: we replace by its tangent plane and trust it only briefly, which is why steps stay small. The gradient shrinks near a minimum, so steps shrink automatically. It fails when outruns the local curvature. This same line, fed by backpropagation, trains every neural network in chapter 9.
The update is a first-order Taylor approximation: we linearize around the current point and take a step proportional to the negative slope. The learning rate controls step size. If has Lipschitz-continuous gradients with constant , convergence is guaranteed for . For MSE, where is the largest eigenvalue, so the optimal learning rate depends on the data's condition number.
w=4, gradient=8, α=0.1. What's the new w after one update?
MSE Gradient for Linear Regression
Rather than assert this formula, derive it. Start from with prediction . The power rule gives the factor 2; the chain rule then multiplies by and carries a minus sign because enters subtracted. So: 2 from the square, from the chain rule, from the MSE's averaging -- drop it and the gradient grows with dataset size, forcing a new for every . Here counts examples, is the residual from the previous topic, the feature. In matrix form, . It is a residual-feature correlation that vanishes when the two are uncorrelated -- the normal equations again.
The partial derivatives and show that the gradient is a weighted sum of residuals. For , each residual contributes equally; for , each residual is weighted by its corresponding . Points with large have more influence on the slope gradient, which is why feature scaling matters — it normalizes the influence of each dimension.
Residuals [2,-1,3] with features [1,2,1]. Calculate the gradient component.
Learning Rate (α)
The gradient fixes direction; is the free knob you choose, and it decides whether training works. Too small (say ) still converges, but the curve is a nearly straight decline needing a hundred times more epochs than you will sit through. Too large overshoots the valley and lands higher up the far wall, so error grows by a constant factor per step: loss reads 12, 40, 300, 1e6, then inf, then nan once inf - inf reaches the update -- and nan weights never recover. Just right drops the loss steeply, then flattens. Practically: standardize features, sweep 0.001 to 0.1 logarithmically, keep the largest value whose loss still falls monotonically.
Too large: the update overshoots the minimum. For a quadratic , the update is . Convergence requires , so . Too small: convergence takes steps to reach error . Learning rate schedules like start fast and slow down, combining the benefits of large and small rates. The condition number determines how much the optimal rate varies across directions.
With α=0.001, loss barely changes. With α=1, loss explodes. What do?
Batch Gradient Descent
Taken literally, the update rule averages the gradient over all training examples, giving the exact gradient of the loss rather than an estimate. One pass over the data -- one epoch -- yields exactly one update, costing for examples, features. Nothing is sampled, so the trajectory is deterministic and with a sane the loss falls smoothly and monotonically -- the easiest variant to debug. It breaks on scale: with in the millions, a whole pass buys one step, and must fit in memory. A few hundred updates is a short journey, so wall-clock convergence is far slower than its clean curve suggests.
Batch GD computes the exact gradient using all samples: . Each step costs for parameters. The trajectory is smooth and deterministic but each step is expensive. For convex problems, batch GD converges at rate — after iterations, the error is at most proportional to . The gradient direction is the same regardless of data ordering, which means batch GD is reproducible but cannot exploit structure in the data order.
1M samples, 100 epochs of batch GD. How many gradient computations?
Stochastic Gradient Descent (SGD)
Batch GD wastes work: after a thousand similar examples the direction is clear, yet it reads the remaining 999,000 before moving. SGD takes the opposite extreme: draw one random example , compute from it, update, repeat -- so one epoch delivers updates at each. The estimate is unbiased, , so on average it moves the right way, but variance is large and individual steps point wrong constantly. The loss curve is jagged, and at fixed parameters never settle but rattle inside a noise ball whose radius scales with . Decaying shrinks it. Shuffle every epoch, or ordering becomes systematic bias.
SGD approximates the full gradient with a single sample: for a randomly chosen . The estimate is unbiased () but has high variance . Each step costs instead of , so SGD can make updates in the time of one batch step. The noise acts as implicit regularization — SGD tends to find flatter minima that generalize better, which is why it often outperforms batch GD on test data.
1M samples, 1 epoch of SGD. How many updates?
Mini-Batch Gradient Descent
Neither extreme is what anyone runs. Mini-batch averages the gradient over randomly drawn examples: recovers batch GD, recovers SGD, one dial spanning both. Averaging independent estimates divides gradient variance by , so noise falls only as -- diminishing returns past a hundred or so. The usual 32 to 256 dominates for hardware reasons, not statistical ones: a batch is one matrix multiply, and vectorized CPU or GPU units chew through 32 rows in nearly the time of one, so that variance reduction is near-free. Push into the thousands and gradients get so quiet that must be raised and warmed up.
Mini-batch GD uses samples per step, giving a variance reduction of compared to SGD: . Typical batch sizes are 32-256. The sweet spot balances two factors: larger batches reduce gradient noise (more stable updates) but have diminishing returns — doubling only reduces variance by . GPU parallelism means takes nearly the same wall-clock time as , making mini-batch the standard choice in practice.
1M samples, batch size 256, 1 epoch. How many updates?
Convergence
"Stop when the gradient is zero" is useless in practice: floating-point arithmetic rarely lands on zero, and under SGD the gradient is noisy and never settles there. Real criteria are three, usually combined. A gradient norm test, , scale-dependent and meaningless unless features are standardized. A relative loss test, over several consecutive epochs -- scikit-learn's tol with n_iter_no_change is exactly this. And a hard iteration cap, so a diverging run still terminates. Better still, early-stop on a validation loss with patience. Always plot the curve: flat from step one means too small or features unscaled; rising or nan means too large.
GD has converged when or . For strongly convex functions (eigenvalues of Hessian bounded below by ), batch GD converges exponentially: , where is the smoothness constant. The condition number determines convergence speed — ill-conditioned problems () zigzag slowly. Feature scaling reduces by making the loss surface more spherical.
Loss: epoch 1=100, epoch 10=50, epoch 100=49.5, epoch 1000=49.4. Is it converged?
Code Examples
The three learning-rate regimes on one problem
import numpy as np
rng = np.random.default_rng(0)
n = 200
X = np.c_[np.ones(n), rng.normal(size=n)] # bias column + 1 feature
y = X @ np.array([2.0, -3.0]) + rng.normal(scale=0.5, size=n)
def run(lr, steps=700):
w = np.zeros(2)
for _ in range(steps):
err = X @ w - y # grad = (2/n) X^T err
w -= lr * (2 / n) * (X.T @ err)
return float(np.mean((X @ w - y) ** 2)), w
start = float(np.mean(y ** 2)) # loss at w = 0
with np.errstate(over="ignore", invalid="ignore"):
for lr in (0.001, 0.1, 1.5):
loss, w = run(lr)
print(f"lr={lr:<6} loss {start:.2f} -> {loss:10.4g} w={np.round(w, 3)}")One batch gradient descent loop, three learning rates, same data (true weights 2.0 and -3.0, irreducible MSE 0.25). Notice that lr = 0.001 is not wrong, only slow: after 700 steps its loss is still about 1.16, roughly five times the noise floor, with weights near 1.46 and -2.19 instead of 2 and -3. lr = 0.1 lands on 0.2617 with the right weights. lr = 1.5 exceeds the stability limit of two divided by the largest Hessian eigenvalue (about 1 here), so its loss prints as `inf` and its weights blow up past 1e212 -- that, followed by `nan`, is what divergence looks like in a real training log.
Batch vs mini-batch vs SGD at equal epochs
import numpy as np
rng = np.random.default_rng(1)
n, d = 2000, 5
X = np.c_[np.ones(n), rng.normal(size=(n, d))]
y = X @ rng.normal(size=d + 1) + rng.normal(scale=0.5, size=n)
def train(batch, epochs=5, lr=0.05):
w, updates = np.zeros(d + 1), 0
for _ in range(epochs):
order = rng.permutation(n) # reshuffle each epoch
for s in range(0, n, batch):
idx = order[s:s + batch]
w -= lr * (2 / len(idx)) * (X[idx].T @ (X[idx] @ w - y[idx]))
updates += 1
return updates, float(np.mean((X @ w - y) ** 2))
for name, B in [("batch (B=n)", n), ("mini-batch (B=32)", 32), ("SGD (B=1)", 1)]:
print("%-18s updates=%-6d final MSE=%.4f" % ((name,) + train(B)))All three see the data exactly five times; only the update frequency differs. Batch GD gets 5 updates and ends at MSE 1.4556, still far from the solution. Mini-batch gets 315 updates and reaches 0.2421, essentially the 0.25 noise floor. SGD gets 10,000 updates but finishes at 0.3933 -- worse than mini-batch, because at a constant learning rate it rattles inside its noise ball instead of settling. That ordering is the whole argument for mini-batch, and the cure for SGD's gap is a decaying learning rate, not more epochs.
Theory Exercise
Problem:
Why might gradient descent fail to converge? List 3 possible reasons and their solutions.
Hints:
- Consider the learning rate
- Think about the shape of the loss surface
- What about feature scales?
Coding Exercise
Problem:
Build a regression dataset with make_regression, standardize the features, and implement batch gradient descent for linear regression with numpy (including a bias term). Track the MSE loss each iteration to confirm it decreases, then compare your final weights to the normal-equation / sklearn solution.
Hints:
- Prepend a column of ones to X so the bias is learned as w[0]; initialize w to zeros.
- MSE gradient is (2/n) · Xᵀ(Xw - y); update w ← w - lr·grad with a learning rate like 0.1.
- Append each loss to a list and print the first vs last value; compare w to np.concatenate([[sk.intercept_], sk.coef_]).
Everything in the previous topic, Simple Linear Regression, had one input: one feature, one slope, one line through a scatter plot. Almost no real problem looks like that. A house price depends on floor area and bedroom count and age and distance to a station at once, and if you fit area alone, its slope quietly absorbs the effect of every other variable that moves with it — the number you get is not the effect of area at all. Multiple regression fixes this by giving each feature its own weight, turning the line into a hyperplane.
The objective does not change: you are still minimising mean squared error over the residuals , and only the number of weights grows. We first write the model as a weighted sum over features, then compress that sum into matrix notation so all predictions become one product . That form hands us a closed-form solution, the normal equation, and its one fragile piece, the inverse , sets up everything after it: scaling features so they are comparable, reading weights as importance, and recognising multicollinearity, the case where that inverse nearly fails.
Definition
Multiple linear regression models a scalar target as an affine function of input features, , choosing the weights that minimise the sum of squared residuals on the training data. Stacking all examples turns the model into one matrix equation , whose least-squares solution is whenever has full column rank.
In this topic
Multiple Features
With one predictor you can only ask how price moves with area, never how it moves at a fixed bedroom count. Multiple regression fixes that by giving every feature its own weight. Here is the -th of features, is its weight, and is the intercept, the prediction when all features are zero. Defining folds the intercept in, so the model is one dot product . Each is an all-else-equal slope, meaningful only if the others can be held fixed — which fails when predictors move together. The objective remains the previous topic's mean squared error.
With features, the model is . This defines a hyperplane in -dimensional space. Each weight represents the partial derivative — the change in prediction per unit change in feature , holding all other features constant. This "all else equal" interpretation is critical: in simple regression, the slope captures both the direct effect and indirect effects through correlated variables.
House price model: ŷ = 50000 + 100×sqft + 5000×bedrooms. Predict price for 2000 sqft, 3 bed.
Matrix Form
Computing in a Python loop over examples is slow and hides the structure; stacking them turns the dataset into one multiplication. Row of the design matrix is example , so is : rows, one per example, columns, one per feature. To absorb the intercept you prepend a column of ones, making and with first, so each row contributes . Then is , matching . Mismatched shapes are the usual bug; worse, omitting the ones column raises no error and silently forces the fit through the origin.
Stacking all observations gives the design matrix (with a column of ones for the intercept) and the system . The MSE becomes . The gradient is . Matrix notation is not just compact — it reveals that regression is really about projecting onto the column space of . The predicted values are exactly this projection.
3 samples, 2 features. What's the shape of X, w, and ŷ?
Normal Equation
Gradient descent walks downhill to the minimum; for squared error you can jump straight to the bottom. Setting the gradient of to zero gives , hence the closed form. is the Gram matrix and is , so is too: no learning rate, no iterations, no convergence check. The inverse needs full column rank, so it fails when a feature is an exact combination of others, or when . Inversion costs , which is why past a few thousand features gradient descent's per step wins.
Setting gives , solved by . Computing this requires inverting a matrix, which costs . When , this becomes impractical and gradient descent is preferred. The matrix must be invertible, which fails when features are linearly dependent (multicollinearity). Regularization fixes this by adding to make always invertible.
1000 samples, 10 features. Normal equation or gradient descent?
Feature Scaling: Standardization
Gradient descent zigzagged in the previous topic because one feature ranged over thousands and another over single digits, stretching the loss contours into a narrow valley. Standardization removes the units: subtract the feature's mean and divide by its standard deviation , column by column, leaving every feature with mean 0 and variance 1. A scaled value reads as a number of standard deviations from the mean, which makes weights comparable in the next concept. Prefer it for roughly bell-shaped or unbounded features. The non-negotiable rule: fit and on the training set only and reuse those numbers on test data; recomputing them there leaks information.
Standardization transforms each feature to have zero mean and unit variance: . This rescales the loss surface so gradient descent converges faster — without scaling, a feature ranging 0-1000 dominates one ranging 0-1 in the gradient computation. Geometrically, standardization makes the MSE contours more circular (condition number ), eliminating the zigzag behavior that occurs when contours are elongated ellipses.
Feature x: μ=100, σ=25. Standardize x=150.
Feature Scaling: Normalization
Standardization leaves values unbounded, which is wrong when a model expects a fixed input range, or a feature has hard limits like pixel intensity 0 to 255. Min-max normalization maps the training range onto : subtract the column minimum , divide by the range , so the smallest training value becomes 0 and the largest 1. It also leaves exact zeros at zero when the minimum is 0, which matters for sparse data. Its weakness: both endpoints are single observations, so one outlier stretches the denominator and squashes every real value into a thin band — standardize instead. Again both statistics come from training data only.
Min-max normalization maps features to : . Unlike standardization, this preserves zero entries (important for sparse data) and bounds the range. The choice between standardization and normalization depends on whether the feature distribution is roughly Gaussian (standardize) or has clear bounds (normalize). Both must be fit on training data only — applying test statistics causes data leakage.
Age range [20, 80]. Normalize age=50.
Feature Importance
Once fitted, you want to know which features drive the prediction. The sign of gives direction, and on standardized features gives the effect of a one-standard-deviation change, so magnitudes can be ranked. On raw features they cannot: the same measurement in millimetres rather than metres carries a weight 1000 times smaller with identical predictive power, so a large coefficient may only mean a small-numbered unit. Two caveats. A weight carries estimation uncertainty, so weigh it against its standard error (the -statistic) first. And every weight is conditional on the other features present, which is what makes it unreliable under the collinearity described next.
After standardization, the magnitude directly indicates feature importance because all features are on the same scale. Without standardization, a feature measured in millimeters will have a weight 1000 larger than the same feature measured in meters, even though the predictive power is identical. The t-statistic tests whether feature has a statistically significant relationship with , accounting for estimation uncertainty.
Standardized model: ŷ = 0 + 0.8×sqft_std - 0.3×age_std. Which feature matters more?
Multicollinearity
Two features carrying nearly the same information, like living area and total area, leave no way to divide credit between them. Their columns are almost linearly dependent, so has a near-zero eigenvalue, blows up, and inflates. The symptom is distinctive: huge coefficients with huge standard errors that flip sign when you refit on a slightly different sample, while stays good. It damages interpretation, not accuracy, so it hides unless you check a variance inflation factor (above 5 to 10 is trouble). Fix it by dropping or merging columns, or by adding a ridge penalty making invertible — topic 4.
When features are highly correlated, becomes nearly singular (small eigenvalues), making have very large entries. The variance of the OLS estimator is , so near-singularity inflates weight variances dramatically. The Variance Inflation Factor , where is the from regressing feature on all others, quantifies this: signals serious multicollinearity.
Features: sqft_living and sqft_total are 99% correlated. What happens?
Code Examples
Normal equation end to end, with every shape printed
import numpy as np
rng = np.random.default_rng(0)
n, d = 200, 3
X_raw = rng.normal(size=(n, d)) * np.array([10.0, 0.5, 1.0]) # different scales
w_true = np.array([5.0, -2.0, 0.5, 3.0]) # [w0, w1, w2, w3]
y = w_true[0] + X_raw @ w_true[1:] + rng.normal(scale=0.05, size=n)
X = np.hstack([np.ones((n, 1)), X_raw]) # prepend the intercept column
XtX = X.T @ X
print("X", X.shape, "| XtX", XtX.shape, "| y", y.shape)
print("rank", np.linalg.matrix_rank(XtX), "of", XtX.shape[0])
w = np.linalg.solve(XtX, X.T @ y) # normal equation (solve, never inv)
print("w_hat ", np.round(w, 3), "\nw_true", w_true)
# Drop the ones column: shapes still work, but the fit is forced through the origin.
w_nb = np.linalg.solve(X_raw.T @ X_raw, X_raw.T @ y)
print("no-bias w", np.round(w_nb, 3))
print("MSE bias/no-bias:", round(float(np.mean((y - X @ w)**2)), 4),
round(float(np.mean((y - X_raw @ w_nb)**2)), 4))Follow the shapes. `X_raw` is 200x3; prepending the ones column makes `X` 200x4, so `XtX` is 4x4 and the solution vector is 4x1 — one weight per feature plus the intercept. `matrix_rank` prints 4 of 4, which is exactly the full-column-rank condition that lets the inverse exist, and the recovered `w_hat` is [4.998, -2.0, 0.49, 3.003] against a true [5, -2, 0.5, 3]. Then the last block drops the ones column: shapes still line up and nothing raises an error, but the fit is forced through the origin and the MSE jumps from 0.0025 to 24.79. That is the failure mode you can only catch by reading the numbers. Note also `np.linalg.solve` rather than `inv` — same answer, better conditioned.
Scaling fit on train only, and why raw coefficients are not importance
import numpy as np
from sklearn.linear_model import LinearRegression
from sklearn.model_selection import train_test_split
from sklearn.preprocessing import StandardScaler
rng = np.random.default_rng(1)
sqft = rng.normal(1800, 500, 500) # hundreds to thousands
beds = rng.normal(3, 1, 500) # single digits
price = 50000 + 100 * sqft + 5000 * beds + rng.normal(0, 5000, 500)
X = np.column_stack([sqft, beds])
Xc = np.column_stack([X, sqft + rng.normal(0, 0.01, 500)]) # near-duplicate column
Xtr, Xte, ytr, yte = train_test_split(X, price, test_size=0.2, random_state=0)
sc = StandardScaler().fit(Xtr) # statistics come from TRAIN only
raw = LinearRegression().fit(Xtr, ytr)
std = LinearRegression().fit(sc.transform(Xtr), ytr)
print("raw coefs [sqft, beds]:", np.round(raw.coef_, 1))
print("standardized[sqft, beds]:", np.round(std.coef_, 1))
print("train mean/std:", np.round(sc.mean_, 1), np.round(sc.scale_, 1))
print("test R2:", round(raw.score(Xte, yte), 4), round(std.score(sc.transform(Xte), yte), 4))
print("cond(XtX): %.0e -> %.0e" % (np.linalg.cond(X.T @ X), np.linalg.cond(Xc.T @ Xc)))The raw coefficients print as [100.5, 5475.6], which read naively says bedrooms matter about 55 times more than square footage. After standardization they are [46481.2, 5784.0]: area dominates by roughly 8 times, which is the truth here, because one standard deviation of area (462.7 sq ft, from the printed train statistics) is worth far more than one extra bedroom. Notice the test R2 is 0.9868 either way — scaling changes interpretation and optimizer behaviour, not the quality of the closed-form fit — and that `StandardScaler` is fit on `Xtr` alone and merely applied to `Xte`. The final line appends a near-copy of `sqft`, and the condition number of the Gram matrix leaps from 2e+06 to 1e+11: multicollinearity, made visible.
Theory Exercise
Problem:
You have two features: area (100-5000 sq ft) and bedrooms (1-5). Why is feature scaling important here, and which method would you use?
Hints:
- Compare the ranges of the features
- Think about gradient descent convergence
- Consider the weight interpretation
Coding Exercise
Problem:
Load the diabetes dataset and solve multiple linear regression directly via the normal equation w = (XᵀX)⁻¹Xᵀy (with a bias column). Verify it matches sklearn, then add a near-duplicate feature and show how multicollinearity blows up the condition number of XᵀX.
Hints:
- Add a ones column with np.hstack, then w = np.linalg.inv(Xb.T @ Xb) @ Xb.T @ y.
- Use np.allclose to compare w[1:] to sk.coef_ and w[0] to sk.intercept_.
- np.linalg.cond(Xb.T @ Xb) measures conditioning; appending a copy of one feature makes it explode (multicollinearity).
Related Problems on PixelBank
You fit the best straight line your data allows, and it is still wrong in a patterned way: plot the residuals from Simple Linear Regression and they bow — negative through the middle, positive at both ends. No choice of slope and intercept repairs that, because a line has no curvature to offer. The temptation is to reach for a different algorithm. You do not need one. This topic shows how to fit curves while leaving ordinary least squares completely intact, by changing the features rather than the model: replace with and run the same fit. We start with that reframing, because everything else rests on it, then generalise it to arbitrary feature maps, add interaction terms so features can modify each other, confront the two costs — choosing the degree, and the combinatorial blow-up in the number of columns — and finish with scikit-learn's PolynomialFeatures. You will need the design matrix and the Normal Equation from Multiple Regression, the previous topic; both carry over untouched.
Definition
Polynomial regression fits a curved function of the input by applying ordinary linear regression to a transformed feature vector instead of to itself. The model is nonlinear in but remains linear in the parameters — the only linearity least squares ever required — so the normal equation, the convexity of the MSE loss, and gradient descent all apply unchanged.
In this topic
Polynomial Regression
A straight line has the same slope everywhere, so it cannot bend toward the data in one region without pulling away in another. Polynomial regression buys curvature by adding powers of the input as columns. Here is the prediction, the input feature, the degree, and the parameters — one per power, being the intercept. Each is treated as its own feature, so the design matrix from Matrix Form gains columns and the Normal Equation recovers unchanged. The catch: past the training range the highest power dominates and predictions diverge, so extrapolation is unusable.
A degree- polynomial model is still linear in the parameters even though it is nonlinear in . The transformation maps the input to a -dimensional feature space where ordinary linear regression applies. The number of parameters grows as for a single feature, but as for features — for and , that is 286 features.
Fit ŷ = w₀ + w₁x + w₂x² to data and get w=[3, 2, -0.5]. Predict at x=4.
Feature Transformation
Powers of are only one option; the general move is to keep the model linear and put the flexibility into a fixed map applied before fitting. takes a -dimensional input row and returns a -dimensional one with ; the estimator learns one weight per transformed column, so a curve in -space is a flat hyperplane in -space. Nothing forces to be polynomial — and are valid too. The failure mode is scale: gives , which stalls gradient descent and ill-conditions , so apply Feature Scaling: Standardization after expanding, and apply the identical at test time.
The mapping with lifts the data into a higher-dimensional space where linear separation (or linear regression) becomes possible. For polynomial features, . The Weierstrass approximation theorem guarantees that any continuous function on a closed interval can be approximated arbitrarily well by a polynomial of sufficient degree — so polynomial regression is a universal approximator, given enough degree.
x=3, degree=3. What's the transformed feature vector?
Interaction Terms
With two inputs and no product term the model is additive: is a flat plane, and the return on is whatever does. Adding the column with weight tilts that plane, because the marginal effect becomes — a slope that is itself a function of the other feature. Positive means the features reinforce each other, negative means one saturates the other. The danger is Multicollinearity: the product column correlates strongly with and unless both are centered, inflating the weight variances. Centering before forming the product largely removes it.
For two features, the product captures their joint effect. The full degree-2 expansion of is — 6 terms. In general, degree- polynomial features for inputs produce terms. The interaction means the effect of on depends on the value of : . This is how polynomial regression captures nonlinear relationships between features.
Ad clicks model: w₃=0.5 for impressions×targeting_score. What does this mean?
Degree Selection
Nothing picks for you: it is the largest lever on how the model behaves. Because a degree- model contains every degree- model — set the top weight to zero — training Mean Squared Error only falls as rises, so it is worthless here. Validation error, measured on rows the fit never saw, traces a U instead: high at where the model is too rigid to follow the curve (underfitting), lowest near the data's true complexity, then climbing as spare powers fit noise (overfitting). The widening gap between the two curves is the signal. Degrees 2–3 usually suffice. Chapter 4 develops this machinery properly.
Higher degree polynomials have more parameters and can fit training data more closely (lower training MSE), but risk overfitting — the model memorizes noise rather than learning the underlying pattern. A degree- polynomial can perfectly interpolate points, but the interpolant typically oscillates wildly between points (Runge's phenomenon). Cross-validation estimates the test error for each degree: split data into folds, train on , evaluate on the held-out fold, and pick the degree with the lowest average validation error.
CV scores: degree 1=70%, degree 3=85%, degree 10=60%. Which degree?
Curse of Dimensionality
With one input, degree 5 costs six columns and nobody notices. With many inputs the same degree is ruinous. A degree- expansion of features produces columns — one per multiset of at most factors drawn from the features. For fixed that grows like , so each extra degree multiplies the column count by roughly . Once approaches the sample count , becomes singular, the Normal Equation has no unique solution, and weight variances explode well before that. The rule of thumb is 10–20 rows per parameter. The fixes are interaction_only=True, feature selection, or the ridge penalty from the next topic.
The number of polynomial features grows combinatorially: for input features and degree , there are terms. With and , that is 1,771 features — many more than the typical dataset has samples. The model needs enough data to estimate each parameter reliably; as a rule of thumb, you need at least 10-20 samples per parameter. Regularization or feature selection becomes essential when the feature count approaches or exceeds the sample count.
100 features, degree=2 polynomial with interactions. How many terms?
Sklearn PolynomialFeatures
Writing the expansion by hand is tedious and easy to get wrong between training and inference: a different column order at predict time pairs each weight with the wrong feature. PolynomialFeatures is a transformer — fit records the column layout for the given degree and transform replays it on later rows. Two arguments matter. include_bias=False drops the leading column of ones, which you want when the estimator fits its own intercept, since two constant columns make singular. interaction_only=True keeps cross-terms like but drops pure powers, blunting the Curse of Dimensionality. Chain it inside a Pipeline with Feature Scaling: Standardization so the scaler sees the expanded columns.
PolynomialFeatures(degree=d, interaction_only=False, include_bias=True) generates the full polynomial expansion. Setting interaction_only=True excludes pure powers like , keeping only cross-terms like . The include_bias parameter controls whether the constant column of ones is included. The transform is typically chained with StandardScaler (polynomial features can have wildly different scales: ranges 0-10 but ranges 0-1000) and then LinearRegression or Ridge in a pipeline.
PolynomialFeatures(degree=2) on [x₁, x₂]. What features are created?
Code Examples
Polynomial regression IS linear regression
import numpy as np
from sklearn.linear_model import LinearRegression
from sklearn.preprocessing import PolynomialFeatures
rng = np.random.default_rng(0)
x = rng.uniform(-2, 2, 60).reshape(-1, 1)
y = 1.0 + 2.0 * x.ravel() - 0.5 * x.ravel() ** 3 + rng.normal(0, 0.1, 60)
# Change the FEATURES, not the model: one column per power of x
Phi = PolynomialFeatures(degree=3, include_bias=True).fit_transform(x)
print("design matrix shape:", Phi.shape)
# Route 1: the normal equation from Multiple Regression, applied unchanged
w_normal = np.linalg.solve(Phi.T @ Phi, Phi.T @ y)
# Route 2: ordinary linear regression on the same expanded columns
w_sklearn = LinearRegression(fit_intercept=False).fit(Phi, y).coef_
print("normal equation :", np.round(w_normal, 3))
print("LinearRegression :", np.round(w_sklearn, 3))
print("max abs difference:", float(np.abs(w_normal - w_sklearn).max()))
print("true coefficients : [1.0, 2.0, 0.0, -0.5]")Both routes print the identical weight vector `[1.031, 2.011, -0.017, -0.506]`, agreeing to about `6e-15` — the normal equation and `LinearRegression` are solving the same problem on the same 60×4 design matrix. Compare against the true `[1.0, 2.0, 0.0, -0.5]`: the cubic is recovered, and the $x^2$ weight lands at −0.017, near the zero it should be. Nothing about the solver changed. The curve came entirely from the extra columns, which is what 'linear in the parameters' means in practice.
How fast the column count explodes
import numpy as np
from math import comb
from sklearn.preprocessing import PolynomialFeatures
print("features d=2 d=3 d=4")
for p in (2, 5, 10, 50, 100):
print(f"{p:8d} {comb(p + 2, 2):6d} {comb(p + 3, 3):8d} {comb(p + 4, 4):9d}")
n_rows, n_feats = 30, 10
X = np.ones((n_rows, n_feats))
for d in (1, 2, 3, 4):
cols = PolynomialFeatures(degree=d).fit_transform(X).shape[1]
verdict = "solvable" if cols < n_rows else "SINGULAR: more columns than rows"
print(f"degree {d}: {n_feats} features -> {cols:6d} columns, {n_rows} rows ({verdict})")The printed grid is $\binom{p+d}{d}$ evaluated directly: 10 features give 66 columns at degree 2, 286 at degree 3 and 1001 at degree 4, while 100 features give 5151, then 176851, then 4598126. The column counts sklearn actually returns match the formula exactly. The second block is the practical consequence — with 30 training rows, degree 2 over only 10 features already yields 66 columns, so $\mathbf{X}^T\mathbf{X}$ is singular long before the degree looks large.
Theory Exercise
Problem:
Your data follows a parabolic pattern but linear regression gives poor results. How do you fix this using polynomial features?
Hints:
- What degree polynomial fits a parabola?
- How do you create the polynomial features?
- What about overfitting risk?
Coding Exercise
Problem:
Generate noisy data from a cubic function, then sweep PolynomialFeatures degree from 1 to 10 inside a pipeline with LinearRegression. Track test MSE on a held-out split and identify the U-shaped curve where mid-range degrees generalize best while high degrees overfit.
Hints:
- Use make_pipeline(PolynomialFeatures(d), LinearRegression()) so the design matrix is rebuilt per degree.
- Split with train_test_split(random_state=0) and evaluate mean_squared_error only on the test set.
- Low degrees underfit (high test MSE), the minimum sits near the true degree, and very high degrees rise again (overfitting / curse of dimensionality).
Related Problems on PixelBank
A model that scores 0.98 on its training data and 0.41 on data it has never seen has not learned anything useful, and the coefficients are usually the giveaway: instead of the modest numbers you expected, you find weights in the thousands with alternating signs. You have already met two routes to that failure. Multicollinearity, in Multiple Regression, left near-singular, so the Normal Equation could trade an enormous against an enormous at almost no cost in error. Polynomial Features at high degree gave the curve enough freedom to thread every training point and oscillate wildly between them. Both are the same disease — too much freedom, spent on noise — and regularization is the shared cure: keep minimising squared error, but charge the model for the size of its coefficients.
The topic builds that idea in order. Ridge comes first: its squared penalty has a closed form and repairs the singular matrix outright. Lasso follows with the absolute-value penalty, then the geometric and subgradient argument for why only Lasso reaches exact zeros. Elastic Net blends the two, decides how hard either one pulls, and the final concept explains why none of it works on unstandardized features.
Definition
Regularization fits a model by minimising a penalised objective rather than the error alone, where measures the size of the coefficient vector and sets how heavily that size counts against the fit. Ridge takes , Lasso takes , and Elastic Net mixes both — each trading a little bias for a large cut in variance.
In this topic
Ridge Regression (L2)
Ordinary least squares cares about error, not about the size of a coefficient, so with two nearly collinear features it happily pairs a huge against a huge that almost cancel — the multicollinearity failure from Multiple Regression. Ridge attaches a price tag. is the objective minimised, holds the coefficients, MSE is the mean squared error, and is the penalty strength. Because the cost is quadratic, a weight of 10 costs a hundred times a weight of 1, so the optimiser prefers many small weights to one large one. Ridge shrinks every coefficient but zeroes none.
Ridge adds a penalty to the loss, giving . The closed-form solution becomes . Adding shifts all eigenvalues of by , guaranteeing invertibility even with multicollinearity. Geometrically, Ridge constrains to lie inside a hypersphere ; the constraint surface is smooth, so the solution smoothly shrinks all weights toward zero without eliminating any.
Weights w=[10, -8, 5], λ=0.1. What's the L2 penalty?
Lasso Regression (L1)
Ridge shrinks but never eliminates: with a thousand candidate features you still carry a thousand nonzero coefficients and no idea which matter. Lasso penalises , the L1 norm, where is feature 's coefficient, the feature count, and the strength. Absolute value has a kink at zero and no derivative there, so there is no Normal-Equation-style closed form; solvers use coordinate descent, soft-thresholding one coefficient at a time. The payoff is exact zeros — feature selection done by the fit itself, making Feature Importance from Multiple Regression automatic. Caveats: among correlated features Lasso keeps one arbitrarily, and when it selects at most .
Lasso uses , yielding . Unlike Ridge, L1 has no closed-form solution due to the non-differentiable absolute value. The constraint region is a diamond (cross-polytope) , which has corners on the coordinate axes. The MSE contours are more likely to first touch the constraint at a corner, where one or more weights are exactly zero — this is why Lasso performs automatic feature selection.
Before Lasso: w=[0.5, 0.01, 0.8, 0.02]. After Lasso: w=[0.4, 0, 0.7, 0]. What happened?
Why L1 Produces Sparsity
Both penalties shrink, so why does only one land on exact zeros? Reframe the fit as constrained optimisation: minimise MSE subject to (L1) or (L2), the budget tightening as grows. In two dimensions the L1 region is a diamond with corners on the axes; the L2 region is a circle with none. The MSE contours are ellipses centred on the OLS solution from Simple Linear Regression, expanding until they touch the region. A sharp corner catches an expanding ellipse from many directions, and a corner is exactly where a coefficient is zero. A circle touches an axis only by chance.
For a single weight, the L1 subgradient at is the whole interval . If there, zero is already optimal and the weight stays pinned — the penalty overwhelms the data signal. The asymmetry lies in how the two penalties behave near the origin: L1's derivative is , a constant that does not fade however small becomes, so it keeps pushing all the way in; L2's derivative is , which shrinks in proportion to and vanishes at zero, so it approaches without ever arriving. Formally, the L1 proximal operator is soft-thresholding, , which clips small weights to exactly zero; L2's is the rescaling , which never does.
In 2D: L1 constraint is |w₁|+|w₂|≤c. Why does solution hit corners?
Elastic Net
Lasso's arbitrary choice among correlated features is a real problem: with two near-duplicate sensors, resampling noise picks the survivor, and a report built on it is not reproducible. Elastic Net charges both. sets the overall strength and the mixing parameter divides it — is pure Lasso, is pure Ridge, in between both. The L1 part zeroes weak features; the L2 part supplies a grouping effect that makes correlated features share weight instead of compete, so a group enters or leaves together. The costs: a second hyperparameter, and a naming trap — scikit-learn calls the strength alpha and this ratio l1_ratio.
Elastic Net combines both penalties: , controlled by a mixing ratio where the L1 ratio is and L2 ratio is . This addresses Lasso's limitation with correlated features: pure L1 arbitrarily selects one from a group of correlated features, while Elastic Net tends to include or exclude the group together. The L2 term also makes the optimization strictly convex, ensuring a unique solution even when .
Two highly correlated features. Lasso picks one randomly. How does Elastic Net help?
Regularization Strength (λ)
Every formula above hinges on one number you supply. multiplies the penalty — the exchange rate between fitting the data and keeping coefficients small. At the penalty vanishes and you recover ordinary least squares — minimum bias, maximum variance, free to overfit. As every coefficient is crushed to zero and the model predicts the training mean — zero variance, maximum bias. Useful values sit between, and you do not pick one by eye: sweep a log-spaced grid and keep the best cross-validated error, as RidgeCV and LassoCV do. Judging on training error always picks zero, as in Degree Selection.
The hyperparameter controls the bias-variance tradeoff. As , the solution approaches unregularized OLS (low bias, high variance). As , all weights shrink to zero and the model predicts (high bias, zero variance). The optimal minimizes the expected test error, which is estimated via cross-validation. A common approach is to search over a logarithmic grid: , since the effect of is multiplicative rather than additive.
λ=0: train=90%, test=60%. λ=10: train=75%, test=80%. λ=1000: train=60%, test=60%. Best λ?
Standardize Before Regularization
The penalty is computed on raw coefficient values, which makes it scale-dependent, unlike plain OLS. Halve a feature's units and its coefficient must double for the same prediction; the fit is unchanged, but the L2 charge on it quadruples. Record a length in kilometres, not millimetres, and its coefficient grows a millionfold, so its L2 penalty grows by — and the model effectively stops using it. The fix is the standardization from Multiple Regression: subtract each column's mean and divide by its standard deviation, so one means the same thing everywhere. One exception: the intercept goes unpenalised, since shrinking it only drags predictions off the data's centre.
Regularization penalises coefficient magnitude, but magnitude is a function of the units the feature was recorded in. Scaling a column by scales its fitted coefficient by , which changes the L1 charge by and the L2 charge by : the same length in kilometres rather than metres carries a coefficient a thousand times larger, and so a squared penalty a million times larger, for identical predictions. Standardizing to zero mean and unit variance removes the dependence, so acts on each feature's effect per standard deviation — a quantity comparable across columns — rather than on an artefact of the measuring instrument. The intercept stays out of the penalty: it only shifts the prediction surface up or down, adds no flexibility that could chase noise, and penalising it would bias predictions toward zero instead of toward .
Feature A: range [0,1]. Feature B: range [0,1000]. Without scaling, which gets penalized more?
Code Examples
Ridge makes a singular XᵀX invertible
import numpy as np
rng = np.random.default_rng(0)
x1 = rng.normal(size=50)
x2 = x1.copy() # a perfectly collinear duplicate feature
X = np.column_stack([x1, x2])
y = 3 * x1 + rng.normal(scale=0.1, size=50)
XtX = X.T @ X
print("rank(X^T X) =", np.linalg.matrix_rank(XtX), "out of", XtX.shape[0])
for lam in [0.0, 1.0, 10.0]:
A = XtX + lam * np.eye(2)
try:
w = np.linalg.solve(A, X.T @ y)
print(f"lambda={lam:5}: cond={np.linalg.cond(A):9.2e} w={np.round(w, 3)}")
except np.linalg.LinAlgError as err:
print(f"lambda={lam:5}: cond={np.linalg.cond(A):9.2e} FAILED ({err})")The two columns are identical, so XᵀX has rank 1 out of 2 and an infinite condition number — the Normal Equation has no unique solution. numpy does not necessarily complain: depending on the LAPACK build it either raises 'Singular matrix' or silently returns one arbitrary point off the infinite solution line, here the lopsided [0.989, 2.0], even though the two features are interchangeable and only their sum, about 3, is pinned down. Adding λI fixes both problems: the condition number collapses to 85.7 at λ = 1 and the answer becomes the symmetric [1.477, 1.477], splitting the effect evenly between the duplicates. Raising λ to 10 shrinks the pair further, to [1.337, 1.337].
Why unstandardized features break the penalty
import numpy as np
from sklearn.linear_model import Ridge
from sklearn.preprocessing import StandardScaler
rng = np.random.default_rng(0)
a = rng.normal(size=200) # feature A, recorded in metres
b = rng.normal(size=200) # feature B, identical real effect
y = 5 * a + 5 * b + rng.normal(scale=0.5, size=200)
X_raw = np.column_stack([a, b * 1000]) # but B was recorded in millimetres
raw = Ridge(alpha=100.0).fit(X_raw, y)
print("raw coefs :", np.round(raw.coef_, 5))
print("raw effect per SD:", np.round(raw.coef_ * X_raw.std(axis=0), 3))
std = Ridge(alpha=100.0).fit(StandardScaler().fit_transform(X_raw), y)
print("std effect per SD:", np.round(std.coef_, 3))Both features have the same true coefficient of 5, but B was recorded in units a thousand times smaller, so its fitted coefficient comes back a thousand times smaller (0.00492 against 3.26114) and its L2 charge a million times smaller. Compare the last two printed lines instead: both report effect per standard deviation, so they are directly comparable. On raw data A is shrunk from 5 down to 3.135 while B sails through almost untouched at 5.046 — the penalty landed on one feature and not the other purely because of units. After StandardScaler the two come back at 3.148 and 3.365, shrunk by nearly the same amount, which is what a single λ is supposed to mean.
Theory Exercise
Problem:
You have 1000 features but suspect only ~50 are relevant. Which regularization method should you use and why?
Hints:
- Which method performs feature selection?
- What happens to irrelevant feature weights?
- Consider model interpretability
Coding Exercise
Problem:
On a standardized make_regression dataset with many irrelevant features, fit Lasso (L1) and Ridge (L2) across increasing alpha values. Count how many coefficients each drives to (near-)zero to show that Lasso induces sparsity while Ridge only shrinks coefficients without eliminating them.
Hints:
- StandardScaler the features first — regularization penalizes coefficient size, so features must be on the same scale.
- Loop alpha over something like [0.1, 1, 5, 20]; fit Lasso(alpha=a, max_iter=10000) and Ridge(alpha=a).
- Count np.sum(np.abs(coef_) > 1e-8): Lasso's count drops as alpha grows, Ridge's stays at the full feature count.
Related Problems on PixelBank
Every topic in this chapter has handed you a number and left the hard question unasked. The closed-form solution gives a slope, the normal equation gives a weight vector, and Regularization gives you a chosen by cross-validation — but each is trustworthy only if the data behaves the way least squares quietly assumes it does. That is the failure this topic addresses: a model can post an excellent and still be wrong in a way no accuracy metric will reveal, with coefficients that flip sign on a resampled dataset, confidence intervals half the width they should be, and one mistyped row steering the fit. Everything here builds on the residual from Simple Linear Regression, because almost every diagnostic is a way of looking at those residuals. We take the four assumptions about the errors in the order you should check them — linearity, independence, constant variance, normality — then the single condition on the features, and finally the two tools that operationalise all five: systematic residual analysis and influence diagnostics. For each you get what it claims, the plot or statistic that catches a violation, and the fix.
Definition
Regression diagnostics are the checks that decide whether the conditions making ordinary least squares unbiased, efficient, and valid for inference actually hold in your data: a linear conditional mean , errors that are independent of one another, a constant error variance, approximately normal errors for inference alone, and feature columns carrying no near-linear dependencies. Each condition pairs with a diagnostic plot or statistic, and with a remedy.
In this topic
Linearity Assumption
Least squares does not assume the data lie on a line; it assumes the conditional mean does, so the average at any input equals . When the truth curves, the error lives in the specification rather than the noise, so more rows only estimate the wrong line more precisely. Detect it with the residual-versus-fitted plot, built from the residual of Simple Linear Regression: healthy is a formless band around zero, while a smile- or frown-shaped band means the fit runs high through the middle and low at both ends. The remedy is polynomial or interaction terms from Polynomial Features, or a genuinely nonlinear model.
Linear regression assumes — the conditional expectation is linear in . If the true relationship is where is nonlinear, the linear model suffers from specification bias: . This bias does not decrease with more data — 10 million points on a parabola still won't be well-fit by a line. Residual vs. fitted plots reveal this: if residuals show a systematic curved pattern, the linearity assumption is violated.
Residual plot shows U-shaped curve (negative at low/high fitted, positive in middle). What's wrong?
Independence
Every row must contribute fresh information: the error on observation should tell you nothing about the error on observation . Time series and clustered data break this routinely — yesterday's under-prediction of a return usually means today's is low too, and pupils in one classroom share a teacher. The damage is quiet: weights from the normal equation stay unbiased, but correlated rows carry the information of far fewer independent ones, so standard errors come out too small and p-values look better than they are. Detect it by plotting residuals in collection order, hunting for long runs of one sign, or with the Durbin-Watson statistic. Fix it with lag features, a time-series model, or Newey-West standard errors.
The errors must be independent: for . Violation is common in time series where correlates with (autocorrelation). The Durbin-Watson statistic tests for first-order autocorrelation: means no correlation, means positive correlation, means negative. When independence is violated, OLS weights are still unbiased but standard errors are wrong, making hypothesis tests unreliable.
Stock returns model. Durbin-Watson = 0.5 (range 0-4, 2=no autocorrelation). Problem?
Homoscedasticity
Homoscedasticity says every error is drawn from a distribution with the same spread, so no row deserves more trust than another. The squared-error loss from Simple Linear Regression bakes that in: it charges the same price for a thousand-unit miss on a cheap house as on a mansion. When spread grows with the fitted value, the residual-versus-fitted plot widens into a funnel; the scale-location plot of against shows it more clearly by discarding the sign, and the Breusch-Pagan test puts a p-value on it. Estimates stay unbiased but stop being minimum-variance, and the standard errors are wrong. Fixes in ascending order of commitment: model , report White robust standard errors, or run weighted least squares with .
Constant error variance for all is called homoscedasticity. Heteroscedasticity (non-constant variance, e.g., prediction error growing with income) makes OLS inefficient — it gives equal weight to high-variance and low-variance observations. The Breusch-Pagan test regresses squared residuals on predictors: a significant fit indicates heteroscedasticity. Weighted least squares (WLS) fixes this by weighting each observation inversely proportional to its estimated variance: .
Income prediction from a household survey. For families earning under 40,000 dollars a year the residuals sit within about 1,000 dollars; for families above 500,000 they scatter by 50,000 dollars or more. Which assumption is this, and what breaks?
Normality of Residuals
This is the most over-stated assumption in regression, so be precise about what it buys. The Gauss-Markov result making least squares the best linear unbiased estimator never mentions normality; errors need only zero mean, constant variance, and no correlation. Normality buys exactly one thing: exact small-sample inference — the and distributions behind p-values, confidence intervals on the weights, and prediction intervals. With a few hundred rows the central limit theorem makes the sampling distribution of roughly normal anyway, so this matters mainly when is small. Check it with a Q-Q plot of residual quantiles against normal quantiles: a straight diagonal is clean, an S-bend at the ends means heavy tails, a bow means skew.
OLS does not require normal errors for unbiased weights — it only requires normality for valid confidence intervals and hypothesis tests (t-tests, F-tests). The central limit theorem helps: with large , the sampling distribution of is approximately normal regardless of error distribution. Q-Q plots compare residual quantiles against theoretical normal quantiles; deviations at the tails indicate heavy-tailed or skewed error distributions. The Shapiro-Wilk test formally tests for normality but is overly sensitive with large samples.
Q-Q plot: points follow diagonal line mostly, but tails curve up/down. Concern?
No Multicollinearity
Multiple Regression introduced multicollinearity as a nuisance; the variance inflation factor measures it. Regress feature on all the others, call that fit , and set — the factor by which the variance of is multiplied relative to an uncorrelated design. So means , nine tenths of that column reconstructable from the others, and a standard error times larger than needed. Convention flags as worth a look and as a problem. Predictions stay fine; coefficients turn unstable and can flip sign on a resample. Drop the redundant column, use principal components, or use Ridge, whose term buys stability for a little bias.
Perfect multicollinearity ( exactly) makes singular and OLS unsolvable. Near-perfect multicollinearity makes the inverse numerically unstable with inflated diagonal entries. The condition number (ratio of largest to smallest singular value) measures this: signals problematic multicollinearity. Solutions include dropping redundant features, PCA to create orthogonal components, or Ridge regression which adds to stabilize the inversion.
Height in cm and height in inches both in model. VIF = 500 for both. What happens?
Residual Analysis
No single number catches misspecification — a model can post an of 0.95 while violating every assumption above — so diagnostics mean viewing the residual from several angles. Residuals against fitted values tests linearity and constant variance together; the scale-location plot of against isolates variance by discarding the sign; the Q-Q plot tests normality; residuals against leverage exposes influence. Plot residuals against each individual feature too, since that localises a fault to a specific column, and against row order when the data carries time structure. Standardize first, dividing by , because raw residuals at high-leverage rows are artificially small.
A well-specified model produces residuals that look like white noise: zero mean, constant variance, no patterns. The residual vs. fitted plot checks linearity and homoscedasticity simultaneously. The scale-location plot ( vs. ) highlights variance changes. The residual vs. leverage plot identifies points that are both outliers (large residual) and influential (high leverage ). Cook's distance combines both into a single influence measure.
Residuals vs X₁: random scatter. Residuals vs X₂: clear negative slope. What does this tell us?
Influential Points
Three distinct things get conflated here. An outlier has an unusual and so a large residual, but if its sits mid-data it barely moves the line — it mostly inflates . A high-leverage point has an unusual ; leverage measures how far row sits from the centre of the feature cloud, sums across rows to the parameter count , and so averages , making anything beyond far out. High leverage alone is harmless when the point lies on the trend. A point is influential only when it is both unusual in and off the pattern — precisely the product Cook's distance forms, with as a screen and as a serious flag.
Leverage measures how far observation is from the center of the feature space. High-leverage points (large ) can disproportionately determine the fitted line. An outlier is a point with a large residual; an influential point is one whose removal substantially changes the fit. Cook's distance or flags influential observations. The DFFITS statistic measures the change in the fitted value for observation when it is deleted: .
One point has Cook's D = 2.5. Without it: slope=5. With it: slope=2. What to do?
Code Examples
Reading a residual plot without drawing one
import numpy as np
from sklearn.linear_model import LinearRegression
rng = np.random.default_rng(0)
x = np.linspace(-3, 3, 300)
sigma = 0.3 + 0.35 * (x + 3) # error spread grows left to right
y = 2 + 1.5 * x - 0.8 * x**2 + rng.normal(0, sigma) # curved mean + funnel noise
thirds = np.array_split(np.arange(len(x)), 3) # low / mid / high x
def diagnose(X, label):
resid = y - LinearRegression().fit(X, y).predict(X)
means = " ".join(f"{resid[t].mean():+6.2f}" for t in thirds)
sds = " ".join(f"{resid[t].std():6.2f}" for t in thirds)
print(f"{label:10s} mean residual (low/mid/high x): {means}")
print(f"{label:10s} sd residual (low/mid/high x): {sds}")
diagnose(x.reshape(-1, 1), "linear")
diagnose(np.column_stack([x, x**2]), "quadratic")Splitting residuals into terciles turns the two classic plot shapes into numbers you can check. Under the linear fit the tercile means are -1.10, +2.18, -1.07 — that is the U shape, and proof the linearity assumption fails. Adding an x-squared column flattens them to +0.02, -0.06, +0.04. Now read the standard deviations of that same quadratic fit: 0.68, 1.31, 2.27, still climbing steadily. Fixing linearity did nothing for the funnel, because heteroscedasticity is a separate violation needing a separate remedy.
Outlier, high leverage, influential: three different things
import numpy as np
rng = np.random.default_rng(1)
n = 40
x = rng.normal(size=n)
y = 1 + 2 * x + rng.normal(0, 0.3, n) # true slope 2
def add(px, py, label): # refit with one extra row
X = np.column_stack([np.ones(n + 1), np.append(x, px)])
yy = np.append(y, py)
b = np.linalg.lstsq(X, yy, rcond=None)[0]
e = yy - X @ b
h = np.einsum("ij,jk,ik->i", X, np.linalg.inv(X.T @ X), X)
mse = (e ** 2).sum() / (n - 1)
d = e ** 2 / (2 * mse) * h / (1 - h) ** 2 # Cook's distance, p = 2
print(f"{label:26s} leverage {h[-1]:.2f} Cook's D {d[-1]:6.2f} slope {b[1]:.2f}")
print("clean slope %.2f" % np.linalg.lstsq(np.column_stack([np.ones(n), x]), y, rcond=None)[0][1])
add(0.0, 8.0, "outlier: odd y, mid x")
add(5.0, 11.0, "high leverage: on trend")
add(5.0, 0.0, "influential: odd x + off")One clean dataset with slope 2.04, then the same data plus one added row of each kind. The outlier at the centre of the x range gets leverage 0.02 and Cook's D 0.47, and leaves the slope at 2.04. The high-leverage row, far out in x but sitting on the trend, gets leverage 0.43 yet Cook's D of only 0.12, and the slope holds at 2.03. Only the third row, far out in x AND off the line, is influential: same leverage 0.43, but Cook's D 14.19 and the slope collapses to 1.12. Leverage alone predicts nothing; leverage times residual does.
Theory Exercise
Problem:
Your residual plot shows a funnel shape (residuals spread out as fitted values increase). What assumption is violated and how do you fix it?
Hints:
- What does constant variance look like in residuals?
- A funnel shape suggests variance changes
- Consider transformations
Coding Exercise
Problem:
Fit a linear regression on the diabetes dataset, compute residuals, and run two diagnostic checks: a heteroscedasticity check via the correlation between fitted values and |residuals|, and a multicollinearity check via the Variance Inflation Factor (VIF) of each feature.
Hints:
- Residuals = y - model.predict(X); for OLS their mean should be ~0.
- Heteroscedasticity: a nonzero corr between fitted values and absolute residuals suggests non-constant error variance.
- VIF for feature i = 1/(1 - R²) from regressing feature i on all other features; VIF > 10 flags multicollinearity.