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.
🎯 Learning Objectives
- Write the multiple regression model and interpret each coefficient as "holding the others constant"
- Explain and escape the dummy variable trap — and read one-hot coefficients against the dropped baseline category
- Define multicollinearity, compute Variance Inflation Factors, and act on the standard thresholds
- Distinguish R² from adjusted R² and know precisely why the penalty exists
- Read every line of a statsmodels OLS summary — coefficients, standard errors, t-tests, p-values, F-statistic, AIC
- Run backward elimination end to end, recognise borderline p-values as judgement calls, and report the final model
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.
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.
| Quantity | Value | Reading |
|---|---|---|
| Rows | 50 | Comfortable for 3 numeric predictors — see the sample-size rule in Section 3 |
| R&D Spend | 0 – 165,349 | Strongest single correlate of Profit (r = 0.973) |
| Administration | 51,283 – 182,346 | Weak correlate (r = 0.201) |
| Marketing Spend | 0 – 471,784 | Strong (r = 0.748) — but also correlates 0.724 with R&D… |
| State | 3 categories | Nominal — Chapter 3 says one-hot; Section 2 adds the catch |
| Profit (target) | mean 112,013 | Continuous → regression |
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:
| const | NY | Cal | Fla | NY+Cal+Fla |
|---|---|---|---|---|
| 1 | 1 | 0 | 0 | 1 |
| 1 | 0 | 1 | 0 | 1 |
| 1 | 0 | 0 | 1 | 1 |
| 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:
- Intercept b₀: predicted Profit for the baseline state (California) when spends are 0
- Florida coefficient: Profit difference between Florida and California, holding spends constant
- New York coefficient: same, New York vs California
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.
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 assumption | What it demands | How to check | If 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.
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:
| Feature (50_Startups) | VIF | Verdict |
|---|---|---|
| R&D Spend | 2.47 | Fine — mild inflation |
| Administration | 1.18 | Fine |
| Marketing Spend | 2.33 | Fine — 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.
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:
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:
| coef | std err | t | P>|t| | [0.025, 0.975] | Real value (feature) |
|---|---|---|---|---|---|
| 50,125 | ~8,700 | 5.75 | 0.000 | … | const |
| 0.806 | 0.046 | 17.37 | 0.000 | … | R&D Spend |
| −0.027 | 0.052 | −0.52 | 0.608 | … | Administration |
| 0.027 | 0.017 | 1.56 | 0.123 | … | Marketing Spend |
| 199 | 3,300 | 0.06 | 0.953 | … | Florida (vs Cal.) |
| −42 | 3,200 | −0.01 | 0.990 | … | New York (vs Cal.) |
- coef — bⱼ: the ceteris-paribus effect (Section 1's phrase).
- std err — the coefficient's own uncertainty: how much bⱼ would wobble across fresh samples.
- t = coef / std err — Chapter 2's t-statistic reborn: signal over noise. R&D's 17.4 is a shout; Administration's −0.5 is a whisper.
- P>|t| — the two-tailed p-value for H₀: βⱼ = 0. R&D's 0.000: overwhelmingly nonzero. Administration's 0.608: indistinguishable from zero.
- [0.025, 0.975] — the 95% confidence interval for βⱼ; notice Administration's straddles zero, matching its p-value (Chapter 2's two views of one uncertainty).
The summary's bottom rows carry model-wide verdicts:
- F-statistic = 169.9 (p ≈ 1.3×10⁻²⁷) — a joint test of H₀ "all βⱼ = 0 simultaneously": the model as a whole is overwhelmingly better than predicting the mean.
- R² = 0.9508, Adj R² = 0.9452 — the ledger and the penalty.
- AIC = 1062.8 — an information criterion: fit minus a complexity charge, comparable across models on the same data — lower is better. Section 6 will use it.
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:
- Choose a significance level for staying (conventionally SL = 0.05)
- Fit the full model with all predictors
- Delete the predictor with the highest p-value, if it exceeds SL
- Refit and repeat step 3
- Stop when every remaining p-value ≤ SL
Run on 50_Startups (real output, full dataset, California baseline):
| Step | Action | p-value | R² | Adj R² |
|---|---|---|---|---|
| 0 | All 5 features | — | 0.9508 | 0.9452 |
| 1 | drop New York | 0.990 | 0.9508 | 0.9452 |
| 2 | drop Florida | 0.940 | 0.9508 | 0.9464 |
| 3 | drop Administration | 0.602 | 0.9507 | 0.9475 |
| 4 | drop Marketing… | 0.060 | 0.9505 | 0.9483 |
| final | R&D only | 0.000 | 0.9465 | 0.9454 |
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² | AIC | Test R² (split 0.2, seed 0) | Test RMSE |
|---|---|---|---|---|---|
| All 5 features (source model) | 0.9508 | 0.9452 | 1062.8 | 0.9347 | 9,138 |
| R&D only | 0.9465 | 0.9454 | 1058.9 | 0.9465 | 8,275 |
| R&D + Marketing | 0.9505 | 0.9483 | 1057.1 | 0.9474 | 8,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.
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.
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:
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
- Load the data; one-hot State with
drop='first'. Which state is the baseline, and why? - Compute VIFs for the three numeric spends. Any concern above 5?
- Fit the full 5-feature model with statsmodels; identify every p-value above 0.05.
- Run backward elimination at SL = 0.05 — list each dropped feature with its p-value. What survives?
- Re-run at SL = 0.10. Compare the two finalists on adjusted R², AIC, and test R² (split 0.2, seed 0).
- 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
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".
One-hot a k-category feature into k−1 dummies: keeping all k reproduces the intercept — the dummy trap, perfect multicollinearity by encoding.
Dummy coefficients are differences against the dropped baseline category. Choose the baseline deliberately; never compare across different baselines.
VIF = 1/(1−R²ⱼ) prices each feature's collinearity: investigate above 5, act above 10. Multicollinearity is among predictors — the target is not involved.
Adjusted R² charges rent per predictor; AIC balances fit and complexity across models. Training R² alone cannot — it never falls with more features.
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: