Part II — Regression

Chapter 5 · Multiple Linear Regression

Real problems have many features. Multiple linear regression extends Chapter 4's line to a hyperplane — and pays for the extra power with new obligations: escaping the dummy variable trap, proving predictors are not collinear, and earning each feature's place with a p-value. This chapter fits the full model on 50_Startups.csv, reads the statsmodels summary table line by line, and runs backward elimination to its honest conclusion — every number real, every decision defensible.

📖 Chapter 5 of 33 ⏱ ~110 min read Mostly Core Stable 📝 Quiz · 30 MCQs

🎯 Learning Objectives

1. From One Feature to Many

Chapter 4 predicted salary from one number. The 50_Startups problem is richer: predict annual Profit from R&D Spend, Administration Spend, Marketing Spend, and State (New York / California / Florida) — 50 startups, four features, one continuous target. Multiple linear regression (MLR) is the direct extension: a linear coefficient for every feature, all fitted by the same least-squares logic.

ŷ  =  b0  +  b1x1  +  b2x2  +  …  +  bpxp
In matrix form, for all n observations at once: b = (XᵀX)⁻¹Xᵀy — the same normal equations as Chapter 4, generalised. With one feature the model is a line in 2-D; with two, a plane (Figure 5.1); with more, a hyperplane. The geometry bends; the machinery does not.
x₁ (R&D Spend) x₂ Profit (y) ŷ = b₀ + b₁x₁ + b₂x₂ (plane)
Figure 5.1 — Two features: the fitted model is a plane; residuals remain the vertical gaps. More features generalise the picture, not the idea.

1.1 The phrase that unlocks every coefficient

A slope in MLR is no longer "the average change in y per unit of x" full stop — other features could be moving too. The correct reading adds four words: holding the other features constant. Each coefficient isolates its feature's contribution ceteris paribus. This is precisely what makes MLR more valuable than running five separate simple regressions: it disentangles overlapping influences.

QuantityValueReading
Rows50Comfortable for 3 numeric predictors — see the sample-size rule in Section 3
R&D Spend0 – 165,349Strongest single correlate of Profit (r = 0.973)
Administration51,283 – 182,346Weak correlate (r = 0.201)
Marketing Spend0 – 471,784Strong (r = 0.748) — but also correlates 0.724 with R&D…
State3 categoriesNominal — Chapter 3 says one-hot; Section 2 adds the catch
Profit (target)mean 112,013Continuous → regression
📝
Note: R&D and Marketing correlating at 0.724 with each other is this chapter's live specimen of multicollinearity — predictors sharing information. Park that thought until Section 4, where the VIF puts a number on it.

2. Categorical Predictors & the Dummy Trap — Solved

Chapter 3 one-hot encoded State into three dummies and promised Chapter 5 would spring the trap. Here it is. With all three columns present, every row sums the dummies to exactly 1 — which is exactly what the intercept's constant column does:

constNYCalFlaNY+Cal+Fla
11001
10101
10011
Dummies reproduce the constant column exactly → perfect multicollinearity

The design matrix loses rank: the "equation" XᵀX b = Xᵀy no longer has a unique solution — infinitely many coefficient sets fit identically, because any amount can be shuffled between the intercept and the three dummies without changing a single prediction. This is the dummy variable trap: a special case of multicollinearity created by encoding, not by the world.

2.1 The cure: drop one category

import pandas as pd

dummies = pd.get_dummies(df['State'], drop_first=True)   # drops 'California'
# -> columns: Florida, New York.  California is implied by both being 0.

Listing 5.1 — Two dummies carry all three categories' information.

The dropped category becomes the baseline (reference) category, and every dummy coefficient changes meaning — it is now a difference against that baseline:

💡
Pro Tip — the sklearn footnote: this chapter's source code keeps all three dummies and trains LinearRegression anyway — without complaint. sklearn's solver uses a pseudo-inverse that quietly returns one of the infinitely many solutions (minimum-norm), so predictions stay correct while the coefficients are arbitrary. statsmodels' classic OLS will not be so forgiving with inference. Moral: sklearn's silence is not absolution — for interpretable coefficients, drop the category.
⚠
Warning: The baseline is whoever gets dropped — with drop_first=True that is the alphabetically first category (California here). That choice is arbitrary but not innocent: dummy coefficients and their p-values are measured against the baseline. Pick the baseline deliberately (the most common category is a sensible default), and never compare coefficients across models built on different baselines.

3. New Assumptions for Multiple Features

