01

Learning Objectives

After completing this chapter, you will be able to:

1
Extend simple linear regression to multiple features using matrix notation Y = XW + ε
2
Derive and apply the Normal Equation W = (XᵀX)⁻¹XᵀY from first principles
3
Build polynomial regression models using feature expansion (x², x³, …, xᵈ)
4
Create feature interaction terms (x₁·x₂) and understand their role in capturing synergies
5
Diagnose and fix multicollinearity using the Variance Inflation Factor (VIF)
6
Use Adjusted R², AIC, and BIC for principled model comparison and selection
7
Perform forward, backward, and stepwise feature selection algorithms
8
Check regression assumptions via residual plots, Q-Q plots, and the Durbin-Watson test
9
Recognize and prevent overfitting when polynomial degree is too high
10
Implement complete regression pipelines in Python, Scikit-Learn, and TensorFlow
02

Introduction

Why Move Beyond Simple Linear Regression?

In Chapter 4, we modelled the relationship between one input variable and one output. But the real world is rarely that simple. A house's price depends on its area, and the number of bedrooms, and its distance from a metro station, and the neighbourhood's safety rating. A crop's yield depends on rainfall, and fertilizer usage, and soil pH, and temperature.

This chapter introduces two powerful extensions:

  • Multiple Linear Regression: Using multiple input features (x₁, x₂, …, xₙ) to predict the output while keeping the relationship linear in the parameters.
  • Polynomial Regression: Creating new features from powers and products of existing features (x², x₁·x₂, x³) to capture nonlinear patterns — while still using the linear regression machinery.

Together, these methods form the backbone of predictive analytics across industries — from stock market forecasting and energy demand modelling to agricultural yield prediction and healthcare cost estimation.

Chapter Roadmap

We will progress through the following journey:

  1. Theory: Matrix formulation → Normal equation → Polynomial expansion → Interaction terms
  2. Diagnostics: VIF for multicollinearity → Adjusted R², AIC, BIC for model selection → Assumption checking
  3. Implementation: From-scratch Python → Scikit-Learn pipelines → TensorFlow multi-input models
  4. Applications: Indian stock markets, agricultural yield, energy demand, global health data
03

Historical Background

From Gauss to Modern Data Science

The history of multiple regression is deeply intertwined with the history of statistics itself:

  • 1805 — Adrien-Marie Legendre: Published the method of least squares in Nouvelles méthodes pour la détermination des orbites des comètes. He used it to fit astronomical data with multiple parameters.
  • 1809 — Carl Friedrich Gauss: Independently developed least squares and connected it to the normal (Gaussian) distribution, providing the first probabilistic justification for the method.
  • 1885 — Francis Galton: Coined the term "regression" while studying heredity. He observed that tall parents' children tend to be shorter — "regressing toward the mean."
  • 1908 — George Udny Yule: Applied multiple regression to study poverty, using multiple socioeconomic features simultaneously — a true pioneer of applied multivariate analysis.
  • 1922 — Ronald A. Fisher: Formalized the analysis of variance (ANOVA) and introduced the F-test, which became the standard for testing whether adding features improves a regression model.
  • 1970 — Hirotugu Akaike: Proposed the Akaike Information Criterion (AIC), revolutionizing model selection by balancing goodness-of-fit with model complexity.
  • 1978 — Gideon Schwarz: Introduced the Bayesian Information Criterion (BIC), adding a stronger penalty for model complexity.

The Polynomial Regression Thread

Polynomial curve fitting predates even Legendre. Isaac Newton (1687) used polynomial interpolation in the Principia. Joseph-Louis Lagrange (1795) formalized polynomial interpolation. However, the idea of using polynomials as regression features (rather than interpolation) became popular only with the rise of computational statistics in the mid-20th century.

04

Conceptual Explanation

4.1 Multiple Linear Regression — The Big Picture

Instead of fitting a line in 2D (one feature), we are now fitting a hyperplane in (n+1)-dimensional space. With two features, the model is a plane in 3D space. With three features, it is a hyperplane in 4D — impossible to visualize but mathematically identical.

Multiple Linear Regression Model
y = w₁x₁ + w₂x₂ + w₃x₃ + … + wₙxₙ + b

Or in compact form: ŷ = Σᵢ wᵢxᵢ + b = wᵀx + b

Each weight wᵢ represents the partial effect of feature xᵢ on y, holding all other features constant. This is a crucial distinction from simple regression where the slope captures the total effect.

4.2 Polynomial Regression — Curves from Lines

What if the relationship between x and y is not a straight line but a curve? For instance, a plant's growth rate initially increases with temperature but decreases after a certain point — a parabolic (quadratic) relationship.

The trick: create new features from powers of x:

Polynomial Regression (Degree d)
y = w₀ + w₁x + w₂x² + w₃x³ + … + wdxᵈ

Equivalent to: y = wᵀφ(x) where φ(x) = [1, x, x², …, xᵈ]ᵀ

By defining z₁ = x, z₂ = x², z₃ = x³, we reduce polynomial regression to multiple linear regression in the transformed features. All our linear algebra tools — normal equation, gradient descent — apply unchanged.

4.3 Feature Interactions — Synergy Between Variables

Sometimes two features interact: the effect of fertilizer on crop yield might be different depending on the amount of rainfall. We capture this with cross terms:

Interaction Model
y = w₁x₁ + w₂x₂ + w₃(x₁ · x₂) + b

The term w₃(x₁ · x₂) captures the interaction effect

Interpretation: If w₃ > 0, features x₁ and x₂ have a synergistic effect — their combined impact is greater than the sum of their individual effects. If w₃ < 0, they have an antagonistic interaction.

4.4 The Bias-Variance Tradeoff in Polynomial Degree

Choosing the polynomial degree d is a critical decision:

  • d too low (underfitting): The model is too simple, cannot capture the data's pattern. High bias, low variance.
  • d just right: Captures the true pattern without fitting noise. Balanced bias and variance.
  • d too high (overfitting): The model fits the noise in the training data, creating wild oscillations between data points. Low bias, high variance.

A degree-20 polynomial through 21 data points will pass through every single point (zero training error) but will be useless for prediction. This is a central lesson of this chapter.

05

Mathematical Foundation

5.1 Matrix Formulation of Multiple Regression

Given m training samples with n features each, we organize the data into matrices:

Data Matrices
X = ⎡ 1  x₁₁  x₁₂  …  x₁ₙ ⎤    Y = ⎡ y₁ ⎤    W = ⎡ w₀ ⎤
    ⎢ 1  x₂₁  x₂₂  …  x₂ₙ ⎥        ⎢ y₂ ⎥        ⎢ w₁ ⎥
    ⎢ ⋮   ⋮    ⋮   ⋱   ⋮  ⎥        ⎢ ⋮  ⎥        ⎢ ⋮  ⎥
    ⎣ 1  xₘ₁  xₘ₂  …  xₘₙ ⎦        ⎣ yₘ ⎦        ⎣ wₙ ⎦

X is m×(n+1),  Y is m×1,  W is (n+1)×1

The column of 1s in X accounts for the bias (intercept) term w₀. The model in matrix form:

Matrix Model
Ŷ = XW      (predicted values)
Y = XW + ε      (with error vector ε)

5.2 The Normal Equation — Closed-Form Solution

The cost function (sum of squared errors) in matrix form:

Cost Function
J(W) = (Y - XW)ᵀ(Y - XW) = ‖Y - XW‖²

Taking the gradient and setting it to zero:

Normal Equation Derivation (Summary)
∇ᵤJ = -2Xᵀ(Y - XW) = 0

⟹ XᵀXW = XᵀY

W = (XᵀX)⁻¹XᵀY

This is the Normal Equation. It gives the exact optimal weights in one matrix computation — no iterations, no learning rate. It works when XᵀX is invertible (more on this in the multicollinearity section).

5.3 Variance Inflation Factor (VIF)

Multicollinearity occurs when two or more features are highly correlated. It inflates the variance of the estimated weights, making them unstable.

VIF for Feature j
VIF_j = 1 / (1 - R²_j)

where R²_j = R² when regressing xⱼ on all other features

Interpretation rules:

  • VIF = 1: No correlation with other features
  • VIF 1–5: Moderate correlation — usually acceptable
  • VIF 5–10: High correlation — investigate and consider removal
  • VIF > 10: Severe multicollinearity — must address

5.4 Adjusted R² — Penalizing Complexity

The regular R² can only increase (or stay the same) as you add more features — even random noise features. Adjusted R² corrects this:

Adjusted R²
R²_adj = 1 - [(1 - R²)(m - 1)] / (m - n - 1)

where m = number of samples, n = number of features

If a new feature doesn't improve the model enough, R²_adj will actually decrease, signaling that the feature is not useful.

5.5 AIC and BIC for Model Selection

Akaike Information Criterion (AIC)
AIC = 2k - 2 ln(L̂)
For linear regression with normal errors:
AIC = m·ln(RSS/m) + 2k
Bayesian Information Criterion (BIC)
BIC = k·ln(m) - 2 ln(L̂)
For linear regression:
BIC = m·ln(RSS/m) + k·ln(m)

Where k = number of parameters (including intercept), m = number of samples, L̂ = maximum likelihood, RSS = residual sum of squares. Lower AIC/BIC = better model. BIC penalizes complexity more strongly than AIC when m > 8 (since ln(m) > 2 for m ≥ 8).

5.6 Durbin-Watson Statistic

Tests for autocorrelation in residuals (important for time-series data):

Durbin-Watson Test
DW = Σᵢ₌₂ᵐ (eᵢ - eᵢ₋₁)² / Σᵢ₌₁ᵐ eᵢ²

DW ≈ 2(1 - ρ) where ρ is the lag-1 autocorrelation
  • DW ≈ 2: No autocorrelation (ideal)
  • DW < 1.5: Positive autocorrelation — model misses a pattern
  • DW > 2.5: Negative autocorrelation
06

Formula Derivations (From First Principles)

6.1 Deriving the Normal Equation Step by Step

Goal: Find W that minimizes J(W) = ‖Y - XW‖² = (Y - XW)ᵀ(Y - XW).

Step 1: Expand the cost function.

J(W) = (Y - XW)ᵀ(Y - XW)
= YᵀY - YᵀXW - (XW)ᵀY + (XW)ᵀXW
= YᵀY - WᵀXᵀY - YᵀXW + WᵀXᵀXW

Since YᵀXW is a scalar, it equals its transpose: (YᵀXW)ᵀ = WᵀXᵀY. So:

J(W) = YᵀY - 2WᵀXᵀY + WᵀXᵀXW

Step 2: Take the gradient with respect to W using matrix calculus rules.

  • ∂/∂W (WᵀXᵀY) = XᵀY   (gradient of linear term)
  • ∂/∂W (WᵀXᵀXW) = 2XᵀXW   (gradient of quadratic form, since XᵀX is symmetric)
∇ᵥJ = -2XᵀY + 2XᵀXW

Step 3: Set gradient to zero and solve.

-2XᵀY + 2XᵀXW = 0
XᵀXW = XᵀY
W = (XᵀX)⁻¹XᵀY    ✓

Condition for invertibility: XᵀX must be invertible, which requires that the columns of X are linearly independent (no perfect multicollinearity) and that m ≥ n + 1 (more samples than features).

6.2 Deriving the VIF from First Principles

Claim: The variance of the estimated weight ŵⱼ is proportional to 1/(1 - R²ⱼ).

Starting point: Under the OLS assumptions, the covariance matrix of the weight vector is:

Cov(Ŵ) = σ²(XᵀX)⁻¹

where σ² is the error variance. The variance of the j-th weight is the j-th diagonal element of (XᵀX)⁻¹ scaled by σ².

For centered and scaled features, XᵀX/(m-1) becomes the correlation matrix C. It can be shown that:

Var(ŵⱼ) = σ² · (C⁻¹)ⱼⱼ / (m·Var(xⱼ))

And (C⁻¹)ⱼⱼ = 1 / (1 - R²ⱼ) = VIFⱼ

When R²ⱼ → 1 (feature j is perfectly predicted by others), VIF → ∞, meaning the weight estimate becomes infinitely uncertain — this is the multicollinearity problem.

6.3 Deriving AIC from Maximum Likelihood

Setup: For linear regression with normally distributed errors, y = Xw + ε, ε ~ N(0, σ²I), the log-likelihood is:

ln L = -(m/2)ln(2π) - (m/2)ln(σ²) - (1/2σ²) Σ(yᵢ - ŷᵢ)²

The maximum likelihood estimate of σ² is σ̂² = RSS/m. Substituting:

ln L̂ = -(m/2)ln(2π) - (m/2)ln(RSS/m) - m/2

The AIC formula is AIC = 2k - 2 ln L̂. Dropping constants that don't depend on the model:

AIC = m·ln(RSS/m) + 2k   (up to a constant)

This form shows the tradeoff: the first term rewards fit (lower RSS), while the second term penalizes complexity (more parameters k).

6.4 Deriving Adjusted R²

Starting from R²:

R² = 1 - RSS/TSS = 1 - [Σ(yᵢ - ŷᵢ)²] / [Σ(yᵢ - ȳ)²]

R² always increases (or stays the same) when features are added because the model has more flexibility to fit. The fix: compare unbiased estimates of the error variance and total variance:

Unbiased error variance: s² = RSS / (m - n - 1)
Unbiased total variance: s²_total = TSS / (m - 1)

R²_adj = 1 - s²/s²_total = 1 - [RSS/(m-n-1)] / [TSS/(m-1)]
= 1 - [(1-R²)(m-1)] / (m-n-1)    ✓
07

Worked Numerical Examples

Example 1: Normal Equation with 2 Features (By Hand)

Problem: Predict house price (y, in ₹ lakhs) from area (x₁, in 100 sq ft) and rooms (x₂). Data:

SampleArea (x₁)Rooms (x₂)Price (y)
18245
212365
315480
410355

Step 1: Construct the design matrix X (with bias column) and Y:

X = ⎡ 1  8   2 ⎤    Y = ⎡ 45 ⎤
    ⎢ 1  12  3 ⎥        ⎢ 65 ⎥
    ⎢ 1  15  4 ⎥        ⎢ 80 ⎥
    ⎣ 1  10  3 ⎦        ⎣ 55 ⎦

Step 2: Compute XᵀX:

XᵀX = ⎡ 4    45   12  ⎤
      ⎢ 45   533  141 ⎥
      ⎣ 12   141  38  ⎦

Step 3: Compute XᵀY:

XᵀY = ⎡ 245   ⎤     (1·45 + 1·65 + 1·80 + 1·55)
      ⎢ 2895  ⎥     (8·45 + 12·65 + 15·80 + 10·55)
      ⎣ 775   ⎦     (2·45 + 3·65 + 4·80 + 3·55)

Step 4: Solve W = (XᵀX)⁻¹XᵀY using cofactor method or Gaussian elimination:

W ≈ ⎡ w₀ ⎤ = ⎡ 2.14 ⎤ ⎢ w₁ ⎥ ⎢ 3.57 ⎥ ⎣ w₂ ⎦ ⎣ 5.00 ⎦

Model: ŷ = 2.14 + 3.57x₁ + 5.00x₂

Interpretation: Each additional 100 sq ft adds ~₹3.57 lakhs. Each additional room adds ~₹5.00 lakhs (holding area constant).

Verify for sample 1: ŷ₁ = 2.14 + 3.57(8) + 5.00(2) = 2.14 + 28.56 + 10.00 = 40.70 ≈ 45 ✓ (residual ≈ 4.30)

Example 2: Polynomial Regression (Degree 2, By Hand)

Problem: Fit y = w₀ + w₁x + w₂x² to the data: (1, 2), (2, 7), (3, 16).

Step 1: Create the design matrix with polynomial features:

X = ⎡ 1  1  1 ⎤    Y = ⎡ 2  ⎤
    ⎢ 1  2  4 ⎥        ⎢ 7  ⎥
    ⎣ 1  3  9 ⎦        ⎣ 16 ⎦

Step 2: XᵀX and XᵀY:

XᵀX = ⎡ 3   6   14 ⎤    XᵀY = ⎡ 25  ⎤
      ⎢ 6   14  36 ⎥          ⎢ 63  ⎥
      ⎣ 14  36  98 ⎦          ⎣ 173 ⎦

