Skip to the content

Algorithms on This Page

1.6 Linear Regression 1.7 Polynomial Regression 1.8 Ridge Regression (L2 Regularisation) 1.9 Lasso Regression (L1 Regularisation) 1.10 Random Forest Regression

1.6  Linear Regression

ParametricClosed-Form
DEFINITION

Linear Regression models the relationship between a dependent variable \(y\) and one or more independent variables \(\mathbf{x}\) as a linear function. The parameters \(\boldsymbol{\beta}\) are estimated by minimising the sum of squared residuals (OLS — Ordinary Least Squares), yielding a closed-form solution.

1.6.1  Mathematical Foundation

FORMULAE

Model:

\[\hat{y} = \beta_0 + \beta_1 x_1 + \beta_2 x_2 + \cdots + \beta_p x_p = \boldsymbol{\beta}^\top \tilde{\mathbf{x}}\]

OLS Cost Function (RSS):

\[\text{RSS}(\boldsymbol{\beta}) = \sum_{i=1}^{N}(y_i - \hat{y}_i)^2 = \|\mathbf{y} - \mathbf{X}\boldsymbol{\beta}\|^2\]

Normal Equation (closed-form solution):

\[\hat{\boldsymbol{\beta}} = (\mathbf{X}^\top \mathbf{X})^{-1}\mathbf{X}^\top \mathbf{y}\]

Coefficient of Determination:

\[R^2 = 1 - \frac{\text{RSS}}{\text{TSS}} = 1 - \frac{\sum(y_i-\hat{y}_i)^2}{\sum(y_i-\bar{y})^2}\]

Assumptions: Linearity · Independence · Homoscedasticity · Normality (LINE)

1.6.2  How It Works

The normal equation gives the exact solution in \(O(Np^2 + p^3)\) time — forming \(\mathbf{X}^\top\mathbf{X}\) costs \(O(Np^2)\) and dominates whenever \(N \gg p\), which is the usual case; the \(O(p^3)\) term is the solve. Gradient descent is preferred when \(p\) is very large. Each coefficient \(\beta_j\) represents the expected change in \(y\) per unit increase in \(x_j\), holding all others constant. Diagnostics include residual plots (heteroscedasticity), Q-Q plots (normality), and VIF (multicollinearity). Adjusted \(R^2\) penalises for additional predictors: \(\bar{R}^2 = 1 - (1-R^2)\frac{N-1}{N-p-1}\).

1.6.3  Assumptions and Failure Modes

ASSUMES
  • Linearity · Independence · Homoscedasticity · Normality of residuals
BREAKS WHEN
  • Predictors are collinear — coefficients become unstable and flip sign
  • Residual variance grows with the fitted value — inference is invalid
  • Outliers are present — squared error gives them outsized leverage

1.6.4  Worked Examples

FINANCE

📊 House Price Prediction

Predicting property selling price ($) from area (m²), bedrooms, distance to city centre, and age, over 1,200 transactions. The fitted coefficient on area is the point of the exercise — it estimates the marginal value of a square metre, holding the other predictors fixed, which is the number an appraiser actually wants.

Area m²BedsDist kmPrice $k
8224.2245
14548.1398
5812.0198
AGRICULTURE

🌧️ Crop Yield vs. Rainfall

Predicting wheat yield (tonnes/ha) from annual rainfall (mm), fertiliser use (kg/ha), and temperature. 10-year panel data from 200 farms. The rainfall coefficient estimates marginal yield per additional 100 mm. Note that panel data breaks the independence assumption — the same farm recurs every year — so standard errors must be clustered by farm or the significance of every coefficient is overstated.

Rain mmFert kg/haTemp°CYield t/ha
680120184.2
820180165.1
48080223.0
MEDICINE

💉 Drug Dosage-Response

Modelling the linear relationship between warfarin dosage (mg/day) and INR (blood clotting ratio). 300 patient records show a strong linear relationship: \(\text{INR} = 0.8 + 0.42 \times \text{dose}\).

Dose mg/dayWeight kgAgeINR
2.572551.8
5.068622.7
7.580703.5

1.6.5  Code

Linear Regression
import numpy as np
import pandas as pd
from sklearn.linear_model import LinearRegression
from sklearn.model_selection import train_test_split, cross_val_score
from sklearn.metrics import mean_squared_error, r2_score
from sklearn.preprocessing import StandardScaler

np.random.seed(42)
N = 1200

# ── Simulate House Price Dataset ──────────────────────────────
area     = np.random.uniform(45, 200, N)
beds     = np.random.randint(1, 6, N).astype(float)
dist_km  = np.random.uniform(1, 25, N)
age_yr   = np.random.uniform(0, 50, N)
noise    = np.random.normal(0, 15, N)

price = (2.4*area + 18*beds - 4.2*dist_km - 0.8*age_yr + 80 + noise)  # $k
df = pd.DataFrame({'area':area,'beds':beds,'dist_km':dist_km,'age_yr':age_yr,'price':price})

X = df[['area','beds','dist_km','age_yr']].values
y = df['price'].values

X_tr, X_te, y_tr, y_te = train_test_split(X, y, test_size=0.2, random_state=42)

# ── Train OLS ─────────────────────────────────────────────────
lr = LinearRegression()
lr.fit(X_tr, y_tr)
y_pred = lr.predict(X_te)