Everything from Chapter 4's LINE suite carries over unchanged — linearity now means "linear in each coefficient", independence, normality and equal variance still apply to the residuals, and the residual-vs-fitted plot remains the diagnostic workhorse. Multiple features add three new obligations:

New assumptionWhat it demandsHow to checkIf violated
No multicollinearity Predictors not near-perfectly related to each other Correlation matrix; VIF (Section 4) Coefficients unstable, signs flip, p-values misleading
No dummy trap One category dropped per one-hot group (its multicollinearity, by encoding) Design-matrix rank; the Section 2 table No unique solution — inference impossible
Adequate sample size Enough rows per predictor (working rule: at least 10–20 observations per predictor) n / p ratio; power grows with n Overfit; noisy estimates; p-values untrustworthy

The sample-size rule deserves a sentence of arithmetic: 50 rows ÷ 3 numeric predictors ≈ 17 rows per predictor — acceptable; add three dummies and the ratio falls to 10 — the boundary. Small datasets make every added feature expensive, which is exactly why Section 6's elimination matters here.

📝
Note: A fourth, subtler addition: no single observation should dominate the fit (high leverage). One startup with extreme spend values can tilt the whole plane. Cook's distance flags such points; with 50 clean rows here we note it and move on.

4. Multicollinearity & the VIF

Multicollinearity is a relationship among predictors — the target is not involved. Pairwise correlations reveal the obvious cases (R&D ↔ Marketing at 0.724), but collinearity can hide in combinations: three features where any pair looks innocent, yet one is nearly a linear mix of the other two. The Variance Inflation Factor (VIF) catches what pairwise correlation misses, per feature:

VIFj  =  1 / (1 − R²j)
R²j comes from regressing feature j on all the other features. If feature j is barely explainable by the rest, R²j ≈ 0 and VIF ≈ 1 (no inflation). If the rest nearly determine it, R²j → 1 and VIF explodes. The name is literal: VIF multiplies the variance of bj relative to an uncorrelated world.
Feature (50_Startups)VIFVerdict
R&D Spend2.47Fine — mild inflation
Administration1.18Fine
Marketing Spend2.33Fine — the 0.724 correlation barely registers

The conventional thresholds: VIF > 5 — investigate; VIF > 10 — serious, act. Despite the eye-catching 0.724 pairwise correlation, all three features pass comfortably. The lesson generalises: correlation that looks large in a scatter can still leave a feature plenty of independent variation — let the VIF, not the anxiety, decide.

💡
Pro Tip — remedies when VIF does explode: (1) drop one of the intertwined features (keep the cheaper or more interpretable one); (2) combine them into a single index (sum, average); (3) in the modern toolkit, regularisation (ridge) tames collinearity without dropping anything — a technique you will meet when the course extends beyond OLS.
from statsmodels.stats.outliers_influence import variance_inflation_factor
import statsmodels.api as sm

X = sm.add_constant(startups[['R&D Spend', 'Administration', 'Marketing Spend']])
for i, col in enumerate(X.columns):
    print(col, round(variance_inflation_factor(X.values, i), 2))

Listing 5.2 — Computing VIFs with statsmodels (always add the constant column first).

5. Adjusted R² & the p-Value Toolkit

5.1 Adjusted R² — the penalty clause

Chapter 4's Trap 2 returns with teeth: training R² never falls when you add a predictor — even pure noise. With five features and 50 rows, R² alone cannot distinguish signal from arithmetic. Adjusted R² amends the contract:

Adj R²  =  1 − (1 − R²) · (n − 1) / (n − p − 1)
n = observations, p = predictors. Each added feature must buy back more fit than the penalty charges; otherwise adjusted R² falls. On 50_Startups the full five-feature model scores R² = 0.9508 but adjusted R² = 0.9452 — five predictors on 50 rows pay a visible premium.

5.2 The statsmodels summary — a guided tour

scikit-learn answers "what is the prediction?" statsmodels answers "what do we know about the coefficients?" Here is the real summary for the full model (three spends + Florida and New York dummies, California baseline), annotated column by column:

coefstd errtP>|t|[0.025, 0.975]Real value (feature)
50,125~8,7005.750.000…const
0.8060.04617.370.000…R&D Spend
−0.0270.052−0.520.608…Administration
0.0270.0171.560.123…Marketing Spend
1993,3000.060.953…Florida (vs Cal.)
−423,200−0.010.990…New York (vs Cal.)

The summary's bottom rows carry model-wide verdicts:

⚠
Warning: A high p-value does not prove a feature is useless — it says this sample cannot distinguish its coefficient from zero (Chapter 2: absence of evidence). Collinear partners inflate each other's p-values; a feature can look dead only because its twin stole the credit. Eliminate with judgement, not blind scissors.

