COMP328 Fundamentals of Machine Learning

1 Learning objectives

By the end of this tutorial you will be able to

  • Run a complete regression project on a real, messy dataset — from framing through to a written report.
  • Audit a feature list for leakage before fitting anything, and remove features that will not be available at prediction time.
  • Clean a dataset responsibly: handle missing values, encode categorical variables, and detect implausible values, documenting every decision.
  • Build a scikit-learn Pipeline that keeps preprocessing inside the training fold, eliminating scaling leakage.
  • Split data into training, validation and test sets, and use the validation set for every modelling decision.
  • Diagnose the fitted model with residual plots, a learning curve and a validation curve, and act on what they show.
  • Interpret coefficients honestly — including which ones can be read causally and which cannot.
  • Write a model report that a non-technical stakeholder can act on, including limitations and the range of validity.

Estimated time

45 minutes of reading — this is the longest tutorial in the unit — plus 60–90 minutes to reproduce the notebook yourself. This tutorial is designed to be worked through at a keyboard, not skimmed.

2 Motivating example

The previous six tutorials built a single model on eight rows of synthetic bike-share data. Every technique was introduced in isolation, on clean numbers, with the arithmetic visible. That was necessary, but it is not what real work looks like.

Real work starts with a CSV file someone else produced, at a time when the questions you need answered were not the questions they were thinking about. It contains missing values, inconsistent units, categorical columns stored as free text, and one column that turns out to be a target leak. The relationships are noisy, the R² is moderate, and the residuals never look quite as clean as you hoped.

This tutorial walks through that reality. The dataset is a synthetic housing dataset with 1,200 records, deliberately constructed to include the kinds of problems a real file has — a mix of numeric and categorical features, a handful of missing values, one implausible outlier, one leaked feature, and a genuinely non-linear relationship.

A regional estate agency wants to give sellers an instant online price estimate. The estimate will be displayed before any agent has visited the property, so it must be based only on information the seller enters into the form.

That framing is doing real work. It tells us the prediction has to be made from a limited set of self-reported features. It rules out anything a surveyor would measure. And it sets the standard for what counts as useful: an estimate that is off by £40,000 is not helpful to a seller deciding whether to book a valuation.

What this tutorial is not

It is not a new algorithm, a new derivation or a new metric. Every tool used here was introduced in an earlier tutorial. What is new is the order of operations, the decisions that have to be made at each step, and the discipline of refusing to touch the test set until the end. That discipline is the actual content of this tutorial.

3 Concept explanation

3.1 The order of operations

Most of the damage in a regression project is done before a single model is fitted. The single most important decision is the order in which things happen, and the order that works is not the order that feels natural.

  1. Frame the problem What is the target, in what units? Who is the prediction for? What is the cost of being wrong? Which features will be available at prediction time?
  2. Audit the features for leakage Before anything else. Remove every feature that is recorded after the outcome, or is a consequence of it. Do this on paper, not in code.
  3. Split the data Train, validation and test. Do this before any preprocessing, so that nothing computed from the validation or test data influences the training pipeline.
  4. Explore the training set only Distributions, missing values, implausible values, relationships. Everything you learn here should be learned from training data alone.
  5. Build a pipeline Cleaning, encoding and scaling wrapped so that every transformation is fitted on training data and applied to validation and test.
  6. Fit and evaluate on validation Iterate here. Add features, transform features, change the model. Every decision is made against validation performance.
  7. Diagnose Residual plots, learning curves, validation curves. This is where you find out whether the model is underfitting, overfitting, mis-specified or leaky.
  8. Report Metrics, baseline comparison, residual plot, range of validity, known failure modes. Then, and only then, evaluate on the test set once.

The counter-intuitive parts are steps 2 and 3. Leakage is audited before looking at the data, because once you have seen that a leaked feature produces a beautiful fit, it is psychologically very hard to remove it. And the split happens before exploration, because exploration itself is a form of fitting — if you discover the median of a column by looking at the whole dataset and then use it to fill missing values, the test set has influenced your training pipeline.

3.2 The feature audit

The estate agency scenario tells us the prediction is made from a web form before anyone visits the property. So the question for every column is: will this value be known at the moment the form is submitted?

Feature audit — every column in the raw file
Column Type Available at prediction time? Decision
floor_area_m2 Numeric Yes — seller enters it Include
bedrooms Numeric Yes Include
bathrooms Numeric Yes Include
property_type Categorical Yes Include — one-hot encode
age_years Numeric Yes Include
distance_to_centre_km Numeric Yes Include
epc_rating Ordinal Yes Include — encode as ordered integer
has_garden Binary Yes Include
postcode High-cardinality categorical Yes Treat with caution — see below
sale_price Numeric No — this is the target Target, not a feature
days_on_market Numeric No — recorded after the sale Remove — leakage
survey_valuation Numeric No — a professional valuation of the sold property Remove — leakage
agent_id Identifier Technically yes Remove — see below

Three columns need a note.

days_on_market is the number of days the property was listed before selling. It is only known once the sale has completed — which is precisely the outcome we are trying to predict. It is a textbook target leak, and a model that included it would report an excellent R² and be useless in production.

survey_valuation is a professional valuation carried out by the buyer's surveyor. It is close to the sale price by construction, and it is not available when the seller fills in the online form. Including it would be trivial leakage.

agent_id is subtler. It is available at prediction time in the sense that the seller could be assigned to an agent. But the agent is assigned as a result of the property being listed, and the choice of agent correlates with the type and price of property. Using it would bake the estate agency's internal routing policy into the price estimate. And in deployment, a new property would not yet have an agent. Remove it.

The test of a leaked feature

Ask: would this value be different if the sale had happened at a different price? If yes, it leaks the target. days_on_market would be different for a property that sold cheaply. survey_valuation would be different. floor_area_m2 would not — the property has the same floor area regardless of what it sells for. That question cuts through almost every case.

3.3 Cleaning: what to do with each problem

Cleaning is not a single operation. Each problem type has a standard remedy, and the choice between them is a decision you should be able to defend.

Problem types and standard remedies
Problem Remedy Caveat
Missing numeric values Impute the training-set mean or median Median is safer when the column is skewed. Either way, the imputation value must be computed on training data only.
Missing categorical values Impute the most frequent category, or create an explicit "Unknown" category "Unknown" is often the better choice, because missingness may itself be informative.
Implausible values Investigate first; then correct, remove, or flag Never delete silently. A negative floor area is a data-entry error; a very large one may be genuine.
Duplicate rows Remove exact duplicates Check whether the duplicates are genuine repeated listings or a data export artefact.
Categorical columns One-hot encode for nominal; ordered integer for ordinal One-hot encoding a high-cardinality column creates hundreds of features and invites overfitting.
Skewed target or features Log-transform, if the values are strictly positive Changes the quantity being minimised — a model with a log target minimises percentage error, not absolute error.

Two of these deserve more than a row in a table.

Log-transforming the target. House prices are right-skewed: most properties cluster around a middle value, with a long tail of expensive ones. A model that minimises squared error on the raw price will spend most of its effort getting the expensive properties approximately right, because those are the ones with the largest absolute errors. If the goal is a percentage-accurate estimate, fitting on log(price) and exponentiating the prediction is usually better. It is a different model, optimising a different quantity, and it should be chosen deliberately.

High-cardinality categoricals like postcode. A postcode is genuinely informative about house prices. But one-hot encoding 400 distinct postcodes creates 400 binary features, most of which have very few observations. That is a recipe for overfitting. Two options: reduce the cardinality by grouping postcodes into larger areas, or use a target-encoding scheme where each postcode is replaced by the average price in that postcode — computed on the training set only, and inside the cross-validation folds.

Target encoding is a leak waiting to happen

Replacing a category with the mean target value for that category is powerful, but if you compute those means on the full training set and then use them as features, the model can partially memorise the training targets. The correct procedure computes the encoding using only the fold's training data during cross-validation. scikit-learn's TargetEncoder exists precisely to handle this. In this tutorial we will take the simpler route and group postcodes into broad regions.

3.4 The pipeline

A scikit-learn Pipeline chains transformations with a final estimator. Its value is not convenience — it is correctness. When a pipeline is used inside cross-validation or a train/validation split, every transformation is fitted on the training portion of that split and applied to the held-out portion. That is exactly the discipline Tutorial 6 demanded, enforced by construction.

For this project the pipeline has three stages:

  1. Preprocessing. A ColumnTransformer that applies different transformations to numeric and categorical columns. Numeric columns are imputed and scaled; categorical columns are imputed and one-hot encoded.
  2. Feature engineering — optional, and discussed in the worked example. Derived features such as rooms-per-square-metre can be added as their own numeric column.
  3. Estimator. LinearRegression, which is what this unit has been building towards.

The whole thing is a single object with fit, predict and score methods, and it can be handed directly to cross_val_score or GridSearchCV without any further care about leakage.

3.5 What to expect from the results

Before fitting anything, it is worth being clear about what a good outcome looks like. This is a decision that shapes the whole project and it is worth making explicitly.

A regional house price model on self-reported features, with no survey, no photographs and no condition assessment, should be expected to achieve a validation R² somewhere in the range 0.65 to 0.85. Below 0.65 suggests the features are too weak or something is broken. Above 0.85 on this kind of data is possible but should prompt a leakage check — particularly if a feature like survey_valuation slipped through the audit.

In absolute terms, if the median price is around £280,000, an RMSE of about £45,000 to £60,000 is realistic. That means a typical prediction is off by roughly 15–20% — which is useful for a ballpark estimate and nowhere near precise enough to be a formal valuation.

That last sentence is the one that matters, and it belongs in the report.

3.6 Reading the coefficients, carefully

A linear model on house prices produces coefficients that stakeholders will want to read as statements about the market: "each extra bedroom adds £18,000". That reading is almost always wrong, and understanding why is as much a part of the deliverable as the metrics.

What a coefficient does and does not license
Reading Licensed by the model? Why
"In this dataset, properties with one more bedroom were associated with a £18,000 higher price, holding the other features constant." Yes This is exactly what the coefficient measures. It is a descriptive statement about the data, conditional on the model being approximately correct.
"Adding a bedroom would increase the sale price by £18,000." No This is a causal claim. Converting a study into a bedroom is a different property, in a different market segment. The model has no evidence about what happens when you intervene.
"Bedrooms are worth £18,000 each." No This treats the coefficient as a universal constant. It depends on the dataset, the other features included, and the range of properties covered.
"A property with one extra bedroom is predicted to sell for £18,000 more than an otherwise identical property." With care Closer, but "otherwise identical" is doing heavy lifting. Two properties that differ only in bedroom count and have identical floor area, location, age and condition are rare. The coefficient describes a comparison that may barely exist in the data.

The practical rule: report coefficients as associations within the dataset, never as effects of interventions. And check whether the comparison the coefficient describes actually occurs in the data — with correlated features like floor area and bedroom count, it often does not.

4 Visual illustration

The end-to-end regression pipeline for house price prediction A horizontal flowchart with eight stages connected left to right. The stages are: frame the problem, audit features for leakage, split the data, explore the training set, build a pipeline, fit and evaluate on validation, diagnose, and report. The pipeline stage expands downward to show a column transformer applying numeric and categorical transformations to a linear regression estimator. 1 · Frame target, units, cost 2 · Audit remove leaks 3 · Split train / val / test 4 · Explore training only 5 · Pipeline no leakage 6 · Fit & evaluate on validation 7 · Diagnose residuals, curves 8 · Report test set once Pipeline detail numeric columns impute median + standard scale categorical columns impute mode + one-hot encode LinearRegression the estimator every transformation is fitted on training data and applied to validation and test
Figure 1. The eight stages of the project, and the internals of the pipeline. The order matters: the feature audit comes before any exploration, and the split comes before any transformation. Wrapping the preprocessing in a pipeline is what makes the correct order enforceable rather than merely intended.
Distribution of sale price showing right skew and the effect of a log transformation Two histograms side by side. The left panel shows the distribution of raw sale price, which is right-skewed with a long tail of expensive properties. The right panel shows the distribution after a log transformation, which is roughly symmetric and bell-shaped. Raw sale price — right-skewed median long right tail squared error on this scale mostly cares about expensive homes log(price) — roughly symmetric roughly normal, no dominant tail errors become percentage errors — usually what a seller cares about
Figure 2. Why the target transformation is a decision, not a default. On the raw price scale, squared error is dominated by the handful of expensive properties in the right tail. On the log scale, the distribution is roughly symmetric and the model optimises percentage error — which is closer to what a seller actually wants when they ask "how accurate is this estimate?"
Fitted coefficients with confidence intervals and caution flags A horizontal dot-and-whisker plot showing the fitted coefficient for each feature. Some coefficients are marked as reliable and interpretable, others are marked with a caution symbol indicating correlated features or a coefficient that should not be read causally. 0 floor_area_m2 distance_to_centre age_years bedrooms bathrooms has_garden epc_rating ✓ ✓ ✓ ! ! ! ✓ ✓ reliable and interpretable ! correlated — do not read causally
Figure 3. Coefficient magnitudes are not the same as importance, and not every coefficient is interpretable. Floor area and distance to centre have large, stable coefficients with clear meaning. Bedrooms, bathrooms and garden have small coefficients because they are correlated with floor area — the model has already accounted for size, and these features add little on top. Their coefficients describe a comparison that barely exists in the data.
Residual plot for the final house price model A scatter plot of residuals against fitted values for the final model. The residuals form an unstructured cloud centred on zero, with a slight widening at higher fitted values. No curvature is visible, indicating that the non-linear structure in the data has been captured. Fitted sale price (£) Residual (£) no curvature — good mild widening at high prices — noted the cloud is shapeless — the model has captured the structure it can
Figure 4. The final model's residual plot. The important feature is the absence of curvature: the log transform and the engineered features have absorbed the non-linear structure that a naive linear fit would have left in the residuals. The mild widening at high prices is heteroscedasticity, and it should be reported even though it does not invalidate the model.

5 Worked example

The full walkthrough, in the order a practitioner would carry it out. Each step states what we are doing and why, because the reasoning is the transferable part.

Step 1 — Load and inspect the raw file

The dataset has 1,200 rows and 13 columns. Before touching anything, look at it.

import pandas as pd
import numpy as np

df = pd.read_csv("housing.csv")

print(df.shape)              # (1200, 13)
print(df.dtypes)
print(df.isna().sum())
print(df.describe())
(1200, 13)

floor_area_m2            float64
bedrooms                 float64
bathrooms                float64
property_type             object
age_years                float64
distance_to_centre_km    float64
epc_rating                object
has_garden                object
postcode                  object
days_on_market           float64
survey_valuation         float64
agent_id                  object
sale_price               float64

Missing values per column:
floor_area_m2             14
bedrooms                   8
bathrooms                 11
age_years                  6
distance_to_centre_km      0
epc_rating                22
has_garden                 4
... (others complete)

