Act I · The tension
House prices depend on size and bedrooms and age and lot size — all at once.
A bivariate regression of price on bedrooms alone says each bedroom is worth $23,667. But bigger houses also have more bedrooms. How much is the bedroom really worth, once we hold size fixed? Multiple regression is the tool that separates tangled effects.
A multiple-regression slope answers “all else equal” — the question economics actually asks.
Act II · The investigation
29 house sales in Davis, California (1999), with size, bedrooms, bathrooms, lot size, age, month sold.
\[\widehat{\text{price}} = b_1 + b_2\,\text{size} + b_3\,\text{bedrooms} + \cdots + b_7\,\text{monthsold}\]
On its own, bedrooms looks like a $23,667 driver. The suspicion: that number is really size showing up in disguise. To find out, we first look at how the variables move together.
import pandas as pd # tabular data
import seaborn as sns # statistical visualization
url = "https://raw.githubusercontent.com/quarcs-lab/data-open/master/AED/AED_HOUSE.DTA"
data = pd.read_stata(url) # 29 Davis, CA house sales
plot_vars = ["price", "size", "bedrooms", "age"] # columns to cross-plot
g = sns.pairplot(data[plot_vars], # every two-way scatter + KDE on the diagonal
diag_kind="kde",
plot_kws={"color": "#1fb2dd"}) # brand cyan markersEach off-diagonal panel is one two-way scatter; the diagonal shows each variable’s distribution. Price climbs with size most clearly.
Look before you model: the matrix shows which predictors move with price — and with each other.
import pandas as pd # tabular data
import seaborn as sns # heatmap
url = "https://raw.githubusercontent.com/quarcs-lab/data-open/master/AED/AED_HOUSE.DTA"
data = pd.read_stata(url) # load the 29 house sales
corr_vars = ["price", "size", "bedrooms", "bathrooms", "lotsize", "age"]
corr = data[corr_vars].corr() # pandas .corr() → pairwise correlation matrix
sns.heatmap(corr, annot=True, fmt=".2f", # print each r in its cell
cmap="coolwarm", center=0) # diverging colours around 0
corr # show the raw matrix tooCorrelation heatmap of the six variables. Price–size is the hottest cell; size–bedrooms is warm.
\(r(\text{price},\text{size}) = 0.79\) · \(r(\text{price},\text{bedrooms}) = 0.43\) · \(r(\text{size},\text{bedrooms}) = 0.52\) — size and bedrooms overlap, so their effects will be hard to separate.
!pip install -q pyfixest # install pyfixest in Colab (-q = quiet; not preinstalled)
import pandas as pd # tabular data
import pyfixest as pf # pyfixest → OLS regression, R-style formulas
url = "https://raw.githubusercontent.com/quarcs-lab/data-open/master/AED/AED_HOUSE.DTA"
data = pd.read_stata(url) # load the 29 Davis, CA house sales
fit = pf.feols("price ~ size + bedrooms + bathrooms + lotsize + age + monthsold",
data=data) # OLS with six regressors at once
fit.summary() # partial slopes, SEs, t, p, R²Coefficient estimates with 95% confidence intervals. Only size’s interval excludes zero.
size \(= \$68.37\)/sq ft, \(t = 4.44\), \(p < 0.001\) (95% CI ≈ $36–$100). Every other interval straddles zero.
size is a textbook confounder of bedrooms — and adding it as a control fixes the estimate.A partial effect is the honest number: what one extra bedroom adds, holding size fixed.
import pandas as pd
import pyfixest as pf # pyfixest OLS
data = pd.read_stata("https://raw.githubusercontent.com/quarcs-lab/data-open/master/AED/AED_HOUSE.DTA")
# Step 1 — strip out the part of size explained by the other regressors
others = pf.feols("size ~ bedrooms + bathrooms + lotsize + age + monthsold", data=data)
data["resid_size"] = others._u_hat # residualized size (size "net of" the rest)
# Step 2 — regress price on residualized size alone
pf.feols("price ~ resid_size", data=data).coef() # → 68.37, exactly the full-model size slopeActual vs. predicted price for the full model: points track the 45° line, but scatter remains.
Simple (size only): \(R^2 = 0.617\), \(\bar{R}^2 = 0.603\). Full (six regressors): \(R^2 = 0.651\) but \(\bar{R}^2\) falls to \(0.555\).
With few observations, parsimony wins: don’t buy a lower \(\bar{R}^2\) with variables that add nothing.
Act III · The resolution
Objection. “If more predictors help, why not add size_twice = 2 × size?”
Response. Perfect collinearity. size_twice carries zero new information, so OLS has no unique solution — the software drops one of the pair (pyfixest warns which). Like identical twins with one set of measurements: distinct names, identical data. Watch for it whenever one regressor is an exact combination of others.
Correlation among regressors is normal; an exact linear dependence is fatal to estimation.
sns.pairplot) + correlation matrix (.corr()).