Step 3: Solving the 3×3 system (using elimination or Cramer's rule):

W = ⎡ w₀ ⎤ = ⎡ 1 ⎤ ⎢ w₁ ⎥ ⎢ -1 ⎥ ⎣ w₂ ⎦ ⎣ 2 ⎦

Model: ŷ = 1 - x + 2x²

Verification:

  • x=1: ŷ = 1 - 1 + 2(1) = 2 ✓
  • x=2: ŷ = 1 - 2 + 2(4) = 7 ✓
  • x=3: ŷ = 1 - 3 + 2(9) = 16 ✓

Perfect fit since we have 3 data points and 3 parameters (the system is exactly determined).

Example 3: VIF Calculation (By Hand)

Problem: Given three features x₁, x₂, x₃. When we regress x₁ on x₂ and x₃, we get R²₁ = 0.85. When we regress x₂ on x₁ and x₃, R²₂ = 0.30. When we regress x₃ on x₁ and x₂, R²₃ = 0.72. Compute VIF for each and interpret.

VIF₁ = 1 / (1 - 0.85) = 1 / 0.15 = 6.67
VIF₂ = 1 / (1 - 0.30) = 1 / 0.70 = 1.43
VIF₃ = 1 / (1 - 0.72) = 1 / 0.28 = 3.57

Interpretation:

  • x₁ (VIF=6.67): High multicollinearity! 85% of x₁'s variation is explained by x₂ and x₃. Consider removing or combining.
  • x₂ (VIF=1.43): Very low — almost no redundancy. Keep this feature.
  • x₃ (VIF=3.57): Moderate — acceptable but monitor if model is unstable.
08

Visual Diagrams (ASCII)

Multiple Linear Regression — Hyperplane in 3D
          y (price)
          │
     80 ──┤                          ·  (15,4,80)
          │                     ╱╱╱╱╱╱╱
     65 ──┤              ·  ╱╱╱╱╱╱╱╱  (12,3,65)
          │          ╱╱╱╱╱╱╱╱╱╱╱
     55 ──┤       ╱╱╱╱╱╱╱·╱╱╱╱  (10,3,55)
          │    ╱╱╱╱╱╱╱╱╱╱╱╱
     45 ──┤ ·╱╱╱╱╱╱╱╱╱╱╱  (8,2,45)
          │╱╱╱╱╱╱╱╱╱╱╱╱   ← This is a PLANE
          ╱╱╱╱╱╱╱╱╱╱╱╱       (not a line!)
          └────────────────────────── x₁ (area)
         ╱
       ╱  x₂ (rooms)

     ŷ = 2.14 + 3.57·x₁ + 5.00·x₂
    
Polynomial Regression — Underfitting vs. Good Fit vs. Overfitting
    DEGREE 1 (Underfit)       DEGREE 3 (Good Fit)       DEGREE 15 (Overfit)
    y│     ·                  y│     ·                   y│    ·╲
     │   ·   ·                 │   ╭─╮·                  │  ╭╯  ╰╮·
     │ ·  /   ·                │  ╱   ╰╮                 │╭╯╭╮╭╮ ╰╮
     │·──/─────·               │·╱     ╰·                │╯╭╯╰╯╰╮ ·
     │ /  ·                    │╱   ·                     │╭╯     ╰─╮
     │/·                       │· ╱                       ·╯    ·   ╰
     └──────── x               └──────── x                └──────── x

     High Bias                 Balanced                   High Variance
     Low Variance              Bias=Variance              Low Bias
     Train R²: 0.65            Train R²: 0.96             Train R²: 1.00
     Test  R²: 0.60            Test  R²: 0.94             Test  R²: 0.30
    
Feature Interaction Visualization
     WITHOUT INTERACTION               WITH INTERACTION (x₁ · x₂)
     y = w₁x₁ + w₂x₂ + b              y = w₁x₁ + w₂x₂ + w₃(x₁·x₂) + b

     yield│                             yield│
          │  ---- high rain              │       ---- high rain
          │  ─── low rain                │       ─── low rain
          │  ╱╱╱╱╱╱╱╱                   │           ╱╱╱╱╱
          │  ╱╱╱╱╱╱                     │       ╱╱╱╱╱
          │  ╱╱╱╱╱╱╱                    │     ╱╱╱╱
          │  ──────────                 │   ╱╱
          │  ────────                   │  ──────────
          └──────────── fertilizer       └──────────── fertilizer

     Lines are PARALLEL              Lines DIVERGE (synergy!)
     (no interaction)                  high rain + high fertilizer
                                       → extra boost in yield
    
Multicollinearity Effect on Weight Estimates
     Low Multicollinearity (VIF ≈ 1)     High Multicollinearity (VIF ≈ 10)

     ŵ₁ distribution:                    ŵ₁ distribution:
              ╱╲                                ╱──────╲
           ╱╱    ╲╲                          ╱╱          ╲╲
         ╱╱        ╲╲                     ╱╱                ╲╲
       ╱╱            ╲╲               ╱╱                      ╲╲
     ╱╱                ╲╲          ╱╱                            ╲╲
     ─────────────────────          ─────────────────────────────────
         true w₁                              true w₁

     TIGHT estimate                    WIDE estimate
     (confident)                       (uncertain, unstable)
    
09

Flowcharts (ASCII)

Regression Model Selection Flowchart
                          ┌─────────────────┐
                          │   START: Data    │
                          └────────┬────────┘
                                   │
                          ┌────────▼────────┐
                          │ Explore features │
                          │ (scatter plots,  │
                          │  correlations)   │
                          └────────┬────────┘
                                   │
                    ┌──────────────▼──────────────┐
                    │  Linear relationship?        │
                    └──────┬──────────────┬───────┘
                       YES │              │ NO
                    ┌──────▼──────┐ ┌─────▼──────────┐
                    │  Multiple   │ │ Try Polynomial  │
                    │  Linear Reg │ │ (degree 2,3,4)  │
                    └──────┬──────┘ └─────┬──────────┘
                           │              │
                    ┌──────▼──────────────▼───────┐
                    │   Check VIF for all features │
                    └──────────────┬──────────────┘
                            ┌──────▼──────┐
                            │  VIF > 10?  │
                            └──┬──────┬───┘
                           YES │      │ NO
                    ┌──────────▼──┐ ┌─▼───────────────┐
                    │ Remove/PCA   │ │ Fit model &      │
                    │ combine feat │ │ check Adj R²     │
                    └──────────┬──┘ └─┬───────────────┘
                               │      │
                    ┌──────────▼──────▼──────────────┐
                    │ Compare models: AIC, BIC        │
                    │ Use Forward/Backward selection   │
                    └──────────────┬─────────────────┘
                                   │
                    ┌──────────────▼─────────────────┐
                    │ Check assumptions:              │
                    │ • Residual plots (linearity)    │
                    │ • Q-Q plot (normality)          │
                    │ • Durbin-Watson (independence)  │
                    └──────────────┬─────────────────┘
                                   │
                    ┌──────────────▼─────────────────┐
                    │ Cross-validate → report test R² │
                    └──────────────┬─────────────────┘
                                   │
                          ┌────────▼────────┐
                          │  DEPLOY MODEL   │
                          └─────────────────┘
    
Forward Feature Selection Algorithm
     ┌───────────────────────────────────┐
     │ Initialize: selected = {}         │
     │            remaining = {all feat} │
     └───────────────┬───────────────────┘
                     │
     ┌───────────────▼───────────────────┐
     │  For each feature f in remaining: │
     │    fit model with selected ∪ {f}  │
     │    compute AIC (or Adj R²)        │
     └───────────────┬───────────────────┘
                     │
     ┌───────────────▼───────────────────┐
     │  Best f* = feature with lowest    │
     │  AIC (or highest Adj R²)          │
     └───────────────┬───────────────────┘
                     │
     ┌───────────────▼───────────────────┐
     │  Does f* improve model            │──── NO ──→  STOP
     │  significantly? (AIC decreases    │             Return selected
     │  or p-value < threshold)          │
     └───────────────┬───────────────────┘
                 YES │
     ┌───────────────▼───────────────────┐
     │  selected = selected ∪ {f*}       │
     │  remaining = remaining \ {f*}     │
     └───────────────┬───────────────────┘
                     │
                     └──── Loop back to top ────┘
    
10

Python Implementation (From Scratch)

10.1 Multiple Linear Regression — Normal Equation

multiple_regression_scratch.py
import numpy as np
import matplotlib.pyplot as plt

class MultipleLinearRegression:
    """Multiple Linear Regression using the Normal Equation.
    
    Solves: W = (XᵀX)⁻¹XᵀY exactly — no iterations needed.
    """
    
    def __init__(self):
        self.weights = None   # includes bias as w[0]
        self.r_squared = None
        self.adj_r_squared = None
    
    def fit(self, X, y):
        """Fit the model using the Normal Equation.
        
        Parameters:
            X: numpy array of shape (m, n) — feature matrix
            y: numpy array of shape (m,) — target values
        """
        m, n = X.shape
        
        # Add bias column (column of 1s)
        X_b = np.column_stack([np.ones(m), X])
        
        # Normal Equation: W = (XᵀX)⁻¹XᵀY
        XtX = X_b.T @ X_b            # (n+1) x (n+1)
        XtY = X_b.T @ y              # (n+1) x 1
        self.weights = np.linalg.solve(XtX, XtY)  # more stable than inv()
        
        # Compute R² and Adjusted R²
        y_pred = X_b @ self.weights
        ss_res = np.sum((y - y_pred) ** 2)
        ss_tot = np.sum((y - np.mean(y)) ** 2)
        self.r_squared = 1 - ss_res / ss_tot
        self.adj_r_squared = 1 - ((1 - self.r_squared) * (m - 1)) / (m - n - 1)
        
        return self
    
    def predict(self, X):
        """Predict target values for new data."""
        m = X.shape[0]
        X_b = np.column_stack([np.ones(m), X])
        return X_b @ self.weights
    
    @property
    def intercept(self):
        return self.weights[0]
    
    @property
    def coefficients(self):
        return self.weights[1:]


# ===== DEMO: House Price Prediction =====
np.random.seed(42)
m = 100  # samples

# Generate synthetic data: price = 5 + 3*area + 8*rooms + noise
area = np.random.uniform(5, 20, m)       # 100 sq ft units
rooms = np.random.randint(1, 6, m).astype(float)
noise = np.random.normal(0, 3, m)
price = 5 + 3 * area + 8 * rooms + noise

X = np.column_stack([area, rooms])
y = price

# Fit the model
model = MultipleLinearRegression()
model.fit(X, y)

print("=" * 50)
print("MULTIPLE LINEAR REGRESSION RESULTS")
print("=" * 50)
print(f"Intercept (w₀):         {model.intercept:.4f}")
print(f"Area coefficient (w₁):  {model.coefficients[0]:.4f}")
print(f"Rooms coefficient (w₂): {model.coefficients[1]:.4f}")
print(f"R²:                     {model.r_squared:.6f}")
print(f"Adjusted R²:            {model.adj_r_squared:.6f}")
print(f"\nTrue coefficients: w₀=5, w₁=3, w₂=8")

# Predictions
y_pred = model.predict(X)
print(f"\nFirst 5 predictions vs actual:")
for i in range(5):
    print(f"  Predicted: {y_pred[i]:.2f}, Actual: {y[i]:.2f}, "
          f"Error: {abs(y[i]-y_pred[i]):.2f}")

10.2 Polynomial Regression from Scratch

polynomial_regression_scratch.py
import numpy as np
import matplotlib.pyplot as plt

class PolynomialRegression:
    """Polynomial Regression from scratch.
    
    Transforms x → [1, x, x², ..., xᵈ] then applies Normal Equation.
    """
    
    def __init__(self, degree=2):
        self.degree = degree
        self.weights = None
    
    def _create_features(self, x):
        """Create polynomial feature matrix."""
        m = len(x)
        X = np.ones((m, self.degree + 1))
        for d in range(1, self.degree + 1):
            X[:, d] = x ** d
        return X
    
    def fit(self, x, y):
        """Fit polynomial to data."""
        X = self._create_features(x)
        # Normal equation: W = (XᵀX)⁻¹XᵀY
        self.weights = np.linalg.solve(X.T @ X, X.T @ y)
        return self
    
    def predict(self, x):
        """Predict using fitted polynomial."""
        X = self._create_features(x)
        return X @ self.weights
    
    def r_squared(self, x, y):
        """Compute R² score."""
        y_pred = self.predict(x)
        ss_res = np.sum((y - y_pred) ** 2)
        ss_tot = np.sum((y - np.mean(y)) ** 2)
        return 1 - ss_res / ss_tot


# ===== OVERFITTING DEMONSTRATION =====
np.random.seed(42)

# True function: y = 0.5x² - 2x + 1 + noise
x_train = np.sort(np.random.uniform(-3, 3, 15))
y_true = 0.5 * x_train**2 - 2 * x_train + 1
y_train = y_true + np.random.normal(0, 1.5, len(x_train))

x_test = np.linspace(-3.5, 3.5, 200)

degrees = [1, 2, 3, 5, 12]
print("=" * 55)
print(f"{'Degree':<10} {'Train R²':<15} {'Overfit?'}")
print("=" * 55)

fig, axes = plt.subplots(1, len(degrees), figsize=(20, 4))
for ax, deg in zip(axes, degrees):
    model = PolynomialRegression(degree=deg)
    model.fit(x_train, y_train)
    r2 = model.r_squared(x_train, y_train)
    
    overfit = "✓ YES" if deg > 4 else "  No"
    print(f"  {deg:<10} {r2:<15.6f} {overfit}")
    
    # Plot
    y_fit = model.predict(x_test)
    ax.scatter(x_train, y_train, c='#059669', s=40, zorder=5)
    ax.plot(x_test, y_fit, color='#0891b2', linewidth=2)
    ax.set_title(f'Degree {deg} (R²={r2:.3f})')
    ax.set_ylim(-15, 25)
    ax.set_xlabel('x')

plt.tight_layout()
plt.savefig('overfitting_demo.png', dpi=150, bbox_inches='tight')
plt.show()
print("\n⚠ Notice: Degree 12 has R²≈1.0 on training but will fail on test data!")

10.3 VIF Computation from Scratch

vif_from_scratch.py
import numpy as np

def compute_vif(X, feature_names=None):
    """Compute Variance Inflation Factor for each feature.
    
    VIF_j = 1 / (1 - R²_j)
    where R²_j is from regressing feature j on all other features.
    """
    n_features = X.shape[1]
    vif_values = []
    
    if feature_names is None:
        feature_names = [f"x{i+1}" for i in range(n_features)]
    
    for j in range(n_features):
        # Target: feature j
        y_j = X[:, j]
        # Predictors: all other features
        X_others = np.delete(X, j, axis=1)
        
        # Add bias column
        m = X_others.shape[0]
        X_b = np.column_stack([np.ones(m), X_others])
        
        # Fit regression: feature j ~ all others
        w = np.linalg.solve(X_b.T @ X_b, X_b.T @ y_j)
        y_pred = X_b @ w
        
        # R² of this regression
        ss_res = np.sum((y_j - y_pred) ** 2)
        ss_tot = np.sum((y_j - np.mean(y_j)) ** 2)
        r2_j = 1 - ss_res / ss_tot
        
        # VIF
        vif_j = 1.0 / (1.0 - r2_j) if r2_j < 1 else float('inf')
        vif_values.append(vif_j)
    
    # Print results
    print("=" * 55)
    print("VARIANCE INFLATION FACTOR (VIF) ANALYSIS")
    print("=" * 55)
    print(f"{'Feature':<15} {'VIF':<10} {'R²_j':<10} {'Status'}")
    print("-" * 55)
    for name, vif in zip(feature_names, vif_values):
        r2_j = 1 - 1/vif
        if vif > 10:
            status = "❌ SEVERE"
        elif vif > 5:
            status = "⚠️  HIGH"
        elif vif > 2:
            status = "🔶 MODERATE"
        else:
            status = "✅ OK"
        print(f"{name:<15} {vif:<10.2f} {r2_j:<10.4f} {status}")
    
    return vif_values


# ===== DEMO =====
np.random.seed(42)
m = 200

# Create features with known multicollinearity
x1 = np.random.normal(0, 1, m)
x2 = 0.9 * x1 + 0.1 * np.random.normal(0, 1, m)  # highly correlated with x1
x3 = np.random.normal(0, 1, m)                      # independent
x4 = 0.5 * x1 + 0.5 * x3 + np.random.normal(0, 0.3, m)  # moderate correlation

X = np.column_stack([x1, x2, x3, x4])
feature_names = ["x1 (original)", "x2 (≈0.9·x1)", "x3 (independent)", "x4 (mixed)"]

vif = compute_vif(X, feature_names)

10.4 AIC/BIC and Model Selection from Scratch

aic_bic_model_selection.py
import numpy as np

def compute_aic_bic(y, y_pred, k):
    """Compute AIC and BIC for a regression model.
    
    Args:
        y: actual values
        y_pred: predicted values
        k: number of parameters (including intercept)
    
    Returns:
        (AIC, BIC)
    """
    m = len(y)
    rss = np.sum((y - y_pred) ** 2)
    
    # AIC = m * ln(RSS/m) + 2k
    aic = m * np.log(rss / m) + 2 * k
    
    # BIC = m * ln(RSS/m) + k * ln(m)
    bic = m * np.log(rss / m) + k * np.log(m)
    
    return aic, bic


def forward_selection(X, y, feature_names=None, criterion='aic'):
    """Forward feature selection using AIC or BIC.
    
    Starts with no features, greedily adds the best feature at each step.
    """
    m, n = X.shape
    if feature_names is None:
        feature_names = [f"x{i}" for i in range(n)]
    
    selected = []
    remaining = list(range(n))
    
    print("FORWARD FEATURE SELECTION")
    print("=" * 60)
    
    # Baseline: intercept-only model
    y_mean = np.full(m, np.mean(y))
    best_aic, best_bic = compute_aic_bic(y, y_mean, k=1)
    best_score = best_aic if criterion == 'aic' else best_bic
    
    print(f"Step 0: Intercept only → {criterion.upper()} = {best_score:.2f}")
    
    step = 1
    while remaining:
        scores = {}
        for j in remaining:
            # Try adding feature j
            trial = selected + [j]
            X_trial = X[:, trial]
            X_b = np.column_stack([np.ones(m), X_trial])
            w = np.linalg.solve(X_b.T @ X_b, X_b.T @ y)
            y_pred = X_b @ w
            aic, bic = compute_aic_bic(y, y_pred, k=len(trial)+1)
            scores[j] = aic if criterion == 'aic' else bic
        
        # Best candidate
        best_j = min(scores, key=scores.get)
        if scores[best_j] < best_score:
            best_score = scores[best_j]
            selected.append(best_j)
            remaining.remove(best_j)
            print(f"Step {step}: Add '{feature_names[best_j]}' "
                  f"→ {criterion.upper()} = {best_score:.2f}")
            step += 1
        else:
            print(f"\nStopping: no feature improves {criterion.upper()}")
            break
    
    print(f"\nSelected features: {[feature_names[i] for i in selected]}")
    return selected


# ===== DEMO =====
np.random.seed(42)
m = 150
x1 = np.random.normal(0, 1, m)   # useful
x2 = np.random.normal(0, 1, m)   # useful
x3 = np.random.normal(0, 1, m)   # noise
x4 = np.random.normal(0, 1, m)   # noise
X = np.column_stack([x1, x2, x3, x4])
y = 3*x1 + 5*x2 + np.random.normal(0, 2, m)  # only x1, x2 matter

names = ["area", "rooms", "noise_1", "noise_2"]
selected = forward_selection(X, y, names, criterion='bic')
11

Scikit-Learn Implementation

11.1 Multiple Linear Regression with sklearn

sklearn_multiple_regression.py
import numpy as np
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, mean_absolute_error
from sklearn.preprocessing import StandardScaler

# ===== Generate Data =====
np.random.seed(42)
m = 500
X = np.column_stack([
    np.random.uniform(5, 25, m),        # area (100 sq ft)
    np.random.randint(1, 6, m),          # rooms
    np.random.uniform(0.5, 15, m),       # distance to metro (km)
    np.random.uniform(1, 30, m),         # age of building (years)
    np.random.uniform(50, 100, m),       # safety score
])
feature_names = ['area', 'rooms', 'metro_dist', 'building_age', 'safety']

# True model: price = 5 + 3*area + 7*rooms - 2*metro + noise
y = 5 + 3*X[:,0] + 7*X[:,1] - 2*X[:,2] - 0.5*X[:,3] + 0.3*X[:,4]
y += np.random.normal(0, 5, m)

# Train-test split
X_train, X_test, y_train, y_test = train_test_split(
    X, y, test_size=0.2, random_state=42
)

# Scale features
scaler = StandardScaler()
X_train_scaled = scaler.fit_transform(X_train)
X_test_scaled = scaler.transform(X_test)

# Fit model
model = LinearRegression()
model.fit(X_train_scaled, y_train)

# Results
y_pred = model.predict(X_test_scaled)
print("SKLEARN MULTIPLE REGRESSION RESULTS")
print("=" * 50)
print(f"Intercept: {model.intercept_:.4f}")
for name, coef in zip(feature_names, model.coef_):
    print(f"  {name:>15}: {coef:>8.4f}")
print(f"\nTrain R²: {model.score(X_train_scaled, y_train):.6f}")
print(f"Test R²:  {r2_score(y_test, y_pred):.6f}")
print(f"RMSE:     {np.sqrt(mean_squared_error(y_test, y_pred)):.4f}")
print(f"MAE:      {mean_absolute_error(y_test, y_pred):.4f}")

# Cross-validation
cv_scores = cross_val_score(model, X_train_scaled, y_train, cv=5, scoring='r2')
print(f"\n5-Fold CV R²: {cv_scores.mean():.6f} ± {cv_scores.std():.6f}")

11.2 Polynomial Features + Pipeline + Cross-Validation

sklearn_polynomial_pipeline.py
import numpy as np
import matplotlib.pyplot as plt
from sklearn.preprocessing import PolynomialFeatures, StandardScaler
from sklearn.linear_model import LinearRegression
from sklearn.pipeline import Pipeline
from sklearn.model_selection import cross_val_score, learning_curve

# ===== Generate Nonlinear Data =====
np.random.seed(42)
x = np.sort(np.random.uniform(-3, 3, 80))
y = 0.5 * x**3 - 2 * x**2 + x + 3 + np.random.normal(0, 3, 80)
X = x.reshape(-1, 1)

# ===== Compare Different Polynomial Degrees =====
print("POLYNOMIAL DEGREE COMPARISON (5-Fold CV)")
print("=" * 60)
print(f"{'Degree':<10} {'Num Features':<15} {'CV R² Mean':<15} {'CV R² Std'}")
print("-" * 60)

results = {}
for degree in range(1, 11):
    pipeline = Pipeline([
        ('poly', PolynomialFeatures(degree=degree, include_bias=False)),
        ('scaler', StandardScaler()),
        ('reg', LinearRegression())
    ])
    
    cv_scores = cross_val_score(pipeline, X, y, cv=5, scoring='r2')
    n_features = PolynomialFeatures(degree=degree, include_bias=False).fit_transform(X).shape[1]
    
    results[degree] = {
        'mean': cv_scores.mean(),
        'std': cv_scores.std(),
        'n_features': n_features
    }
    
    marker = " ◄── BEST" if degree == 3 else ""
    print(f"  {degree:<10} {n_features:<15} {cv_scores.mean():<15.6f} {cv_scores.std():.6f}{marker}")

# ===== Best Model: Degree 3 =====
best_pipe = Pipeline([
    ('poly', PolynomialFeatures(degree=3, include_bias=False)),
    ('scaler', StandardScaler()),
    ('reg', LinearRegression())
])
best_pipe.fit(X, y)

# ===== Visualize =====
x_plot = np.linspace(-3.5, 3.5, 200).reshape(-1, 1)
fig, axes = plt.subplots(1, 3, figsize=(15, 4))

for ax, deg, title in zip(axes, [1, 3, 9], 
                           ['Underfit', 'Good Fit', 'Overfit']):
    pipe = Pipeline([
        ('poly', PolynomialFeatures(degree=deg, include_bias=False)),
        ('scaler', StandardScaler()),
        ('reg', LinearRegression())
    ])
    pipe.fit(X, y)
    y_plot = pipe.predict(x_plot)
    
    ax.scatter(x, y, c='#059669', alpha=0.6, s=25, label='Data')
    ax.plot(x_plot, y_plot, c='#0891b2', linewidth=2, label=f'Degree {deg}')
    ax.set_title(f'{title} (degree={deg})')
    ax.set_ylim(-30, 40)
    ax.legend()

plt.tight_layout()
plt.savefig('poly_comparison.png', dpi=150)
plt.show()


# ===== Feature Interactions =====
print("\n\nFEATURE INTERACTION EXAMPLE")
print("=" * 60)
# 2D features with interaction
X_2d = np.column_stack([
    np.random.uniform(0, 10, 200),
    np.random.uniform(0, 10, 200)
])
y_2d = 2*X_2d[:,0] + 3*X_2d[:,1] + 0.5*X_2d[:,0]*X_2d[:,1] + np.random.normal(0,2,200)

poly2d = PolynomialFeatures(degree=2, include_bias=False, interaction_only=False)
X_poly = poly2d.fit_transform(X_2d)
print(f"Original features: {X_2d.shape[1]}")
print(f"Poly features (degree=2): {X_poly.shape[1]}")
print(f"Feature names: {poly2d.get_feature_names_out(['x1','x2'])}")

model_2d = LinearRegression().fit(X_poly, y_2d)
print(f"\nCoefficients:")
for name, coef in zip(poly2d.get_feature_names_out(['x1','x2']), model_2d.coef_):
    print(f"  {name:>10}: {coef:.4f}")
print(f"  Intercept: {model_2d.intercept_:.4f}")

11.3 VIF Analysis with statsmodels

vif_analysis_statsmodels.py
import numpy as np
import pandas as pd
from statsmodels.stats.outliers_influence import variance_inflation_factor
from statsmodels.stats.stattools import durbin_watson
import statsmodels.api as sm
from scipy import stats

# ===== Create Dataset =====
np.random.seed(42)
m = 300

data = pd.DataFrame({
    'area': np.random.uniform(5, 25, m),
    'rooms': np.random.randint(1, 6, m),
    'bathrooms': np.random.randint(1, 4, m),
    'metro_km': np.random.uniform(0.5, 15, m),
    'age_years': np.random.uniform(1, 30, m),
})
# Add correlated feature (total_rooms ≈ rooms + bathrooms)
data['total_rooms'] = data['rooms'] + data['bathrooms'] + np.random.normal(0, 0.5, m)

# Target
y = (5 + 3*data['area'] + 7*data['rooms'] + 5*data['bathrooms']
     - 2*data['metro_km'] - 0.5*data['age_years']
     + np.random.normal(0, 5, m))

# ===== VIF Calculation =====
X = data.values
print("VIF ANALYSIS")
print("=" * 50)
for i, name in enumerate(data.columns):
    vif = variance_inflation_factor(X, i)
    flag = " ← ⚠️ HIGH!" if vif > 5 else ""
    print(f"  {name:>15}: VIF = {vif:.2f}{flag}")

# ===== Full OLS Report with statsmodels =====
X_ols = sm.add_constant(data)
ols_model = sm.OLS(y, X_ols).fit()
print("\n" + "=" * 50)
print("OLS REGRESSION SUMMARY (via statsmodels)")
print("=" * 50)
print(ols_model.summary())

# ===== Durbin-Watson Test =====
dw = durbin_watson(ols_model.resid)
print(f"\nDurbin-Watson statistic: {dw:.4f}")
if 1.5 < dw < 2.5:
    print("  → No significant autocorrelation ✅")
else:
    print("  → Autocorrelation detected ⚠️")

# ===== Q-Q Plot of Residuals =====
import matplotlib.pyplot as plt
fig, axes = plt.subplots(1, 3, figsize=(15, 4))

# Residual vs Fitted
axes[0].scatter(ols_model.fittedvalues, ols_model.resid, alpha=0.5, c='#059669', s=15)
axes[0].axhline(y=0, color='red', linestyle='--')
axes[0].set_xlabel('Fitted Values')
axes[0].set_ylabel('Residuals')
axes[0].set_title('Residuals vs Fitted')

# Q-Q plot
stats.probplot(ols_model.resid, dist="norm", plot=axes[1])
axes[1].set_title('Q-Q Plot of Residuals')

# Histogram of residuals
axes[2].hist(ols_model.resid, bins=25, color='#0891b2', alpha=0.7, edgecolor='white')
axes[2].set_title('Residual Distribution')
axes[2].set_xlabel('Residual')

plt.tight_layout()
plt.savefig('diagnostics.png', dpi=150)
plt.show()
12

TensorFlow Implementation

Multi-Input Regression Model with TensorFlow/Keras

tensorflow_multi_regression.py
import numpy as np
import tensorflow as tf
from tensorflow import keras
from sklearn.model_selection import train_test_split
from sklearn.preprocessing import StandardScaler

# ===== Generate Synthetic Data (5 features) =====
np.random.seed(42)
tf.random.set_seed(42)
m = 1000

X = np.column_stack([
    np.random.uniform(5, 25, m),         # area
    np.random.randint(1, 6, m).astype(float),  # rooms
    np.random.uniform(0.5, 15, m),       # metro distance
    np.random.uniform(1, 30, m),         # building age
    np.random.uniform(50, 100, m),       # safety score
])

# Non-linear relationship with interactions
y = (5 + 3*X[:,0] + 7*X[:,1] - 2*X[:,2] - 0.5*X[:,3] + 0.3*X[:,4]
     + 0.1*X[:,0]*X[:,1]    # interaction term
     - 0.05*X[:,0]**2       # quadratic term
     + np.random.normal(0, 5, m))

# Split and scale
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2, random_state=42)
scaler = StandardScaler()
X_train_s = scaler.fit_transform(X_train)
X_test_s = scaler.transform(X_test)

# ===== Model 1: Simple Linear (no hidden layers) =====
model_linear = keras.Sequential([
    keras.layers.Dense(1, input_shape=(5,), activation='linear',
                       kernel_initializer='normal')
], name='Linear_Model')

model_linear.compile(optimizer='adam', loss='mse', metrics=['mae'])
model_linear.fit(X_train_s, y_train, epochs=100, batch_size=32, 
                 verbose=0, validation_split=0.1)

# ===== Model 2: Deep Regression (captures nonlinearity) =====
model_deep = keras.Sequential([
    keras.layers.Dense(64, input_shape=(5,), activation='relu'),
    keras.layers.BatchNormalization(),
    keras.layers.Dropout(0.2),
    keras.layers.Dense(32, activation='relu'),
    keras.layers.BatchNormalization(),
    keras.layers.Dropout(0.1),
    keras.layers.Dense(16, activation='relu'),
    keras.layers.Dense(1, activation='linear')
], name='Deep_Regression')

model_deep.compile(
    optimizer=keras.optimizers.Adam(learning_rate=0.001),
    loss='mse',
    metrics=['mae']
)

# Train with early stopping
early_stop = keras.callbacks.EarlyStopping(
    monitor='val_loss', patience=15, restore_best_weights=True
)

history = model_deep.fit(
    X_train_s, y_train,
    epochs=200, batch_size=32,
    validation_split=0.15,
    callbacks=[early_stop],
    verbose=0
)