The file is a mix of numeric and object columns, with modest missingness in a handful of numeric and categorical columns. Nothing catastrophic. The next step is the important one and it does not involve the data at all.

Step 2 — Audit the features for leakage before exploring

From Section 3.2, three columns are removed on principle:

# These columns are recorded after the sale or are a consequence of it.
LEAKY = ["days_on_market", "survey_valuation", "agent_id"]

df = df.drop(columns=LEAKY)

TARGET = "sale_price"
y = df[TARGET]
X_raw = df.drop(columns=[TARGET])

Why this happens first

Once a model has been fitted with survey_valuation included and produced an R² of 0.97, removing it feels like breaking something that works. The order matters because of how it feels, not because of the algorithm.

Step 3 — Split before any preprocessing

from sklearn.model_selection import train_test_split

# 60% train, 20% validation, 20% test
X_train, X_temp, y_train, y_temp = train_test_split(
    X_raw, y, test_size=0.40, random_state=42
)
X_val, X_test, y_val, y_test = train_test_split(
    X_temp, y_temp, test_size=0.50, random_state=42
)

print(f"train {len(X_train):>5}   val {len(X_val):>5}   test {len(X_test):>5}")
train   720   val   240   test   240

Every subsequent decision — imputation values, scaling statistics, feature selection, model choice — is made using the training and validation sets. The test set is locked away and will be used once, at Step 10.

Step 4 — Explore the training set only

print(y_train.describe())
print(f"skewness: {y_train.skew():.2f}")

print(X_train["property_type"].value_counts())
print(X_train["epc_rating"].value_counts())
print(X_train["has_garden"].value_counts())

# Implausible values?
print("floor area <= 0 :", (X_train["floor_area_m2"] <= 0).sum())
print("age < 0         :", (X_train["age_years"] < 0).sum())
print("bedrooms > 10   :", (X_train["bedrooms"] > 10).sum())
print("n postcodes     :", X_train["postcode"].nunique())
count      720.00
mean    284,318.42
std      94,207.16
min      58,000.00
25%     218,500.00
50%     276,000.00
75%     340,750.00
max     612,000.00
skewness: 0.94

property_type   detached 241 · semi 268 · terraced 174 · flat 37
epc_rating      E 41 · D 138 · C 291 · B 217 · A 33
has_garden      True 519 · False 201

floor area <= 0 : 1
age < 0         : 2
bedrooms > 10   : 1
n postcodes     : 386

Three things to act on.

  • The target is right-skewed (0.94). A log transform will help. We will fit both and compare on validation rather than assuming.
  • Four implausible numeric values. One non-positive floor area, two negative ages, one property with 11 bedrooms. Each needs investigation before a decision.
  • 386 postcodes in 720 training rows. One-hot encoding would create 386 features averaging 1.9 observations each. That is a guaranteed overfit. We will group into broad regions instead.

Step 5 — Handle the implausible values

# Inspect the suspicious rows before deciding
print(X_train.loc[X_train["floor_area_m2"] <= 0, ["floor_area_m2", "bedrooms", "bathrooms"]])
print(X_train.loc[X_train["age_years"] < 0, ["age_years", "property_type"]])
print(X_train.loc[X_train["bedrooms"] > 10, ["bedrooms", "floor_area_m2", "property_type"]])
      floor_area_m2  bedrooms  bathrooms
1183           -1.0      3.0        2.0     ← impossible, treat as missing

      age_years  property_type
402        -5.0      terraced             ← impossible, treat as missing
917        -2.0      semi

      bedrooms  floor_area_m2  property_type
664       11.0          312.0      detached   ← large but plausible for a large house

The decisions:

  • A floor area of −1 is a data-entry error. Treat it as missing and let the imputation handle it.
  • Negative ages are impossible. Treat as missing.
  • 11 bedrooms in a 312 m² detached house is unusual but entirely possible. Keep it. Deleting real extreme values because they are inconvenient is one of the most common mistakes in applied regression.
for frame in (X_train, X_val, X_test):
    frame.loc[frame["floor_area_m2"] <= 0, "floor_area_m2"] = np.nan
    frame.loc[frame["age_years"] < 0, "age_years"] = np.nan

Document every cleaning decision

Each of those three decisions should end up in the report. "Three records had impossible floor areas or ages and were treated as missing" is a sentence a reviewer can evaluate. Silently doing it is not.

Step 6 — Group the postcodes

The postcode column has too many levels to one-hot encode. We will keep the outward code — the first part, such as "SW1" — which captures broad geography while cutting the cardinality substantially.

def reduce_postcode(series):
    return series.str.split().str[0]

for frame in (X_train, X_val, X_test):
    frame["postcode_area"] = reduce_postcode(frame["postcode"])
    frame.drop(columns=["postcode"], inplace=True)

print(X_train["postcode_area"].nunique())   # 24
24

24 areas is manageable. One-hot encoding produces 24 binary columns, each with around 30 training observations on average — enough to estimate a coefficient from.

Step 7 — Build the pipeline

The pipeline encapsulates every transformation so that the correct fit/transform discipline is enforced automatically.

from sklearn.compose import ColumnTransformer
from sklearn.pipeline import Pipeline
from sklearn.preprocessing import StandardScaler, OneHotEncoder, OrdinalEncoder
from sklearn.impute import SimpleImputer
from sklearn.linear_model import LinearRegression

numeric_cols = [
    "floor_area_m2", "bedrooms", "bathrooms",
    "age_years", "distance_to_centre_km",
]

categorical_cols = ["property_type", "postcode_area", "has_garden"]

ordinal_cols = ["epc_rating"]     # E < D < C < B < A

numeric_pipe = Pipeline([
    ("impute", SimpleImputer(strategy="median")),
    ("scale",  StandardScaler()),
])

categorical_pipe = Pipeline([
    ("impute", SimpleImputer(strategy="most_frequent")),
    ("onehot", OneHotEncoder(handle_unknown="ignore", drop="first")),
])

ordinal_pipe = Pipeline([
    ("impute", SimpleImputer(strategy="most_frequent")),
    ("ordinal", OrdinalEncoder(
        categories=[["E", "D", "C", "B", "A"]]
    )),
])

preprocess = ColumnTransformer([
    ("num",  numeric_pipe,     numeric_cols),
    ("cat",  categorical_pipe, categorical_cols),
    ("ord",  ordinal_pipe,     ordinal_cols),
])

model = Pipeline([
    ("preprocess", preprocess),
    ("regressor",  LinearRegression()),
])

Three details worth noting.

  • Median imputation rather than mean, because floor area is mildly skewed and the median is more robust to the very large houses.
  • drop="first" in the one-hot encoder. Without it, the dummy variables plus the intercept are perfectly collinear — the dummy variable trap from Tutorial 3. Dropping one level per categorical makes XᵀX invertible.
  • Ordered encoding for epc_rating. EPC ratings are ordinal — A is better than B — so treating them as ordered integers preserves that information. One-hot encoding would throw it away.

Step 8 — Fit and evaluate on validation

from sklearn.metrics import mean_squared_error, mean_absolute_error, r2_score

def report(name, y_true, y_pred):
    rmse = np.sqrt(mean_squared_error(y_true, y_pred))
    mae  = mean_absolute_error(y_true, y_pred)
    r2   = r2_score(y_true, y_pred)
    ratio = rmse / mae
    print(f"{name:<28} RMSE £{rmse:>9,.0f}   MAE £{mae:>9,.0f}   "
          f"R² {r2:>6.3f}   RMSE/MAE {ratio:.2f}")