r2   = r2_score(y_te, y_pred)
rmse = np.sqrt(mean_squared_error(y_te, y_pred))
print("=== Linear Regression — House Price ===")
print(f"R²   = {r2:.4f}")
print(f"RMSE = {rmse:.2f} $k")
print(f"Intercept: {lr.intercept_:.3f}")
for name, coef in zip(['area','beds','dist_km','age_yr'], lr.coef_):
    print(f"  {name:10s}: β = {coef:+.4f}")

# ── 5-fold CV ─────────────────────────────────────────────────
cv_r2 = cross_val_score(lr, X, y, cv=5, scoring='r2')
print(f"\nCV R²: {cv_r2.mean():.4f} ± {cv_r2.std():.4f}")

# ── Residual diagnostics (print summary) ─────────────────────
residuals = y_te - y_pred
print(f"\nResidual stats — Mean:{residuals.mean():.3f}, Std:{residuals.std():.3f}")
print(f"  Skewness: {pd.Series(residuals).skew():.3f}")

# ── Agriculture: Crop Yield ───────────────────────────────────
np.random.seed(7)
n2 = 500
rain = np.random.uniform(400, 1000, n2)
fert = np.random.uniform(50, 250, n2)
temp = np.random.uniform(12, 28, n2)
yield_t = 0.35*(rain/100) + 0.012*fert - 0.08*temp + 2.5 + np.random.normal(0,.3,n2)
Xa = np.column_stack([rain, fert, temp])
ya = yield_t
lr_agr = LinearRegression().fit(Xa, ya)
print(f"\nCrop Yield R² = {r2_score(ya, lr_agr.predict(Xa)):.4f}")
set.seed(42)
N <- 1200

# ── Simulate House Price Data ─────────────────────────────────
area    <- runif(N, 45, 200)
beds    <- sample(1:5, N, replace=TRUE)
dist_km <- runif(N, 1, 25)
age_yr  <- runif(N, 0, 50)
price   <- 2.4*area + 18*beds - 4.2*dist_km - 0.8*age_yr + 80 + rnorm(N,0,15)

df <- data.frame(area, beds, dist_km, age_yr, price)

# ── Train / Test Split ────────────────────────────────────────
idx <- sample(1:N, 0.8*N)
tr  <- df[idx,]; te <- df[-idx,]

# ── OLS Linear Regression ─────────────────────────────────────
model <- lm(price ~ ., data=tr)
summary(model)

# ── Evaluate on test set ──────────────────────────────────────
y_pred <- predict(model, te)
ss_res <- sum((te$price - y_pred)^2)
ss_tot <- sum((te$price - mean(te$price))^2)
r2     <- 1 - ss_res/ss_tot
rmse   <- sqrt(mean((te$price - y_pred)^2))
cat(sprintf("Test R² = %.4f,  RMSE = %.2f $k\n", r2, rmse))

# ── Residual Diagnostics ──────────────────────────────────────
resid  <- te$price - y_pred
cat(sprintf("Residuals — Mean: %.3f, SD: %.3f\n", mean(resid), sd(resid)))

# ── 5-fold CV using caret ─────────────────────────────────────
library(caret)
ctrl  <- trainControl(method="cv", number=5)
cv_m  <- train(price~., data=df, method="lm", trControl=ctrl)
cat(sprintf("CV RMSE = %.2f, CV R² = %.4f\n",
            cv_m$results$RMSE, cv_m$results$Rsquared))

1.7  Polynomial Regression

Non-LinearFeature Engineering
DEFINITION

Polynomial Regression extends linear regression by adding polynomial terms of the predictor variables, allowing the model to capture non-linear relationships. It remains a linear model in terms of the parameters \(\boldsymbol{\beta}\) — only the features are transformed.

1.7.1  Mathematical Foundation

FORMULAE

Univariate degree-\(d\) polynomial:

\[\hat{y} = \beta_0 + \beta_1 x + \beta_2 x^2 + \cdots + \beta_d x^d\]

Multivariate (interaction + higher-order terms):

\[\hat{y} = \boldsymbol{\beta}^\top \phi(\mathbf{x}), \quad \phi(\mathbf{x}) = [1,\, x_1,\, x_2,\, x_1^2,\, x_1 x_2,\, x_2^2,\, \ldots]\]

Number of features for \(p\) vars, degree \(d\):

\[\binom{p+d}{d} \quad \text{(grows combinatorially!)}\]

Bias-Variance: degree \(\uparrow\) → bias \(\downarrow\), variance \(\uparrow\) (use cross-validation to choose \(d\))

1.7.2  How It Works

PolynomialFeatures transforms the original features into polynomial and interaction terms before fitting an ordinary linear regression. Choosing degree \(d\) via cross-validation prevents overfitting. For degree 2 with \(p=5\) features, the expanded space has 21 features; degree 3 gives 56. Regularisation (Ridge/Lasso) is critical for higher degrees. Polynomial regression is ideal when domain knowledge suggests a curved relationship.

1.7.3  Assumptions and Failure Modes

ASSUMES
  • The relationship is smooth and polynomial in form
BREAKS WHEN
  • Degree is chosen without cross-validation — the fit oscillates wildly
  • Extrapolating beyond the training range — polynomials diverge fast
  • Terms are not centred — the design matrix becomes badly conditioned

1.7.4  Worked Examples

FINANCE

📉 Non-Linear Interest Rate Curve