# ===== Evaluate Both =====
from sklearn.metrics import r2_score, mean_squared_error

for name, model in [('Linear', model_linear), ('Deep', model_deep)]:
    y_pred = model.predict(X_test_s, verbose=0).flatten()
    r2 = r2_score(y_test, y_pred)
    rmse = np.sqrt(mean_squared_error(y_test, y_pred))
    print(f"{name:>8} Model → R²: {r2:.6f}, RMSE: {rmse:.4f}")

# ===== Model 3: Functional API (Multi-Input) =====
# For more complex architectures
input_continuous = keras.Input(shape=(3,), name='continuous')  # area, metro, age
input_discrete = keras.Input(shape=(2,), name='discrete')     # rooms, safety

# Process branches separately
cont_branch = keras.layers.Dense(32, activation='relu')(input_continuous)
cont_branch = keras.layers.Dense(16, activation='relu')(cont_branch)

disc_branch = keras.layers.Dense(16, activation='relu')(input_discrete)

# Merge
merged = keras.layers.Concatenate()([cont_branch, disc_branch])
merged = keras.layers.Dense(16, activation='relu')(merged)
output = keras.layers.Dense(1, activation='linear')(merged)

model_multi = keras.Model(
    inputs=[input_continuous, input_discrete],
    outputs=output,
    name='Multi_Input_Model'
)
model_multi.compile(optimizer='adam', loss='mse')

# Prepare multi-input data
X_train_cont = X_train_s[:, [0, 2, 3]]  # area, metro, age
X_train_disc = X_train_s[:, [1, 4]]     # rooms, safety
X_test_cont = X_test_s[:, [0, 2, 3]]
X_test_disc = X_test_s[:, [1, 4]]

model_multi.fit(
    [X_train_cont, X_train_disc], y_train,
    epochs=100, batch_size=32, verbose=0, validation_split=0.1
)

y_pred_multi = model_multi.predict([X_test_cont, X_test_disc], verbose=0).flatten()
print(f"{'Multi':>8} Model → R²: {r2_score(y_test, y_pred_multi):.6f}")

model_multi.summary()
13

Indian Case Studies

🇮🇳 Case Study 1: NSE Nifty50 Stock Market Forecasting

Finance NSE/BSE Multiple Regression 10+ Features

Problem: Predict the next-day Nifty50 closing price using 12 macroeconomic and technical indicators.

Features Used

  1. Previous day close price (lagged value)
  2. 5-day moving average
  3. 20-day moving average
  4. Trading volume (in crores)
  5. USD/INR exchange rate
  6. FII net investment (₹ crores)
  7. DII net investment (₹ crores)
  8. India VIX (volatility index)
  9. Crude oil price (Brent, $/barrel)
  10. 10-year government bond yield
  11. S&P 500 previous close (global cue)
  12. RSI (Relative Strength Index, 14-day)
nifty50_prediction.py
import numpy as np
import pandas as pd
from sklearn.linear_model import LinearRegression
from sklearn.preprocessing import PolynomialFeatures, StandardScaler
from sklearn.pipeline import Pipeline
from sklearn.model_selection import TimeSeriesSplit
from sklearn.metrics import r2_score, mean_absolute_error

# Simulated Nifty50 data (1000 trading days)
np.random.seed(42)
n_days = 1000

data = pd.DataFrame({
    'prev_close': 18000 + np.cumsum(np.random.normal(5, 150, n_days)),
    'ma_5': 18000 + np.cumsum(np.random.normal(5, 100, n_days)),
    'ma_20': 18000 + np.cumsum(np.random.normal(5, 50, n_days)),
    'volume_cr': np.random.uniform(8000, 25000, n_days),
    'usd_inr': 82 + np.cumsum(np.random.normal(0.01, 0.3, n_days)),
    'fii_net_cr': np.random.normal(500, 2000, n_days),
    'dii_net_cr': np.random.normal(800, 1500, n_days),
    'india_vix': np.random.uniform(10, 30, n_days),
    'crude_oil': 75 + np.cumsum(np.random.normal(0, 2, n_days)),
    'bond_yield': 7.0 + np.random.normal(0, 0.5, n_days),
    'sp500': 4500 + np.cumsum(np.random.normal(2, 30, n_days)),
    'rsi_14': np.random.uniform(20, 80, n_days),
})

# Target: next-day close (simplified model)
data['next_close'] = (data['prev_close'] * 0.85 + data['ma_5'] * 0.1
                      + data['fii_net_cr'] * 0.02
                      - data['india_vix'] * 5
                      + np.random.normal(0, 100, n_days))

features = data.columns[:-1]
X = data[features].values
y = data['next_close'].values

# Time-series aware cross-validation
tscv = TimeSeriesSplit(n_splits=5)

# Model 1: Linear
pipe_linear = Pipeline([
    ('scaler', StandardScaler()),
    ('reg', LinearRegression())
])

# Model 2: Polynomial degree 2 (with interactions)
pipe_poly = Pipeline([
    ('scaler', StandardScaler()),
    ('poly', PolynomialFeatures(degree=2, interaction_only=True)),
    ('reg', LinearRegression())
])

print("NIFTY50 PREDICTION — TIME SERIES CV RESULTS")
print("=" * 55)
for name, pipe in [('Linear', pipe_linear), ('Poly-Interaction', pipe_poly)]:
    r2_scores = []
    mae_scores = []
    for train_idx, test_idx in tscv.split(X):
        pipe.fit(X[train_idx], y[train_idx])
        y_pred = pipe.predict(X[test_idx])
        r2_scores.append(r2_score(y[test_idx], y_pred))
        mae_scores.append(mean_absolute_error(y[test_idx], y_pred))
    print(f"  {name:>20}: R² = {np.mean(r2_scores):.4f} ± {np.std(r2_scores):.4f}"
          f"  |  MAE = ₹{np.mean(mae_scores):.0f}")

Key Findings

  • Previous close and moving averages dominate the prediction (expected for time-series).
  • FII flows have a measurable but noisy effect on next-day returns.
  • Adding polynomial interaction terms (e.g., VIX × crude oil) improves accuracy slightly.
  • VIF analysis revealed: prev_close and MA features are highly collinear (VIF > 20). Solution: Use only the difference (prev_close - MA_20) instead.

🇮🇳 Case Study 2: Energy Consumption Prediction for Indian States

Energy NITI Aayog Polynomial Regression

Problem: Predict monthly electricity demand (GWh) for Indian states using multiple energy sources and economic indicators.

Features

  • Population (crores), GDP per capita (₹), urbanization rate (%)
  • Coal generation (MW), solar capacity (MW), wind capacity (MW), hydro capacity (MW)
  • Average temperature (°C), monsoon intensity index, industrial output index

Results

Using polynomial degree-2 with interaction terms, the model achieved R² = 0.89 on test data. Key insight: the interaction term (temperature × urbanization) was highly significant — urban areas have much stronger temperature-demand coupling due to air conditioning. A state like Tamil Nadu with high urbanization and hot summers shows a steeper demand curve per degree increase than a rural state like Jharkhand.

The model correctly predicted the 2024 summer peak demand surge in Rajasthan and Uttar Pradesh within 5% accuracy.

🇮🇳 Case Study 3: Agricultural Yield Prediction (Multi-Input)

Agriculture ICAR Feature Interactions

Problem: Predict rice yield (tonnes/hectare) across Indian districts using soil, weather, and farming inputs.

Feature Set (8 features)

FeatureUnitSourceVIF
Rainfallmm/monthIMD1.8
Temperature°CIMD2.3
Soil pHpH unitsICAR Soil Survey1.5
Nitrogen (N)kg/hectareFarm survey3.2
Phosphorus (P)kg/hectareFarm survey4.1
Potassium (K)kg/hectareFarm survey2.7
Irrigation %%Census1.9
Seed quality index0–100IARI1.4

Key Discovery: Rainfall-Fertilizer Interaction

The most significant interaction term was rainfall × nitrogen (p < 0.001). In districts with high rainfall, adding more nitrogen fertilizer had a dramatically larger effect on yield than in drought-prone districts — because water is needed to transport nutrients to roots. This interaction was missed by simple additive models.

Adjusted R² improved from 0.72 (no interactions) to 0.84 (with key interaction terms). Polynomial terms (temperature²) captured the inverted-U relationship: rice yield peaks at 28–30°C and drops at extremes.

14

Global Case Studies

🌍 Case Study 1: NASDAQ Prediction with Economic Indicators

Finance NASDAQ USA

Problem: Predict NASDAQ Composite monthly returns using macroeconomic indicators.

Features

  • Federal Funds Rate, CPI (Consumer Price Index), Unemployment rate
  • GDP growth rate, Consumer Confidence Index, Manufacturing PMI
  • 10-year Treasury yield, US Dollar Index (DXY), M2 money supply growth

Findings

A polynomial model with degree-2 terms outperformed linear: R² improved from 0.31 (linear) to 0.47 (polynomial). The interaction (Fed rate × CPI) was the strongest interaction — during high inflation, rate hikes have a disproportionately negative impact on tech stocks. VIF analysis identified Treasury yield and Fed rate as collinear (VIF=8.3); using the yield spread (10y - 2y) as a single feature reduced VIF to 2.1.

🌍 Case Study 2: WHO Health Expenditure vs Life Expectancy (Multi-Country)

Healthcare WHO 193 Countries

Problem: Model life expectancy across 193 countries using health and socioeconomic features.

Features (from WHO Global Health Observatory)

  • Health expenditure (% of GDP), number of physicians per 1000 people
  • GDP per capita (PPP), adult literacy rate, access to clean water (%)
  • Infant mortality rate, vaccination coverage (DPT3), HIV prevalence

Key Results

Simple linear model: R² = 0.74. Adding polynomial terms for GDP per capita (quadratic): R² = 0.82. The quadratic term captured the "law of diminishing returns" — going from $1,000 to $5,000 GDP/capita adds ~10 years of life expectancy, but $50,000 to $54,000 adds only ~0.5 years.

The interaction (health expenditure × clean water access) was highly significant: health spending without clean water infrastructure yields diminishing returns. This insight has policy implications for developing nations including India.

India's position: With GDP/capita ≈ $2,500 and health expenditure ≈ 3.5% of GDP, the model predicts life expectancy of 68.5 years (actual: 69.7). India's Ayushman Bharat health coverage expansion is projected to improve this significantly.

15

Startup Applications

Real-World Startup Use Cases

  • Zerodha (Fintech): Uses multiple regression to estimate portfolio risk using 15+ market factors. Polynomial features capture the non-linear volatility smile in options pricing.
  • Cred (Fintech): Predicts credit card bill payment probability using salary, spending patterns, credit utilization ratio, and their interactions.
  • Nykaa (E-commerce): Forecasts product demand using price, discount percentage, brand popularity, season, and the interaction (price × discount) — steep discounts on premium brands drive disproportionate sales.
  • BigBasket (Grocery delivery): Predicts delivery time using distance, order size, time of day, traffic index, and weather — with polynomial terms for peak hour congestion.
  • Ola/Uber (Ride-hailing): Dynamic pricing uses multiple regression with demand features, supply features, and interaction terms like (rain × surge zone × time of day).
16

Government Applications

Multiple Regression in Indian Government & Public Policy

  • NITI Aayog's SDG Index: Uses multiple regression to understand which factors (education spending, healthcare access, sanitation coverage) contribute most to state-level SDG achievement scores.
  • RBI Monetary Policy: Models inflation (CPI) using crude oil prices, monsoon index, MSP changes, global commodity prices, and money supply — a textbook multiple regression problem.
  • Smart Cities Mission: Predicts traffic congestion using road width, vehicle density, signal timing, time of day, and weather — polynomial terms for rush hours.
  • PMAY (Housing for All): Housing demand estimation using population growth, urbanization rate, income levels, and land availability per district.
  • Jal Jeevan Mission: Models clean water access improvement using budget allocation, terrain difficulty, existing infrastructure, and population density.
17

Industry Applications

Multiple & Polynomial Regression Across Industries

IndustryApplicationKey FeaturesWhy Polynomial?
Real EstateProperty valuationArea, location, age, amenities (10+ features)Area² captures diminishing returns for very large properties
ManufacturingQuality predictionTemperature, pressure, humidity, speedInteractions (temp × pressure) capture process sweet spots
PharmaDrug dosage optimizationWeight, age, kidney function, co-medicationsAge² captures non-linear metabolism changes
Automotive (Tata, Mahindra)Fuel efficiency modellingEngine size, weight, aero drag, tire pressureSpeed² for aerodynamic drag (quadratic)
Retail (Reliance, DMart)Sales forecastingPrice, promotion, competition, seasonPrice × promotion interaction drives non-linear sales lift
Telecom (Jio, Airtel)Churn predictionUsage, complaints, tenure, plan cost, data consumedTenure² captures the "loyalty curve"
18

Mini Projects

🔨 Mini Project 1: Indian Stock Market Predictor

Difficulty: Intermediate Time: 4–6 hours Data: Yahoo Finance API

Objective

Build a regression pipeline that predicts the next-day Nifty50 closing price using at least 8 technical and fundamental indicators.

Requirements

  1. Download 3 years of Nifty50 data using yfinance library
  2. Engineer features: SMA(5), SMA(20), EMA(12), MACD, RSI(14), Bollinger Band width, volume change %, previous day return
  3. Check VIF for all features, remove collinear features (VIF > 10)
  4. Compare: (a) Linear regression, (b) Polynomial degree-2 with interactions, (c) Polynomial degree-3
  5. Use TimeSeriesSplit (not random split!) with 5 folds
  6. Report: Adjusted R², RMSE, MAE, and direction accuracy (did you predict up/down correctly?)
  7. Create residual diagnostic plots (residual vs fitted, Q-Q plot, Durbin-Watson)

Starter Code

project1_starter.py
import yfinance as yf
import pandas as pd
import numpy as np
from sklearn.pipeline import Pipeline
from sklearn.preprocessing import PolynomialFeatures, StandardScaler
from sklearn.linear_model import LinearRegression
from sklearn.model_selection import TimeSeriesSplit
from sklearn.metrics import r2_score, mean_squared_error
from statsmodels.stats.outliers_influence import variance_inflation_factor

# Step 1: Download data
nifty = yf.download('^NSEI', start='2021-01-01', end='2024-01-01')

# Step 2: Feature engineering
df = pd.DataFrame()
df['close'] = nifty['Close']
df['sma_5'] = df['close'].rolling(5).mean()
df['sma_20'] = df['close'].rolling(20).mean()
df['rsi_14'] = ...  # Implement RSI
df['volume_pct'] = nifty['Volume'].pct_change()
df['return_1d'] = df['close'].pct_change()
df['bollinger_width'] = ...  # Implement Bollinger Bands

# Step 3: Target (next-day close)
df['target'] = df['close'].shift(-1)
df.dropna(inplace=True)

# Step 4: VIF check
features = ['sma_5', 'sma_20', 'rsi_14', 'volume_pct', 'return_1d']
X = df[features].values
for i, f in enumerate(features):
    print(f"VIF({f}) = {variance_inflation_factor(X, i):.2f}")

# Step 5: Model comparison
# ... YOUR CODE HERE ...

# Step 6: Diagnostics
# ... YOUR CODE HERE ...

Expected Deliverables

  • A Jupyter notebook with clear commentary
  • A comparison table of model metrics
  • Diagnostic plots with interpretation
  • A 1-page summary of findings and limitations

🔨 Mini Project 2: Energy Demand Forecaster for Indian States

Difficulty: Advanced Time: 6–8 hours Data: POSOCO / data.gov.in

Objective

Build a state-wise energy demand prediction model using weather, economic, and infrastructure features.

Requirements

  1. Collect data for at least 5 Indian states (Maharashtra, Tamil Nadu, UP, Karnataka, Rajasthan)
  2. Features: temperature, humidity, GDP per capita, population, industrial index, installed solar/wind capacity, season indicator
  3. Build separate polynomial models per state
  4. Identify the most important interaction terms per state
  5. Use AIC/BIC for optimal polynomial degree selection
  6. Implement forward feature selection to find the minimal feature set
  7. Compare linear, polynomial (degree 2), and polynomial (degree 3)
  8. Bonus: Build a single model for all states using state as a categorical feature (one-hot encoded)

Expected Output

  • Model comparison table across states
  • Feature importance rankings per state
  • Policy recommendation: which features should governments optimize to manage peak demand?

🔨 Mini Project 3: Overfitting Explorer — Interactive Demo

Difficulty: Beginner Time: 2–3 hours Tool: Matplotlib/Streamlit

Objective

Build an interactive visualization that demonstrates overfitting in polynomial regression.

Requirements

  1. Generate 20 noisy data points from a known function (e.g., y = sin(x) + noise)
  2. Allow the user to select polynomial degree from 1 to 20 (slider)
  3. Display: training R², test R², the fitted curve, and the data points
  4. Plot the bias-variance tradeoff curve: training error and test error vs. polynomial degree
  5. Highlight the "sweet spot" where test error is minimized
19

End-of-Chapter Exercises

Q1Easy
Given a dataset with 3 features and 50 samples, write out the dimensions of X, W, Y, XᵀX, and XᵀY.
Q2Easy
Explain why adding a column of 1s to the design matrix X handles the bias/intercept term.
Q3Medium
Compute the normal equation solution by hand for the data: (x₁=1, x₂=2, y=7), (x₁=2, x₂=1, y=8), (x₁=3, x₂=3, y=15). Show all matrix multiplications.
Q4Easy
If a model has R² = 0.85 with 5 features and 100 samples, compute the Adjusted R². Does it change if we use 20 features instead?
Q5Medium
A polynomial regression with degree 4 on 30 data points achieves training R² = 0.99 and test R² = 0.45. Diagnose the issue and suggest two remedies.
Q6Easy
Compute VIF for a feature with R²_j = 0.92. Is this acceptable? What would you do?
Q7Medium
Why does the normal equation fail when XᵀX is singular? Give two practical scenarios where this happens.
Q8Easy
How many features does PolynomialFeatures(degree=3) create from 2 input features? List them all (including interactions).
Q9Medium
Derive the gradient of J(W) = (Y - XW)ᵀ(Y - XW) step by step. Verify it matches the normal equation.
Q10Hard
Prove that adding a random noise feature to a regression model always increases R² but may decrease Adjusted R². Provide a mathematical proof.
Q11Medium
Write Python code to implement backward elimination: start with all features, remove the least significant one at each step (highest p-value > 0.05).
Q12Easy
What is the Durbin-Watson statistic? If DW = 0.8, what does it indicate? How would you fix it?
Q13Medium
Compare AIC and BIC for two models: Model A (4 features, RSS=120, m=100) and Model B (7 features, RSS=95, m=100). Which is preferred by each criterion?
Q14Hard
Implement ridge regression from scratch (adding λI to XᵀX in the normal equation). How does this solve multicollinearity? (Preview of Chapter 6.)
Q15Easy
What is the difference between interaction_only=True and interaction_only=False in sklearn's PolynomialFeatures?
Q16Medium
You have 10,000 features and 500 samples. Can you use the normal equation? Why or why not? What alternative would you use?
Q17Hard
Derive the expression for the F-statistic that tests whether adding a group of features significantly improves the model. Use it to compare a 3-feature and 5-feature model on synthetic data.
Q18Medium
Create a Q-Q plot for the residuals of a polynomial regression. If the plot deviates from the diagonal at the tails, what does it mean?
Q19Easy
In the house price example (Example 1), predict the price for a house with area=18 and rooms=5 using the fitted model.
Q20Medium
Implement a complete pipeline: load the Boston Housing dataset replacement (California Housing), apply polynomial features (degree 2), scale, fit, evaluate with 5-fold CV, and report Adjusted R².
Q21Hard
Prove that the OLS estimator is BLUE (Best Linear Unbiased Estimator) under the Gauss-Markov theorem. State all required assumptions.
Q22Medium
Build a TensorFlow model with 3 input features that includes a custom feature cross layer (compute x₁·x₂ inside the network). Compare with manual polynomial features.
20

Multiple Choice Questions

1. Polynomial regression is:

(A) Nonlinear in both parameters and inputs
(B) Linear in parameters, nonlinear in inputs
(C) Nonlinear in parameters, linear in inputs
(D) A completely different algorithm from linear regression
(B) Polynomial regression creates nonlinear features (x², x³) but the model y = w₁x + w₂x² is still linear in the parameters w₁, w₂. This is why OLS and the normal equation still work.

2. The normal equation W = (XᵀX)⁻¹XᵀY fails when:

(A) The dataset is too large
(B) XᵀX is singular (non-invertible)
(C) Y contains negative values
(D) The features are standardized
(B) XᵀX is singular when features are perfectly collinear or when n ≥ m (more features than samples). The inverse doesn't exist. Solutions: remove collinear features, use pseudo-inverse, or add regularization (ridge regression).

3. If VIF = 20 for feature x₃, it means:

(A) x₃ has low predictive power
(B) x₃ is an outlier
(C) 95% of x₃'s variance is explained by other features
(D) x₃ should always be kept in the model
(C) VIF = 20 means R²₃ = 1 - 1/20 = 0.95, so 95% of x₃'s variation is explained by other features. This indicates severe multicollinearity — the weight estimate for x₃ will be unstable.

4. Adjusted R² can decrease when you add a new feature because:

(A) The penalty for model complexity outweighs the improvement in fit
(B) The computation has a bug
(C) R² always equals Adjusted R²
(D) The new feature has negative correlation with y
(A) Adjusted R² = 1 - [(1-R²)(m-1)]/(m-n-1). When n increases by 1, the denominator shrinks. If the tiny R² improvement doesn't compensate, Adjusted R² drops — correctly penalizing unnecessary complexity.

5. PolynomialFeatures(degree=2) applied to [x₁, x₂] produces:

(A) [x₁, x₂, x₁², x₂²]
(B) [1, x₁, x₂, x₁², x₁x₂, x₂²]
(C) [x₁, x₂, x₁x₂]
(D) [x₁², x₂², x₁x₂]
(B) By default, PolynomialFeatures includes the bias term (1), all original features, all squared terms, and all cross-product terms. With include_bias=False, the 1 is dropped.

6. The Durbin-Watson statistic value of 1.2 indicates:

(A) Positive autocorrelation in residuals
(B) Negative autocorrelation in residuals
(C) No autocorrelation
(D) Heteroscedasticity
(A) DW ≈ 2 means no autocorrelation. DW < 1.5 indicates positive autocorrelation (consecutive residuals tend to have the same sign), suggesting the model misses a systematic pattern.

7. Which criterion penalizes model complexity MORE for large datasets?

(A) AIC (penalty = 2k)
(B) BIC (penalty = k·ln(m))
(C) R²
(D) MSE
(B) BIC's penalty is k·ln(m), which grows with sample size m. For m ≥ 8, ln(m) > 2, so BIC penalizes more than AIC. BIC tends to select simpler (more parsimonious) models.

8. The time complexity of solving the normal equation is:

(A) O(m·n)
(B) O(m²·n)
(C) O(n³) for the inversion + O(m·n²) for XᵀX
(D) O(m³)
(C) Computing XᵀX requires O(m·n²) operations, and inverting the (n+1)×(n+1) matrix requires O(n³). This makes the normal equation impractical for very large n (e.g., NLP with millions of features).

9. In forward feature selection, you should use _____ instead of random train-test splits for time-series data:

(A) KFold
(B) TimeSeriesSplit
(C) StratifiedKFold
(D) ShuffleSplit
(B) TimeSeriesSplit ensures the model is always trained on past data and tested on future data, preventing look-ahead bias. Random splits would leak future information into training.

10. A Q-Q plot that shows heavy tails (deviating upward at right, downward at left) suggests:

(A) Residuals are perfectly normal
(B) Residuals have a heavy-tailed (leptokurtic) distribution
(C) The model is underfitting
(D) Multicollinearity is present
(B) Heavy tails in the Q-Q plot mean extreme residuals are more common than expected under normality. This could indicate outliers or that the error distribution is t-distributed rather than normal. Robust regression methods may be needed.
21

Interview Questions