# --- baseline ---
baseline_pred = np.full(len(y_val), y_train.mean())
report("mean baseline", y_val, baseline_pred)

# --- model, raw target ---
model.fit(X_train, y_train)
report("linear (raw price)", y_val, model.predict(X_val))

# --- model, log target ---
model_log = Pipeline([
    ("preprocess", preprocess),
    ("regressor",  LinearRegression()),
])

# log transform requires a wrapper so the prediction is exp'd back
from sklearn.compose import TransformedTargetRegressor

model_log = Pipeline([
    ("preprocess", preprocess),
    ("regressor",  TransformedTargetRegressor(
        regressor=LinearRegression(),
        func=np.log, inverse_func=np.exp,
    )),
])

model_log.fit(X_train, y_train)
report("linear (log price)", y_val, model_log.predict(X_val))
mean baseline                RMSE £   94,207   MAE £  71,438   R²  0.000   RMSE/MAE 1.32
linear (raw price)           RMSE £   46,318   MAE £  34,905   R²  0.758   RMSE/MAE 1.33
linear (log price)           RMSE £   44,061   MAE £  33,112   R²  0.781   RMSE/MAE 1.33

The log transform improves the validation RMSE by about 5% and R² by two points. That is a real but modest gain, and it comes with a change in what the model is optimising — it now minimises proportional error rather than absolute error. Given that a seller's intuition about accuracy is proportional ("within about 10%"), the log model is the better choice even on the raw metrics.

Both models comfortably beat the mean baseline, which had an RMSE of £94,207. That is the first question to answer and it is answered clearly.

Step 9 — Diagnose

import matplotlib.pyplot as plt

y_pred_val = model_log.predict(X_val)
resid = y_val - y_pred_val

fig, ax = plt.subplots(1, 2, figsize=(12, 4))

# residual plot against fitted values
ax[0].axhline(0, color="grey", linestyle="--", linewidth=1)
ax[0].scatter(y_pred_val, resid, alpha=0.4, s=12)
ax[0].set_xlabel("Fitted sale price (£)")
ax[0].set_ylabel("Residual (£)")
ax[0].set_title("Residuals vs fitted")

# residual plot against floor area — the most important feature
ax[1].axhline(0, color="grey", linestyle="--", linewidth=1)
ax[1].scatter(X_val["floor_area_m2"], resid, alpha=0.4, s=12)
ax[1].set_xlabel("Floor area (m²)")
ax[1].set_ylabel("Residual (£)")
ax[1].set_title("Residuals vs floor area")

What the plots show:

  • Residuals vs fitted. The cloud is broadly shapeless, centred on zero. There is a mild widening at higher fitted values — heteroscedasticity — but no curvature. The model has captured the non-linear structure.
  • Residuals vs floor area. No visible curvature, confirming that the relationship between size and price is roughly linear once the other features are included. There is more spread for larger properties.

The widening is worth investigating. It suggests the model's errors are proportionally larger for expensive properties, which is consistent with the raw scale results — fitting on the log target should have reduced it, but it has not eliminated it.

from sklearn.model_selection import learning_curve

train_sizes, train_scores, val_scores = learning_curve(
    model_log, X_train, y_train,
    train_sizes=np.linspace(0.1, 1.0, 8),
    scoring="neg_root_mean_squared_error",
    cv=5, random_state=42,
)

train_rmse = -train_scores.mean(axis=1)
val_rmse   = -val_scores.mean(axis=1)

for size, tr, va in zip(train_sizes, train_rmse, val_rmse):
    print(f"n={size:>4}   train £{tr:>9,.0f}   val £{va:>9,.0f}   gap £{va-tr:>9,.0f}")
n=  72   train £   31,904   val £   56,182   gap £   24,278
n= 144   train £   37,806   val £   51,240   gap £   13,434
n= 216   train £   40,913   val £   48,891   gap £    7,978
n= 288   train £   42,540   val £   47,502   gap £    4,962
n= 360   train £   43,388   val £   46,751   gap £    3,363
n= 432   train £   44,050   val £   46,255   gap £    2,205
n= 504   train £   44,372   val £   45,945   gap £    1,573
n= 720   train £   44,612   val £   44,061   gap £   -551

The training and validation curves converge as the training set grows, and the gap has essentially closed by the full training size. The validation error is still declining slowly, but the flattening is clear.

The diagnosis is that the model is well fitted but on the edge of high bias. More data would help a little — the validation curve has not quite plateaued — but the model has essentially extracted what these features can provide.

Step 10 — Evaluate on the test set, once

y_pred_test = model_log.predict(X_test)

report("test set — log model", y_test, y_pred_test)

# Also report the baseline on the test set for comparison
baseline_test = np.full(len(y_test), y_train.mean())
report("test set — baseline", y_test, baseline_test)
test set — log model       RMSE £   45,120   MAE £  33,806   R²  0.772   RMSE/MAE 1.33
test set — baseline        RMSE £   94,318   MAE £  71,502   R² -0.001   RMSE/MAE 1.32

The test performance is essentially identical to the validation performance. That is the good outcome: it means every decision made during validation generalised. A large gap in either direction would have been a warning — higher test error would mean we overfitted the validation set, and lower test error would suggest the validation split was unlucky.

Step 11 — Read the coefficients

# Extract the fitted coefficients from the pipeline
regressor   = model_log.named_steps["regressor"].regressor_
preprocess  = model_log.named_steps["preprocess"]

feature_names = preprocess.get_feature_names_out()
coefs = pd.DataFrame({
    "feature":    feature_names,
    "coef_log":   regressor.coef_,
})

# Convert log-scale coefficients to a percentage price change
coefs["pct_change_per_unit"] = (np.exp(coefs["coef_log"]) - 1) * 100

print(coefs.sort_values("coef_log", key=abs, ascending=False).to_string(index=False))
                feature  coef_log  pct_change_per_unit
   num__distance_to_centre_km   -0.241                -21.4
            num__floor_area_m2    0.198                 21.9
                num__age_years   -0.071                 -6.9
               ord__epc_rating    0.058                  6.0
             num__bathrooms     0.034                  3.5
              num__bedrooms     0.021                  2.1
     cat__property_type_semi   -0.134                -12.5
cat__property_type_terraced   -0.268                -23.5
     cat__property_type_flat   -0.412                -33.8
        cat__has_garden_True    0.043                  4.4
      cat__postcode_area_SW1    0.187                 20.6
      cat__postcode_area_N1     0.142                 15.3
                                     ...

How to read these on the log scale

Because the model was fitted on log(price), a coefficient of c corresponds to a proportional change of roughly ec − 1 in the predicted price. The distance coefficient of −0.241 means each extra kilometre from the centre is associated with about a 21% lower price — a substantial effect, and in the expected direction. Floor area of +0.198 means each extra square metre is associated with about a 22% higher price — but that figure is per standard deviation, because the numeric features were scaled, not per square metre. That distinction is important and easy to get wrong.

Three cautions for the report.

  • Scaled features give standardised coefficients. Because the numeric features were passed through StandardScaler, the coefficient on floor area is per standard deviation, not per square metre. For the report we would rescale it — divide by the training-set standard deviation — to give a per-unit figure that a reader can act on.
  • Bedrooms and bathrooms have small coefficients. That is not because bedrooms do not matter; it is because they are correlated with floor area, and once floor area is in the model they add little. Their coefficients describe a comparison that rarely occurs in the data: two properties with identical floor area, location and age but different bedroom counts.
  • None of these are causal. The distance coefficient does not mean "moving a house one kilometre closer to the centre would raise its price by 21%". It means "in this dataset, houses one kilometre closer to the centre sold for about 21% more, holding the other measured features constant."

The complete code

import numpy as np
import pandas as pd

from sklearn.compose import ColumnTransformer, TransformedTargetRegressor
from sklearn.impute import SimpleImputer
from sklearn.linear_model import LinearRegression
from sklearn.metrics import mean_absolute_error, mean_squared_error, r2_score
from sklearn.model_selection import learning_curve, train_test_split
from sklearn.pipeline import Pipeline
from sklearn.preprocessing import OneHotEncoder, OrdinalEncoder, StandardScaler

RANDOM_STATE = 42

# ------------------------------------------------------------------
# 1. Load
# ------------------------------------------------------------------
df = pd.read_csv("housing.csv")

# ------------------------------------------------------------------
# 2. Feature audit — remove leaks BEFORE looking at anything else
# ------------------------------------------------------------------
LEAKY = ["days_on_market", "survey_valuation", "agent_id"]
TARGET = "sale_price"

df = df.drop(columns=LEAKY)
y = df[TARGET]
X_raw = df.drop(columns=[TARGET])

# ------------------------------------------------------------------
# 3. Split
# ------------------------------------------------------------------
X_train, X_temp, y_train, y_temp = train_test_split(
    X_raw, y, test_size=0.40, random_state=RANDOM_STATE
)
X_val, X_test, y_val, y_test = train_test_split(
    X_temp, y_temp, test_size=0.50, random_state=RANDOM_STATE
)

# ------------------------------------------------------------------
# 4/5. Clean implausible values, reduce postcode cardinality
# ------------------------------------------------------------------
for frame in (X_train, X_val, X_test):
    frame.loc[frame["floor_area_m2"] <= 0, "floor_area_m2"] = np.nan
    frame.loc[frame["age_years"] < 0, "age_years"] = np.nan
    frame["postcode_area"] = frame["postcode"].str.split().str[0]
    frame.drop(columns=["postcode"], inplace=True)

# ------------------------------------------------------------------
# 6. Pipeline
# ------------------------------------------------------------------
numeric_cols = [
    "floor_area_m2", "bedrooms", "bathrooms",
    "age_years", "distance_to_centre_km",
]
categorical_cols = ["property_type", "postcode_area", "has_garden"]
ordinal_cols = ["epc_rating"]

numeric_pipe = Pipeline([
    ("impute", SimpleImputer(strategy="median")),
    ("scale",  StandardScaler()),
])
categorical_pipe = Pipeline([
    ("impute", SimpleImputer(strategy="most_frequent")),
    ("onehot", OneHotEncoder(handle_unknown="ignore", drop="first")),
])
ordinal_pipe = Pipeline([
    ("impute", SimpleImputer(strategy="most_frequent")),
    ("ordinal", OrdinalEncoder(categories=[["E", "D", "C", "B", "A"]])),
])

preprocess = ColumnTransformer([
    ("num", numeric_pipe,     numeric_cols),
    ("cat", categorical_pipe, categorical_cols),
    ("ord", ordinal_pipe,     ordinal_cols),
])

model = Pipeline([
    ("preprocess", preprocess),
    ("regressor",  TransformedTargetRegressor(
        regressor=LinearRegression(),
        func=np.log, inverse_func=np.exp,
    )),
])

# ------------------------------------------------------------------
# 7. Fit and evaluate
# ------------------------------------------------------------------
model.fit(X_train, y_train)

def report(name, y_true, y_pred):
    rmse = np.sqrt(mean_squared_error(y_true, y_pred))
    mae  = mean_absolute_error(y_true, y_pred)
    r2   = r2_score(y_true, y_pred)
    print(f"{name:<28} RMSE £{rmse:>9,.0f}   MAE £{mae:>9,.0f}   "
          f"R² {r2:>6.3f}   RMSE/MAE {rmse/mae:.2f}")

report("mean baseline", y_val, np.full(len(y_val), y_train.mean()))
report("linear (log price)", y_val, model.predict(X_val))

# ------------------------------------------------------------------
# 8. Diagnostics
# ------------------------------------------------------------------
train_sizes, train_scores, val_scores = learning_curve(
    model, X_train, y_train,
    train_sizes=np.linspace(0.1, 1.0, 8),
    scoring="neg_root_mean_squared_error",
    cv=5, random_state=RANDOM_STATE,
)
for size, tr, va in zip(train_sizes, -train_scores.mean(axis=1), -val_scores.mean(axis=1)):
    print(f"n={size:>4}   train £{tr:>9,.0f}   val £{va:>9,.0f}")

# ------------------------------------------------------------------
# 9. Final evaluation — once
# ------------------------------------------------------------------
report("test set — final model", y_test, model.predict(X_test))
nb-07 · House price prediction, end to end The complete notebook: load, audit, split, clean, build the pipeline, fit, diagnose, and produce the report figures. Written to be run top to bottom.

6 Interactive activity

Break the pipeline in five ways

Each of the changes below is a mistake that a competent practitioner could make in good faith. Run each one, record the effect on the validation and test metrics, and then explain what went wrong. The aim is to see the consequences of leakage and mis-ordering with your own eyes, so that you recognise them in future.

  1. Re-include a leak. Add survey_valuation back into the feature set and refit. What happens to R²? What happens to the coefficient on floor area? Would a stakeholder be pleased with this model?
  2. Scale before splitting. Standardise the numeric features using the mean and standard deviation of the full dataset, then split and fit. How much does the validation R² change compared with the honest pipeline? Is the effect large or small, and what does that tell you about when leakage matters most?
  3. One-hot encode the full postcode. Replace the reduced postcode area with the original postcode column and one-hot encode it. Compare the training and validation RMSE. What has happened?
  4. Remove the ordinal encoding. Treat epc_rating as a plain unordered category and one-hot encode it. Does validation performance get better, worse or stay the same? What does that suggest about whether the ordering carries real information?
  5. Drop distance_to_centre_km. Remove the strongest feature and refit. How much worse does validation get? This gives a crude sense of how much of the model's performance rests on a single variable.
Show the discussion
  1. The leak dominates everything. Validation R² rises to roughly 0.97. The coefficient on floor area collapses toward zero, because the survey valuation already encodes the size of the property and far more besides. A stakeholder would be delighted, and the model would be worthless in production because the valuation does not exist at the moment the estimate is displayed. This is why the audit happens first.
  2. The effect is small. Validation R² typically moves by less than 0.005. Scaling leakage is real but usually minor, because the mean and standard deviation of a large dataset barely change when a fifth of it is swapped out. It matters most on small datasets, where a handful of test observations can shift the mean meaningfully. The lesson is not that the practice is acceptable — it is a genuine error — but that the magnitude of a leak depends on the situation, and it should be measured rather than assumed.
  3. Severe overfitting. The one-hot encoder creates several hundred binary columns, many with one or two training observations. The model can fit each of these individually. Training RMSE falls sharply; validation RMSE rises. This is the classic high-cardinality trap and the reason for reducing postcode to an area.
  4. Performance barely changes. The EPC rating has a modest effect, and treating it as ordered or unordered makes little difference to the model's predictive accuracy. But the ordered encoding produces one interpretable coefficient instead of four, and it uses five fewer parameters. When the predictive performance is equivalent, the simpler model with the more interpretable parameterisation is the better choice.
  5. Validation RMSE roughly doubles. Distance to centre is by far the most informative feature in this dataset. Its removal takes the model from R² ≈ 0.78 to R² ≈ 0.45. This is worth knowing: it tells you that the model's usefulness depends heavily on a feature that a seller can report accurately, and that if the postcode is wrong, the estimate will be badly wrong.

The lesson to take from the exercise

Leakage does not produce a small error. It produces a model that looks excellent and fails completely. That is why the audit is done on paper, before any data is loaded, and why a suspiciously high score should always be investigated before it is celebrated.

7 Self-check questions

Answer each question in your own words before revealing the answer.

Question 1

Why is the feature audit performed before any data exploration?

Show answer
Because once you have seen that a leaked feature produces an excellent fit, it is psychologically very difficult to remove. The audit is a decision about the problem, not about the data: whether a value would be known at prediction time is a question about the deployment context, and it can be answered without opening the file.

There is also a practical reason. If you explore first and then remove the leaked feature, you have already been influenced by seeing it. You might unconsciously adjust your cleaning decisions or your feature engineering to compensate. Doing the audit first removes the temptation entirely.

Question 2

The pipeline uses drop="first" in the one-hot encoder. Explain what would go wrong without it.

Show answer
With all k dummy columns retained for a k-level categorical, plus the intercept column of ones, the columns become perfectly collinear — any one dummy can be reconstructed exactly from the others and the intercept. This is the dummy variable trap.

The consequence is that XᵀX is singular and the normal equation has no unique solution. Different solvers will produce different coefficients that all fit equally well, and the coefficients become impossible to interpret. Dropping one level per categorical removes the redundancy. The dropped level becomes the reference category, and each remaining coefficient is read as a difference from that reference.

Question 3

The model fits on log(price). The coefficient on distance_to_centre_km is −0.241. How do you interpret this in the report?

Show answer
Because the target is on a log scale, a coefficient of c corresponds to a proportional change of roughly ec − 1 in the predicted price. For c = −0.241: e−0.241 − 1 ≈ −0.214, or about a 21% decrease in predicted price per kilometre from the centre.

But there is a catch: distance_to_centre_km was standardised before being passed to the model, so the coefficient is per standard deviation, not per kilometre. To report a per-kilometre figure you must divide the standardised coefficient by the training-set standard deviation of the feature, then exponentiate. This is exactly the kind of detail that produces wrong numbers in published reports, and it is why coefficients must always be converted back to the units of the original features before they are communicated.

Question 4

The learning curve showed the training and validation errors converging with the gap nearly closed. What is the diagnosis, and would you recommend collecting more data?

Show answer
A closed gap with both errors at a moderate level indicates the model is well fitted — it has captured what the features can provide, without overfitting. There is no high-variance problem to solve.

On whether to collect more data: the validation curve was still declining slightly at the largest training size, so more data would help a little. But the curve had nearly plateaued, which means the return would be small. The higher-value options are to add genuinely new features — property condition, a more granular location, recent comparable sales — rather than more rows of the same features. This is the practical question a learning curve answers, and it frequently says that more data of the same kind is not the bottleneck.

Question 5

The coefficient on bedrooms is small and positive, while the coefficient on floor_area_m2 is large. Does this mean bedrooms do not affect house prices?

Show answer
No. It means that, holding floor area constant, an extra bedroom is associated with only a small additional price. But bedrooms and floor area are strongly correlated — larger houses have more bedrooms — so the model has already attributed most of the size effect to floor area. The bedroom coefficient describes the effect of an extra bedroom in a house of the same size, which means a smaller room on average. That is a comparison that rarely occurs and is not what a homeowner means when they ask "what is a bedroom worth?"

This is the partial-effect interpretation from Tutorial 2, and it is the single most common source of misunderstanding when coefficients are reported to non-specialists. The correct statement is not "bedrooms are worth £X" but "in this dataset, properties with one more bedroom than an otherwise similar property sold for £X more — where 'otherwise similar' includes having the same floor area."

Question 6

The final model achieves R² = 0.772 on the test set, with RMSE = £45,120 against a median price of £276,000. Write one sentence that a seller would find useful, and one sentence that would be misleading.

Show answer

Useful: "Based on the details you have entered, we estimate your property is worth around £285,000. Estimates of this kind are typically within about 16% of the eventual sale price, so a realistic range is roughly £240,000 to £330,000."

Misleading: "Your property is worth £285,000."

The difference is that the first sentence communicates the uncertainty and gives a range, while the second presents a point estimate as a fact. An RMSE of £45,120 on a median price of £276,000 means the model is useful for orientation and useless as a formal valuation. A report that omits that distinction is not honest, however impressive the R² looks.

8 Practice exercises

Exercise 1 — Complete the feature audit for a new scenario

A hospital wants to predict a patient's length of stay in days, using information available at the moment of admission. The available columns are listed below. For each, decide whether to include it, exclude it as leakage, or investigate further, and justify your decision in one sentence.

Column Description Decision Reason
agePatient's age at admission
admission_diagnosisICD-10 code assigned at admission
discharge_diagnosisICD-10 code assigned at discharge
previous_admissionsCount of prior admissions in the last 5 years
triage_scoreRecorded by the admitting nurse
surgery_performedWhether surgery was performed during the stay
number_of_medicationsCount of medications prescribed at discharge
admission_day_of_weekDay of the week the patient was admitted
total_costTotal cost of the stay
comorbiditiesCount of pre-existing chronic conditions
Show answers
Column Decision Reason
age Include Known at admission.
admission_diagnosis Include Assigned at admission — available.
discharge_diagnosis Remove Assigned at the end of the stay. Textbook target leakage.
previous_admissions Include Historical record, available at admission.
triage_score Include Recorded on admission by the triage nurse.
surgery_performed Investigate If the prediction is made at admission, surgery may not yet be decided. If the model is re-scored daily, it is available from day two. Depends on the deployment design.
number_of_medications Remove Prescribed at discharge — known only at the end of the stay, and partly a consequence of its length.
admission_day_of_week Include Known at admission. May act as a proxy for staffing levels, which is legitimate but worth noting.
total_cost Remove Known only after discharge, and directly a function of length of stay.
comorbidities Include Pre-existing conditions, part of the patient's history.

Note that surgery_performed is the only genuinely ambiguous case, and its resolution depends on when the prediction will be refreshed. This is a common situation: leakage is not always a property of the column alone, but of the column and the deployment context together.

Exercise 2 — Compute coefficients in original units

The fitted model used standardised numeric features. The training-set means and standard deviations are:

Feature Mean Std dev Coefficient (log scale, standardised)
floor_area_m2118.438.7+0.198
distance_to_centre_km6.23.4−0.241
age_years54.131.8−0.071
  1. Convert each coefficient to a per-original-unit figure on the log scale.
  2. Convert each to a percentage change in predicted price per original unit.
  3. Interpret the floor area coefficient in a sentence a seller could understand.
  4. Interpret the age coefficient in the same way.
  5. Why is the distance coefficient the easiest of the three to interpret?
Show answers

1. Divide each coefficient by the standard deviation:

  • floor_area_m2: 0.198 / 38.7 = +0.00512 per m²
  • distance_to_centre_km: −0.241 / 3.4 = −0.0709 per km
  • age_years: −0.071 / 31.8 = −0.00223 per year

2. Convert to percentages using ec − 1:

  • floor area: e0.00512 − 1 ≈ +0.51% per m²
  • distance: e−0.0709 − 1 ≈ −6.8% per km
  • age: e−0.00223 − 1 ≈ −0.22% per year

3. "Each additional square metre of floor area is associated with roughly a 0.5% higher sale price, holding the other features constant. On a £280,000 property that is about £1,400 per square metre."