Modelling the yield curve (bond yield vs. maturity in years) which is non-linear. Degree-3 polynomial fits the curve well: \(y = 1.2 + 0.8t - 0.08t^2 + 0.003t^3\) where \(t\) is maturity in years.

Maturity yrYield %
11.95
53.12
103.78
304.20
AGRICULTURE

🌱 Fertiliser Response Curve

Crop yield initially increases with nitrogen fertiliser but plateaus and even declines at very high doses (Mitscherlich-Baule effect). Degree-2 polynomial: \(\hat{y} = 1.8 + 0.07N - 0.0002N^2\).

N kg/haYield t/ha
01.8
1006.3
2008.5
3007.2
MEDICINE

⏱️ Pharmacokinetics (Drug Concentration)

Plasma drug concentration follows a non-linear decay after oral dosing. Degree-3 polynomial captures the absorption-distribution-elimination curve across 8 time points post-dose.

Time hrConc ng/mL
0.5120
2310
6185
1242

1.7.5  Code

Polynomial Regression
import numpy as np
import pandas as pd
from sklearn.preprocessing import PolynomialFeatures
from sklearn.linear_model import LinearRegression
from sklearn.pipeline import Pipeline
from sklearn.model_selection import cross_val_score, train_test_split
from sklearn.metrics import r2_score, mean_squared_error

np.random.seed(42)
N = 300

# ── Simulate Fertiliser Response (Quadratic) ──────────────────
nitrogen = np.random.uniform(0, 350, N)
yield_t  = 1.8 + 0.07*nitrogen - 0.0002*nitrogen**2 + np.random.normal(0, 0.25, N)

X = nitrogen.reshape(-1, 1)
y = yield_t

X_tr, X_te, y_tr, y_te = train_test_split(X, y, test_size=0.2, random_state=42)

# ── Compare degrees via cross-validation ─────────────────────
print("Degree | CV R²     | CV RMSE")
print("-------|-----------|--------")
for deg in range(1, 6):
    pipe = Pipeline([
        ('poly', PolynomialFeatures(degree=deg, include_bias=False)),
        ('lr',   LinearRegression())
    ])
    cv_r2   = cross_val_score(pipe, X_tr, y_tr, cv=5, scoring='r2')
    cv_rmse = np.sqrt(-cross_val_score(pipe, X_tr, y_tr, cv=5,
                                        scoring='neg_mean_squared_error'))
    print(f"  d={deg}  | {cv_r2.mean():.4f}±{cv_r2.std():.3f} | {cv_rmse.mean():.4f}")

# ── Best model (degree 2) ─────────────────────────────────────
best_pipe = Pipeline([
    ('poly', PolynomialFeatures(degree=2, include_bias=True)),
    ('lr',   LinearRegression())
])
best_pipe.fit(X_tr, y_tr)
y_pred = best_pipe.predict(X_te)
print(f"\nTest R² = {r2_score(y_te, y_pred):.4f}")
print(f"RMSE    = {np.sqrt(mean_squared_error(y_te, y_pred)):.4f}")

lr_step = best_pipe.named_steps['lr']
print(f"\nCoefficients: intercept={lr_step.intercept_:.4f}, β₁={lr_step.coef_[1]:.6f}, β₂={lr_step.coef_[2]:.8f}")

# ── Predict optimal N ─────────────────────────────────────────
n_grid = np.linspace(0, 350, 1000).reshape(-1,1)
y_grid = best_pipe.predict(n_grid)
optimal_N = n_grid[np.argmax(y_grid)][0]
print(f"\nOptimal Nitrogen dose: {optimal_N:.1f} kg/ha → Yield: {y_grid.max():.2f} t/ha")

# ── Medicine: Pharmacokinetics ────────────────────────────────
np.random.seed(3)
time  = np.random.uniform(0.25, 24, 100)
conc  = 400 * time * np.exp(-0.35*time) + np.random.normal(0, 10, 100)
pipe_pk = Pipeline([('poly', PolynomialFeatures(3)), ('lr', LinearRegression())])
pipe_pk.fit(time.reshape(-1,1), conc)
print(f"\nPharmacokinetics Polynomial R² = {pipe_pk.score(time.reshape(-1,1), conc):.4f}")
set.seed(42); N <- 300

# ── Simulate Fertiliser Response Data ────────────────────────
nitrogen <- runif(N, 0, 350)
yield_t  <- 1.8 + 0.07*nitrogen - 0.0002*nitrogen^2 + rnorm(N,0,.25)
df <- data.frame(N=nitrogen, yield=yield_t)

idx <- sample(1:N, 0.8*N)
tr  <- df[idx,]; te <- df[-idx,]

# ── Compare polynomial degrees ───────────────────────────────
for(deg in 1:5) {
  form <- as.formula(paste("yield ~", paste0("poly(N,",deg,")")))
  m    <- lm(form, data=tr)
  pred <- predict(m, te)
  r2   <- 1 - sum((te$yield-pred)^2)/sum((te$yield-mean(te$yield))^2)
  cat(sprintf("Degree %d: Test R² = %.4f\n", deg, r2))
}

# ── Best model: degree 2 ──────────────────────────────────────
best_model <- lm(yield ~ poly(N, 2, raw=TRUE), data=tr)
summary(best_model)

y_pred <- predict(best_model, te)
r2_val <- 1 - sum((te$yield-y_pred)^2)/sum((te$yield-mean(te$yield))^2)
rmse   <- sqrt(mean((te$yield-y_pred)^2))
cat(sprintf("\nTest R² = %.4f, RMSE = %.4f\n", r2_val, rmse))