6. Feature Selection: Backward Elimination

The full model spends five predictors, yet its summary says three of them look like noise. Backward elimination formalises the pruning, p-value by p-value:

  1. Choose a significance level for staying (conventionally SL = 0.05)
  2. Fit the full model with all predictors
  3. Delete the predictor with the highest p-value, if it exceeds SL
  4. Refit and repeat step 3
  5. Stop when every remaining p-value ≤ SL

Run on 50_Startups (real output, full dataset, California baseline):

StepActionp-valueR²Adj R²
0All 5 features—0.95080.9452
1drop New York0.9900.95080.9452
2drop Florida0.9400.95080.9464
3drop Administration0.6020.95070.9475
4drop Marketing…0.0600.95050.9483
finalR&D only0.0000.94650.9454
Step 0 · all 5 features drop New York (p=0.99) · Florida (p=0.94) drop Administration (p=0.60) Marketing p = 0.060 SL 0.05 → drop SL 0.05: R&D only adj R² 0.9454 · AIC 1058.9 SL 0.10: R&D + Marketing adj R² 0.9483 · AIC 1057.1
Figure 5.2 — The elimination funnel, and the fork at Marketing's p = 0.060.

6.1 The borderline: p = 0.060

Here is the honest fork. At a strict SL = 0.05, Marketing goes and the final model is R&D alone. At SL = 0.10 — a threshold many practitioners deliberately choose for retention ("be a little generous before firing a feature") — elimination stops at R&D + Marketing. The tiebreakers say keep it:

Model (full-data fit)R²Adj R²AICTest R² (split 0.2, seed 0)Test RMSE
All 5 features (source model)0.95080.94521062.80.93479,138
R&D only0.94650.94541058.90.94658,275
R&D + Marketing0.95050.94831057.10.94748,199

The two-feature model wins adjusted R², wins AIC, and wins the held-out test set — the source model's extra three predictors bought in-sample glitter and paid for it in generalisation. Parsimony is not asceticism; it is arithmetic.

⚠
Warning — stepwise's fine print: backward elimination runs many tests on one sample, so some "significant" survivors are luck; results can be unstable across splits. Treat it as a transparent, interpretable starting point — not proof. Cross-validate the final choice (Chapter 31), and know that modern practice often prefers regularisation, which shrinks coefficients instead of the all-or-nothing guillotine.
📋 Stable content — Reviewed: August 2026

7. Implementation in Python