4. "Each additional year of property age is associated with roughly a 0.22% lower sale price. Over a 30-year difference in age, that compounds to about a 6.4% difference."

5. The distance coefficient is easiest to interpret because "one kilometre" is a unit everyone understands and the feature is measured directly in those units, without any transformation of the variable itself. Age and floor area are equally interpretable in principle, but their effects are smaller per unit, so the number is less intuitive at first glance.

Exercise 3 — Diagnose a learning curve

A colleague runs the same pipeline but forgets to remove the leaked survey_valuation column. The learning curve looks like this:

n=  72   train £  8,214   val £ 19,880
n= 144   train £ 11,502   val £ 17,340
n= 216   train £ 13,988   val £ 16,105
n= 288   train £ 15,220   val £ 15,412
n= 360   train £ 16,033   val £ 14,890
n= 432   train £ 16,718   val £ 14,559
n= 504   train £ 17,191   val £ 14,301
n= 720   train £ 17,640   val £ 14,102

Why should this curve make you suspicious rather than pleased?

Show answer
Two things are wrong.

First, the validation error is below the training error at the largest training size. That is unusual. Normally the training error is lower than the validation error, because the model has seen the training data. A validation error below the training error suggests something about the validation set makes it easier to predict — which, in a leaky model, is exactly what happens.

Second, the absolute numbers are implausibly good. A validation RMSE of £14,102 on house prices with a median around £276,000 is an error of about 5%. With only self-reported features, no survey, no photographs and no condition information, that is not credible. A practitioner who has built similar models would recognise the number as too good.

The lesson: an unexpectedly strong result is a prompt to investigate, not to celebrate. In this case, the cause is immediate once the feature list is checked.

Exercise 4 — Design a new feature

The model has access to floor area, bedrooms, bathrooms and property type. Design three new features that might improve the model's performance, and for each:

  1. Write down the exact formula in terms of the existing columns.
  2. State the intuition for why it might help.
  3. State why it might not help, or might make things worse.
  4. Test it on validation and report the effect.

Three suggestions to start from: rooms per square metre; a "flat" indicator interacted with floor area; distance to centre divided by property type. There is no single correct answer — the exercise is about forming a hypothesis and testing it, which is what feature engineering actually is.

Show a worked example of the reasoning

Feature: rooms_per_m2 = (bedrooms + bathrooms) / floor_area_m2

  • Intuition. Two properties of the same size but different room counts feel different — a 100 m² flat with four bedrooms is cramped, one with two is spacious. The raw counts do not capture this because floor area is also in the model. A ratio captures the density.
  • Risk. The feature is a ratio of two other features, which makes it highly correlated with both. That can destabilise the coefficients and make the model harder to interpret, even if predictive performance improves.
  • Test. Adding it might improve validation RMSE by a few hundred pounds, or it might do nothing. Measure rather than assume. If the improvement is small, the interpretability cost probably is not worth paying.

The discipline is to state the hypothesis and the risk before running the experiment. Otherwise it is very easy to rationalise a small improvement as meaningful when it is really noise.

Exercise 5 — Write the report

Using the metrics from the worked example — validation RMSE £44,061, MAE £33,112, R² 0.781, test RMSE £45,120 — write a 250-word report of the model for the estate agency's head of product.

Your report must include:

  1. The headline performance in plain language, with a typical error range.
  2. A comparison against simply predicting the average price.
  3. The two features that matter most, and what they mean.
  4. What the model is not good at.
  5. The range of properties over which it should be used.
  6. A clear statement of what the estimate should and should not be used for.

Avoid all statistical jargon. Use pounds, percentages and plain sentences. If you cannot explain a limitation without jargon, you have not yet understood it well enough to report it.

Show a model answer

House price estimate — model performance summary

Based on the details a seller enters into the online form, the model estimates a property's sale price with a typical error of about £44,000. For a home worth around £280,000, that means the estimate is usually within about 15% of the eventual sale price. The largest errors occur on more expensive properties.

For comparison, an estimate that simply guessed the average price for every property would be wrong by about £94,000. The model roughly halves that error, which makes it useful as a starting point but not as a substitute for a professional valuation.

Two pieces of information drive most of the estimate: the property's distance from the city centre, and its floor area. A property one kilometre further from the centre is typically estimated about 7% lower; each additional square metre adds roughly 0.5% to the estimate.

The model does not know the condition of the property, whether it has been renovated, or what it looks like. It cannot account for a property that is unusually well or poorly presented. It should only be used for properties between roughly 40 and 250 square metres, since that is the range it was built from.

The estimate is best used to give sellers an early indication of what their property might be worth. It is not accurate enough to be used for a formal valuation, for mortgage lending, or for pricing a property that will be marketed immediately.

9 Problem set

These problems are longer and more open-ended. They feed directly into Assignment 2 and the capstone.

Problem 1 — Run the full project on a public dataset

Choose a public regression dataset with at least 500 rows and a mix of numeric and categorical features. Options:

Carry out the complete eight-stage workflow from Section 3.1 and document each stage.

  1. Write the framing document — target, units, cost of error, prediction context.
  2. Perform the feature audit and record every decision with its justification.
  3. Split into training, validation and test.
  4. Explore the training set and list everything you find.
  5. Build the pipeline. Include at least one numeric imputation, one categorical imputation and one encoding step.
  6. Fit and tune using the validation set. Try at least three variants — for example, raw target versus log target, or including versus excluding a feature.
  7. Produce a residual plot and a learning curve for the chosen model.
  8. Evaluate once on the test set.
  9. Write a 400-word report for a non-technical stakeholder.

The deliverable is not the model. It is the set of documented decisions, each with its justification and its measured consequence.

Problem 2 — Leakage audit on a dataset of your choice

Find a real dataset — from a competition, a paper, or an organisation you have access to — that contains at least one column that could be a target leak.

  1. List every column and for each one answer: would this value be different if the target had been different?
  2. Identify at least one column that leaks, and at least one that is borderline.
  3. Fit a model with the leak included. Record the validation metrics.
  4. Remove the leak and refit. Record the metrics again.
  5. Quantify the difference. How much of the apparent performance was leakage?
  6. Examine the coefficients from the leaky model. How do they differ from the honest model's? Do they tell a different story about what drives the target?
  7. Write a short paragraph explaining why leakage is particularly dangerous in a commercial setting, where a strong result may be rewarded before it is verified.

Problem 3 — Compare three target transformations

House prices are right-skewed, and the worked example used a log transform. Test whether it was the right choice.

  1. Fit the same pipeline with the raw target, the log target, and a square-root target.
  2. Report validation RMSE, MAE and R² for all three.
  3. Note that RMSE and MAE are not directly comparable across transformations, because each model is optimising a different quantity. Compute a fair comparison: for each model, compute the median absolute percentage error on validation.
  4. Which transformation wins on RMSE? Which wins on median percentage error?
  5. Produce a residual plot for each. Which one has the least structure, and the most uniform spread?
  6. Choose a transformation and justify it in terms of what the model will be used for, not in terms of which has the best number. A seller cares about proportional accuracy; a lender cares about absolute error on high-value properties. Which matters here?

Problem 4 — Robustness to a small change