# ── Optimal nitrogen dose ─────────────────────────────────────
n_seq  <- data.frame(N=seq(0, 350, by=0.5))
preds  <- predict(best_model, n_seq)
opt_N  <- n_seq$N[which.max(preds)]
cat(sprintf("Optimal N = %.1f kg/ha → Yield = %.2f t/ha\n",
            opt_N, max(preds)))

1.8  Ridge Regression (L2 Regularisation)

RegularisationShrinkage
DEFINITION

Ridge Regression adds an L2 penalty on the magnitude of coefficients to the OLS cost function. This shrinks coefficients towards zero (but never exactly to zero), reducing variance at the cost of a small bias — particularly effective when features are correlated (multicollinearity).

1.8.1  Mathematical Foundation

FORMULAE

Ridge objective:

\[\mathcal{L}_{\text{ridge}}(\boldsymbol{\beta}) = \underbrace{\sum_{i=1}^{N}(y_i - \hat{y}_i)^2}_{\text{RSS}} + \lambda \underbrace{\sum_{j=1}^{p}\beta_j^2}_{\text{L2 penalty}}\]

Closed-form solution:

\[\hat{\boldsymbol{\beta}}_{\text{ridge}} = (\mathbf{X}^\top\mathbf{X} + \lambda\mathbf{I})^{-1}\mathbf{X}^\top\mathbf{y}\]

SVD insight — shrinkage acts on directions, not on individual features:

Write \(\mathbf{X} = UDV^\top\) with singular values \(d_1 \ge d_2 \ge \cdots \ge d_p\). In the rotated (principal-component) basis \(\boldsymbol{\gamma} = V^\top\boldsymbol{\beta}\), Ridge shrinks each coordinate independently:

\[\hat{\gamma}_j^{\text{ridge}} = \frac{d_j^2}{d_j^2 + \lambda}\,\hat{\gamma}_j^{\text{OLS}}\]

Small \(d_j\) means low variance along that direction, so the factor \(d_j^2/(d_j^2+\lambda)\) is small: Ridge shrinks the low-variance directions hardest and leaves high-variance directions nearly untouched. The factor applies per original feature only in the special case where \(\mathbf{X}\) has orthonormal columns — precisely the case Ridge exists to handle the absence of.

Constraint form (equivalently): \(\min_{\boldsymbol{\beta}} \text{RSS}\) subject to \(\sum_j \beta_j^2 \le t\)

1.8.2  How It Works

As \(\lambda \to 0\), Ridge → OLS; as \(\lambda \to \infty\), all coefficients shrink to 0. The \((\mathbf{X}^\top\mathbf{X} + \lambda\mathbf{I})\) term makes the matrix invertible even when \(\mathbf{X}^\top\mathbf{X}\) is singular (perfect multicollinearity). Ridge keeps all features — it does not perform variable selection. The optimal \(\lambda\) is found via cross-validation. Feature standardisation is essential before applying Ridge because L2 penalty is not scale-invariant.

1.8.3  Assumptions and Failure Modes

ASSUMES
  • Features standardised — the L2 penalty is not scale-invariant
BREAKS WHEN
  • Only a few predictors truly matter — Ridge keeps them all, so use Lasso
  • The intercept is penalised — it should be excluded from the penalty

1.8.4  Worked Examples

FINANCE

💹 Portfolio Return Prediction

Predicting monthly portfolio returns using 40 correlated technical and macro-economic factors. Ridge shrinks noisy coefficients and controls variance. With 40 heavily correlated factors, OLS coefficients become large and unstable — flipping sign between samples — while Ridge keeps them small and stable; the gap in held-out performance widens as the factors become more correlated.

FactorOLS βRidge β (λ=10)
P/E Ratio0.840.61
Market Beta1.421.18
Momentum-0.32-0.24
AGRICULTURE

🌡️ Climate Multi-Factor Crop Model

Predicting corn yield using 25 correlated climate variables (weekly temperature, rainfall, solar radiation). Ridge regression handles multicollinearity between consecutive weekly measurements better than OLS.

VariablesOLS RMSERidge RMSE
25 climate0.820.61
ConditionOverfitStable
MEDICINE

🧪 Gene Expression to Drug Response

Predicting IC50 (drug sensitivity) from 200 correlated gene expression values for 150 cancer cell lines (wide data: \(p \gg n\)). Ridge provides stable estimates where OLS fails due to singular matrix.

SettingR² (test)Stability
OLSN/ASingular
Ridge λ=50.65Good
Ridge λ=500.58Best

1.8.5  Code

Ridge Regression
import numpy as np
import pandas as pd
from sklearn.linear_model import Ridge, RidgeCV, LinearRegression
from sklearn.model_selection import train_test_split
from sklearn.preprocessing import StandardScaler
from sklearn.metrics import r2_score, mean_squared_error

np.random.seed(42)
N, P = 300, 40

# ── Simulate Highly Correlated Financial Factors ──────────────
base      = np.random.randn(N, 5)
X_raw     = base @ np.random.randn(5, P) + 0.3*np.random.randn(N, P)
true_beta = np.random.randn(P) * np.random.choice([0,1], P, p=[0.6,0.4])
y         = X_raw @ true_beta + np.random.normal(0, 1.5, N)