The full workflow, faithful to the source code but with the dummy trap defused. Preprocessing needs no imputation (no gaps) and no scaling (OLS solves exactly — Chapter 4's reasoning still holds; rescale only if you need comparable coefficients):

import numpy as np
import pandas as pd
import matplotlib.pyplot as plt

# 1. Load; split features / target
dataset = pd.read_csv('50_Startups.csv')
X = dataset.iloc[:, :-1].values          # 3 spends + State
y = dataset.iloc[:, -1].values           # Profit

# 2. One-hot State WITHOUT the trap: drop one category
from sklearn.compose import ColumnTransformer
from sklearn.preprocessing import OneHotEncoder
ct = ColumnTransformer(
    transformers=[('state', OneHotEncoder(sparse_output=False, drop='first'), [3])],
    remainder='passthrough')
X = np.array(ct.fit_transform(X))        # -> [Florida, New York, R&D, Admin, Marketing]

# 3. Split (source convention)
from sklearn.model_selection import train_test_split
X_train, X_test, y_train, y_test = train_test_split(
    X, y, test_size=0.2, random_state=0)     # 40 train / 10 test

# 4. Fit; read the coefficients
from sklearn.linear_model import LinearRegression
regressor = LinearRegression().fit(X_train, y_train)
print('Test R2:', round(regressor.score(X_test, y_test), 4))   # 0.9347 (5 features)

# 5. Predict; compare side by side (the source's print convention)
y_pred = regressor.predict(X_test)
np.set_printoptions(precision=2)
print(np.concatenate((y_pred.reshape(-1, 1), y_test.reshape(-1, 1)), axis=1))

Listing 5.3 — The source workflow, trap-free (drop='first', California baseline).

7.1 Backward elimination, automated

import statsmodels.api as sm

SL = 0.05
cols = list(range(X.shape[1]))                    # start with all features
while True:
    X_opt = sm.add_constant(X[:, cols])           # statsmodels needs the constant added manually
    model = sm.OLS(y, X_opt).fit()
    pvals = model.pvalues[1:]                     # exclude the intercept
    if pvals.max() <= SL:
        break
    cols.pop(int(pvals.argmax()))                 # remove the highest-p feature

print('Survivors  :', cols)
print(model.summary())

Listing 5.4 — The five-step loop as code; set SL = 0.10 to keep Marketing.

💡
Pro Tip: Note the sm.add_constant() call — statsmodels never adds an intercept for you, while sklearn never forgets one. Mixing the two libraries (as professionals do) means remembering which convention you are standing in.

7.2 Visualising predictions vs reality

plt.scatter(X_train[:, 2], y_train, color='red')          # R&D on the x-axis
plt.plot(sorted(X_train[:, 2]),
         regressor.predict(X_train)[np.argsort(X_train[:, 2])],
         color='blue')
plt.title('Profit vs R&D Spend (Training set)')
plt.xlabel('R&D Spend'); plt.ylabel('Profit')
plt.show()

Listing 5.5 — With five features a full plot is impossible; slice one feature at a time.

8. Reading, Reporting & What's Next

With the chosen final model (R&D + Marketing, full-data fit), the professional report writes itself:

💡
Report: "Profit is modelled as a linear function of R&D and Marketing spend across 50 startups. Each additional rupee of R&D spend is associated with about ₹0.80 of additional profit, and each rupee of Marketing with about ₹0.03, holding the other constant (both p < 0.001, R&D; p = 0.06, Marketing). The model explains 95.0% of profit variation (adjusted R² = 0.948) and, critically, outperformed the full five-feature version on held-out data (test R² 0.947 vs 0.935). Administration spend and State showed no detectable effect in this sample." — effects, precision, fit, validation, and the null results. Five sentences, complete honesty.

Notice what got reported that tutorials omit: the null findings (Administration, State) and the borderline call (Marketing at p = 0.06, kept on adjusted R², AIC and test evidence). Professionals are paid for the judgement layer, not the .fit() call.

The road ahead

MLR still draws straight planes. When the shape of the relationship bends — and Chapter 6's salary-vs-level data bends hard — polynomial regression keeps the linear machinery and bends the feature space, introducing the bias-variance tradeoff that governs everything from Chapter 6 to Chapter 33. Before that: this chapter's quiz, thirty questions, every section fair game.

🛠 Hands-On: Run the Elimination YourselfIntermediate

The board wants to know which investments actually drive profit, and whether location matters. Run the complete analysis on 50_Startups.csv — trap-free encoding, VIFs, full summary, backward elimination at both SL = 0.05 and SL = 0.10 — and recommend a final model with evidence.

Steps

  1. Load the data; one-hot State with drop='first'. Which state is the baseline, and why?
  2. Compute VIFs for the three numeric spends. Any concern above 5?
  3. Fit the full 5-feature model with statsmodels; identify every p-value above 0.05.
  4. Run backward elimination at SL = 0.05 — list each dropped feature with its p-value. What survives?
  5. Re-run at SL = 0.10. Compare the two finalists on adjusted R², AIC, and test R² (split 0.2, seed 0).
  6. Write the five-sentence board report: effects, precision, fit, validation, null results.
View Solution / Walkthrough
import numpy as np, pandas as pd
import statsmodels.api as sm
from sklearn.model_selection import train_test_split
from sklearn.linear_model import LinearRegression
from sklearn.metrics import r2_score

ds = pd.read_csv('50_Startups.csv')
dum = pd.get_dummies(ds['State'], drop_first=True)         # baseline: California
X = np.concatenate([dum.values.astype(float),
                    ds[['R&D Spend', 'Administration', 'Marketing Spend']].values], axis=1)
y = ds['Profit'].values

def eliminate(X, y, SL):
    cols = list(range(X.shape[1]))
    while True:
        m = sm.OLS(y, sm.add_constant(X[:, cols])).fit()
        p = m.pvalues[1:]
        if p.max() <= SL: return cols, m
        print('drop col %d (p=%.3f)' % (cols[int(p.argmax())], p.max()))
        cols.pop(int(p.argmax()))

cols5, m5 = eliminate(X, y, 0.05)     # -> drops NY .99, FL .94, Admin .60, Marketing .060
print('SL 0.05 survivors:', cols5, ' adjR2=%.4f' % m5.rsquared_adj)

cols10, m10 = eliminate(X, y, 0.10)   # -> stops at R&D + Marketing
print('SL 0.10 survivors:', cols10, ' adjR2=%.4f' % m10.rsquared_adj)

X_tr, X_te, y_tr, y_te = train_test_split(X, y, test_size=0.2, random_state=0)
for name, cc in [('all 5', list(range(X.shape[1]))), ('SL.05', cols5), ('SL.10', cols10)]:
    r = LinearRegression().fit(X_tr[:, cc], y_tr)
    print('%-6s test R2 = %.4f' % (name, r2_score(y_te, r.predict(X_te[:, cc]))))

Findings:

  • Baseline: California — alphabetically first under drop_first=True; both state dummies read as differences vs California.
  • VIFs: 2.47 / 1.18 / 2.33 — all clear; the 0.724 R&D–Marketing correlation is real but not crippling.
  • Elimination log (SL 0.05): New York p = 0.990 → Florida p = 0.940 → Administration p = 0.602 → Marketing p = 0.060 → stops at R&D alone.
  • The fork: SL 0.10 keeps Marketing. Evidence to keep it: adj R² 0.9483 vs 0.9454, AIC 1057.1 vs 1058.9, test R² 0.9474 vs 0.9465 — and the kept model also beats the source's 5-feature model (test 0.9347).
  • Board report: mirror Section 8's paragraph — R&D ₹0.80/profit-rupee, Marketing ₹0.03 (borderline, kept on validation), Administration and State: no detectable effect in this sample.

🎯 Key Takeaways

1

MLR adds a coefficient per feature, all fitted by the same least squares — and every coefficient reads "per unit of xⱼ, holding the others constant".

2

One-hot a k-category feature into k−1 dummies: keeping all k reproduces the intercept — the dummy trap, perfect multicollinearity by encoding.

3

Dummy coefficients are differences against the dropped baseline category. Choose the baseline deliberately; never compare across different baselines.

4

VIF = 1/(1−R²ⱼ) prices each feature's collinearity: investigate above 5, act above 10. Multicollinearity is among predictors — the target is not involved.

5

Adjusted R² charges rent per predictor; AIC balances fit and complexity across models. Training R² alone cannot — it never falls with more features.

6

Backward elimination prunes by p-value (drop the max, refit, repeat) — but thresholds are conventions. On 50_Startups, three fewer features improved the test score.

Test Your Understanding

30 questions across the chapter — model form, the dummy trap, assumptions, VIF, adjusted R², the summary table, backward elimination and reporting. Choose one answer per question, then check your answers.

Q1.Multiple linear regression models:

Q2.The correct reading of a multiple-regression coefficient bⱼ is:

Q3.Keeping all three state dummies plus an intercept creates a problem because:

Q4.The standard cure for the dummy variable trap is:

Q5.With California dropped as baseline, the coefficient on the New York dummy reads as:

Q6.Which assumption is NEW in multiple regression compared to Chapter 4's LINE suite?

Q7.Multicollinearity refers to correlation:

Q8.The practical damage from severe multicollinearity is:

Q9.The VIF for feature j is defined as:

Q10.The conventional VIF thresholds for concern and action are:

Q11.The working sample-size rule for multiple regression is:

Q12.Adjusted R² exists because ordinary R²:

Q13.You add a pure-noise feature to a regression. Almost certainly:

Q14.Model A: 5 predictors, R² = 0.9508, adj R² = 0.9452. Model B: 2 predictors, R² = 0.9505, adj R² = 0.9483. The fair conclusion:

Q15.The null hypothesis behind a coefficient's p-value is:

Q16.R&D Spend shows p ≈ 0.000 in the summary. At SL = 0.05 the decision is:

Q17.Administration shows p = 0.608 in backward elimination at SL = 0.05. The algorithm's action:

Q18.Backward elimination begins with:

Q19.On 50_Startups at strict SL = 0.05, the last feature eliminated — at p = 0.060, just above the line — is:

Q20.The F-statistic in the summary table tests:

Q21.In this chapter's analysis, which model won on held-out test R²?

Q22.The practical division of labour between the two libraries in this chapter:

Q23.Before fitting with sm.OLS(...) you must call sm.add_constant(X) because:

Q24.The source code trains sklearn with all three state dummies plus intercept and sees no error. Why?

Q25.R&D and Marketing correlate at 0.724. Their VIFs come out 2.47 and 2.33. The correct conclusion:

Q26.The final model estimates 0.80 for R&D Spend. In the board report this reads:

Q27.All VIFs of a model compute to ≈ 1.0. This means:

Q28.AIC is used in this chapter to:

Q29.The honest criticism of stepwise methods like backward elimination is that they:

Q30.When the relationship itself bends — not just more features — the next step in this course is: