Data Cleaning and Feature Engineering: Predict Rents From a Messy Export
Hands-on lab · IDE in your browser

Data Cleaning and Feature Engineering: Predict Rents From a Messy Export

Take a messy real-world export to a model that beats the baseline by 70%: fix units, spellings and mixed rent periods, compare against no-model baselines, build a scikit-learn pipeline with imputation, missing-value indicators, scaling and one-hot encoding, model a log target, run an ablation to keep only features that help, and look at the test set once.

Time
50 min
Checked steps
5
Level
Beginner
Setup
None
Read step 1

Hands-on labs require Pro · $29.99/mo · cancel anytime

Lab cockpit50 min · 5 stepsSession running
2 / 5 steps passingMissing values carry information · step 3 of 5
rent.py▶ Run✓ Check
def cv_mae(model, X, y):    """Mean absolute error over 5-fold cross-validation (KFold, shuffle=True, random_state=42), in whole pounds."""    folds = KFold(n_splits=5, shuffle=True, random_state=42)    return round(-cross_val_score(model, X, y, cv=folds, scoring="neg_mean_absolute_error").mean())  # ---------- Step 3: missing values carry information ----------def epc_score(df):    """A copy of df with epc_score: 1 for EPC A up to 7 for G, NaN where the rating is missing."""    def missing_values_report(X, y):    """Cross-validated MAE of three ways to use the EPC rating (X must already have epc_score):    "one_hot": NUMERIC + CATEGORICAL (the rating as a category, missing as its own category);    "score": NUMERIC + epc_score with borough and property_type, median-imputed without indicators;    "score_indicator": the same with missing-value indicators."""     
TerminalOutput

The job

Hearthside Lettings wants to suggest an asking rent for every new flat. Its 2,400 past listings come from three agents' systems that disagree on units, spellings and even whether rent is per week or per month. You clean the export, then engineer and test features until the model prices flats far better than the borough median.

5 steps, each checked when you finish it

A check runs your work at the end of every step. Hints and the full solution are there if you get stuck.

  1. 1

    Clean the export

    Hearthside Lettings wants to suggest an asking rent for every new flat it lists.

    You writeclean()
  2. 2

    A baseline and a pipeline

    Before you judge a model, know what no model scores.

    You writebaseline_mae()make_model()
  3. 3

    Missing values carry information

    A quarter of the listings have no EPC energy rating.

    You writeepc_score()missing_values_report()
  4. 4

    Features that match the market

    Rent grows by percentages.

    You writeablation()
  5. 5

    One look at the test set

    Every decision so far used cross-validation on the training data.

    You writepredict_new()

Step 1 as it appears in the lab

The lab’s own text. The hint and the solution stay inside the lab.

Step 1: Clean the export

Hearthside Lettings wants to suggest an asking rent for every new flat it lists. You have 2,400 past listings in listings.csv, exported from three agents' systems that disagree about nearly everything (see data_notes.md). Some agents quote rent per week and area in square feet. Their boroughs and property types are spelt a dozen ways.

A model trusts every number you give it. A weekly rent reads as a flat going for a quarter of the price, and "camden" is a different place from "Camden". Cleaning comes before any model.

Do this

1. Write clean(raw): one spelling per borough and property type, every area in square metres, every rent per calendar month, furnished as 1/0, epc_rating as a letter or missing, and listed_on as a date.

2. Run it. Compare the messy rows before and after, and check the value counts.

rent.py, the file you edit116 lines
"""Predict monthly rents for Hearthside Lettings from its listings export."""
import numpy as np
import pandas as pd
from sklearn.compose import ColumnTransformer, TransformedTargetRegressor
from sklearn.impute import SimpleImputer
from sklearn.linear_model import Ridge
from sklearn.metrics import mean_absolute_error
from sklearn.model_selection import KFold, cross_val_score, train_test_split
from sklearn.pipeline import Pipeline
from sklearn.preprocessing import OneHotEncoder, StandardScaler

TARGET = "rent"
SQFT_PER_SQM = 10.764
EPC = "ABCDEFG"


# ---------- Step 1: clean the export ----------
def clean(raw):
    """One consistent table: borough in Title Case without spaces around it; property_type one of flat, house,
    studio; floor_area in square metres; rent per calendar month (pw * 52 / 12, rounded to whole pounds);
    furnished 1/0 (NaN when blank); epc_rating a letter or NaN; listed_on a date. Drops area_unit and
    rent_period, which the conversions make redundant."""
    # TODO (Step 1): work on raw.copy().
    # - borough: strip spaces, Title Case.  property_type: "studio" if it mentions studio, "house" if it mentions
    #   house, otherwise "flat" (np.select on the lowercased text).
    # - floor_area: rows with area_unit "sqft" divided by SQFT_PER_SQM (round to 1).  rent: rows with rent_period
    #   "pw" times 52 / 12 (round) - only when the rent column exists.
    # - furnished: y/yes -> 1, n/no -> 0, anything else NaN.  epc_rating: keep A-G, anything else NaN.
    # - listed_on: pd.to_datetime.  Drop area_unit and rent_period.
    raise NotImplementedError("Step 1: write clean()")


