Lesson 6 of 7

Imputing Missing Values in Features

You are building a model to predict the monthly rent of city apartments. Your scraper pulled 240 listings, each with the number of bedrooms, the floor, the rent, and the floor area in square feet. Floor area is the single strongest clue to rent. And about one listing in seven has it blank, because that landlord just never filled the box in.

Most models will not even start with a hole in the data. So you have to put a number in every gap. The catch, and the whole point of this lesson, is that the obvious ways to do it quietly cheat: they let your test set sneak into the answer, and they flatten the very signal you are trying to model. Here you will fill gaps the honest way.

By the end you will be able to:

  • Impute a numeric feature with the median and keep a flag that remembers where the gap was
  • Explain why imputation leaks unless the fill is learned on the training set alone, and do it leak-free
  • Fill smarter by borrowing from similar rows (a group, the nearest neighbours, a small model)

Prerequisites: you can write a mutate() and a pipe (The dplyr Verbs), you know what a train/test split and data leakage are (Train, Validation, Test, and Data Leakage), and you have met NA and the three missingness mechanisms (Missing Value Treatment). The new ideas are taught from scratch.

Press Run on the table below: each blank floor area is filled with the column median, the simplest possible stand-in.

Why you cannot skip it

A gap is not a number, so the model stops

Hand a linear regression, a glmnet, or a kNN a column with a single NA in it and most of them refuse to run, or silently drop every row that has one. With about one in seven listings missing its area, dropping those rows would throw away a seventh of your data before training even begins, and in the last lesson on missing data you saw the worse problem: the rows that go missing are rarely a fair sample, so deleting them bends the answer.

So the move in a modeling pipeline is almost always to impute: put a plausible number in each gap and keep the row. Here is the shift in mindset that makes this a feature-engineering lesson and not just data cleaning. A filled value is not a recovered measurement; it is a new feature you are manufacturing, a deliberate guess. And like any feature, it can be built carelessly (leaking, distorting) or built well.

First, the data. Each lesson runs in a fresh R session, so we build the 240 listings right here (run this once). The area depends on the number of bedrooms, with some noise, exactly the kind of structure a smart fill can exploit later.

RInteractive R
library(dplyr) set.seed(42) n <- 240 flats <- data.frame( bedrooms = sample(1:4, n, replace = TRUE, prob = c(.30, .40, .20, .10)), floor = sample(1:18, n, replace = TRUE) ) flats$area_sqft <- round(320 * flats$bedrooms + rnorm(n, 0, 110) + 180) flats$rent <- round(2.1 * flats$area_sqft + 240 * flats$bedrooms + rnorm(n, 0, 250)) # Knock a blank into 36 of the 240 floor areas (the landlord left the box empty). set.seed(7) flats$area_sqft[sample(n, 36)] <- NA colSums(is.na(flats)) #> bedrooms floor area_sqft rent #> 0 0 36 0

  

Thirty-six blanks, all in area_sqft, one listing in seven. Now the question that drives the rest of the lesson: what number do we put in each one?