Act I · The tension
We regress the price of 29 Davis, CA houses on size, bedrooms, bathrooms, lotsize, age, and months-on-market.
\[\text{price} = \beta_1 + \beta_2\,\text{size} + \beta_3\,\text{bedrooms} + \cdots + \beta_7\,\text{monthsold} + u\]
Every coefficient comes back non-zero. But a point estimate is not evidence — which of these effects could just be sampling noise? Inference is how we tell signal from luck.
* ** *** flag the 10%, 5%, 1% thresholds at a glanceEach extra parameter costs a degree of freedom — in a sample of 29, that cost is real.
Act II · The investigation
!pip install -q pyfixest # pyfixest isn't preinstalled in Colab (-q = quiet)
import pandas as pd # tabular data
import pyfixest as pf # OLS with R-style formulas
url = "https://raw.githubusercontent.com/quarcs-lab/data-open/master/AED/AED_HOUSE.DTA"
data = pd.read_stata(url) # 29 Davis, CA house sales (1999)
fit = pf.feols("price ~ size + bedrooms + bathrooms + lotsize + age + monthsold",
data=data) # multiple regression, intercept auto-added
fit.summary() # coefficients, SEs, t, p for all 7 parameterssize clears the bar — at \(\$68.37\) per square footNon-zero is not the same as significant: five of six slopes could plausibly be zero.
\[se(b_j) = \frac{s_e}{\sqrt{\sum_i \widetilde{x}_{ji}^2}}, \qquad \widetilde{x}_{ji} = x_j \text{ residualized on all other regressors}\]
\(se(b_j)\) shrinks when the model fits well (\(s_e\) small) and when \(x_j\) carries variation the other regressors don’t. Because bigger houses tend to have more bedrooms (\(r = 0.52\)), residualization strips out about 42% of the variation in bedrooms — inflating its SE.
With \(n=29\) and correlated predictors, estimates are imprecise by construction — the data are stretched thin.
import pandas as pd # tabular data
import pyfixest as pf # estimation
from scipy import stats # t critical value
url = "https://raw.githubusercontent.com/quarcs-lab/data-open/master/AED/AED_HOUSE.DTA"
data = pd.read_stata(url) # load the 29 houses
fit = pf.feols("price ~ size + bedrooms + bathrooms + lotsize + age + monthsold", data=data)
fit.confint() # 95% CI: b_j ± t(n-k, 0.025) · se(b_j)
t_crit = stats.t.ppf(0.975, df=29 - 7) # critical value from T(22) ≈ 2.074size’s interval misses zeroEach bar is a 95% confidence interval. size sits entirely above zero; every other interval straddles it.
size: \([\$36.45,\ \$100.29]\) — even the low end adds \(\$36\)/sq ft. An interval excluding zero is significance at 5%.
import numpy as np # arrays
# from the fitted model: b_size = 68.37, se_size = 15.39, df = 22
fit = pf.feols("price ~ size + bedrooms + bathrooms + lotsize + age + monthsold", data=data)
b, se = fit.coef()["size"], fit.se()["size"] # estimate and its standard error
t = (b - 50) / se # H0: beta_size = 50 → t = (68.37 - 50)/15.39
p = 2 * (1 - stats.t.cdf(abs(t), df=29 - 7)) # two-sided p-value from T(22)
print(f"t = {t:.2f}, p = {p:.3f}") # t = 1.19, p = 0.245 → fail to rejectTesting bedrooms, bathrooms, lotsize, age, monthsold one by one ignores that they’re correlated — and inflates the chance of a false “significant.”
Instead, compare two models by their residual sum of squares:
\[F = \frac{(\text{RSS}_R - \text{RSS}_U)/q}{\text{RSS}_U/(n-k)} \sim F(q,\, n-k)\]
The restricted model drops the variables; the unrestricted keeps them. Does dropping them hurt the fit?
import numpy as np # arrays
from scipy import stats # F distribution
full = pf.feols("price ~ size + bedrooms + bathrooms + lotsize + age + monthsold", data=data)
size_only = pf.feols("price ~ size", data=data) # restricted: drop the other 5
rss_u, rss_r = np.sum(full._u_hat**2), np.sum(size_only._u_hat**2)
q, n_k = 5, 29 - 7 # 5 restrictions, 22 residual df
F = ((rss_r - rss_u) / q) / (rss_u / n_k) # joint F-statistic
p = 1 - stats.f.cdf(F, q, n_k) # upper-tail p-value
print(f"F({q},{n_k}) = {F:.2f}, p = {p:.3f}") # F = 0.42, p = 0.832The \(F(q,\,n-k)\) distribution is right-skewed and positive; we reject in the far right tail.
size. Fail to reject.The model works — but all of its work is being done by size alone.
Act III · The resolution
Report size as \(68.37^{***}\) and the rest unstarred — a reader sees in one glance where the evidence lives. The robust SE for size barely moves (\(\$15.39 \to \$15.36\)); the others shift both ways (−26% for lot size to +23% for bathrooms), but no conclusion changes.
Actual vs. predicted price. The size-only model tracks the full model almost exactly.
\(R^2\) never falls with more variables; adjusted \(R^2\) charges for the complexity. Prefer the model that earns its parameters.
Objection. “The five variables are insignificant — so bedrooms and bathrooms don’t affect price.”
Response. Failing to reject is not accepting. With \(n = 29\) and predictors that move together, the data simply can’t resolve those effects — not that they’re zero. A larger, less collinear sample might.
Inference quantifies what this sample can support; small, correlated data support less than they seem to.
size alone is enough.