# ---------- Step 2: a fair test and a baseline to beat ----------
def split(df):
    """80/20 train/test split with random_state=42."""
    return train_test_split(df.drop(columns=[TARGET]), df[TARGET], test_size=0.2, random_state=42)


def baseline_mae(X_train, y_train, X_test, y_test):
    """MAE on the test set of two no-model guesses: the median rent of the training set ("median"), and the
    median training rent of the listing's borough ("borough_median"), rounded to whole pounds."""
    raise NotImplementedError("baseline_mae() arrives in Step 2")


NUMERIC = ["bedrooms", "bathrooms", "floor_area", "tube_km", "garden", "furnished"]
CATEGORICAL = ["borough", "property_type", "epc_rating"]


def make_model(numeric=NUMERIC, categorical=CATEGORICAL, indicator=True, log_target=False):
    """Ridge regression behind a ColumnTransformer. Numeric columns: median imputation (with missing-value
    indicator columns when indicator=True), then scaling. Categorical: missing values become the category
    "missing", then one-hot encoding that ignores unseen categories. log_target=True fits on log(rent)."""
    raise NotImplementedError("make_model() arrives in Step 2")


def cv_mae(model, X, y):
    """Mean absolute error over 5-fold cross-validation (KFold, shuffle=True, random_state=42), in whole pounds."""
    folds = KFold(n_splits=5, shuffle=True, random_state=42)
    return round(-cross_val_score(model, X, y, cv=folds, scoring="neg_mean_absolute_error").mean())


# ---------- Step 3: missing values carry information ----------
def epc_score(df):
    """A copy of df with epc_score: 1 for EPC A up to 7 for G, NaN where the rating is missing."""
    raise NotImplementedError("epc_score() arrives in Step 3")


def missing_values_report(X, y):
    """Cross-validated MAE of three ways to use the EPC rating (X must already have epc_score):
    "one_hot": NUMERIC + CATEGORICAL (the rating as a category, missing as its own category);
    "score": NUMERIC + epc_score with borough and property_type, median-imputed without indicators;
    "score_indicator": the same with missing-value indicators."""
    raise NotImplementedError("missing_values_report() arrives in Step 3")


# ---------- Step 4: features that match how rent works ----------
def add_features(df):
    """A copy of df with: log_area = log(floor_area), log_tube = log1p(tube_km), area_per_room =
    floor_area / (bedrooms + 1), listed_month = the month number of listed_on."""
    out = df.copy()
    out["log_area"] = np.log(out["floor_area"])
    out["log_tube"] = np.log1p(out["tube_km"])
    out["area_per_room"] = out["floor_area"] / (out["bedrooms"] + 1)
    out["listed_month"] = out["listed_on"].dt.month
    return out


BASE_NUMERIC = ["bedrooms", "bathrooms", "floor_area", "tube_km", "garden", "furnished", "epc_score"]
BASE_CATEGORICAL = ["borough", "property_type"]
# candidate feature -> the column it replaces (None when it is simply added)
CANDIDATES = {"log_area": "floor_area", "log_tube": "tube_km", "area_per_room": None, "listed_month": None}


def ablation(X, y):
    """For each candidate: the cross-validated MAE of the log-target model on BASE_NUMERIC with every candidate in
    place ("all"), and without that one candidate (its replaced column comes back). Returns
    {"all": mae, candidate: gain, ...} where gain = MAE without it - MAE with all (positive = it helps)."""
    raise NotImplementedError("ablation() arrives in Step 4")


# ---------- Step 5: one look at the test set ----------
FINAL_NUMERIC = list(BASE_NUMERIC)


def prepare(raw):
    """clean(), then epc_score() and add_features()."""
    return add_features(epc_score(clean(raw)))


def final_model():
    return make_model(FINAL_NUMERIC, BASE_CATEGORICAL, indicator=True, log_target=True)


def predict_new(model, raw_new):
    """A DataFrame with listing_id and predicted_rent (whole pounds) for the new listings."""
    raise NotImplementedError("predict_new() arrives in Step 5")
Provided for you:data_notes.mdlistings.csvtry_it.py

Frequently asked questions

Should I drop rows with missing values?

Usually not. Impute inside a pipeline so the imputer learns from training data only, and add missing-value indicator columns: whether a value is missing often carries information, such as older properties lacking an energy certificate.

When should I log-transform the target?

When the target grows by percentages, as prices and rents do. A linear model on log(price) turns multiplicative effects into additive ones; TransformedTargetRegressor fits on the log and predicts back in the original units.

How do I know if a feature helps?

Run an ablation: score the model with every candidate feature by cross-validation, then leave each one out in turn. Keep features whose removal makes the error noticeably worse.

Data cleaning and feature engineering with scikit-learn

Most of the work in a machine learning project happens before the model: getting units, categories and missing values right, and building features that match how the target behaves. In this lab you do that work on a realistic lettings export: convert weekly rents and square feet, merge a dozen spellings of each borough, compare the model against no-model baselines, impute with missing-value indicators, model a log target, and use an ablation to keep only features that earn their place.