NewTurn any lesson into a live trading bot with one prompt, using AgenKitagenkit.xyz

Part 4 · Case studiesLesson 1410 minQuant Bible §5.1

Case: predicting New York housing prices

A Two Sigma-style case: encode, normalise, log-transform and regress, then deal with correlated inputs.

Quant research interviews often end with an open modelling question. There is no single right answer; the interviewer is watching how you go from raw data to a model you would trust. Here is a full pass at a Two Sigma-style case.

Step 1: the main idea

Start simple and say it out loud: a multiple regression of sale price on square footage, beds, baths, year built and location. Everything after this is about making that regression valid and useful.

Step 2: encode each input

  • Square footage and year built are integers, but with so many values they behave like continuous variables. Keep them as numbers (for now).
  • Beds and baths are small ordered counts. They can go in as numbers too.
  • Borough and neighbourhood are categories with no order. Use one-hot encoding: one 0/1 column per category, dropping one to avoid perfect collinearity with the intercept.

Step 3: normalise, and look at the shapes

Put features on comparable scales (subtract the mean, divide by the standard deviation). It avoids numerical trouble and makes coefficients comparable. But first look at each distribution.

Square footage is never negative, clusters around a typical one- or two-storey home, has a short left tail and a long right tail: some mansions are dozens of times the average. That shape is lognormal. Take the logarithm, then standardise. Sale price itself is usually lognormal too, and regressing log⁡(price)\log(\text{price}) makes coefficients read as percentage effects.

Figure 14.1Log-transform lab
Simulated home sizes. Raw, the histogram is right-skewed and the mean sits above the median. Switch to log(square feet) and the shape becomes symmetric, the normal curve fits, and skewness drops to about zero. Increase the spread to exaggerate the effect.

Step 4: correlations between inputs

Square footage, beds and baths obviously move together. Measure it first. Pearson correlation assumes linear relationships between continuous variables; with a discrete ordered variable (beds) against a continuous one (log size), Spearman’s rank correlation is the better tool.

Step 5: deal with multicollinearity

Correlated inputs inflate standard errors and make coefficients unstable (lesson 10). Options, from simplest:

  • Ridge to shrink the correlated coefficients together, or lasso to keep the strongest of the group and drop the others (lesson 11).
  • Successive orthogonalisation: regress the most important variable first, then put in the others’ residuals after removing it, so each coefficient means “extra information beyond the earlier ones”.
  • Interpretation pass: compute t-statistics, F-test the weak group, and consider subset selection to drop what isn’t pulling its weight.

What a strong answer adds

  • Validation: hold out recent sales and measure error on them, since the goal is predicting future prices.
  • Location done properly: neighbourhood dummies are crude. Latitude and longitude with a smooth function, or nearby sale prices (a kernel average, like the next case), capture it better.
  • Time: prices drift, so add a sale-date trend or month effects.

In code

import numpy as np
import pandas as pd
from sklearn.linear_model import RidgeCV
from sklearn.model_selection import train_test_split

rng = np.random.default_rng(12)
n = 5000
sqft = np.exp(rng.normal(np.log(900), 0.55, n))
beds = np.clip(np.round(sqft / 450 + rng.normal(0, 0.7, n)), 0, 6)
baths = np.clip(np.round(beds * 0.7 + rng.normal(0, 0.5, n)), 1, 5)
boro = rng.choice(["Manhattan", "Brooklyn", "Queens", "Bronx", "Staten Island"], n)
prem = pd.Series(boro).map({"Manhattan": 0.6, "Brooklyn": 0.3, "Queens": 0.1, "Bronx": -0.1, "Staten Island": 0.0})
log_price = 5.3 + 0.9 * np.log(sqft) + 0.05 * baths + prem.values + rng.normal(0, 0.2, n)

X = pd.get_dummies(pd.DataFrame({"log_sqft": np.log(sqft), "beds": beds, "baths": baths, "boro": boro}),
                   columns=["boro"], drop_first=True, dtype=float)
X = (X - X.mean()) / X.std()
print(pd.DataFrame({"sqft": sqft, "beds": beds, "baths": baths}).corr(method="spearman").round(2))

Xtr, Xte, ytr, yte = train_test_split(X, log_price, random_state=0)
model = RidgeCV(alphas=np.logspace(-3, 3, 13)).fit(Xtr, ytr)
print("held-out R^2:", round(model.score(Xte, yte), 3))

Check yourself

You regress log(price) on log(square feet) and get a coefficient of 0.9. What does it mean?

Progress is saved in this browser only.