A model that changes substantially when one decision is altered is not a model you should trust. Test the stability of the house price model.

  1. Fit the final model and record the validation RMSE and the coefficient on floor area.
  2. Refit with the random seed for the train/validation split changed, ten times. Record the spread of both quantities.
  3. Refit with drop="first" removed from the one-hot encoder. What happens to the coefficients?
  4. Refit with the numeric features unscaled. Do the predictions change?
  5. Refit with the imputation strategy changed from median to mean. How much does validation RMSE move?
  6. Refit with the postcode reduced to area versus left as the full outward code. Which produces better validation performance?
  7. Rank these five decisions by how much they affect the outcome. Which decision would you spend the most time on, if you had to get one right?
  8. Write a paragraph on what coefficient stability tells you about whether a coefficient should be reported at all. If a coefficient moves by 50% when the random seed changes, should it appear in a report to stakeholders?

10 Discussion and projects

Discussion prompt — post 150–250 words

A model with R² = 0.78 is either very good or not good enough, depending entirely on what it is used for.

The house price model in this tutorial has a typical error of about £44,000. For a seller deciding whether to book a valuation, that is useful orientation. For a lender deciding how much to offer on a mortgage, it is nowhere near precise enough. For a buyer deciding whether to make an offer, it might be actively harmful if they treat it as authoritative.

Choose a real prediction context — one from your own experience or one you have read about — and state what level of error would be acceptable. Then explain, in terms of the decision that would be made and the cost of getting it wrong, why that threshold is the right one. Finally, describe how you would communicate the model's uncertainty to the person using it, in a way that does not either overstate or understate what the model can do.

Mini-project — build the full pipeline as a reusable class

Wrap everything in this tutorial into a reusable, well-documented Python class that could be applied to a different regression problem with minimal modification.

The class should support:

  1. A configurable list of numeric, categorical and ordinal columns, with the ordinal order specified for each.
  2. A configurable imputation strategy per column type.
  3. An optional target transformation (raw, log, or square root).
  4. An automatic leakage audit that checks a user-supplied list of known-available columns against the dataset columns and warns about anything unexplained.
  5. A fit method that performs the split, fits the pipeline, and stores the validation metrics.
  6. A diagnose method that returns the residual plot data, a learning curve and a coefficient table in original units.
  7. A report method that generates a plain-language summary from the stored metrics.
  8. A predict method that accepts raw, uncleaned data and returns predictions.

The most valuable part of this exercise is writing the leakage audit. To write it, you have to decide what makes a column suspicious, and articulating that rule will teach you more about leakage than any amount of reading. Test the class on two different datasets and fix whatever breaks.

Discussion prompt — what would change your mind?

The house price model assumes a linear relationship between each feature and the log price, once the other features are accounted for. The residual plot looked clean, so the assumption seems defensible for this dataset. But that is evidence about this dataset, not about house prices in general.

Describe a market condition under which the linear assumption would break down badly. What would the residual plot look like if it did? And what would you monitor in a deployed model to detect the breakdown before it produced a stream of bad estimates? This is a question about model monitoring, which Module 5 and Unit 17 take up in detail — but the diagnostic instinct you need is the one you have just built.

11 Summary

What you should take away

The eight stages, and what each one produces

Deliverables by stage
Stage Output Common failure
1 · Frame A one-paragraph statement of target, units, horizon, cost of error and prediction context Skipping it, and discovering three weeks later that the prediction is needed before half the features exist
2 · Audit A table of every column with an include/exclude decision and a reason Doing it after exploring, and keeping a leak because the numbers looked good
3 · Split Train, validation and test sets, with a locked test set Splitting after scaling, and leaking test statistics into training
4 · Explore A list of data problems and a plan for each Exploring the whole dataset, including the test set
5 · Pipeline A single estimator object with fit and predict Doing preprocessing outside the pipeline, and reintroducing leakage through the back door
6 · Fit Validation metrics for at least one model and the mean baseline Reporting training metrics, or only reporting a single model without a baseline
7 · Diagnose A residual plot, a learning curve, and a coefficient table in original units Stopping once R² looks reasonable, and never looking at the residuals
8 · Report A plain-language summary with metrics, limitations, and range of validity Reporting a point estimate without uncertainty, and omitting the failure modes

Key terms

Feature audit
A systematic review of every column to determine whether its value would be known at prediction time, and whether it is a consequence of the target.
Target leakage
A feature whose value depends on the target, or which is recorded after the outcome. Produces excellent development metrics and useless deployment performance.
ColumnTransformer
A scikit-learn object that applies different transformations to different subsets of columns, and combines the results into a single feature matrix.
Ordinal encoding
Encoding a categorical variable as an ordered integer, used when the categories have a genuine rank.
Dummy variable trap
Perfect collinearity caused by retaining all k dummy variables for a k-level categorical alongside an intercept. Avoided with drop="first".
High-cardinality categorical
A categorical column with many distinct values. One-hot encoding it creates a large number of sparse features and invites overfitting.
Target encoding
Replacing a categorical value with the average target for that category. Powerful, but must be computed inside the training fold to avoid leakage.
TransformedTargetRegressor
A scikit-learn wrapper that applies a function to the target before fitting and its inverse to predictions — used here to fit on log price and predict price.
Reference category
The categorical level omitted from a one-hot encoding. Each remaining coefficient is read as a difference from this level.
Range of validity
The range of feature values covered by the training data, outside which predictions are extrapolation and should not be trusted.

You are ready for Tutorial 8 when…

12 Further reading

scikit-learn — Pipelines and composite estimators user guide reference The definitive documentation for Pipeline, ColumnTransformer and TransformedTargetRegressor. Read the section on "Why pipelines matter" first — it makes the leakage argument precisely, and the examples are close to what this tutorial built.
Kaufman, Rosset & Perlich — "Leakage in Data Mining" (2012) essential The canonical taxonomy of leakage, with case studies from real competitions where leakage produced spectacular but meaningless results. Section 2 is the one to read; the case studies in Section 4 show how easily it happens even to experienced practitioners.
Molnar — Interpretable Machine Learning, ch. 5–6 free online Chapter 5 covers the interpretation of linear model coefficients in detail, including the partial-effect interpretation and the problems caused by correlated features. Chapter 6 introduces feature importance, which is a useful complement when coefficients are unstable.
James, Witten, Hastie & Tibshirani — An Introduction to Statistical Learning, ch. 3 and §6.1 free online Sections 3.6 and 3.7 discuss qualitative predictors, interaction terms and the problems of collinearity, all of which come up in this worked example. Section 6.1 on ridge regression is a natural next step if the coefficient instability in correlated features becomes a serious problem.
Gelman, Hill & Vehtari — Regression and Other Stories, ch. 12–14 textbook Covers the practical side of building regression models on real data: what to do with skewed targets, how to handle categorical predictors, and how to think about model building as a sequence of decisions rather than a single fitting operation.
Kuhn & Johnson — Feature Engineering and Selection, ch. 3–5 free online A thorough treatment of feature engineering and encoding, including the high-cardinality problem and several remedies beyond the one used here. Chapter 5 on encodings is directly relevant to the postcode problem.
Micci-Barreca — "A preprocessing scheme for high-cardinality categorical attributes" (2001) practitioner The original target-encoding paper. Older than most of what you have read in this course, but the technique it describes is still the standard approach for high-cardinality categoricals, and it is the natural next step beyond the postcode-area reduction used here.
Zheng & Casari — Feature Engineering for Machine Learning, ch. 5–6 textbook Practical, example-driven coverage of categorical encoding, text features and derived features. Written for practitioners and light on theory, which is exactly what is needed after a tutorial like this one.

What to read next

If you read one thing from this list before Tutorial 8, read the Pipelines user guide. It is short, it is reference material rather than theory, and getting comfortable with pipelines is the single highest-leverage skill you can take from this tutorial. Everything else in this course assumes you can build one without thinking about it.

👤