Multiple Linear Regression — Expert Pack
Trainer: Pushpjeet Cholkar · Builds on the Beginner pack · Every number in the examples and labs was produced by code that was actually run.
Notation used throughout:
n= number of rows (observations),p= number of features (excluding the intercept),X= n × (p+1) design matrix,y= n × 1 target vector,β= (p+1) × 1 coefficient vector,ᵀ= transpose,‖v‖²= sum of squared entries ofv. A hat (β̂,ŷ) means "estimated from data".
1. Recap of simple linear regression (one paragraph)
Simple linear regression models one numeric target y as y = β₀ + β₁x + ε, where ε is an error term. Ordinary least squares (OLS) chooses β₀, β₁ to minimise the sum of squared residuals Σ(yᵢ − β₀ − β₁xᵢ)², which gives β̂₁ = Σ(xᵢ − x̄)(yᵢ − ȳ) / Σ(xᵢ − x̄)² and β̂₀ = ȳ − β̂₁x̄. Fit is summarised with R² (fraction of variance of y explained) and RMSE (typical error size in units of y). Everything below generalises this to many features at once.
2. The multiple linear regression model
yᵢ = β₀ + β₁·xᵢ₁ + β₂·xᵢ₂ + … + β_p·xᵢp + εᵢ , i = 1 … n
β₀is the intercept: the expected value ofywhen every feature equals 0.β_jis the partial slope of featurej: the change in the expected value ofyfor a one-unit increase inx_j, holding all other features in the model fixed.εᵢis the unobserved error: everything aboutyᵢthat the features do not capture.
"Linear" means linear in the parameters β, not necessarily linear in the raw inputs. y = β₀ + β₁x + β₂x² + β₃·log(z) + ε is still a linear regression model.
2.1 Matrix form and the design matrix
Stack all n equations:
y = Xβ + ε
⎡ y₁ ⎤ ⎡ 1 x₁₁ x₁₂ … x₁p ⎤ ⎡ β₀ ⎤ ⎡ ε₁ ⎤
y = ⎢ y₂ ⎥ X = ⎢ 1 x₂₁ x₂₂ … x₂p ⎥ β = ⎢ β₁ ⎥ ε = ⎢ ε₂ ⎥
⎢ ⋮ ⎥ ⎢ ⋮ ⋮ ⋮ ⋮ ⎥ ⎢ ⋮ ⎥ ⎢ ⋮ ⎥
⎣ yₙ ⎦ ⎣ 1 xₙ₁ xₙ₂ … xₙp ⎦ ⎣ β_p⎦ ⎣ εₙ ⎦
n×1 n×(p+1) (p+1)×1 n×1
The design matrix X has one row per observation and one column per parameter. The first column is all ones: the intercept column. Multiplying it by β₀ adds the same constant to every prediction. Libraries differ in whether they add this column for you:
| Tool | Adds intercept automatically? |
|---|---|
sklearn.linear_model.LinearRegression |
Yes (fit_intercept=True by default) |
statsmodels.api.OLS |
No — call sm.add_constant(X) |
statsmodels.formula.api.ols("y ~ x1 + x2", df) |
Yes |
np.linalg.lstsq(X, y) |
No — you build the column yourself |
3. OLS derivation: from ‖y − Xβ‖² to the normal equation
The residual vector for a candidate β is e(β) = y − Xβ. OLS minimises the residual sum of squares:
S(β) = ‖y − Xβ‖² = (y − Xβ)ᵀ(y − Xβ)
= yᵀy − 2βᵀXᵀy + βᵀXᵀXβ
(The two cross terms yᵀXβ and βᵀXᵀy are equal scalars, so they combine into 2βᵀXᵀy.)
Take the gradient with respect to β using ∂(βᵀa)/∂β = a and ∂(βᵀAβ)/∂β = 2Aβ for symmetric A:
∇S(β) = −2Xᵀy + 2XᵀXβ
Set the gradient to zero:
XᵀX β̂ = Xᵀy ← the normal equations (p+1 linear equations)
β̂ = (XᵀX)⁻¹ Xᵀy ← valid when XᵀX is invertible
Why this is a minimum, not a maximum: the Hessian is ∇²S = 2XᵀX, which is positive semi-definite because vᵀXᵀXv = ‖Xv‖² ≥ 0 for every v. S is therefore convex. If X has full column rank (rank(X) = p+1), XᵀX is positive definite and the minimiser is unique.
When XᵀX is not invertible: if one column is an exact linear combination of others (for example, all one-hot dummies plus an intercept, or a feature duplicated in two units), or if p+1 > n, then rank(X) < p+1, XᵀX is singular, and infinitely many β give the same minimum S. The usual choices are to remove the redundant column, use the minimum-norm solution from the pseudo-inverse, or regularise (ridge).
Two consequences of the normal equations (true for every OLS fit, not assumptions):
Xᵀe = 0wheree = y − Xβ̂: residuals are orthogonal to every column ofX.- If
Xcontains an intercept column, the first row ofXᵀe = 0givesΣeᵢ = 0: residuals sum to zero, and the fitted plane passes through the point of means(x̄₁, …, x̄_p, ȳ).
4. Geometric view: projection and the hat matrix
Think of y as a vector in n-dimensional space. Every possible prediction Xβ lies in the column space of X, a (p+1)-dimensional subspace. OLS picks the point in that subspace closest to y in Euclidean distance; that point is the orthogonal projection of y onto the column space.
ŷ = Xβ̂ = X(XᵀX)⁻¹Xᵀ y = H y H = X(XᵀX)⁻¹Xᵀ (n × n)
e = y − ŷ = (I − H) y
H is called the hat matrix because it "puts the hat on y". Its properties:
| Property | Statement | Meaning |
|---|---|---|
| Symmetric | Hᵀ = H |
It is an orthogonal (not oblique) projection |
| Idempotent | HH = H |
Projecting twice changes nothing |
| Trace | trace(H) = p + 1 |
Equals the number of fitted parameters |
| Residual maker | (I − H)X = 0 |
Residuals contain no component along any column of X |
| Leverage | hᵢᵢ = i-th diagonal entry, 0 ≤ hᵢᵢ ≤ 1 |
How much yᵢ influences its own fitted value ŷᵢ |
| Average leverage | mean(hᵢᵢ) = (p+1)/n |
Baseline for "high leverage" rules of thumb |
Pythagoras in this geometry gives the variance decomposition (with an intercept in the model):
‖y − ȳ1‖² = ‖ŷ − ȳ1‖² + ‖y − ŷ‖²
SS_tot = SS_reg + SS_res
5. Why practitioners do not compute (XᵀX)⁻¹ explicitly
The formula β̂ = (XᵀX)⁻¹Xᵀy is for derivation and hand calculation. Numerical code avoids forming the inverse for three reasons:
- Condition number squares. The 2-norm condition number satisfies
cond(XᵀX) = cond(X)². Ifcond(X) = 10⁶, thencond(XᵀX) ≈ 10¹². With double precision (about 16 significant digits), roughlylog₁₀(cond)digits can be lost, so formingXᵀXcan destroy most of the accuracy thatXitself still supports. Lab 1 measures this. - Inverting is more work and less stable than solving. Even when you do use the normal equations,
np.linalg.solve(XᵀX, Xᵀy)(Cholesky/LU factorisation) is preferred toinv(XᵀX) @ Xᵀy. - Rank deficiency. A singular
XᵀXhas no inverse; factorisation-based methods still return a sensible answer.
Methods used in practice:
| Method | What it does | Notes |
|---|---|---|
| QR decomposition | Factor X = QR (Q has orthonormal columns, R upper-triangular). Then Rβ̂ = Qᵀy, solved by back-substitution. |
Works on X directly, so the error depends on cond(X), not cond(X)². Standard choice for full-rank problems. |
| SVD | Factor X = UΣVᵀ. Then β̂ = VΣ⁺Uᵀy, where Σ⁺ inverts the non-zero singular values. |
Most robust; handles rank deficiency by discarding tiny singular values. Gives the minimum-norm solution. np.linalg.lstsq uses an SVD-based LAPACK routine (gelsd). |
Moore–Penrose pseudo-inverse X⁺ |
β̂ = X⁺y, with X⁺ = VΣ⁺Uᵀ |
Same answer as SVD route; np.linalg.pinv. Use when you need the matrix itself; otherwise call lstsq. |
| Cholesky on XᵀX | Factor XᵀX = LLᵀ and solve two triangular systems |
Fastest for very tall, well-conditioned X (n ≫ p); inherits the squared condition number. |
| Iterative methods (gradient descent, conjugate gradient, SGD) | Approach the minimum step by step | Used when X is too large for memory or arrives in streams. |
sklearn.linear_model.LinearRegression solves the least-squares problem with SciPy's lstsq (dense input), so it also avoids explicit inversion.
6. Gradient descent for linear regression
Use the mean squared error as the cost function:
J(β) = (1/n)·‖y − Xβ‖²
∇J(β) = (2/n)·Xᵀ(Xβ − y)
Batch gradient descent uses all n rows to compute each gradient:
start with β⁽⁰⁾ (for example zeros)
repeat for t = 0, 1, 2, …:
β⁽ᵗ⁺¹⁾ = β⁽ᵗ⁾ − η · ∇J(β⁽ᵗ⁾)
stop when ‖∇J‖ or the change in J falls below a tolerance, or after a fixed number of epochs
η(eta) is the learning rate (step size).- One epoch is one pass through the full data set. In batch GD, one epoch = one update.
- Stochastic GD (SGD) uses one row per update; mini-batch GD uses a subset (for example 32–1024 rows). They are noisier per step but cheaper per update.
Convergence. J is a convex quadratic with Hessian (2/n)XᵀX. Batch GD with a fixed step converges for any start point if
0 < η < 2 / λ_max( (2/n)·XᵀX ) = 1 / λ_max( (1/n)·XᵀX )
where λ_max is the largest eigenvalue. Larger steps make the iterates oscillate with growing amplitude (divergence). Even with a valid η, the number of iterations needed grows with the condition number κ = λ_max / λ_min of XᵀX: the error shrinks roughly by a factor of (κ − 1)/(κ + 1) per step at the best fixed step size, so a large κ means very slow progress along the flattest direction.
Feature scaling. If one feature is in thousands (square feet) and another in single digits (rooms), XᵀX has eigenvalues of very different sizes, κ is huge, and the largest safe η is tiny. Standardising each feature to mean 0 and standard deviation 1 (z = (x − mean)/sd) makes XᵀX/n close to the correlation matrix, brings κ near 1 when features are weakly correlated, and lets a step like η = 0.1 converge in tens of epochs. Lab 2 measures cond(XᵀX) of about 6.7 × 10⁷ unscaled versus about 1.06 scaled for its synthetic data.
Mapping scaled coefficients back to original units: if w_j is the coefficient on standardised z_j = (x_j − μ_j)/s_j and w₀ the intercept, then
β_j = w_j / s_j β₀ = w₀ − Σ_j w_j·μ_j / s_j
When to choose GD over a closed form: very large n (data does not fit in memory, use mini-batches), streaming data (online updates), or when linear regression is one layer inside a larger model trained by gradient methods. For moderate data that fits in memory, QR/SVD is exact and faster.
7. Interpreting coefficients in multiple regression
β̂_jestimates the change in the expectedyfor a one-unit increase inx_jwhile all other features in the model are held constant. It is a partial effect, conditional on the other included features.- The value of
β̂_jdepends on which other features are in the model. Adding a feature correlated withx_jcan change the size and even the sign ofβ̂_j, because the "held constant" set has changed. - Frisch–Waugh–Lovell (FWL) theorem.
β̂_jfrom the full regression equals the slope from regressing (residuals ofyon the other features) on (residuals ofx_jon the other features). This is the precise meaning of "holding others constant": only the part ofx_jnot linearly explained by the other features is used to estimate its effect. - Coefficients are in units of
yper unit ofx_j, so raw magnitudes are not comparable across features. To compare, standardise features first (coefficient = change inyper one standard deviation ofx_j) and still remember that collinearity makes individual magnitudes unstable. - A regression coefficient is an association measured in observational data. It is a causal effect only under additional assumptions (no omitted confounders, correct functional form, no reverse causality).
- Holding other features constant may be impossible in practice when features are mechanically linked (for example
xandx², or a feature and its interaction term). Then interpret the combined effect, for example∂E[y]/∂x = β₁ + 2β₂x.
8. Categorical variables, one-hot encoding and the dummy variable trap
A categorical feature with k levels (for example region ∈ {east, north, south, west}) cannot enter the model as the codes 1, 2, 3, 4, because that would impose an order and equal spacing that do not exist. Instead create indicator (dummy) variables: d_north = 1 if the row is in north, else 0, and so on.
Dummy variable trap. If the model has an intercept and you include all k dummies, then for every row d_east + d_north + d_south + d_west = 1, which equals the intercept column. The design matrix is exactly collinear, rank(X) < p+1, and XᵀX is singular. In a verified 8-row example (intercept, one numeric feature, all 4 region dummies → 6 columns), rank(X) = 5, and cond(XᵀX) came out about 1.5 × 10¹⁷ (singular to machine precision).
Fix: use k − 1 dummies (drop one level, the reference or baseline level) when an intercept is present: pd.get_dummies(..., drop_first=True) or OneHotEncoder(drop="first"). Alternatively keep all k dummies and remove the intercept.
Interpretation: the coefficient on d_north is the expected difference in y between north and the reference level, holding the other features constant. The intercept is the expected y for the reference level when numeric features are 0.
Notes for practice:
- With regularised models (ridge/lasso), keeping all k dummies is common because the penalty makes the solution unique; with plain OLS and inference, drop one.
- High-cardinality categories (thousands of levels) create very sparse, wide matrices; consider grouping rare levels, target encoding with proper cross-fitting to avoid leakage, or regularisation.
- At prediction time, unseen categories must be handled explicitly (OneHotEncoder(handle_unknown="ignore")).
9. Interaction and polynomial terms (still linear in parameters)
Interaction term: y = β₀ + β₁x₁ + β₂x₂ + β₃·(x₁·x₂) + ε. The effect of x₁ now depends on x₂:
∂E[y]/∂x₁ = β₁ + β₃·x₂
So β₁ alone is the effect of x₁ only when x₂ = 0. Centering x₁ and x₂ before forming the product makes β₁ the effect at the mean of x₂ and usually reduces collinearity between main effects and the product.
Polynomial term: y = β₀ + β₁x + β₂x² + β₃x³ + ε. This is a curved function of x but a linear function of β, so the same OLS machinery (normal equation, QR, inference) applies with columns x, x², x³ in X. Cautions:
- Raw powers are highly correlated (large VIF); center first, or use orthogonal polynomials.
- High degree overfits and behaves badly outside the training range (extrapolation).
- Hierarchy principle: if you keep x² or x₁x₂, keep the lower-order terms x, x₁, x₂.
In scikit-learn, PolynomialFeatures(degree=2, include_bias=False) generates powers and pairwise interactions inside a Pipeline.
10. Gauss–Markov assumptions and BLUE
Assumptions (conditional on X):
| # | Assumption | Formal statement |
|---|---|---|
| GM1 | Linear in parameters | y = Xβ + ε |
| GM2 | Full column rank (no perfect multicollinearity) | rank(X) = p + 1 |
| GM3 | Strict exogeneity (zero conditional mean) | E[ε ∣ X] = 0 |
| GM4 | Spherical errors: homoscedastic and uncorrelated | Var(ε ∣ X) = σ²I |
Gauss–Markov theorem: under GM1–GM4, the OLS estimator β̂ is BLUE, the Best Linear Unbiased Estimator: among all estimators that are linear in y and unbiased, OLS has the smallest variance (for every linear combination cᵀβ̂).
Key facts:
- Unbiasedness needs GM1–GM3 only. E[β̂ | X] = β + (XᵀX)⁻¹XᵀE[ε | X] = β.
- Variance: under GM4, Var(β̂ | X) = σ²(XᵀX)⁻¹. Unbiased estimate of σ²: σ̂² = SS_res / (n − p − 1).
- Normality of errors is not a Gauss–Markov assumption. It is an additional assumption (ε | X ~ N(0, σ²I)) that makes the t and F statistics follow exact t and F distributions in finite samples. With large n, these tests are approximately valid without normality (central limit theorem), provided GM1–GM4 hold.
- If GM4 fails (heteroscedasticity or autocorrelation), OLS is still unbiased but no longer "best" (weighted/generalised least squares can do better), and the classical formula σ²(XᵀX)⁻¹ gives wrong standard errors.
- If GM3 fails (omitted variable correlated with included ones, measurement error in X, reverse causality), OLS is biased and usually inconsistent. No diagnostic on residuals alone can fully detect this.
- "BLUE" does not mean lowest mean squared error overall. A biased estimator such as ridge can have lower MSE (Section 15).
11. Diagnostics
Diagnostics check whether the assumptions behind the estimates and their standard errors are reasonable. Use plots first, then tests. Thresholds below are rules of thumb, not laws.
11.1 Residual plots
- Residuals vs fitted values: should look like a structureless horizontal band around 0. A curve suggests a missing non-linear term or interaction; a funnel shape suggests heteroscedasticity.
- Residuals vs each feature (including features not in the model): pattern indicates mis-specification.
- Residuals vs time / row order: waves or runs indicate autocorrelation.
- Prefer studentised residuals
rᵢ = eᵢ / (σ̂·√(1 − hᵢᵢ))because raw residuals at high-leverage points are naturally smaller (Var(eᵢ) = σ²(1 − hᵢᵢ)).
11.2 Heteroscedasticity and the Breusch–Pagan test
Heteroscedasticity means Var(εᵢ | X) changes across observations. Effect: coefficients stay unbiased, but classical standard errors, t-tests, p-values and confidence intervals are unreliable, and OLS is no longer efficient.
Breusch–Pagan test: regress the squared residuals eᵢ² on the features; the statistic LM = n·R²_aux is approximately χ² with p degrees of freedom under the null of homoscedasticity. Small p-value → evidence of heteroscedasticity. (statsmodels.stats.diagnostic.het_breuschpagan.) The White test adds squares and cross-products to the auxiliary regression.
Remedies: heteroscedasticity-consistent ("robust", "sandwich") standard errors such as HC0–HC3 (fit(cov_type="HC3") in statsmodels), transforming y (for example log), or weighted least squares when the variance structure is known. Lab 3 shows that HC3 changes standard errors but leaves coefficients identical.
11.3 Normality of residuals (Q-Q plot)
A Q-Q (quantile–quantile) plot plots sorted standardised residuals against the theoretical quantiles of a normal distribution. Points on a straight line indicate approximately normal residuals; S-shapes indicate heavy or light tails; a bend at one end indicates skew. Formal tests: Jarque–Bera (uses skewness and kurtosis), Shapiro–Wilk. With large n, these tests flag tiny, practically harmless deviations; with small n they have little power. Normality matters mainly for small-sample inference and for prediction intervals.
11.4 Autocorrelation and the Durbin–Watson statistic
Autocorrelation means errors are correlated across observations, typically in time-ordered data (consecutive days, sequential log lines).
DW = Σ_{t=2..n} (e_t − e_{t−1})² / Σ_{t=1..n} e_t² , DW ≈ 2(1 − ρ̂₁)
ρ̂₁ is the lag-1 autocorrelation of residuals. DW ranges from 0 to 4; about 2 means no lag-1 autocorrelation, values toward 0 mean positive autocorrelation, toward 4 negative. A rough rule of thumb treats values between about 1.5 and 2.5 as unconcerning; the formal test uses tabulated lower/upper bounds that depend on n and p. DW is only meaningful when the row order is meaningful (time). With positive autocorrelation, classical standard errors are usually too small, making effects look more significant than they are. Remedies: Newey–West (HAC) standard errors, adding lagged terms, or time-series models. Breusch–Godfrey tests higher-order autocorrelation.
11.5 Multicollinearity: VIF and condition number
Multicollinearity means some features are strongly linearly related to others. It does not bias OLS coefficients and does not hurt in-sample fit, but it inflates their variances, makes individual coefficients unstable (small data changes cause large swings, possibly sign flips), and makes individual t-tests weak even when the overall F-test is highly significant.
Variance Inflation Factor:
VIF_j = 1 / (1 − R²_j)
R²_j is the R² from regressing x_j on all the other features (with intercept). VIF_j is the factor by which Var(β̂_j) is inflated relative to a design where x_j is uncorrelated with the others; the standard error inflates by √VIF_j. Examples: R²_j = 0.8 → VIF = 5 → SE × 2.24; R²_j = 0.9 → VIF = 10 → SE × 3.16. Common rules of thumb: VIF above 5 deserves attention, VIF above 10 indicates serious collinearity. These cut-offs are conventions; the practical question is whether the inflated standard errors matter for your purpose. Compute VIF on a design that includes the intercept column (statsmodels' variance_inflation_factor expects you to include it; omitting it gives different, misleading values for uncentered features).
Condition number of X: κ(X) = σ_max / σ_min (ratio of largest to smallest singular value). Large values mean that some linear combination of columns is nearly zero. It depends on units, so compute it on standardised (or unit-length) columns when judging collinearity; a commonly cited rule of thumb (Belsley, Kuh and Welsch) treats values above about 30 on scaled columns as indicating moderate-to-strong collinearity. statsmodels reports Cond. No. on the design as given (unscaled, with intercept) and prints a warning when it exceeds 1000; for unscaled data this warning can reflect units rather than collinearity.
Remedies: drop or combine redundant features, collect more varied data, center before forming powers/interactions, use ridge regression, or use dimension reduction (PCA regression, partial least squares). If only prediction matters and the collinearity pattern is stable in future data, you may leave it.
11.6 Influential points: leverage and Cook's distance
- Outlier (in y): a point with a large residual.
- Leverage
hᵢᵢ: depends only onX; large when the row's feature values are far from the centroid of the features. Average leverage is(p+1)/n; rules of thumb flaghᵢᵢ > 2(p+1)/n(or3(p+1)/n). - Influence: how much the fitted model changes when the point is removed. A high-leverage point that lies on the trend has little influence; a high-leverage point with a large residual can pull the whole plane.
- Cook's distance:
Dᵢ = [ eᵢ² / ((p+1)·σ̂²) ] · [ hᵢᵢ / (1 − hᵢᵢ)² ]
It combines residual size and leverage, and equals the scaled change in all fitted values when row i is deleted. Rules of thumb: Dᵢ > 1 is large; Dᵢ > 4/n is worth a look. Related measures: DFFITS, DFBETAS (per-coefficient change).
- Action: investigate (data error? different population? valid extreme case?). Do not delete points only because they are influential; report the analysis with and without them, or use robust regression (Huber loss, HuberRegressor).
12. Inference: standard errors, t-tests, p-values, confidence intervals, F-test
Under GM1–GM4 (and normal errors for exact small-sample results):
Var(β̂) = σ²(XᵀX)⁻¹ estimated by σ̂²(XᵀX)⁻¹
σ̂² = SS_res / (n − p − 1) (residual degrees of freedom = n − p − 1)
SE(β̂_j) = σ̂ · √[ (XᵀX)⁻¹ ]_jj
t-test for one coefficient (H₀: β_j = 0):
t_j = β̂_j / SE(β̂_j) compared with a t distribution with n − p − 1 degrees of freedom
p-value: the probability, assuming H₀ is true and the model assumptions hold, of observing a t statistic at least as extreme as the one computed. It is not the probability that H₀ is true, and a large p-value is not evidence that β_j = 0.
Confidence interval (CI) for β_j at level 1 − α:
β̂_j ± t_{1−α/2, n−p−1} · SE(β̂_j)
Interpretation: the procedure produces intervals that contain the true β_j in (1 − α) of repeated samples, if assumptions hold.
Confidence interval vs prediction interval at a new point x₀ (row vector including the leading 1):
mean response: ŷ₀ ± t · σ̂ · √( x₀(XᵀX)⁻¹x₀ᵀ )
new observation: ŷ₀ ± t · σ̂ · √( 1 + x₀(XᵀX)⁻¹x₀ᵀ )
The prediction interval is always wider because it includes the irreducible error variance σ².
Overall F-test (H₀: β₁ = β₂ = … = β_p = 0, intercept unrestricted):
F = (SS_reg / p) / (SS_res / (n − p − 1)) ~ F(p, n − p − 1) under H₀
Partial (nested) F-test compares a restricted model (q coefficients set to 0) with the full model:
F = [ (SS_res,restricted − SS_res,full) / q ] / [ SS_res,full / (n − p − 1) ]
Multiple testing: testing many coefficients at α = 0.05 raises the chance of at least one false positive; use corrections (Bonferroni, Holm, Benjamini–Hochberg) when that matters. Selecting features by p-value and then reporting the same p-values (post-selection inference) overstates significance.
13. Metrics: R², adjusted R², RMSE, MAE
SS_res = Σ(yᵢ − ŷᵢ)² SS_tot = Σ(yᵢ − ȳ)² SS_reg = Σ(ŷᵢ − ȳ)²
R² = 1 − SS_res / SS_tot
adjusted R² = 1 − (1 − R²)·(n − 1)/(n − p − 1)
RMSE = √( SS_res / n )
MAE = (1/n)·Σ|yᵢ − ŷᵢ|
- R² on training data, for OLS with an intercept, lies in [0, 1] and never decreases when a feature is added (the larger model can always reproduce the smaller one by setting the new coefficient to 0).
- Adjusted R² penalises the number of features. It increases when an added feature's |t| > 1 and decreases otherwise, so it can fall when you add noise features. It is a rough in-sample model comparison tool; it is not a substitute for validation on held-out data.
- R² on test data (
sklearn.metrics.r2_score) uses the test-set mean inSS_totand can be negative: the model is worse than predicting the constant test mean. - RMSE is in units of
y, penalises large errors more (squared), and is the metric OLS directly optimises (in-sample). Statistical software often reports the residual standard errorσ̂ = √(SS_res/(n − p − 1)), which differs from RMSE by the degrees-of-freedom correction. - MAE is in units of
y, less sensitive to outliers, and is minimised by the conditional median rather than the mean. - Report metrics on held-out data for any claim about predictive performance.
14. Bias–variance, overfitting, train/test split, k-fold cross-validation
Bias–variance decomposition of expected squared prediction error at a point x, averaging over training sets and noise:
E[(y − f̂(x))²] = Bias[f̂(x)]² + Var[f̂(x)] + σ²
- Bias: error from the model's systematic inability to represent the true function (for example fitting a plane to a curved surface).
- Variance: error from sensitivity of the fitted model to the particular training sample.
- σ²: irreducible noise.
Adding features, interactions or polynomial degree generally lowers bias and raises variance. For OLS with p features and well-specified model, the average in-sample variance contribution grows like σ²(p+1)/n, so many features relative to n hurts.
Overfitting: the model fits noise in the training data; training error is low but error on new data is high. Signs: large gap between training and validation metrics; coefficients with huge magnitudes; instability across folds.
Train/test split: hold out a test set (commonly 20–30%) that is used once, at the end, to estimate generalisation error. All preprocessing and model selection must use only the training portion.
k-fold cross-validation (CV): split the training data into k folds (commonly 5 or 10). For each fold, train on k − 1 folds and validate on the remaining one; average the k scores. Use CV to choose hyperparameters (for example the ridge penalty) and to compare models; report the mean and spread across folds. Variants: stratified (for classification), group k-fold (when rows belong to the same entity, for example several rows per customer), time-series split (train on the past, validate on the future; never shuffle temporal data). Leave-one-out CV (k = n) needs no refitting for OLS: CV_LOO = (1/n)·Σ (eᵢ / (1 − hᵢᵢ))², using the residuals and leverages of the full fit (ridge has the same shortcut with the ridge hat matrix X(XᵀX + λI)⁻¹Xᵀ).
Nested CV: an inner CV loop tunes hyperparameters and an outer loop estimates performance of the whole tuning procedure. Needed if you want an unbiased performance estimate without a separate test set.
15. Regularisation: Ridge (L2), Lasso (L1), Elastic Net
Regularisation adds a penalty on coefficient size to the least-squares objective. It trades a small increase in bias for a potentially large decrease in variance. Always standardise features first, because the penalty treats all coefficients equally and coefficient size depends on units. The intercept is not penalised (libraries center the data and fit the intercept separately).
15.1 Ridge regression (L2)
β̂_ridge = argmin_β ‖y − Xβ‖² + λ‖β‖² (λ ≥ 0, sum over non-intercept coefficients)
Setting the gradient to zero, −2Xᵀ(y − Xβ) + 2λβ = 0, gives the closed form (with centered X and y):
β̂_ridge = (XᵀX + λI)⁻¹ Xᵀy
- For
λ > 0,XᵀX + λIis positive definite and therefore always invertible, even whenXᵀXis singular (perfect collinearity orp > n). - Adding
λto every eigenvalue lowers the condition number fromλ_max/λ_minto(λ_max + λ)/(λ_min + λ). - In SVD terms, ridge multiplies each OLS component by
d_j² / (d_j² + λ), shrinking most strongly the directions with small singular valuesd_j(the unstable, collinear directions). - Coefficients shrink toward 0 but are not exactly 0 for finite
λ. - scikit-learn's
Ridge(alpha=α)minimises exactly‖y − Xw‖² + α‖w‖², soalphaequalsλabove.RidgeCVselectsalphaby efficient leave-one-out CV by default or by k-fold CV ifcvis given.
15.2 Lasso (L1)
β̂_lasso = argmin_β (1/(2n))‖y − Xβ‖² + α‖β‖₁ ‖β‖₁ = Σ|β_j| (scikit-learn's scaling)
There is no closed form in general; it is solved by coordinate descent (scikit-learn) or LARS.
Why lasso produces exact zeros (sparsity):
- Constraint-geometry view. The problem is equivalent to minimising squared error subject to
‖β‖₁ ≤ t. The L1 constraint region is a cross-polytope (a diamond in 2-D) with corners on the axes. The elliptical contours of the squared error usually first touch this region at a corner or edge, where some coordinates are exactly 0. The L2 region (‖β‖² ≤ t) is a smooth ball with no corners, so the touching point generally has all coordinates non-zero. - Subgradient / thresholding view. With an orthonormal design (
XᵀX = I) and objective½‖y − Xβ‖² + λ‖β‖₁, each lasso coefficient is the OLS coefficientz_jpassed through soft-thresholding:
β̂_j,lasso = sign(z_j) · max(|z_j| − λ, 0)
Any OLS coefficient with |z_j| ≤ λ becomes exactly 0. In the same setting ridge (objective ½‖y − Xβ‖² + (λ/2)‖β‖²) gives β̂_j,ridge = z_j / (1 + λ): proportional shrinkage, never exactly 0. Example with λ = 1: z = 3.0 → lasso 2.0, ridge 1.5; z = 0.5 → lasso 0.0, ridge 0.25.
Lasso caveats: among a group of highly correlated features it tends to pick one somewhat arbitrarily and the choice can change between samples; it selects at most n features when p > n; the selected coefficients are biased toward 0 (refitting OLS on the selected set, "relaxed lasso", is one fix).
15.3 Elastic Net
scikit-learn's form:
(1/(2n))‖y − Xβ‖² + α·ρ·‖β‖₁ + (α·(1 − ρ)/2)·‖β‖² ρ = l1_ratio ∈ [0, 1]
ρ = 1 is lasso, ρ = 0 is ridge. Elastic Net keeps sparsity while sharing weight among correlated features (the grouping effect) and is more stable than lasso when features are correlated or p > n. Tune both α and ρ with ElasticNetCV.
15.4 Choosing λ (alpha)
Use cross-validation over a logarithmic grid (for example 10⁻³ … 10³). Optionally use the one-standard-error rule: choose the largest penalty whose CV error is within one standard error of the minimum, to prefer a simpler model. Do not use p-values from a regularised fit as if it were OLS.
16. Feature scaling and data leakage
Feature scaling transforms features to comparable ranges:
- Standardisation (StandardScaler): (x − mean)/sd. Default choice for GD and regularised regression.
- Min-max scaling (MinMaxScaler): maps to [0, 1]; sensitive to outliers.
- Robust scaling (RobustScaler): uses median and interquartile range.
Scaling does not change OLS predictions, R², or the t-statistics and p-values of the slope coefficients (OLS is equivariant to affine rescaling of features; only the intercept's value and test change when features are centered), but it does change the numeric values of coefficients and is essential for GD speed and for regularisation to treat features fairly.
Data leakage is any use of information during training that would not be available at prediction time, which makes validation scores look better than real-world performance. Common forms: - Fitting a scaler, imputer, encoder or feature selector on the full data set before splitting or before CV (test-set statistics leak into training). - Features computed using the target or future information (for example "total refunds this month" when predicting a refund made earlier that month). - Random splitting of time series or of grouped data (the same customer in train and test). - Repeatedly tuning on the test set until it looks good.
Fix: put every data-dependent preprocessing step inside a sklearn.pipeline.Pipeline so it is re-fit inside each CV fold on training data only; use time-aware or group-aware splitters; touch the test set once.
17. Production concerns
- Pipelines as the unit of deployment. Serialise the entire fitted
Pipeline(imputation, encoding, scaling, model), not just coefficients, so training and serving apply identical transformations. Pin library versions; pickled scikit-learn objects are not guaranteed to load across versions. Record the training data snapshot, code version, hyperparameters and metrics (model registry). - Input validation. Enforce a schema at serving time: column names and order, types, allowed ranges, allowed categories, missing-value policy. Linear models extrapolate linearly without warning, so out-of-range inputs produce confident but unsupported predictions.
- Training–serving skew. Differences between how features are computed offline and online (different code paths, time windows, units) silently degrade predictions. Share feature code or use a feature store.
- Drift. Data (covariate) drift: the distribution of
Xchanges. Concept drift: the relationship betweenXandychanges (coefficients would change). Label drift: the distribution ofychanges. Monitor feature distributions (for example population stability index, Kolmogorov–Smirnov tests), prediction distributions, and, once true labels arrive, rolling RMSE/MAE and residual bias by segment. - Retraining. Schedule-based (for example weekly) or trigger-based (drift or error threshold exceeded). Validate each new model against the current one on recent held-out data before promotion (champion/challenger, shadow or canary deployment). Keep the ability to roll back.
- Monitoring. Track latency, error rates, missing-feature rates, out-of-range rates, and model metrics; alert on sustained changes, not single points.
- Explainability. Linear models are transparent: each prediction decomposes as
ŷ = β₀ + Σ β_j·x_j, so per-feature contributionsβ_j·(x_j − x̄_j)relative to an average row can be reported. Caveats: with correlated features, individual contributions are not uniquely attributable; coefficients describe association, not causation; on standardised inputs, convert back to original units for business users. - Uncertainty. Serve prediction intervals (not just point predictions) where decisions depend on risk; check their empirical coverage on recent data.
- Fairness and compliance. Check errors by segment; excluding a sensitive attribute does not remove its influence if correlated proxies remain.
Definitions
Every key term in one or two sentences. Type to filter.
| Term | Definition |
|---|---|
| Multiple linear regression | A model that predicts a numeric target as an intercept plus a weighted sum of two or more features, y = Xβ + ε, with weights estimated from data. |
| Design matrix (X) | The n × (p+1) matrix of feature values, one row per observation, usually with a leading column of ones for the intercept. |
| Intercept column | The column of ones in X; its coefficient β₀ shifts all predictions by a constant. |
| Coefficient (β_j) | The expected change in y per one-unit increase in feature x_j, holding all other features in the model constant. |
| Error term (ε) | The unobserved difference between the true y and the population regression function Xβ. |
| Residual (e) | The observed difference y − ŷ between actual and fitted values; the sample counterpart of ε. |
| Ordinary least squares (OLS) | The estimator that chooses β to minimise ‖y − Xβ‖², the sum of squared residuals. |
| Normal equations | The linear system XᵀXβ̂ = Xᵀy obtained by setting the gradient of the squared-error loss to zero. |
| Full column rank | The property that no column of X is a linear combination of the others; required for a unique OLS solution. |
| Hat matrix (H) | H = X(XᵀX)⁻¹Xᵀ, the projection matrix that maps y to fitted values ŷ = Hy. |
| Projection | The closest point in a subspace to a given vector; OLS fitted values are the orthogonal projection of y onto the column space of X. |
| Column space | The set of all vectors that can be written as Xβ for some β. |
| Leverage (hᵢᵢ) | The i-th diagonal entry of H; measures how unusual row i's feature values are and how strongly yᵢ pulls its own fitted value. |
| Pseudo-inverse (X⁺) | The Moore–Penrose generalised inverse; X⁺y gives the minimum-norm least-squares solution even when X is rank-deficient. |
| QR decomposition | Factorisation X = QR with orthonormal Q and upper-triangular R, used to solve least squares without forming XᵀX. |
| Singular value decomposition (SVD) | Factorisation X = UΣVᵀ; singular values in Σ reveal rank and conditioning. |
| Condition number | Ratio of largest to smallest singular value (of X) or eigenvalue (of XᵀX); measures sensitivity of the solution to small changes in data. |
| Gradient descent | An iterative method that updates parameters in the direction of the negative gradient of the loss. |
| Learning rate (η) | The step size multiplying the gradient in each gradient descent update. |
| Epoch | One full pass over the training data during iterative training. |
| Feature scaling / standardisation | Transforming features to comparable scales, commonly to mean 0 and standard deviation 1. |
| One-hot encoding | Representing a categorical feature with k levels as indicator columns that are 1 for the row's level and 0 otherwise. |
| Dummy variable trap | Perfect collinearity caused by including all k dummies of a categorical feature together with an intercept. |
| Reference (baseline) level | The category whose dummy is dropped; other dummies' coefficients are differences relative to it. |
| Interaction term | A product of features (for example x₁·x₂) that lets the effect of one feature depend on another. |
| Polynomial term | A power of a feature (for example x²) used to model curvature while staying linear in the parameters. |
| Gauss–Markov assumptions | Linearity in parameters, full rank, zero conditional mean of errors, and homoscedastic uncorrelated errors. |
| BLUE | Best Linear Unbiased Estimator: the linear unbiased estimator with the smallest variance; OLS under Gauss–Markov. |
| Exogeneity | The condition E[ε ∣ X] = 0; features carry no information about the error. Its failure (endogeneity) biases OLS. |
| Homoscedasticity | Constant error variance across observations. |
| Heteroscedasticity | Error variance that changes across observations; invalidates classical standard errors. |
| Breusch–Pagan test | A test for heteroscedasticity that regresses squared residuals on the features. |
| Robust (HC) standard errors | Standard errors computed with a sandwich formula that remains valid under heteroscedasticity. |
| Q-Q plot | A plot of sample residual quantiles against theoretical normal quantiles to judge normality. |
| Autocorrelation | Correlation between errors of different observations, typically adjacent in time. |
| Durbin–Watson statistic | A statistic between 0 and 4 for lag-1 residual autocorrelation; about 2 indicates none. |
| Multicollinearity | Strong linear relationships among features that inflate coefficient variances. |
| Variance Inflation Factor (VIF) | 1/(1 − R²_j); the factor by which the variance of β̂_j is inflated by correlation with other features. |
| Influential point | An observation whose removal changes the fitted coefficients or predictions substantially. |
| Cook's distance | A per-row influence measure combining residual size and leverage. |
| Studentised residual | A residual divided by its estimated standard deviation σ̂√(1 − hᵢᵢ). |
| Standard error (SE) | The estimated standard deviation of a coefficient's sampling distribution. |
| t-statistic | A coefficient estimate divided by its standard error; used to test whether the coefficient is zero. |
| p-value | The probability, under the null hypothesis and model assumptions, of a test statistic at least as extreme as the one observed. |
| Confidence interval | A range computed by a procedure that contains the true parameter in a stated fraction of repeated samples. |
| Prediction interval | A range expected to contain a new individual observation at given feature values; wider than the confidence interval for the mean. |
| F-test | A test of whether a group of coefficients (all slopes, or a nested subset) are jointly zero. |
| Degrees of freedom (residual) | n − p − 1: the number of observations minus the number of estimated parameters. |
| R² | 1 − SS_res/SS_tot; the fraction of variance in y explained by the model on the data evaluated. |
| Adjusted R² | R² penalised for the number of features: 1 − (1 − R²)(n − 1)/(n − p − 1). |
| RMSE | Root mean squared error, √(SS_res/n), in units of y. |
| MAE | Mean absolute error, (1/n)Σ∣yᵢ − ŷᵢ∣, in units of y. |
| Bias (of a model) | Systematic error from the model's inability to represent the true relationship. |
| Variance (of a model) | Variability of the model's predictions across different training samples. |
| Overfitting | Fitting noise in the training data so that performance on new data is worse than on training data. |
| Train/test split | Partitioning data into a part used for fitting and a held-out part used once for final evaluation. |
| k-fold cross-validation | Rotating each of k folds as validation data while training on the rest, then averaging scores. |
| Regularisation | Adding a penalty on coefficient size to the loss to reduce variance. |
| Ridge regression (L2) | Least squares plus λ‖β‖²; shrinks coefficients smoothly and always has a unique solution for λ > 0. |
| Lasso (L1) | Least squares plus a multiple of ‖β‖₁; shrinks and can set coefficients exactly to zero. |
| Elastic Net | A penalty mixing L1 and L2; sparse like lasso, stable with correlated features like ridge. |
| Soft-thresholding | The operation sign(z)·max(∣z∣ − λ, 0) that produces lasso solutions under orthonormal design. |
| Hyperparameter | A setting chosen before fitting (for example λ), usually tuned by cross-validation. |
| Data leakage | Use of information in training or validation that would not be available at prediction time. |
| Pipeline | A chained sequence of preprocessing steps and a final estimator that is fit and applied as one object. |
| Data drift | A change over time in the distribution of input features. |
| Concept drift | A change over time in the relationship between features and the target. |
| Training–serving skew | A mismatch between how features are computed during training and during serving. |
Worked Example (a) — Multiple regression by hand with the normal equation
Setting (synthetic, for teaching): monthly cloud bill y (in units of $100) for 6 teams, explained by x₁ = number of VMs and x₂ = storage in TB.
| Row | x₁ (VMs) | x₂ (TB) | y (bill, $100s) |
|---|---|---|---|
| 1 | 1 | 2 | 5 |
| 2 | 2 | 1 | 7 |
| 3 | 3 | 3 | 12 |
| 4 | 4 | 3 | 13 |
| 5 | 5 | 5 | 19 |
| 6 | 6 | 4 | 19 |
n = 6, p = 2. Correlation between x₁ and x₂ is 0.8315 (correlated, but not perfectly).
Step 1 — Design matrix
⎡ 1 1 2 ⎤ ⎡ 5 ⎤
⎢ 1 2 1 ⎥ ⎢ 7 ⎥
X = ⎢ 1 3 3 ⎥ y = ⎢ 12 ⎥
⎢ 1 4 3 ⎥ ⎢ 13 ⎥
⎢ 1 5 5 ⎥ ⎢ 19 ⎥
⎣ 1 6 4 ⎦ ⎣ 19 ⎦
Step 2 — XᵀX
Entries are sums: [n, Σx₁, Σx₂; Σx₁, Σx₁², Σx₁x₂; Σx₂, Σx₁x₂, Σx₂²].
Σx₁ = 21 Σx₂ = 18 Σx₁² = 91 Σx₂² = 64 Σx₁x₂ = 2+2+9+12+25+24 = 74
⎡ 6 21 18 ⎤
XᵀX = ⎢ 21 91 74 ⎥
⎣ 18 74 64 ⎦
Step 3 — (XᵀX)⁻¹ via determinant and adjugate
det(XᵀX) = 324
⎡ 348 −12 −84 ⎤
adj(XᵀX) = ⎢ −12 60 −66 ⎥
⎣ −84 −66 105 ⎦
⎡ 29/27 −1/27 −7/27 ⎤ ⎡ 1.074074 −0.037037 −0.259259 ⎤
(XᵀX)⁻¹ = adj(XᵀX) / 324 = ⎢ −1/27 5/27 −11/54 ⎥ ≈ ⎢ −0.037037 0.185185 −0.203704 ⎥
⎣ −7/27 −11/54 35/108 ⎦ ⎣ −0.259259 −0.203704 0.324074 ⎦
Step 4 — Xᵀy
Σy = 75 Σx₁y = 5+14+36+52+95+114 = 316 Σx₂y = 10+7+36+39+95+76 = 263
Xᵀy = [ 75, 316, 263 ]ᵀ
Step 5 — β̂ = (XᵀX)⁻¹Xᵀy
adj(XᵀX)·Xᵀy = [ 216, 702, 459 ]ᵀ (e.g. 348·75 − 12·316 − 84·263 = 216)
β̂ = [ 216, 702, 459 ]ᵀ / 324 = [ 2/3, 13/6, 17/12 ]ᵀ ≈ [ 0.6667, 2.1667, 1.4167 ]ᵀ
Fitted model: ŷ = 0.6667 + 2.1667·x₁ + 1.4167·x₂
Interpretation: holding storage constant, each additional VM is associated with an increase of about 2.1667 in the predicted bill ($216.67). Holding the number of VMs constant, each additional TB is associated with about 1.4167 ($141.67). The intercept (0.6667, i.e. $66.67) is the predicted bill at 0 VMs and 0 TB, which lies outside the data range and is a mathematical anchor, not a reliable estimate.
Step 6 — Predictions and residuals
| Row | y | ŷ (exact) | ŷ | e = y − ŷ | leverage hᵢᵢ | Cook's D |
|---|---|---|---|---|---|---|
| 1 | 5 | 17/3 | 5.6667 | −0.6667 | 0.6296 | 1.1657 |
| 2 | 7 | 77/12 | 6.4167 | +0.5833 | 0.6574 | 1.0891 |
| 3 | 12 | 137/12 | 11.4167 | +0.5833 | 0.2130 | 0.0669 |
| 4 | 13 | 163/12 | 13.5833 | −0.5833 | 0.2130 | 0.0669 |
| 5 | 19 | 223/12 | 18.5833 | +0.4167 | 0.6574 | 0.5557 |
| 6 | 19 | 58/3 | 19.3333 | −0.3333 | 0.6296 | 0.2914 |
Checks: Σeᵢ = 0 and Xᵀe = [0, 0, 0]ᵀ exactly (orthogonality from Section 3). Σhᵢᵢ = 3 = p + 1.
Step 7 — Sums of squares, R², adjusted R², RMSE, MAE
ȳ = 75/6 = 12.5
SS_res = Σe² = 7/4 = 1.75
SS_tot = Σ(y − ȳ)² = 343/2 = 171.5 (deviations −7.5, −5.5, −0.5, 0.5, 6.5, 6.5)
SS_reg = Σ(ŷ − ȳ)² = 679/4 = 169.75 (check: 169.75 + 1.75 = 171.5)
R² = 1 − 1.75/171.5 = 97/98 ≈ 0.98980
adjusted R² = 1 − (1 − 97/98)·(5/3) = 289/294 ≈ 0.98299
RMSE = √(1.75/6) ≈ 0.5401 ($54.01)
MAE = (1/6)·Σ|e| ≈ 0.5278
Step 8 — Inference (verified with statsmodels)
σ̂² = SS_res/(n − p − 1) = 1.75/3 = 7/12 ≈ 0.5833 σ̂ ≈ 0.7638
SE(β̂_j) = σ̂·√[(XᵀX)⁻¹]_jj
| Coefficient | Estimate | SE | t | p-value (df = 3) | 95% CI |
|---|---|---|---|---|---|
| β₀ (intercept) | 0.6667 | 0.7915 | 0.842 | 0.4615 | [−1.8524, 3.1857] |
| β₁ (VMs) | 2.1667 | 0.3287 | 6.592 | 0.0071 | [1.1207, 3.2126] |
| β₂ (TB) | 1.4167 | 0.4348 | 3.258 | 0.0472 | [0.0330, 2.8004] |
t_{0.975, 3} = 3.1824. Overall F = (169.75/2)/(1.75/3) = 145.5 with (2, 3) df, p ≈ 0.00103.
Prediction at x₁ = 4, x₂ = 4: ŷ = 2/3 + 4·13/6 + 4·17/12 = 15.0. 95% CI for the mean: [13.5967, 16.4033]. 95% prediction interval for a new team: [12.1933, 17.8067].
Critical reading: with n = 6 and 3 parameters there are only 3 residual degrees of freedom, so p-values and intervals are fragile. Rows 1 and 2 have Cook's D above 1 because they sit at the edge of the feature space (leverage ≈ 0.63–0.66 versus an average of 3/6 = 0.5); with so few rows almost every point is influential. This is a hand-computation example, not a model to deploy.
Chart — Example (a): actual vs fitted, with residuals
Left: each row's actual y against its fitted ŷ; vertical amber segments are residuals (distance from the dashed y = ŷ line). Right: the six residuals as bars. Values verified in Python.
Worked Example (b) — Multicollinearity and VIF
Setting (synthetic): throughput y (thousand requests/min) for 8 service configurations; x₁ = vCPUs, x₂ = memory in GB (provisioned at roughly 2 GB per vCPU, so nearly a copy of x₁), x₃ = number of cache nodes (varies independently).
| Row | x₁ | x₂ | x₃ | y |
|---|---|---|---|---|
| 1 | 1 | 2.1 | 5 | 10.4 |
| 2 | 2 | 3.9 | 3 | 13.1 |
| 3 | 3 | 6.2 | 6 | 19.2 |
| 4 | 4 | 7.8 | 2 | 19.9 |
| 5 | 5 | 10.1 | 7 | 27.0 |
| 6 | 6 | 12.2 | 4 | 29.4 |
| 7 | 7 | 13.8 | 8 | 34.9 |
| 8 | 8 | 16.1 | 1 | 34.6 |
Step 1 — Correlations and auxiliary regressions
corr(x₁, x₂) = 0.99942, corr(x₁, x₃) = −0.0476, corr(x₂, x₃) = −0.0465.
For each feature, regress it on the other two (with intercept) and compute VIF_j = 1/(1 − R²_j):
| Feature | R²_j | VIF_j | SE inflation √VIF |
|---|---|---|---|
| x₁ (vCPU) | 0.998841 | 862.59 | ≈ 29.4 |
| x₂ (memory) | 0.998841 | 862.50 | ≈ 29.4 |
| x₃ (cache) | 0.003376 | 1.0034 | ≈ 1.00 |
statsmodels' variance_inflation_factor returns the same values (862.5922, 862.4977, 1.0034).
Step 2 — Symptoms in the OLS fit
| Term | Coefficient | SE | p-value |
|---|---|---|---|
| Intercept | 3.6735 | 0.2537 | 0.00013 |
| x₁ | 1.1764 | 1.0763 | 0.3358 |
| x₂ | 1.3096 | 0.5384 | 0.0718 |
| x₃ | 0.6169 | 0.0367 | 0.00007 |
R² = 0.99963 and the overall F-test p-value is about 2.6 × 10⁻⁷, yet neither x₁ nor x₂ is individually significant at 0.05. This pattern (strong joint fit, weak individual t-tests) is a classic sign of collinearity. x₃, which is not collinear, has a small SE.
Condition numbers: statsmodels' Cond. No. on the unscaled design with intercept is 172.9; on standardised features cond(Z) = 58.79 and cond(ZᵀZ) = 3455.78. Eigenvalues of the feature correlation matrix are 0.00058, 0.9956, 2.0038: the near-zero eigenvalue identifies the near-dependence x₂ ≈ 2·x₁.
Step 3 — Instability under a tiny data change
Change one target value, row 4: y = 19.9 → 20.9 (one unit), and refit.
| Term | Original fit | After change |
|---|---|---|
| x₁ | 1.1764 | 3.4254 |
| x₂ | 1.3096 | 0.1764 |
| x₃ | 0.6169 | 0.5593 |
The individual coefficients of x₁ and x₂ swing dramatically, while the combined effect along the shared direction, β₁ + 2·β₂, barely moves (3.7956 → 3.7782). The data can estimate the joint effect of "more vCPU with proportional memory" but cannot separate the two.
Remedy shown: drop x₂. The reduced model gives x₁ coefficient 3.7926 (SE 0.0517) with R² = 0.99909, and the same perturbation only moves it to 3.7778. Alternative remedy: ridge (Example c).
Worked Example (c) — Ridge vs OLS on small, collinear data
Same 8 rows as Example (b). Features are standardised (mean 0, population SD 1, as StandardScaler does), y is centered, and the intercept (= ȳ) is not penalised. Ridge coefficients are computed with the closed form w = (ZᵀZ + λI)⁻¹Zᵀ(y − ȳ) and cross-checked against sklearn.linear_model.Ridge(alpha=λ) (they match to floating-point precision).
Coefficients on the standardised scale (change in y per 1 SD of the feature)
| λ | w₁ (vCPU) | w₂ (memory) | w₃ (cache) | ‖w‖ | train R² | cond(ZᵀZ + λI) |
|---|---|---|---|---|---|---|
| 0 (OLS) | 2.6955 | 5.9976 | 1.4135 | 6.7257 | 0.9996 | 3455.78 |
| 0.1 | 4.2451 | 4.3932 | 1.3953 | 6.2664 | 0.9995 | 154.16 |
| 1 | 4.0784 | 4.0953 | 1.2364 | 5.9105 | 0.9958 | 16.95 |
| 10 | 2.6610 | 2.6633 | 0.5587 | 3.8061 | 0.8451 | 2.60 |
Intercept for all rows: ȳ = 23.5625.
Same fits after the one-unit change to row 4 (from Example b)
| λ | w₁ | w₂ | w₃ |
|---|---|---|---|
| 0 (OLS) | 7.8487 | 0.8079 | 1.2814 |
| 0.1 | 4.4561 | 4.1456 | 1.2592 |
| 1 | 4.0854 | 4.0544 | 1.1138 |
| 10 | 2.6532 | 2.6506 | 0.4976 |
Leave-one-out cross-validated RMSE (scaler re-fit inside each fold)
| Model | LOOCV RMSE |
|---|---|
| OLS | 0.3551 |
| Ridge λ = 0.1 | 0.2908 |
| Ridge λ = 1 | 0.8768 |
| Ridge λ = 10 | 4.7007 |
What the numbers show
- Stability: under OLS the split between
w₁andw₂changes from (2.70, 6.00) to (7.85, 0.81) after a one-unit change in one target. With λ = 0.1 it changes only from (4.25, 4.39) to (4.46, 4.15). Ridge spreads weight evenly across the two near-duplicate features. - Conditioning: adding λ to the diagonal drops the condition number from 3455.78 to 154.16 (λ = 0.1) and 16.95 (λ = 1).
- Bias–variance trade-off: training R² always decreases as λ grows (ridge cannot beat OLS on training data). Out-of-sample error first improves (λ = 0.1 has the lowest LOOCV RMSE here, 0.2908 vs 0.3551 for OLS), then worsens as shrinkage becomes too strong (λ = 10 underfits). λ must be tuned by cross-validation.
- No exact zeros: ridge shrinks all coefficients but none becomes exactly 0. Lasso would be needed for that (Lab 3 shows a lasso zero).
All numbers in Worked Examples (a), (b) and (c) were computed and cross-checked in Python 3.13 with numpy 2.5.3, scikit-learn 1.9.1, statsmodels 0.15.0 and sympy (exact fractions for Example a). Scripts: verify/verify_examples.py, verify/verify_extra_a.py, verify/verify_c_loo.py, verify/verify_misc.py.
Chart — Example (c): coefficient stability, OLS vs ridge
Standardised coefficients w₁ (vCPU) and w₂ (memory) for each λ, before (solid) and after (hatched) a one-unit change to a single target value. OLS (λ = 0) swings; ridge coefficients change very little.
Trainer: Pushpjeet Cholkar
Environment: local machine only (Linux, macOS or Windows with WSL). No cloud account, no cost, no network access needed after the pip install.
Reference run: Python 3.13.5, numpy 2.5.3, scikit-learn 1.9.1, statsmodels 0.15.0, pandas 3.0.6, matplotlib 3.11.2, scipy 1.18.1.
Expected outputs below were produced by actually running the scripts. Digits beyond the 4th–6th significant figure, and values of order 1e-13 to 1e-16 (floating-point round-off), can differ slightly across machines, BLAS libraries and package versions. The Date/Time lines in the statsmodels summary will show your own run time.
Common setup (do once)
Prerequisites: Python 3.10 or newer, pip, about 300 MB free disk space for packages.
mkdir -p ~/lr-expert-labs && cd ~/lr-expert-labs
python3 -m venv .venv
source .venv/bin/activate # Windows PowerShell: .venv\Scripts\Activate.ps1
pip install --upgrade pip
pip install numpy scipy pandas scikit-learn statsmodels matplotlib
python -c "import numpy, sklearn, statsmodels; print(numpy.__version__, sklearn.__version__, statsmodels.__version__)"
Using a virtual environment keeps these packages isolated from the system Python, and cleanup becomes deleting one folder.
Lab 1 — Normal equation from scratch, cross-checked with lstsq, QR, pinv and scikit-learn
Goal: implement β̂ = (XᵀX)⁻¹Xᵀy, confirm that six different routes give the same coefficients, verify the algebraic properties of the OLS solution (Xᵀe = 0, trace(H) = p + 1, H² = H), and measure why explicit inversion is avoided.
Prerequisites: common setup done; knowledge of Notes Sections 3–5.
Steps
- Save the script below as
lab1_normal_equation.py:
"""Lab 1: OLS via the normal equation from scratch, cross-checked with
np.linalg.lstsq, QR, pinv and scikit-learn LinearRegression."""
import numpy as np
from sklearn.linear_model import LinearRegression
rng = np.random.default_rng(42)
n, p = 200, 3
X_raw = rng.normal(size=(n, p)) # 3 features
beta_true = np.array([4.0, 2.0, -3.0, 0.5]) # [intercept, b1, b2, b3]
X = np.column_stack([np.ones(n), X_raw]) # design matrix with intercept column
y = X @ beta_true + rng.normal(scale=1.0, size=n) # noise sd = 1
# 1) Normal equation, two ways
XtX, Xty = X.T @ X, X.T @ y
beta_inv = np.linalg.inv(XtX) @ Xty # explicit inverse (teaching only)
beta_solve = np.linalg.solve(XtX, Xty) # solve linear system (better)
# 2) Library / factorisation routes
beta_lstsq, _, rank, sv = np.linalg.lstsq(X, y, rcond=None) # SVD-based
Q, R = np.linalg.qr(X)
beta_qr = np.linalg.solve(R, Q.T @ y) # QR route
beta_pinv = np.linalg.pinv(X) @ y # Moore-Penrose pseudo-inverse
sk = LinearRegression().fit(X_raw, y) # sklearn adds intercept itself
beta_sk = np.r_[sk.intercept_, sk.coef_]
print("true beta :", beta_true)
for name, b in [("inv", beta_inv), ("solve", beta_solve), ("lstsq", beta_lstsq),
("qr", beta_qr), ("pinv", beta_pinv), ("sklearn", beta_sk)]:
print(f"{name:<15}: {np.round(b, 4)} max|diff vs lstsq| = {np.max(np.abs(b - beta_lstsq)):.2e}")
print("rank(X) =", rank, " singular values:", np.round(sv, 3))
# 3) Properties of the OLS solution
y_hat = X @ beta_lstsq
e = y - y_hat
H = X @ np.linalg.solve(XtX, X.T) # hat matrix
print("sum of residuals :", f"{e.sum():.2e}")
print("max |X^T e| (orthogonality):", f"{np.max(np.abs(X.T @ e)):.2e}")
print("trace(H) (= p+1) :", round(np.trace(H), 6))
print("H idempotent (max|HH-H|) :", f"{np.max(np.abs(H @ H - H)):.2e}")
r2 = 1 - (e @ e) / np.sum((y - y.mean())**2)
print("R^2 (manual) =", round(r2, 4), " R^2 (sklearn) =", round(sk.score(X_raw, y), 4))
# 4) Why not invert X^T X? Condition number squares.
x1 = rng.normal(size=n)
X_bad = np.column_stack([np.ones(n), x1, x1 + 1e-6 * rng.normal(size=n)])
print("cond(X_bad) = %.3e" % np.linalg.cond(X_bad))
print("cond(X_bad^T X_bad) = %.3e (approx. cond(X)^2)" % np.linalg.cond(X_bad.T @ X_bad))
- Run it:
python lab1_normal_equation.py
Expected output
true beta : [ 4. 2. -3. 0.5]
inv : [ 3.9629 2.0368 -3.0076 0.4483] max|diff vs lstsq| = 2.66e-15
solve : [ 3.9629 2.0368 -3.0076 0.4483] max|diff vs lstsq| = 2.22e-15
lstsq : [ 3.9629 2.0368 -3.0076 0.4483] max|diff vs lstsq| = 0.00e+00
qr : [ 3.9629 2.0368 -3.0076 0.4483] max|diff vs lstsq| = 1.33e-15
pinv : [ 3.9629 2.0368 -3.0076 0.4483] max|diff vs lstsq| = 6.66e-15
sklearn : [ 3.9629 2.0368 -3.0076 0.4483] max|diff vs lstsq| = 1.78e-15
rank(X) = 4 singular values: [14.665 14.421 13.663 12.599]
sum of residuals : -3.60e-13
max |X^T e| (orthogonality): 3.64e-13
trace(H) (= p+1) : 4.0
H idempotent (max|HH-H|) : 4.16e-17
R^2 (manual) = 0.921 R^2 (sklearn) = 0.921
cond(X_bad) = 2.089e+06
cond(X_bad^T X_bad) = 4.358e+12 (approx. cond(X)^2)
Verification
- All six coefficient vectors agree to about 1e-14 or better (
max|diff vs lstsq|). The estimates (3.96, 2.04, −3.01, 0.45) are close to, but not equal to, the true values (4, 2, −3, 0.5) because of the noise term. sum of residualsandmax |X^T e|are zero up to round-off: the normal equations hold.trace(H) = 4.0equalsp + 1(3 features + intercept).cond(X_bad^T X_bad)is roughly the square ofcond(X_bad): (2.089e6)² ≈ 4.36e12. With about 16 significant digits in double precision, formingXᵀXhere leaves only around 4 reliable digits, which is why QR/SVD are used.
Extension (optional)
Append these lines to the script to see an exactly singular design (the last two columns identical):
X_sing = np.column_stack([np.ones(n), x1, x1])
try:
print("inv:", np.linalg.inv(X_sing.T @ X_sing) @ X_sing.T @ y)
except np.linalg.LinAlgError as err:
print("inv failed:", err)
b, _, r, _ = np.linalg.lstsq(X_sing, y, rcond=None)
print("lstsq:", np.round(b, 4), "rank =", r)
Reference output:
inv failed: Singular matrix
lstsq: [4.1809 0.0067 0.0067] rank = 2
(y was generated without this new x1, so its coefficient is near 0.) In the reference run np.linalg.inv raised LinAlgError: Singular matrix (on other inputs it can instead return meaningless huge values without an error), while np.linalg.lstsq returned the minimum-norm solution, split the shared coefficient equally between the two identical columns, and reported rank = 2.
Cleanup
rm lab1_normal_equation.py
Lab 2 — Batch gradient descent with feature scaling, loss curve, match to OLS
Goal: implement batch gradient descent for MSE, show that unscaled features force a tiny learning rate (or diverge), show that standardised features converge fast, map the scaled coefficients back to original units, and match the closed-form OLS solution.
Prerequisites: common setup done; Lab 1; Notes Section 6.
Steps
- Save the script below as
lab2_gradient_descent.py:
"""Lab 2: Batch gradient descent for multiple linear regression, with and
without feature scaling, compared with the closed-form OLS solution."""
import numpy as np
import matplotlib
matplotlib.use("Agg") # no display needed; writes PNG
import matplotlib.pyplot as plt
rng = np.random.default_rng(7)
n = 500
size_sqft = rng.uniform(500, 3500, n) # large scale feature
rooms = rng.integers(1, 6, n).astype(float)
age_years = rng.uniform(0, 40, n)
X_raw = np.column_stack([size_sqft, rooms, age_years])
y = 50 + 0.12 * size_sqft + 8.0 * rooms - 1.5 * age_years + rng.normal(0, 15, n)
def mse(X, y, w): # X includes the intercept column
r = X @ w - y
return (r @ r) / len(y)
def batch_gd(X, y, lr, epochs):
np.seterr(over="ignore", invalid="ignore") # divergence is shown on purpose
w = np.zeros(X.shape[1])
hist = []
for _ in range(epochs):
grad = (2 / len(y)) * X.T @ (X @ w - y) # gradient of MSE
w -= lr * grad
hist.append(mse(X, y, w))
if not np.isfinite(hist[-1]):
break
return w, np.array(hist)
# Closed-form reference on the original scale
X1 = np.column_stack([np.ones(n), X_raw])
w_ols = np.linalg.lstsq(X1, y, rcond=None)[0]
print("OLS (lstsq) original scale :", np.round(w_ols, 4))
# A) Unscaled features
for lr in [1e-7, 1e-6]:
w_u, h_u = batch_gd(X1, y, lr=lr, epochs=2000)
print(f"unscaled lr={lr:g}: final MSE={h_u[-1]:.4g} epochs run={len(h_u)}")
# B) Standardised features (fit mean/std on the data used for training)
mu, sd = X_raw.mean(0), X_raw.std(0)
Xs = np.column_stack([np.ones(n), (X_raw - mu) / sd])
w_s, h_s = batch_gd(Xs, y, lr=0.1, epochs=500)
# Map back to the original scale: b_j = w_j / sd_j ; b0 = w0 - sum(w_j * mu_j / sd_j)
b = w_s[1:] / sd
b0 = w_s[0] - np.sum(w_s[1:] * mu / sd)
w_back = np.r_[b0, b]
print("GD (scaled) mapped back :", np.round(w_back, 4))
print("max |GD - OLS| : %.2e" % np.max(np.abs(w_back - w_ols)))
print("final MSE (GD scaled) : %.4f" % h_s[-1])
print("MSE of OLS : %.4f" % mse(X1, y, w_ols))
print("cond(X^T X), unscaled design : %.3e" % np.linalg.cond(X1.T @ X1))
print("cond(X^T X), scaled design : %.3f" % np.linalg.cond(Xs.T @ Xs))
# GD on MSE is stable only if lr < 1 / lambda_max((1/n) X^T X)
for tag, A in [("unscaled", X1), ("scaled", Xs)]:
lmax = np.linalg.eigvalsh(A.T @ A / n).max()
print(f"largest stable lr, {tag:<8}: {1/lmax:.3e}")
# Too large a learning rate on scaled data
_, h_big = batch_gd(Xs, y, lr=1.1, epochs=50)
print("scaled lr=1.1: MSE after %d epochs = %.3e (diverges)" % (len(h_big), h_big[-1]))
plt.figure(figsize=(7, 4))
plt.plot(h_s, label="scaled, lr=0.1")
plt.yscale("log"); plt.xlabel("epoch"); plt.ylabel("training MSE (log scale)")
plt.axhline(mse(X1, y, w_ols), ls="--", c="k", label="OLS minimum")
plt.legend(); plt.title("Batch gradient descent loss curve"); plt.tight_layout()
plt.savefig("lab2_loss_curve.png", dpi=120)
print("saved lab2_loss_curve.png")
- Run it:
python lab2_gradient_descent.py
Expected output
OLS (lstsq) original scale : [54.4472 0.1189 7.4043 -1.513 ]
unscaled lr=1e-07: final MSE=1017 epochs run=2000
unscaled lr=1e-06: final MSE=inf epochs run=160
GD (scaled) mapped back : [54.4472 0.1189 7.4043 -1.513 ]
max |GD - OLS| : 7.82e-14
final MSE (GD scaled) : 245.0873
MSE of OLS : 245.0873
cond(X^T X), unscaled design : 6.667e+07
cond(X^T X), scaled design : 1.056
largest stable lr, unscaled: 2.059e-07
largest stable lr, scaled : 9.745e-01
scaled lr=1.1: MSE after 50 epochs = 3.573e+13 (diverges)
saved lab2_loss_curve.png
A file lab2_loss_curve.png is written in the current folder. It shows the training MSE (log scale) dropping from about 6 × 10⁴ to the OLS minimum (dashed line, MSE = 245.09) within roughly the first 20–30 epochs and then staying flat.
Verification
GD (scaled) mapped backequalsOLS (lstsq)andmax |GD - OLS|is below 1e-10.- The computed stability limit explains the unscaled runs:
lr = 1e-7is below the limit 2.06e-7 so it is stable, but after 2000 epochs MSE is still about 1017 (far from 245) because convergence is slow whencond(XᵀX)≈ 6.7e7.lr = 1e-6is above the limit and diverges toinf. - With scaling,
cond(XᵀX)≈ 1.06, the limit is about 0.97, andlr = 0.1converges;lr = 1.1exceeds the limit and diverges. - Open
lab2_loss_curve.pngand confirm the curve reaches the dashed line.
Cleanup
rm lab2_gradient_descent.py lab2_loss_curve.png
Lab 3 — End-to-end multiple regression on the scikit-learn diabetes dataset
Dataset: sklearn.datasets.load_diabetes: 442 patients, 10 baseline features (age, sex, body mass index bmi, average blood pressure bp, six blood serum measurements s1–s6), target = a quantitative measure of disease progression one year after baseline. The data file ships inside the scikit-learn package, so it loads offline. The lab uses scaled=False to get features in original units (by default scikit-learn returns features already mean-centered and scaled, which would hide the role of our own scaler). sex is coded 1/2 in the raw data and is used as a numeric binary feature.
Goal: a leakage-safe Pipeline (scaling + model), train/test split, 5-fold CV, statsmodels OLS inference, VIF, residual diagnostics (Breusch–Pagan, Q-Q, Durbin–Watson, influence), heteroscedasticity-robust standard errors, and a RidgeCV / LassoCV comparison on the same split.
Prerequisites: common setup done; Labs 1–2; Notes Sections 10–16.
Steps
- Save the script below as
lab3_diabetes_end_to_end.py:
"""Lab 3: End-to-end multiple linear regression on the scikit-learn
diabetes dataset (ships inside scikit-learn; no download needed)."""
import numpy as np, pandas as pd
import matplotlib
matplotlib.use("Agg")
import matplotlib.pyplot as plt
import statsmodels.api as sm
from statsmodels.stats.outliers_influence import variance_inflation_factor
from statsmodels.stats.diagnostic import het_breuschpagan
from statsmodels.stats.stattools import durbin_watson, jarque_bera
from scipy import stats
from sklearn.datasets import load_diabetes
from sklearn.model_selection import train_test_split, KFold, cross_val_score
from sklearn.pipeline import Pipeline
from sklearn.preprocessing import StandardScaler
from sklearn.linear_model import LinearRegression, RidgeCV, LassoCV
from sklearn.metrics import r2_score, mean_squared_error, mean_absolute_error
pd.set_option("display.width", 120)
# ---- 1. Load raw (unscaled) features so that our own scaler matters --------
data = load_diabetes(as_frame=True, scaled=False)
X, y = data.data, data.target
print("Step 1 | shape:", X.shape, "| target mean = %.2f, sd = %.2f" % (y.mean(), y.std()))
# ---- 2. Hold out a test set BEFORE any fitting (prevents leakage) ----------
X_tr, X_te, y_tr, y_te = train_test_split(X, y, test_size=0.2, random_state=42)
print("Step 2 | train:", X_tr.shape, " test:", X_te.shape)
# ---- 3. Pipeline + 5-fold CV on the training set only ----------------------
kf = KFold(n_splits=5, shuffle=True, random_state=42)
ols_pipe = Pipeline([("scale", StandardScaler()), ("model", LinearRegression())])
cv_r2 = cross_val_score(ols_pipe, X_tr, y_tr, cv=kf, scoring="r2")
cv_rmse = -cross_val_score(ols_pipe, X_tr, y_tr, cv=kf, scoring="neg_root_mean_squared_error")
print("Step 3 | OLS 5-fold CV R2 : mean %.4f sd %.4f folds %s" % (cv_r2.mean(), cv_r2.std(), np.round(cv_r2, 3)))
print(" | OLS 5-fold CV RMSE: mean %.2f" % cv_rmse.mean())
def evaluate(name, pipe):
pipe.fit(X_tr, y_tr)
pred = pipe.predict(X_te)
return dict(model=name,
train_R2=r2_score(y_tr, pipe.predict(X_tr)),
test_R2=r2_score(y_te, pred),
test_RMSE=np.sqrt(mean_squared_error(y_te, pred)),
test_MAE=mean_absolute_error(y_te, pred))
rows = [evaluate("OLS", ols_pipe)]
print("Step 3 | OLS test R2 = %.4f, RMSE = %.2f" % (rows[0]["test_R2"], rows[0]["test_RMSE"]))
# ---- 4. statsmodels OLS for inference (training data, original units) ------
Xc_tr = sm.add_constant(X_tr)
sm_fit = sm.OLS(y_tr, Xc_tr).fit()
print("\nStep 4 | statsmodels OLS summary (training set)")
print(sm_fit.summary())
# ---- 5. VIF (training features, with constant in the design) ---------------
vif = pd.Series([variance_inflation_factor(Xc_tr.values, i) for i in range(1, Xc_tr.shape[1])],
index=X_tr.columns, name="VIF").round(2)
print("\nStep 5 | VIF per feature\n", vif.sort_values(ascending=False).to_string())
# ---- 6. Residual diagnostics ----------------------------------------------
resid, fitted = sm_fit.resid, sm_fit.fittedvalues
bp_lm, bp_p, _, _ = het_breuschpagan(resid, Xc_tr)
jb, jb_p, skew, kurt = jarque_bera(resid)
sh_w, sh_p = stats.shapiro(resid)
dw = durbin_watson(resid)
infl = sm_fit.get_influence()
cooks = infl.cooks_distance[0]
lev = infl.hat_matrix_diag
n_tr, k = Xc_tr.shape
print("\nStep 6 | Breusch-Pagan LM = %.3f, p = %.4f" % (bp_lm, bp_p))
print(" | Jarque-Bera = %.3f, p = %.4f (skew %.3f, kurtosis %.3f)" % (jb, jb_p, skew, kurt))
print(" | Shapiro-Wilk W = %.4f, p = %.4f" % (sh_w, sh_p))
print(" | Durbin-Watson = %.3f" % dw)
print(" | points with Cook's D > 4/n (%.4f): %d ; max Cook's D = %.4f" % (4 / n_tr, (cooks > 4 / n_tr).sum(), cooks.max()))
print(" | points with leverage > 2k/n (%.4f): %d" % (2 * k / n_tr, (lev > 2 * k / n_tr).sum()))
print(" | statsmodels condition number (unscaled design) = %.1f" % sm_fit.condition_number)
# Heteroscedasticity-robust (HC3) standard errors: same coefficients, different SEs
hc3 = sm.OLS(y_tr, Xc_tr).fit(cov_type="HC3")
cmp = pd.DataFrame({"coef_OLS": sm_fit.params, "coef_HC3": hc3.params,
"SE_classic": sm_fit.bse, "SE_HC3": hc3.bse,
"p_classic": sm_fit.pvalues, "p_HC3": hc3.pvalues}).round(4)
print(" | classic vs HC3 robust inference (coefficients identical, SEs differ)")
print(cmp.to_string())
fig, ax = plt.subplots(1, 2, figsize=(11, 4.2))
ax[0].scatter(fitted, resid, s=12, alpha=.6); ax[0].axhline(0, c="k", lw=1)
ax[0].set_xlabel("fitted value"); ax[0].set_ylabel("residual"); ax[0].set_title("Residuals vs fitted")
sm.qqplot(resid, line="45", fit=True, ax=ax[1]); ax[1].set_title("Normal Q-Q of residuals")
plt.tight_layout(); plt.savefig("lab3_residual_diagnostics.png", dpi=120)
print(" | saved lab3_residual_diagnostics.png")
# ---- 7. Regularised models: RidgeCV and LassoCV inside pipelines -----------
ridge_pipe = Pipeline([("scale", StandardScaler()),
("model", RidgeCV(alphas=np.logspace(-3, 3, 61)))])
lasso_pipe = Pipeline([("scale", StandardScaler()),
("model", LassoCV(cv=kf, random_state=42, max_iter=50000))])
rows.append(evaluate("RidgeCV", ridge_pipe))
rows.append(evaluate("LassoCV", lasso_pipe))
print("\nStep 7 | RidgeCV alpha = %.4f" % ridge_pipe.named_steps["model"].alpha_)
print(" | LassoCV alpha = %.4f" % lasso_pipe.named_steps["model"].alpha_)
coefs = pd.DataFrame({
"OLS": ols_pipe.named_steps["model"].coef_,
"Ridge": ridge_pipe.named_steps["model"].coef_,
"Lasso": lasso_pipe.named_steps["model"].coef_}, index=X.columns).round(2)
print(" | coefficients on the standardised scale (target units per 1 SD of feature)")
print(coefs.to_string())
print(" | Lasso zero coefficients:", list(coefs.index[coefs["Lasso"] == 0]))
print("\nStep 8 | Test-set comparison (same split, random_state=42)")
print(pd.DataFrame(rows).set_index("model").round(4).to_string())
- Run it:
python lab3_diabetes_end_to_end.py
Expected output
Step 1 | shape: (442, 10) | target mean = 152.13, sd = 77.09
Step 2 | train: (353, 10) test: (89, 10)
Step 3 | OLS 5-fold CV R2 : mean 0.4804 sd 0.0409 folds [0.47 0.537 0.411 0.491 0.493]
| OLS 5-fold CV RMSE: mean 55.39
Step 3 | OLS test R2 = 0.4526, RMSE = 53.85
Step 4 | statsmodels OLS summary (training set)
OLS Regression Results
==============================================================================
Dep. Variable: target R-squared: 0.528
Model: OLS Adj. R-squared: 0.514
Method: Least Squares F-statistic: 38.25
Date: Fri, 02 Oct 2026 Prob (F-statistic): 5.41e-50
Time: 04:48:13 Log-Likelihood: -1906.1
No. Observations: 353 AIC: 3834.
Df Residuals: 342 BIC: 3877.
Df Model: 10
Covariance Type: nonrobust
==============================================================================
coef std err t P>|t| [0.025 0.975]
------------------------------------------------------------------------------
const -341.3782 74.073 -4.609 0.000 -487.074 -195.683
age 0.1377 0.251 0.549 0.583 -0.356 0.631
sex -23.0645 6.536 -3.529 0.000 -35.921 -10.208
bmi 5.8464 0.829 7.049 0.000 4.215 7.478
bp 1.1971 0.246 4.873 0.000 0.714 1.680
s1 -1.2817 0.621 -2.065 0.040 -2.503 -0.061
s2 0.8112 0.570 1.423 0.156 -0.310 1.933
s3 0.6017 0.858 0.701 0.484 -1.086 2.289
s4 10.1595 6.841 1.485 0.138 -3.297 23.616
s5 67.1090 17.542 3.826 0.000 32.606 101.612
s6 0.2016 0.304 0.663 0.508 -0.397 0.800
==============================================================================
Omnibus: 1.457 Durbin-Watson: 1.794
Prob(Omnibus): 0.483 Jarque-Bera (JB): 1.412
Skew: 0.064 Prob(JB): 0.494
Kurtosis: 2.718 Cond. No. 7.07e+03
==============================================================================
Notes:
[1] Standard Errors assume that the covariance matrix of the errors is correctly specified.
[2] The condition number is large, 7.07e+03. This might indicate that there are
strong multicollinearity or other numerical problems.
Step 5 | VIF per feature
s1 55.25
s2 35.76
s3 14.29
s5 10.07
s4 9.33
bmi 1.57
s6 1.50
bp 1.42
sex 1.27
age 1.22
Step 6 | Breusch-Pagan LM = 19.484, p = 0.0345
| Jarque-Bera = 1.412, p = 0.4937 (skew 0.064, kurtosis 2.718)
| Shapiro-Wilk W = 0.9965, p = 0.6398
| Durbin-Watson = 1.794
| points with Cook's D > 4/n (0.0113): 18 ; max Cook's D = 0.0277
| points with leverage > 2k/n (0.0623): 22
| statsmodels condition number (unscaled design) = 7065.5
| classic vs HC3 robust inference (coefficients identical, SEs differ)
coef_OLS coef_HC3 SE_classic SE_HC3 p_classic p_HC3
const -341.3782 -341.3782 74.0728 74.2562 0.0000 0.0000
age 0.1377 0.1377 0.2508 0.2485 0.5834 0.5795
sex -23.0645 -23.0645 6.5362 6.3047 0.0005 0.0003
bmi 5.8464 5.8464 0.8294 0.8732 0.0000 0.0000
bp 1.1971 1.1971 0.2457 0.2556 0.0000 0.0000
s1 -1.2817 -1.2817 0.6207 0.5944 0.0397 0.0311
s2 0.8112 0.8112 0.5701 0.5336 0.1557 0.1285
s3 0.6017 0.6017 0.8579 0.8345 0.4836 0.4709
s4 10.1595 10.1595 6.8415 6.9594 0.1385 0.1443
s5 67.1090 67.1090 17.5418 16.9635 0.0002 0.0001
s6 0.2016 0.2016 0.3042 0.2916 0.5079 0.4893
| saved lab3_residual_diagnostics.png
Step 7 | RidgeCV alpha = 1.2589
| LassoCV alpha = 0.5420
| coefficients on the standardised scale (target units per 1 SD of feature)
OLS Ridge Lasso
age 1.75 1.82 1.29
sex -11.51 -11.43 -10.30
bmi 25.61 25.75 26.23
bp 16.83 16.71 16.13
s1 -44.45 -32.84 -11.37
s2 24.64 15.64 0.00
s3 7.68 2.57 -6.65
s4 13.14 11.51 7.12
s5 35.16 30.67 23.05
s6 2.35 2.48 2.31
| Lasso zero coefficients: ['s2']
Step 8 | Test-set comparison (same split, random_state=42)
train_R2 test_R2 test_RMSE test_MAE
model
OLS 0.5279 0.4526 53.8534 42.7941
RidgeCV 0.5275 0.4544 53.7650 42.8152
LassoCV 0.5244 0.4613 53.4261 42.8364
A file lab3_residual_diagnostics.png is written: left panel residuals vs fitted values (a band around 0 roughly −150 to +160 with no strong curve), right panel a normal Q-Q plot with points close to the 45° line.
Reading the results
- Generalisation: OLS 5-fold CV R² on the training set is 0.4804 (fold range 0.411–0.537); test R² is 0.4526 with RMSE 53.85. Train R² (0.5279) is only modestly above test R², so the model is not heavily overfitting; it is limited by the information in 10 features (bias), not by variance.
- Inference (Step 4):
bmi,bp,s5andsexhave p < 0.001;s1has p ≈ 0.040.age,s2,s3,s4,s6are not individually significant. Adjusted R² (0.514) is below R² (0.528) because of the 10-feature penalty. The statsmodels condition-number warning (7.07e+03) is computed on unscaled columns with very different units; use VIF to judge collinearity. - VIF (Step 5):
s1(55.25),s2(35.76),s3(14.29) ands5(10.07) exceed the common VIF > 10 rule of thumb;s4(9.33) exceeds 5. These serum measures are algebraically related (for examples4is total cholesterol divided by HDL), so their individual coefficients and signs are unstable; this is whys1ands2have large opposite-signed standardised coefficients. - Heteroscedasticity (Step 6): Breusch–Pagan p ≈ 0.0345 gives some evidence of non-constant variance at the 5% level. Coefficients are unchanged under HC3, but standard errors shift (for example
bmi0.829 → 0.873,s517.54 → 16.96). Conclusions about which features are significant at 0.05 do not change here. - Normality: Jarque–Bera p ≈ 0.49 and Shapiro–Wilk p ≈ 0.64 show no evidence against normal residuals; the Q-Q plot agrees.
- Autocorrelation: Durbin–Watson = 1.794. Rows are patients, not a time series, so DW mainly checks that there is no ordering artefact; a value near 2 is consistent with none.
- Influence: 18 rows exceed the Cook's D > 4/n rule of thumb, but the maximum is only 0.0277 (far below 1), so no single patient dominates the fit.
- Regularisation (Steps 7–8): RidgeCV chose alpha ≈ 1.26 and LassoCV alpha ≈ 0.54. Lasso set
s2exactly to 0 and shranks1from −44.45 to −11.37; ridge shrank the collinear serum coefficients but kept all non-zero. Test RMSE: OLS 53.85, Ridge 53.77, Lasso 53.43. The differences (under 0.5 on a target with SD 77) are small relative to the variability across CV folds, so on this split regularisation mainly buys stability and a simpler model, not a large accuracy gain. Do not over-interpret a single train/test split.
Verification
- The test-set table shows three rows; Lasso test R² (0.4613) ≥ Ridge (0.4544) ≥ OLS (0.4526) on this split.
coef_OLSandcoef_HC3columns are identical; only standard errors and p-values differ.- Re-run with
random_state=0intrain_test_splitto see split variance. In the reference run the test R² dropped to 0.3322 (OLS), 0.3348 (Ridge) and 0.3354 (Lasso), from about 0.45–0.46 withrandom_state=42. The ranking of models is the same, but a single split moves R² by more than 0.1, which is why CV mean and spread matter more than one test number. - Optional leakage check: move
StandardScaleroutside the pipeline, fit it on all ofX, then run CV. For plain OLS the scores do not change (OLS is scale-equivariant), but for RidgeCV/LassoCV the penalty would be tuned on statistics that include test rows. Keep scaling inside the pipeline.
Cleanup
rm lab3_diabetes_end_to_end.py lab3_residual_diagnostics.png
Full cleanup (all labs)
deactivate # leave the virtual environment
cd ~ && rm -rf ~/lr-expert-labs # removes scripts, plots and the .venv folder
Nothing was created outside ~/lr-expert-labs; no cloud resources were used.
Interactive quiz — 10 advanced MCQs
Original teaching items (not from any real exam). Medium = 50 XP, Hard = 80 XP, +10 XP per consecutive correct answer after the first in a streak.
1. Cheat sheet
1.1 Model and estimator
| Item | Formula |
|---|---|
| Model | y = Xβ + ε, X is n × (p+1) with a leading column of ones |
| OLS objective | minimise ‖y − Xβ‖² |
| Normal equations | XᵀXβ̂ = Xᵀy |
| OLS solution (full rank) | β̂ = (XᵀX)⁻¹Xᵀy — compute with QR/SVD (np.linalg.lstsq), not an explicit inverse |
| Hat matrix | H = X(XᵀX)⁻¹Xᵀ, ŷ = Hy, e = (I − H)y, trace(H) = p + 1, H² = H |
| Orthogonality | Xᵀe = 0; with intercept Σeᵢ = 0 |
| Conditioning | cond(XᵀX) = cond(X)² |
| GD update (MSE) | β ← β − η·(2/n)·Xᵀ(Xβ − y); stable if η < 1/λ_max(XᵀX/n) |
| Back-transform from standardised | β_j = w_j/s_j, β₀ = w₀ − Σ w_j μ_j/s_j |
| Interaction effect | ∂E[y]/∂x₁ = β₁ + β₃x₂ for β₃x₁x₂ |
1.2 Inference
| Item | Formula |
|---|---|
| Var(β̂) | σ²(XᵀX)⁻¹ (homoscedastic, uncorrelated errors) |
| σ̂² | SS_res/(n − p − 1) |
| SE(β̂_j) | σ̂·√[(XᵀX)⁻¹]_jj |
| t-statistic | β̂_j / SE(β̂_j), df = n − p − 1 |
| CI for β_j | β̂_j ± t_{1−α/2, n−p−1}·SE(β̂_j) |
| CI for mean at x₀ | ŷ₀ ± t·σ̂·√(x₀(XᵀX)⁻¹x₀ᵀ) |
| Prediction interval at x₀ | ŷ₀ ± t·σ̂·√(1 + x₀(XᵀX)⁻¹x₀ᵀ) |
| Overall F | (SS_reg/p) / (SS_res/(n − p − 1)) |
| Nested F (q restrictions) | [(SS_res,R − SS_res,F)/q] / [SS_res,F/(n − p − 1)] |
1.3 Metrics
| Metric | Formula | Note |
|---|---|---|
| R² | 1 − SS_res/SS_tot |
Never decreases in-sample when features are added (OLS with intercept); can be negative on test data |
| Adjusted R² | 1 − (1 − R²)(n − 1)/(n − p − 1) |
Can decrease when a feature is added (rises only if that feature's |t| > 1) |
| RMSE | √(SS_res/n) |
Units of y; penalises large errors |
| Residual standard error | √(SS_res/(n − p − 1)) |
What statsmodels/R report as σ̂ |
| MAE | (1/n)Σ∣eᵢ∣ |
Units of y; less sensitive to outliers |
| LOOCV (OLS shortcut) | (1/n)Σ(eᵢ/(1 − hᵢᵢ))² |
No refitting needed |
1.4 Regularisation
| Method | Objective (scikit-learn scaling) | Key property |
|---|---|---|
| Ridge | ‖y − Xw‖² + α‖w‖² |
Closed form (XᵀX + αI)⁻¹Xᵀy on centered data; always unique for α > 0; no exact zeros |
| Lasso | (1/(2n))‖y − Xw‖² + α‖w‖₁ |
Exact zeros via soft-thresholding; unstable choice among correlated features |
| Elastic Net | (1/(2n))‖y − Xw‖² + αρ‖w‖₁ + (α(1 − ρ)/2)‖w‖² |
Sparse + grouping of correlated features |
| Orthonormal-design intuition | ridge z/(1 + λ); lasso sign(z)·max(∣z∣ − λ, 0) |
(for ½‖·‖² loss scaling) |
Always: standardise features inside a Pipeline; do not penalise the intercept; tune α by CV on a log grid.
1.5 Diagnostics and rules of thumb
These thresholds are conventions used for screening, not hard rules. Always look at plots and at the practical consequences.
| Check | Tool | Common rule of thumb |
|---|---|---|
| Non-linearity | Residuals vs fitted / vs each feature | Visible curve → add terms or transform |
| Heteroscedasticity | Funnel in residual plot; Breusch–Pagan (het_breuschpagan), White |
Small p-value (for example < 0.05) suggests non-constant variance → robust (HC3) SEs, transform y, or WLS |
| Normality | Q-Q plot; Jarque–Bera; Shapiro–Wilk | Matters mainly for small-n inference and prediction intervals; large n makes tests flag trivial deviations |
| Autocorrelation | Durbin–Watson (time-ordered data); Breusch–Godfrey | About 2 = none; roughly < 1.5 or > 2.5 worth investigating; use tabulated bounds for a formal test |
| Multicollinearity | VIF = 1/(1 − R²_j) |
VIF > 5 deserves attention; VIF > 10 often treated as serious |
| Conditioning | Condition number of standardised X | > 30 often cited (Belsley–Kuh–Welsch) as moderate/strong collinearity; statsmodels warns at > 1000 on the unscaled design |
| Leverage | hᵢᵢ |
> 2(p+1)/n (or 3(p+1)/n) flags unusual feature values |
| Influence | Cook's distance | > 1 large; > 4/n worth a look |
| Outliers in y | Studentised residuals | |r| > 2 (or 3) worth a look |
2. Common mistakes
- Choosing models by training R². R² never decreases as features are added. Compare models by cross-validated error (or at least adjusted R²/AIC/BIC for in-sample comparisons).
- Forgetting the intercept in statsmodels.
sm.OLS(y, X)does not add a constant; withoutsm.add_constant(X)the model is forced through the origin, and the reported R² is uncentered and not comparable. - Falling into the dummy variable trap. Encoding all k levels plus an intercept makes
XᵀXsingular. Use k − 1 dummies (drop="first") for OLS with inference. - Misreading coefficients. A coefficient is a partial association holding the other included features constant; it changes when features are added or removed, is causal only under extra assumptions, and carries units, so raw magnitudes are not comparable across features (standardise first, and remember collinearity makes individual magnitudes unstable).
- Thinking heteroscedasticity biases coefficients. It does not (if exogeneity holds); it breaks the classical standard errors. Fix inference with robust SEs; do not drop features because of it.
- Treating VIF cut-offs as laws. VIF > 5 or > 10 are rules of thumb. High VIF matters when you need individual coefficients; it matters much less for pure prediction if the collinearity pattern is stable.
- Data leakage through preprocessing. Fitting scalers, imputers, encoders or feature selection on all data before splitting or CV. Put them inside a
Pipeline. - Regularising unscaled features. Ridge and lasso penalties depend on feature units; without standardisation, features with large numeric ranges are penalised less.
- Random splits on time series or grouped data. Use
TimeSeriesSplitorGroupKFold; otherwise the validation score is optimistic. Relatedly, p-values reported after stepwise or lasso selection are over-optimistic. - Extrapolating. Linear models will predict outside the training range without warning; validate input ranges at serving time.
3. Interview questions with model answers
Q1. Derive the OLS estimator in matrix form. When does it fail to exist?
Minimise S(β) = ‖y − Xβ‖² = yᵀy − 2βᵀXᵀy + βᵀXᵀXβ. The gradient −2Xᵀy + 2XᵀXβ is zero at XᵀXβ̂ = Xᵀy; the Hessian 2XᵀX is positive semi-definite, so this is a minimum. If X has full column rank, XᵀX is invertible and β̂ = (XᵀX)⁻¹Xᵀy is unique. It fails to be unique when columns are linearly dependent (dummy trap, duplicated features in different units) or when p + 1 > n; then use the pseudo-inverse (minimum-norm solution), drop redundant columns, or use ridge.
Q2. Why do libraries not compute (XᵀX)⁻¹ directly?
Forming XᵀX squares the condition number (cond(XᵀX) = cond(X)²), so precision can be lost before the inverse is even computed, and explicit inversion is slower and less stable than solving. QR (X = QR, solve Rβ = Qᵀy) works at the conditioning of X; SVD additionally handles rank deficiency and gives the minimum-norm solution. np.linalg.lstsq uses an SVD-based LAPACK routine and scikit-learn's LinearRegression calls SciPy's lstsq.
Q3. Explain the hat matrix and leverage.
H = X(XᵀX)⁻¹Xᵀ orthogonally projects y onto the column space of X: ŷ = Hy. It is symmetric and idempotent with trace p + 1. The diagonal hᵢᵢ is the leverage of row i: how far its feature values are from the feature centroid and how strongly yᵢ pulls ŷᵢ. Average leverage is (p+1)/n; residual variance is σ²(1 − hᵢᵢ), so high-leverage points have smaller raw residuals, which is why studentised residuals are used.
Q4. State the Gauss–Markov assumptions. Which are needed for unbiasedness, and where does normality come in?
Linearity in parameters, full column rank, E[ε | X] = 0, and Var(ε | X) = σ²I. The first three give unbiasedness; adding the fourth makes OLS BLUE. Normality is not part of the theorem; it makes t and F statistics exactly t- and F-distributed in finite samples. With large n, inference is approximately valid without it.
Q5. What does heteroscedasticity do to OLS, and how do you handle it?
Coefficients stay unbiased and consistent, but OLS is no longer efficient and the classical σ²(XᵀX)⁻¹ variance is wrong, so t-tests and CIs are unreliable. Detect with residual-vs-fitted plots and Breusch–Pagan or White tests. Handle with heteroscedasticity-consistent (HC0–HC3) standard errors, a variance-stabilising transform of y (for example log), or weighted least squares if the variance structure is known.
Q6. What is multicollinearity, how do you measure it, and when does it matter?
Strong linear dependence among features. It inflates Var(β̂_j) by VIF_j = 1/(1 − R²_j), making coefficients unstable and individual t-tests weak while overall fit and the F-test can stay strong. Measure with VIF (rules of thumb > 5, > 10) and the condition number of standardised X (> 30 often cited). It matters for interpreting individual coefficients; for prediction it matters less if the same collinearity pattern holds in future data. Remedies: drop/combine features, center before forming powers or interactions, ridge, PCA/PLS.
Q7. Ridge vs lasso vs Elastic Net — how do you choose?
Ridge (L2) shrinks all coefficients smoothly, handles collinearity well by sharing weight, always has a unique solution, but never yields exact zeros. Lasso (L1) yields sparse models via soft-thresholding, useful when few features matter, but picks arbitrarily among correlated features and selects at most n features when p > n. Elastic Net mixes both: sparse and stable with correlated groups. Choose by the goal (interpretability/sparsity vs pure prediction) and by cross-validated error; standardise features first.
Q8. Why is polynomial regression still "linear" regression?
Linearity refers to the parameters. y = β₀ + β₁x + β₂x² is a linear combination of the columns 1, x, x² with coefficients β, so OLS, the normal equations and the usual inference apply. The fitted curve is non-linear in x. Caveats: powers are collinear (center or use orthogonal polynomials), high degree overfits and extrapolates badly.
Q9. How do you avoid data leakage in a regression workflow?
Split first (or use CV), then learn every data-dependent transformation (imputation, scaling, encoding, feature selection, target encoding) inside a Pipeline so it is fit only on training folds. Use TimeSeriesSplit for temporal data and GroupKFold when the same entity appears in many rows. Exclude features that would not be available at prediction time. Use the test set once.
Q10. A linear regression model is in production. What do you monitor and when do you retrain?
Inputs: schema violations, missing rates, out-of-range rates, feature distribution drift (for example PSI or KS tests against the training distribution). Outputs: prediction distribution. Performance once labels arrive: rolling RMSE/MAE, residual bias overall and by segment, prediction-interval coverage. Operations: latency and errors. Retrain on a schedule or when drift/error thresholds are crossed, validate the challenger against the current model on recent data, deploy gradually (shadow or canary), and keep rollback. Version the entire pipeline, data snapshot and library versions.
4. Mini project brief — "Explainable price model with honest uncertainty"
Dataset: California housing (sklearn.datasets.fetch_california_housing). It is downloaded once from the internet on first use (then cached under ~/scikit_learn_data); everything else runs locally at no cost. In a check run it returned 20,640 rows, 8 numeric features (MedInc, HouseAge, AveRooms, AveBedrms, Population, AveOccup, Latitude, Longitude) and target MedHouseVal (median house value in units of $100,000), with a maximum of about 5.0, which suggests the target is capped. If network access is not available, use the diabetes dataset from Lab 3.
Deliverables
1. A reproducible script or notebook with fixed random seeds and a pinned requirements.txt.
2. An EDA section: distributions, correlation matrix, VIF table; note the capped target and decide (with justification) how to treat capped rows.
3. A baseline Pipeline(StandardScaler, LinearRegression) with 5-fold CV (report mean ± SD of RMSE and R²) and a single held-out test evaluation.
4. At least two feature-engineering iterations (for example log of skewed features, AveBedrms/AveRooms ratio, polynomial or interaction terms via PolynomialFeatures, spatial bins of latitude/longitude) with CV evidence for each.
5. RidgeCV, LassoCV and ElasticNetCV comparison inside pipelines; a coefficient path or coefficient table on the standardised scale.
6. statsmodels OLS of the final feature set: summary, Breusch–Pagan, Q-Q plot, Cook's distance, and a comparison of classical vs HC3 standard errors.
7. Prediction intervals for 5 example rows and an empirical coverage check of 95% prediction intervals on the test set.
8. A one-page model card: intended use, data, metrics, limitations (capping, spatial correlation, extrapolation), monitoring plan and retraining trigger.
Assessment rubric (suggested) - Correct, leakage-free evaluation (30%) - Diagnostics and correct statistical interpretation (25%) - Regularisation and feature engineering backed by CV evidence (20%) - Production readiness: pipeline serialisation, input validation, monitoring plan (15%) - Clarity of the model card (10%)
5. Flashcards
| # | Question | Answer |
|---|---|---|
| 1 | Normal equations? | XᵀXβ̂ = Xᵀy |
| 2 | Trace of the hat matrix? | p + 1, the number of fitted parameters |
| 3 | Two properties of OLS residuals (with intercept)? | Sum to zero; orthogonal to every column of X (Xᵀe = 0) |
| 4 | Relationship between cond(X) and cond(XᵀX)? | cond(XᵀX) = cond(X)² |
| 5 | What does BLUE stand for? | Best Linear Unbiased Estimator |
| 6 | Is normality a Gauss–Markov assumption? | No; it is needed only for exact small-sample t/F distributions |
| 7 | Effect of heteroscedasticity on OLS coefficients? | None on bias; it invalidates classical standard errors and removes efficiency |
| 8 | VIF formula and SE inflation? | VIF_j = 1/(1 − R²_j); SE inflated by √VIF_j |
| 9 | Durbin–Watson value for no autocorrelation? | About 2 (range 0–4; DW ≈ 2(1 − ρ̂)) |
| 10 | Cook's distance rules of thumb? | > 1 large; > 4/n worth inspecting |
| 11 | Adjusted R² formula? | 1 − (1 − R²)(n − 1)/(n − p − 1) |
| 12 | Ridge closed form? | (XᵀX + λI)⁻¹Xᵀy (centered data, intercept unpenalised) |
| 13 | Why does lasso give exact zeros? | L1 penalty is non-differentiable at 0 → soft-thresholding; L1 ball has corners on the axes |
| 14 | Fix for the dummy variable trap? | Use k − 1 dummies with an intercept (or drop the intercept) |
| 15 | Confidence vs prediction interval? | CI is for the mean response; PI is for a new observation and adds σ², so it is wider |
6. Further reading (official documentation and standard texts)
All links were checked with curl and returned HTTP 200 on 2026-10-02.
scikit-learn
- Linear models user guide (OLS, Ridge, Lasso, Elastic Net): https://scikit-learn.org/stable/modules/linear_model.html
- LinearRegression: https://scikit-learn.org/stable/modules/generated/sklearn.linear_model.LinearRegression.html
- RidgeCV: https://scikit-learn.org/stable/modules/generated/sklearn.linear_model.RidgeCV.html
- LassoCV: https://scikit-learn.org/stable/modules/generated/sklearn.linear_model.LassoCV.html
- ElasticNetCV: https://scikit-learn.org/stable/modules/generated/sklearn.linear_model.ElasticNetCV.html
- Pipeline: https://scikit-learn.org/stable/modules/generated/sklearn.pipeline.Pipeline.html
- Cross-validation: https://scikit-learn.org/stable/modules/cross_validation.html
- Common pitfalls (including data leakage): https://scikit-learn.org/stable/common_pitfalls.html
- Toy datasets (diabetes): https://scikit-learn.org/stable/datasets/toy_dataset.html
statsmodels
- OLS: https://www.statsmodels.org/stable/generated/statsmodels.regression.linear_model.OLS.html
- Linear regression overview: https://www.statsmodels.org/stable/regression.html
- variance_inflation_factor: https://www.statsmodels.org/stable/generated/statsmodels.stats.outliers_influence.variance_inflation_factor.html
- het_breuschpagan: https://www.statsmodels.org/stable/generated/statsmodels.stats.diagnostic.het_breuschpagan.html
- durbin_watson: https://www.statsmodels.org/stable/generated/statsmodels.stats.stattools.durbin_watson.html
- Regression diagnostics example: https://www.statsmodels.org/stable/examples/notebooks/generated/regression_diagnostics.html
NumPy
- numpy.linalg.lstsq: https://numpy.org/doc/stable/reference/generated/numpy.linalg.lstsq.html
- numpy.linalg.pinv: https://numpy.org/doc/stable/reference/generated/numpy.linalg.pinv.html
- numpy.linalg.qr: https://numpy.org/doc/stable/reference/generated/numpy.linalg.qr.html
Books (free online editions from the authors) - James, Witten, Hastie, Tibshirani (and Taylor), An Introduction to Statistical Learning: https://www.statlearning.com/ - Hastie, Tibshirani, Friedman, The Elements of Statistical Learning: https://hastie.su.domains/ElemStatLearn/