X_tr, X_te, y_tr, y_te = train_test_split(X_raw, y, test_size=0.25, random_state=42)
scaler = StandardScaler()
X_tr_sc = scaler.fit_transform(X_tr); X_te_sc = scaler.transform(X_te)

# ── OLS vs Ridge ──────────────────────────────────────────────
ols = LinearRegression().fit(X_tr_sc, y_tr)
ols_r2 = r2_score(y_te, ols.predict(X_te_sc))

alphas = np.logspace(-3, 4, 100)
ridge_cv = RidgeCV(alphas=alphas, cv=5, scoring='r2')
ridge_cv.fit(X_tr_sc, y_tr)
best_alpha = ridge_cv.alpha_
ridge_r2 = r2_score(y_te, ridge_cv.predict(X_te_sc))

print("=== Ridge Regression — Portfolio Factors ===")
print(f"OLS  Test R²   = {ols_r2:.4f}")
print(f"Ridge Test R²  = {ridge_r2:.4f}  (best λ = {best_alpha:.4f})")

# ── Compare coefficient shrinkage ─────────────────────────────
print("\nCoefficient shrinkage (first 6 features):")
print(f"{'Feature':10} | {'OLS β':>10} | {'Ridge β':>10}")
for i in range(6):
    print(f"{'X'+str(i):10} | {ols.coef_[i]:10.4f} | {ridge_cv.coef_[i]:10.4f}")

# ── Ridge Path ────────────────────────────────────────────────
print("\nRidge path (λ → coef norm):")
for lam in [0.01, 1, 10, 100, 1000]:
    r = Ridge(alpha=lam).fit(X_tr_sc, y_tr)
    print(f"  λ={lam:6.2f}  |coef|₂ = {np.linalg.norm(r.coef_):.4f}")
library(glmnet); set.seed(42)

N <- 300; P <- 40
base  <- matrix(rnorm(N*5), N, 5)
X     <- base %*% matrix(rnorm(5*P), 5, P) + 0.3*matrix(rnorm(N*P), N, P)
beta  <- rnorm(P) * sample(c(0,1), P, replace=TRUE, prob=c(0.6,0.4))
y     <- X %*% beta + rnorm(N, 0, 1.5)
X     <- scale(X)

idx  <- sample(1:N, 0.75*N)
X_tr <- X[idx,]; y_tr <- y[idx]
X_te <- X[-idx,]; y_te <- y[-idx]

# ── Ridge via glmnet (alpha=0) ────────────────────────────────
ridge_cv <- cv.glmnet(X_tr, y_tr, alpha=0, nfolds=5)
best_lam <- ridge_cv$lambda.min
cat(sprintf("Best λ = %.4f\n", best_lam))

# ── Predict and evaluate ──────────────────────────────────────
y_pred <- predict(ridge_cv, newx=X_te, s="lambda.min")
ss_res <- sum((y_te - y_pred)^2)
ss_tot <- sum((y_te - mean(y_te))^2)
cat(sprintf("Ridge Test R² = %.4f\n", 1 - ss_res/ss_tot))

# ── OLS baseline ─────────────────────────────────────────────
lm_ols <- lm(y_tr ~ X_tr)
y_ols  <- cbind(1, X_te) %*% coef(lm_ols)
cat(sprintf("OLS   Test R² = %.4f\n",
            1 - sum((y_te-y_ols)^2)/ss_tot))

# ── Ridge path ───────────────────────────────────────────────
cat("\nRidge path:\n")
lams <- c(0.01, 1, 10, 100, 1000)
for(l in lams) {
  coef_r <- coef(glmnet(X_tr, y_tr, alpha=0, lambda=l))[-1]
  cat(sprintf("  λ=%-6.2f  |coef|₂ = %.4f\n", l, sqrt(sum(coef_r^2))))
}

1.9  Lasso Regression (L1 Regularisation)

RegularisationFeature Selection
DEFINITION

Lasso (Least Absolute Shrinkage and Selection Operator) adds an L1 penalty on the absolute values of coefficients. Unlike Ridge, the L1 penalty forces many coefficients to become exactly zero, performing automatic feature selection — making Lasso ideal when only a few predictors are truly relevant.

1.9.1  Mathematical Foundation

FORMULAE

Lasso objective:

\[\mathcal{L}_{\text{lasso}}(\boldsymbol{\beta}) = \sum_{i=1}^{N}(y_i - \hat{y}_i)^2 + \lambda \sum_{j=1}^{p}|\beta_j|\]

No closed-form; solved via coordinate descent. Cycling over \(j\), hold all other coefficients fixed and compute the partial residual correlation

\[\rho_j = \mathbf{x}_j^\top\bigl(\mathbf{y} - \mathbf{X}_{-j}\boldsymbol{\beta}_{-j}\bigr), \qquad z_j = \mathbf{x}_j^\top\mathbf{x}_j\]

then soft-threshold it:

\[\hat{\beta}_j \leftarrow \frac{1}{z_j}\,S(\rho_j,\; \lambda)\]

Note it is \(\rho_j\) that is thresholded — not the gradient of the RSS, which carries the opposite sign and still contains \(\beta_j\). On standardised columns \(z_j = N\).

Soft-thresholding operator \(S\):

\[S(z, \lambda) = \text{sign}(z)\cdot\max(|z| - \lambda, 0)\]

Elastic Net (Ridge + Lasso):

\[\mathcal{L}_{\text{EN}} = \text{RSS} + \lambda\Bigl[\alpha\|\boldsymbol{\beta}\|_1 + \tfrac{1}{2}(1-\alpha)\|\boldsymbol{\beta}\|_2^2\Bigr]\]

The \(\tfrac{1}{2}\) on the L2 term is the standard convention (and glmnet's), so that \(\alpha = 0\) recovers the Ridge objective of the previous card exactly.

1.9.2  How It Works

The L1 norm's diamond-shaped constraint region has corners on the axes, so the optimum often lies exactly on an axis — making coefficients identically zero. Lasso selects at most \(\min(N,p)\) non-zero features. When features are correlated, Lasso tends to select one and discard the rest (Ridge distributes weight equally). Elastic Net combines both penalties, getting the best of both worlds. The regularisation path shows how coefficients shrink from OLS values as \(\lambda\) increases.

1.9.3  Assumptions and Failure Modes

ASSUMES
  • Sparsity: only a few predictors are genuinely relevant
BREAKS WHEN
  • Predictors are correlated — Lasso picks one arbitrarily and drops the rest
  • \(p \gg N\) — at most \(N\) coefficients can be non-zero
  • The selected set is read as 'the important features' without stability selection

1.9.4  Worked Examples

FINANCE

🎯 Feature Selection in Quant Models

From 80 financial features (ratios, momentum signals, macro vars), Lasso automatically selects 12 as relevant for predicting quarterly earnings surprises, discarding redundant collinear indicators.

Feature SetSelectedTest R²
All 80 features800.58
Lasso (λ=0.01)120.71
AGRICULTURE

🔍 Key Environmental Drivers

From 50 environmental variables, Lasso identifies 8 key drivers of forest fire risk: temperature, wind speed, humidity, NDVI, soil moisture — enabling targeted monitoring programmes.

VariableLasso β
Temperature0.42
Wind Speed0.31
NDVI-0.22
Humidity-0.38
MEDICINE

🧬 Biomarker Discovery

Identifying key biomarkers for Alzheimer's diagnosis from 500 protein expression features. Lasso drives most coefficients to exactly zero, leaving a panel short enough to assay affordably. One caveat worth teaching: when predictors are correlated, which features survive is unstable across resamples, so a selected set should be confirmed by stability selection before anyone calls a biomarker significant.

Total proteinsLasso selectedAUC
500180.89

1.9.5  Code

Lasso Regression
import numpy as np
import pandas as pd
from sklearn.linear_model import Lasso, LassoCV
from sklearn.model_selection import train_test_split
from sklearn.preprocessing import StandardScaler
from sklearn.metrics import r2_score

np.random.seed(42)
N, P = 300, 80

# ── Simulate: 80 features, only 12 truly relevant ────────────
X = np.random.randn(N, P)
true_coef = np.zeros(P)
true_coef[:12] = np.random.randn(12) * 2  # only first 12 matter
y = X @ true_coef + np.random.normal(0, 1.5, N)

X_tr, X_te, y_tr, y_te = train_test_split(X, y, test_size=0.25, random_state=42)
sc = StandardScaler()
X_tr_sc = sc.fit_transform(X_tr); X_te_sc = sc.transform(X_te)

# ── LassoCV finds optimal λ ───────────────────────────────────
lasso_cv = LassoCV(cv=5, alphas=np.logspace(-4, 2, 100), max_iter=10000, random_state=42)
lasso_cv.fit(X_tr_sc, y_tr)
best_alpha = lasso_cv.alpha_

print("=== Lasso Regression — Feature Selection ===")
print(f"Best λ (alpha) = {best_alpha:.6f}")
print(f"Test R²        = {r2_score(y_te, lasso_cv.predict(X_te_sc)):.4f}")

# ── Count selected features ───────────────────────────────────
n_nonzero = np.sum(lasso_cv.coef_ != 0)
print(f"Non-zero coefficients: {n_nonzero} / {P}")

# ── Lasso Regularisation Path ─────────────────────────────────
print("\nLasso Path:")
for alpha in [0.001, 0.01, 0.1, 1.0, 10.0]:
    m = Lasso(alpha=alpha, max_iter=10000).fit(X_tr_sc, y_tr)
    r2 = r2_score(y_te, m.predict(X_te_sc))
    nz = np.sum(m.coef_ != 0)
    print(f"  λ={alpha:6.3f}  features={nz:3d}  Test R²={r2:.4f}")

# ── Compare true vs recovered coefficients ────────────────────
print("\nRecovery of true coefficients (first 15):")
for i in range(15):
    print(f"  β{i:02d}: true={true_coef[i]:+.3f}  lasso={lasso_cv.coef_[i]:+.3f}")
library(glmnet); set.seed(42)
N <- 300; P <- 80

X         <- matrix(rnorm(N*P), N, P)
true_coef <- c(rnorm(12)*2, rep(0, P-12))
y         <- X %*% true_coef + rnorm(N, 0, 1.5)
X         <- scale(X)

idx   <- sample(1:N, 0.75*N)
X_tr  <- X[idx,]; y_tr <- y[idx]
X_te  <- X[-idx,]; y_te <- y[-idx]

# ── Lasso via glmnet (alpha=1) ─────────────────────────────────
lasso_cv <- cv.glmnet(X_tr, y_tr, alpha=1, nfolds=5)
best_lam <- lasso_cv$lambda.min
cat(sprintf("Best λ = %.6f\n", best_lam))

# ── Evaluate ──────────────────────────────────────────────────
y_pred  <- predict(lasso_cv, newx=X_te, s="lambda.min")
ss_res  <- sum((y_te-y_pred)^2)
r2      <- 1 - ss_res/sum((y_te-mean(y_te))^2)
cat(sprintf("Test R² = %.4f\n", r2))

coefs   <- coef(lasso_cv, s="lambda.min")[-1]
n_nz    <- sum(coefs != 0)
cat(sprintf("Non-zero coefs: %d / %d\n", n_nz, P))

# ── Regularisation path ───────────────────────────────────────
cat("\nLasso Path:\n")
for(lam in c(0.001, 0.01, 0.1, 1, 10)) {
  cf <- coef(glmnet(X_tr, y_tr, alpha=1, lambda=lam))[-1]
  prd <- cbind(1, X_te) %*% c(coef(glmnet(X_tr, y_tr, alpha=1, lambda=lam))[1], cf)
  r2l <- 1 - sum((y_te-prd)^2)/sum((y_te-mean(y_te))^2)
  cat(sprintf("  λ=%.3f  nz=%-3d  R²=%.4f\n", lam, sum(cf!=0), r2l))
}

1.10  Random Forest Regression

EnsembleBagging
DEFINITION

Random Forest builds an ensemble of \(B\) decision trees, each trained on a bootstrapped sample of the data and with a random subset of features considered at each split. The final prediction is the average over all trees. This combination of bagging and random feature selection drastically reduces variance without significantly increasing bias.

1.10.1  Mathematical Foundation

FORMULAE

Bootstrap sample: \(\mathcal{D}^{*b}\) is a sample of \(N\) drawn with replacement from \(\mathcal{D}\)

Ensemble prediction (regression):

\[\hat{f}(\mathbf{x}) = \frac{1}{B}\sum_{b=1}^{B} T_b(\mathbf{x})\]

Variance reduction:

\[\text{Var}\!\left(\bar{T}\right) = \frac{1+(B-1)\rho}{B}\,\sigma^2_T\]

where \(\rho\) is the correlation between trees. Random feature selection reduces \(\rho\).

Feature importance (Gini / permutation):

\[\text{Imp}(x_j) = \frac{1}{B}\sum_{b=1}^B \sum_{t \in T_b,\,v(t)=j} \Delta \text{impurity}(t)\]

OOB error (free validation): predictions on samples not in the bootstrap

1.10.2  How It Works

Each tree uses approximately \(m = \lfloor p/3 \rfloor\) features at each split (regression). Out-of-bag (OOB) error provides an unbiased estimate of generalisation error without a separate validation set. Random Forests are robust to outliers, handle missing values, and scale well. Feature importances identify the most predictive variables. Compared to a single decision tree, RF reduces variance by a factor roughly equal to the number of trees when trees are uncorrelated. The same forest also does classification: average class probabilities (or take a majority vote) across trees instead of averaging predictions, and use \(m = \lfloor\sqrt{p}\rfloor\) features per split rather than \(\lfloor p/3 \rfloor\). Everything else — bootstrapping, random feature selection, OOB scoring — is unchanged (RandomForestClassifier).

1.10.3  Assumptions and Failure Modes

ASSUMES
  • Trees are decorrelated by bootstrapping and random feature subsets
BREAKS WHEN
  • Extrapolation is needed — a forest cannot predict outside its training range
  • Trees stay correlated — variance reduction stalls well short of \(1/B\)
  • Interpretability matters — the ensemble is not readable like one tree

1.10.4  Worked Examples

FINANCE

🏠 Insurance Premium Prediction

Using 15 features (age, BMI, smoker, region, claims history), Random Forest predicts annual health insurance premium. Out-of-bag scoring gives a validation estimate without holding data back — each tree is scored on the roughly one-third of rows its bootstrap sample missed. Random Forest tends to beat linear regression here because premium depends on sharp interactions, chiefly smoker status crossed with age and BMI.

AgeBMISmokerPremium $
3524.2No4,200
5231.8Yes18,900
2822.0No3,100
AGRICULTURE

🌾 Harvest Yield Estimation

Using satellite NDVI time series (12 monthly values), weather data, and soil properties to predict wheat yield per field. RF achieves RMSE of 0.28 t/ha vs single tree RMSE of 0.51 t/ha.

NDVI_AprRain mmN kg/haYield
0.728201605.2
0.556101003.8
MEDICINE

🔬 Patient Readmission Risk Score

Predicting 30-day readmission probability using lab values, diagnoses, and discharge summary features (20 vars). Readmission models are harder than they look: much of what actually drives a readmission — housing, support at home, access to follow-up — never reaches the record, which caps achievable discrimination regardless of the model. Feature importances are the useful output here, not the AUC.

HbA1cLOS daysPrev admissionsRisk %
9.27342
6.1208

1.10.5  Code

Random Forest Regression
import numpy as np
import pandas as pd
from sklearn.ensemble import RandomForestRegressor
from sklearn.tree import DecisionTreeRegressor
from sklearn.model_selection import train_test_split, cross_val_score
from sklearn.metrics import r2_score, mean_squared_error

np.random.seed(42)
N = 1000

# ── Simulate Insurance Premium Dataset ───────────────────────
age    = np.random.randint(18, 70, N).astype(float)
bmi    = np.random.normal(27, 6, N)
smoker = np.random.binomial(1, 0.2, N).astype(float)
claims = np.random.poisson(0.5, N).astype(float)
region = np.random.randint(0, 4, N).astype(float)

premium = (250*age + 500*bmi + 12000*smoker + 2000*claims +
           300*region + 1000 + np.random.normal(0, 800, N))
premium = np.maximum(premium, 500)  # floor at 500

X = np.column_stack([age, bmi, smoker, claims, region])
feature_names = ['Age','BMI','Smoker','PriorClaims','Region']
y = premium

X_tr, X_te, y_tr, y_te = train_test_split(X, y, test_size=0.2, random_state=42)

# ── Single Tree vs Random Forest ─────────────────────────────
dt = DecisionTreeRegressor(max_depth=6, random_state=42)
dt.fit(X_tr, y_tr)
dt_rmse = np.sqrt(mean_squared_error(y_te, dt.predict(X_te)))
dt_r2   = r2_score(y_te, dt.predict(X_te))

rf = RandomForestRegressor(n_estimators=200, max_features='sqrt',
                            oob_score=True, n_jobs=-1, random_state=42)
rf.fit(X_tr, y_tr)
rf_rmse = np.sqrt(mean_squared_error(y_te, rf.predict(X_te)))
rf_r2   = r2_score(y_te, rf.predict(X_te))

print("=== Random Forest vs Single Tree ===")
print(f"{'':18} | {'R²':>8} | {'RMSE':>10}")
print(f"{'Decision Tree':18} | {dt_r2:8.4f} | {dt_rmse:10.2f}")
print(f"{'Random Forest':18} | {rf_r2:8.4f} | {rf_rmse:10.2f}")
print(f"{'RF OOB R²':18} | {rf.oob_score_:8.4f} |")

# ── Feature Importances ───────────────────────────────────────
print("\nFeature Importances:")
for name, imp in sorted(zip(feature_names, rf.feature_importances_),
                         key=lambda x: -x[1]):
    bar = '█' * int(imp*40)
    print(f"  {name:15s}: {imp:.4f}  {bar}")

# ── Number of trees effect ────────────────────────────────────
print("\nEffect of number of trees on OOB R²:")
for n in [10, 50, 100, 200, 500]:
    rf_n = RandomForestRegressor(n_estimators=n, oob_score=True, n_jobs=-1, random_state=42)
    rf_n.fit(X_tr, y_tr)
    print(f"  n_trees={n:4d}  OOB R²={rf_n.oob_score_:.4f}")
library(randomForest); library(caret); set.seed(42)
N <- 1000

# ── Simulate Insurance Dataset ────────────────────────────────
age    <- sample(18:70, N, replace=TRUE)
bmi    <- rnorm(N, 27, 6)
smoker <- rbinom(N, 1, 0.2)
claims <- rpois(N, 0.5)
region <- sample(0:3, N, replace=TRUE)
premium <- pmax(250*age + 500*bmi + 12000*smoker + 2000*claims + 300*region +
                1000 + rnorm(N,0,800), 500)

df <- data.frame(age, bmi, smoker, claims, region, premium)
idx <- sample(1:N, 0.8*N)
tr  <- df[idx,]; te <- df[-idx,]

# ── Random Forest ─────────────────────────────────────────────
rf_model <- randomForest(premium ~ ., data=tr, ntree=200,
                          importance=TRUE, do.trace=FALSE)
print(rf_model)

# ── Evaluate ──────────────────────────────────────────────────
y_pred <- predict(rf_model, te)
rmse   <- sqrt(mean((te$premium - y_pred)^2))
r2     <- 1 - sum((te$premium-y_pred)^2)/sum((te$premium-mean(te$premium))^2)
cat(sprintf("Test R²=%.4f, RMSE=%.2f\n", r2, rmse))

# ── Feature importance ────────────────────────────────────────
cat("\nFeature Importances (IncMSE):\n")
imp <- importance(rf_model, type=1)
print(imp[order(-imp[,1]),,drop=FALSE])

# ── Partial dependence on 'smoker' ───────────────────────────
partialPlot(rf_model, tr, "smoker", main="Partial: Smoker → Premium")

# ── Tuning mtry ───────────────────────────────────────────────
ctrl  <- trainControl(method="oob")
grid  <- data.frame(mtry=c(2,3,4,5))
rf_cv <- train(premium~., data=tr, method="rf", trControl=ctrl,
               tuneGrid=grid, ntree=100)
cat(sprintf("\nBest mtry=%d  RMSE=%.2f\n",
            rf_cv$bestTune$mtry, min(rf_cv$results$RMSE)))

At a Glance

The same information as the assumption blocks above, side by side — this is the comparison that decides which method to reach for.

AlgorithmAssumesBreaks when
1.6 Linear RegressionLinearity · Independence · Homoscedasticity · Normality of residualsPredictors are collinear — coefficients become unstable and flip sign
1.7 Polynomial RegressionThe relationship is smooth and polynomial in formDegree is chosen without cross-validation — the fit oscillates wildly
1.8 Ridge Regression (L2 Regularisation)Features standardised — the L2 penalty is not scale-invariantOnly a few predictors truly matter — Ridge keeps them all, so use Lasso
1.9 Lasso Regression (L1 Regularisation)Sparsity: only a few predictors are genuinely relevantPredictors are correlated — Lasso picks one arbitrarily and drops the rest
1.10 Random Forest RegressionTrees are decorrelated by bootstrapping and random feature subsetsExtrapolation is needed — a forest cannot predict outside its training range