IQ-1 Explain the difference between simple linear regression and multiple linear regression. When would you use each?
Show Answer
Simple linear regression uses one feature: y = wx + b. Multiple linear regression uses n features: y = w₁x₁ + w₂x₂ + … + wₙxₙ + b. Use simple when you have one clear predictor (e.g., study hours → exam score). Use multiple when the outcome depends on several factors (e.g., house price depends on area, location, age, rooms). In practice, almost all real-world problems require multiple regression because single factors rarely explain outcomes fully.
IQ-2 What is multicollinearity? How do you detect and fix it?
Show Answer
Multicollinearity occurs when two or more features are highly correlated, making it difficult to isolate each feature's individual effect. Detection: (1) Correlation matrix — look for |r| > 0.8. (2) VIF — values > 5 indicate concern, > 10 indicates severe multicollinearity. Fixes: (1) Remove one of the correlated features. (2) Combine them (PCA or domain-specific aggregation). (3) Add regularization (Ridge regression adds λ to diagonal of XᵀX, guaranteeing invertibility). (4) Increase sample size to stabilize estimates.
IQ-3 Why is polynomial regression considered "linear" regression?
Show Answer
"Linear" in linear regression refers to linearity in the parameters, not in the features. The model y = w₁x + w₂x² is linear in w₁ and w₂, even though x² is a nonlinear transformation of x. By defining z = x², we get y = w₁x + w₂z, which is clearly a linear combination of features. This means all linear algebra tools (normal equation, gradient descent) apply unchanged. A truly nonlinear model would be something like y = w₁ · sin(w₂ · x), where the parameters appear nonlinearly.
IQ-4 Explain the bias-variance tradeoff in the context of polynomial regression degree selection.
Show Answer
Low degree (e.g., 1): High bias (the model is too simple to capture the true pattern), low variance (predictions are stable across different datasets). High degree (e.g., 15): Low bias (can fit any pattern), high variance (predictions change dramatically with different training data). The optimal degree minimizes total error = bias² + variance. This is found via cross-validation — train on k-1 folds, test on the remaining fold, average across folds. The degree with the lowest CV error is the sweet spot.
IQ-5 When would you prefer gradient descent over the normal equation for multiple regression?
Show Answer
Prefer gradient descent when: (1) n (features) is very large (> 10,000) — normal equation requires O(n³) matrix inversion. (2) The data doesn't fit in memory — gradient descent can work in mini-batches. (3) You want online learning (updating the model as new data arrives). Prefer normal equation when: (1) n is small (< 10,000). (2) You want an exact solution. (3) There's no hyperparameter tuning needed (no learning rate). In practice, sklearn's LinearRegression uses a variant called SVD decomposition, which is numerically more stable than directly computing (XᵀX)⁻¹.
IQ-6 Explain Adjusted R² and when it differs significantly from R².
Show Answer
R² = 1 - RSS/TSS always increases when features are added because the model has more flexibility. Adjusted R² = 1 - [(1-R²)(m-1)]/(m-n-1) penalizes for the number of features n. They differ significantly when: (1) Many irrelevant features are included — R² may be 0.85 but Adjusted R² is 0.70. (2) Sample size m is small relative to n — the penalty is stronger. Rule of thumb: if m/n < 10, always report Adjusted R² instead of R².
IQ-7 How would you check regression assumptions in a real-world project?
Show Answer
Four key assumptions and their checks: (1) Linearity: Plot residuals vs. fitted values — should show no pattern (random scatter around zero). (2) Normality of errors: Q-Q plot of residuals — points should follow the diagonal. Shapiro-Wilk test for formal testing. (3) Homoscedasticity (constant variance): Residuals vs. fitted plot should not show a "fan" pattern. Breusch-Pagan test. (4) Independence: Durbin-Watson test — should be ≈ 2. Values < 1.5 or > 2.5 indicate autocorrelation. If assumptions are violated: consider transformations (log y), robust standard errors, or non-linear models.
IQ-8 What is the difference between AIC and BIC? When do they disagree?
Show Answer
Both balance model fit and complexity. AIC = m·ln(RSS/m) + 2k; BIC = m·ln(RSS/m) + k·ln(m). The key difference is the penalty: AIC uses 2k (constant per parameter), BIC uses k·ln(m) (grows with sample size). For m > 7 (ln(8) > 2), BIC penalizes more. They disagree when: a moderately complex model improves fit slightly — AIC may prefer it (lower penalty), BIC may reject it (higher penalty). BIC is asymptotically consistent (selects the true model as m → ∞); AIC tends to select slightly more complex models. In practice, report both and let domain knowledge guide the final decision.
IQ-9 How do feature interaction terms improve a regression model? Give a real-world example.
Show Answer
Interaction terms capture synergy/antagonism between features that additive models miss. Example: In agricultural yield prediction, the model y = w₁·rainfall + w₂·fertilizer + w₃·(rainfall × fertilizer) + b. Without the interaction, the model assumes rainfall and fertilizer contribute independently. But in reality, fertilizer is most effective when there's adequate rainfall (water carries nutrients to roots). The interaction term w₃ captures this: if w₃ > 0, high rainfall amplifies the effect of fertilizer — their combined impact exceeds the sum of their individual effects.
IQ-10 You've built a model with 50 features and R² = 0.95. Your manager is impressed, but you're suspicious. What checks would you run?
Show Answer
Red flags for a high R²: (1) Check Adjusted R² — if it drops to 0.80, many features are noise. (2) VIF analysis — check for multicollinearity. (3) Cross-validation — if CV R² drops to 0.60, the model is overfitting. (4) Check for data leakage — is any feature derived from the target? (e.g., using "total revenue" to predict "net profit"). (5) Residual diagnostics — are assumptions met? (6) Hold-out test set — evaluate on truly unseen data. (7) Try a simpler model (10 features) — if R² is 0.93, the extra 40 features add almost nothing but complexity. High R² on training data is not an achievement; high R² on unseen test data with a parsimonious model is.
22

Research Problems

R1Research

Optimal Polynomial Degree Selection via Information-Theoretic Criteria

Investigate whether AIC, BIC, or cross-validation consistently selects the "true" polynomial degree under various noise levels and sample sizes. Design a simulation study with:

  • True functions of known degree (2, 3, 5)
  • Sample sizes: 20, 50, 100, 500, 1000
  • Noise levels: σ = 0.1, 1.0, 5.0

Compare AIC, BIC, and 10-fold CV in terms of: (a) frequency of selecting the correct degree, (b) average test MSE of the selected model. Report which criterion is best for small vs. large samples, and low vs. high noise.

R2Research

Multicollinearity Robustness of Different Regression Methods

Compare the stability of weight estimates under increasing multicollinearity for: OLS, Ridge, LASSO, and Elastic Net. Generate features with pairwise correlations from 0.0 to 0.99. For each method, plot:

  • Variance of estimated weights vs. correlation
  • MSE on test data vs. correlation
  • Number of correctly identified non-zero weights (for LASSO)

Determine the "critical correlation threshold" beyond which each method's performance degrades significantly.

R3Research

Feature Interaction Discovery in Indian Agricultural Data

Using ICRISAT or data.gov.in datasets, systematically discover all pairwise feature interactions that significantly improve crop yield prediction. Propose a method based on:

  • Exhaustive pairwise interaction testing with Bonferroni correction
  • Random interaction search (like random search in hyperparameter tuning)
  • Domain knowledge-guided interaction selection

Compare these three strategies in terms of computational cost, statistical power, and interpretability. Can the discovered interactions provide novel agronomic insights?

23

Key Takeaways

Multiple regression extends simple regression to n features: The matrix formulation Y = XW converts the problem into linear algebra, and the normal equation W = (XᵀX)⁻¹XᵀY gives the exact optimal solution.
Polynomial regression is still linear regression: By creating features x², x³, x₁·x₂, we capture nonlinear patterns while using the same OLS machinery. The trick is in the feature transformation, not in a new algorithm.
VIF detects multicollinearity: VIF = 1/(1-R²_j) measures how much a feature is predicted by others. VIF > 10 signals trouble — weight estimates become unstable and uninterpretable.
Adjusted R² prevents overfitting illusions: Unlike R², it penalizes unnecessary features. Always report Adjusted R² when comparing models of different complexity.
AIC and BIC guide model selection rigorously: Both balance fit and complexity, with BIC applying a stricter penalty. Use them alongside cross-validation for robust model comparison.
Feature selection is critical: Forward, backward, and stepwise selection algorithms systematically identify the most informative features. Too many features → overfitting; too few → underfitting.
Always check regression assumptions: Residual plots, Q-Q plots, and the Durbin-Watson test reveal whether your model's outputs can be trusted. Violated assumptions → unreliable predictions and confidence intervals.
The bias-variance tradeoff is real and visible: Polynomial degree is the clearest demonstration — degree too low (underfit, high bias), degree too high (overfit, high variance). Cross-validation finds the sweet spot.
Interaction terms capture synergies: The term x₁·x₂ reveals whether two features amplify or dampen each other's effects — critical for agricultural, economic, and marketing applications.
24

References & Further Reading

Textbooks

  1. Montgomery, D.C., Peck, E.A., & Vining, G.G. (2021). Introduction to Linear Regression Analysis, 6th Edition. Wiley.
  2. James, G., Witten, D., Hastie, T., & Tibshirani, R. (2021). An Introduction to Statistical Learning (ISLR), 2nd Edition. Springer. [Free online]
  3. Hastie, T., Tibshirani, R., & Friedman, J. (2009). The Elements of Statistical Learning (ESL), 2nd Edition. Springer. [Free online]
  4. Weisberg, S. (2014). Applied Linear Regression, 4th Edition. Wiley.
  5. Géron, A. (2022). Hands-On Machine Learning with Scikit-Learn, Keras, and TensorFlow, 3rd Edition. O'Reilly. Chapters 4 (Training Models).

Research Papers

  1. Akaike, H. (1974). "A new look at the statistical model identification." IEEE Transactions on Automatic Control, 19(6), 716–723.
  2. Schwarz, G. (1978). "Estimating the dimension of a model." Annals of Statistics, 6(2), 461–464.
  3. Marquardt, D.W. (1970). "Generalized inverses, ridge regression, biased linear estimation, and nonlinear estimation." Technometrics, 12(3), 591–612.
  4. Durbin, J. & Watson, G.S. (1950). "Testing for serial correlation in least squares regression." Biometrika, 37(3/4), 409–428.

Online Resources

  1. Scikit-Learn Documentation: Linear Models
  2. TensorFlow Tutorials: Basic Regression
  3. Khan Academy: Regression
  4. NSE India Historical Data: nseindia.com
  5. NITI Aayog Data: Energy Dashboard
  6. data.gov.in: Open Government Data Platform India

Indian References

  1. Mahalanobis, P.C. (1936). "On the generalized distance in statistics." Proceedings of the National Institute of Sciences of India.
  2. ICAR-Indian Agricultural Research Institute. Crop prediction models and methodologies. iari.res.in
  3. Reserve Bank of India. RBI Bulletin — econometric models for inflation forecasting. rbi.org.in