Contents/ Part V Β· Putting It All Together/ Chapter 28

Case Study: Ames Housing Prices

A file of 610 home sales, famous for its missing values. This case study is a decision-making clinic: five columns are missing data for five different reasons, and each one earns a different fix on the way from raw file to analysis-ready table.

⏱️ ~16 min read
🐍 Notebook included
πŸ“Š Chapter 28

The Spotify case study was about scale and structure. This one is about judgment. The Ames housing data is the classic teaching set for missing values, because the gaps are not random, they mean different things, and a good analyst reads that meaning before reaching for a fix.

🏠
The dataset

ames_housing.csv records 610 home sales with sale_price, gr_liv_area (above-grade living area), lot_area, year_built, neighborhood, fireplace_qual, pool_qc, and more. It is gloriously messy: duplicate rows, dates in three formats, the famous "big house sold cheap" outliers, a strongly skewed price, and five columns with missing values ranging from a 0.5% trickle to a 99% flood.

1

First Contact: The Missingness Is the Story

The audit finds 10 duplicate rows and a date column stuck in text, but the headline is the missing values. And how much is missing hints at why, and that determines the fix:

Missing values: how much, and what each one earns pool_qc 99% Β· drop fireplace_qual 48% Β· recode None lot_frontage 16% Β· median impute garage_yr_built 5% Β· flag sale_price 0.5% Β· drop rows (target) Color = the eventual fix. The amount of missing data is a clue; the reason behind it is the decision.
🧭
Percentage is a clue, not the answer

It is tempting to set a rule like "drop any column over 50% missing". But fireplace_qual is 48% missing and we will keep it, because the blanks mean "no fireplace", real information. The percentage tells you where to look; only the reason for the gap tells you what to do. That is the entire lesson of this chapter.

2

Describe & Visualize: Price and Its Outliers

Before cleaning, we get to know the target, sale_price. It is strongly right-skewed (mean $184k, median $173k, skew β‰ˆ +1.2): a few expensive homes pull the average up. And a scatter of living area against price reveals the dataset's most famous quirk.

Living area vs sale price: bigger sells for more, with four exceptions 400100019002800 50200350 living area (sq ft) price ($1000s) 4 huge homes, sold cheap β†’
πŸ”Ž
A picture worth four data points

Almost every home follows a clean upward trend, bigger means pricier. But four houses over 4,000 sq ft sold for under $185k, far below where their size says they should. These are almost certainly partial or family sales, not open-market deals. A single scatter plot caught what no summary statistic would, and we will deal with them in the prep step (see Charts for Numerical Data and Detecting & Treating Outliers).

3

Cleaning: Duplicates, Categories & Dates

Three quick fixes clear the deck before the missing-value work.

Duplicates (see Finding & Removing Duplicates and Inconsistencies). Ten rows are exact copies of other sales; left in, they would double-count those homes. drop_duplicates() takes us from 610 to 600 rows.

Inconsistent category (see Finding & Removing Duplicates and Inconsistencies). central_air was recorded as both "Y" and "Yes" (and "N" / "No") for the same thing. Until those merge, a group-by would split one category into two, so we map them to a single convention.

Dates in three formats (see Combining & Reshaping Data). sale_date arrived as text in three different layouts, "09/07/1955", "1940-03-19", and "19-Dec-1950". A date trapped in a string is useless: you cannot sort it, difference it, or pull the year. One call, pd.to_datetime(sale_date, format="mixed"), parses all three at once (with zero failures) and gives us a real sale_year.

Two more category columns get the same standardizing treatment here: kitchen_qual (po, PO, Ta … → uppercased) and exterior_color (where grey and gray are merged). Cleaning them now means they are ready to encode later.

πŸ“…
Always check the parse

After parsing dates, count the failures (NaT values). A format that silently fails to parse turns a date into a hidden missing value. Here all 600 rows parsed cleanly, so sale_year is trustworthy.

4

Missing Values: One Column, One Decision

This is the heart of the chapter. Five columns are missing values, and each gets a different treatment, driven entirely by why the value is absent.

ColumnMissingWhy it is missingDecision
pool_qc99%The home has no poolDrop the column (almost no signal)
fireplace_qual48%The home has no fireplaceRecode to "None" (keep as a real category)
lot_frontage16%An ordinary measurement gapMedian-impute
garage_yr_built5%The home has no garageFlag has_garage, drop the year
sale_price0.5%Simply not recordedDrop the rows (it is the target)
🎯
Informative missingness

Notice that three of these gaps are informative: a blank pool, fireplace, or garage field is telling you the feature does not exist, not that the data is lost. You never impute a fake pool quality. You either drop the column (when it is almost all blank, like pool_qc) or recode the blank into a meaningful category or flag (like fireplace_qual and garage_yr_built). Only lot_frontage is a true measurement gap worth imputing, and the target, sale_price, is never imputed at all (see Handling Missing Data).

5

Preparing: Outliers & Transform

Remove the anomalies (see Detecting & Treating Outliers). The four big-but-cheap houses break the size-to-price relationship, so we remove them. The payoff is immediate: the correlation between living area and price jumps from 0.47 to 0.58 once those four contrarian points are gone.

Transform the skew (see Transformations). Price still has a long right tail (skew β‰ˆ +1.15). A natural-log transform compresses that tail into a near-symmetric, bell-shaped distribution (skew β‰ˆ +0.02), which is exactly what regression and most summaries prefer. This is why housing models almost always predict log(price):

sale_price, raw (skew +1.15) log(sale_price) (skew +0.02) long right tail of expensive homes symmetric and bell-shaped

Encode by type (see Feature Engineering). The file also carries two more category columns we cleaned along the way: kitchen_qual (Po / Fa / TA / Gd / Ex) and exterior_color (white / beige / gray / brick / blue), both in messy casing (including the classic gray vs grey). They are encoded differently because one has an order and one does not: kitchen_qual is ordinal, so it becomes integer codes 1 to 5 that keep the quality order; exterior_color and neighborhood are nominal, so they become one-hot 1/0 columns.

Encode by type: ordinal keeps the order, one-hot does not invent one kitchen_qual Β· ORDINAL β†’ 1…5 "po", "PO" "fa", "FA" "ta", "Ta" "gd", "GD" "ex", "EX" β†’β†’β†’β†’β†’ Po (poor) = 1 Fa = 2 TA = 3 Gd = 4 Ex (excellent) = 5 one column; 1 < 2 < 3 < 4 < 5 keeps the real quality order exterior_color Β· NOMINAL β†’ one-hot (1/0) whitebeigegraybrick "White" 1000 "grey" 0010 "Brick" 0001 one column per color; note "grey" was first standardized to "gray" Label-encoding the colors (white=1, gray=2…) would be wrong: it invents an order colors do not have.

Here is the whole journey, every row accounted for:

From raw file to analysis-ready: every row accounted for 610raw rows 600deduped 597target kept 593outliers gone 593Γ—21analysis-ready βˆ’10 dupesCh 19 βˆ’3 no targetCh 20 βˆ’4 outliersCh 21 missing Β· log Β· encodeCh 20–24 593 clean sales: numeric + has_garage + kitchen_qual (ordinal) + log_price + 8 neighborhood + 5 color columns = 21 model-ready columns.
6

Exploring the Clean Data

With the data clean, the measurements and grouped charts finally tell the truth, and they point to a single, valuable conclusion about what drives a home's price.

Measurements: profiling the price

Measurementsale_priceWhat it tells us
Mean / median / mode (see Measures of Central Tendency)$184k / $173k / $161kMean > median confirms the right skew
Std dev / IQR (see Measures of Dispersion & Measures of Position)$64k / $79kWide spread of home values
Coeff. of variation (see Measures of Dispersion)35%Prices vary substantially around the mean
Quartiles Q1 / Q3 (see Measures of Position)$139k / $217kThe middle 50% of homes sold here
Skewness (see Shape of a Distribution)+1.15Right-skewed, hence the log transform

The numeric relationships: a correlation heatmap (see Multivariate & Specialized Visuals)

A heatmap only takes numbers, so both categoricals are encoded to join it: neighborhood by its median price (mean encoding) and kitchen_qual as an ordinal 1 to 5. Now location, kitchen quality, and the numeric features all sit in one matrix. Scan the price row first.

Correlation: numeric features + encoded neighborhood & kitchen price nbhd kitch area beds baths year lot front price nbhd kitch area beds baths year lot front 1.00 0.63 0.52 0.58 0.47 0.34 0.22 -0.06 -0.04 0.63 1.00 0.35 0.08 0.11 0.08 -0.01 -0.08 -0.07 0.52 0.35 1.00 0.28 0.25 0.20 0.11 -0.05 -0.02 0.58 0.08 0.28 1.00 0.77 0.52 -0.03 -0.03 -0.01 0.47 0.11 0.25 0.77 1.00 0.70 -0.02 0.00 -0.02 0.34 0.08 0.20 0.52 0.70 1.00 0.01 0.01 -0.03 0.22 -0.01 0.11 -0.03 -0.02 0.01 1.00 0.02 0.01 -0.06 -0.08 -0.05 -0.03 0.00 0.01 0.02 1.00 0.03 -0.04 -0.07 -0.02 -0.01 -0.02 -0.03 0.01 0.03 1.00 +10−1 price drivers: nbhd .63 > area .58 > kitchen .52 > beds .47 two encodings: nbhd = nominal (mean- encoded), kitchen = ordinal 1..5 size block: area/beds/baths overlap (collinear) outlined = price's two strongest drivers (neighborhood, kitchen); red = positive, blue = negative.

Both encodings prove their worth. Mean-encoded neighborhood is the strongest correlate of price (0.63), and the ordinal kitchen quality is third (0.52), ahead of bedrooms, a category that would have been invisible to a numbers-only heatmap now reads as a real driver, with its order intact. The size features still form their own collinear block (0.5 to 0.77), and the lot columns stay near zero. (Caution as before: the price-based neighborhood encoding is mildly optimistic and should be cross-fit for a real model; see Feature Engineering.)

Where price really comes from (see Charts for Numerical Data and Multivariate & Specialized Visuals)

The most useful housing chart groups price by neighborhood. The box plots fan out dramatically: NorthRidge homes sell for nearly double those in OldTown.

Sale price by neighborhood: location is the biggest lever 50250450 price ($1000s) NorthRidg CollegeCr Somerset Gilbert BrookSide Sawyer Edwards OldTown box = middle 50% (IQR), line = median; NorthRidge median ~$276k, OldTown ~$146k.
πŸ“
Location beats size

Living area is the strongest single numeric driver of price (r β‰ˆ 0.58 after cleaning), but the box plots show neighborhood matters even more: the median jumps from about $146k in OldTown to about $276k in NorthRidge, nearly double, before you account for a single square foot. "Location, location, location" is not folklore here; it is the data (see Charts for Numerical Data and Multivariate & Specialized Visuals).

The last visual check: all the prep work, at a glance

Prep-work scorecard: raw file β†’ analysis-ready Duplicates Β· 10 exact sales removed Ch 19 Category Β· central_air Yes/No standardized to Y/N Ch 19 Dates Β· three text formats parsed to real datetimes Ch 23 Missing (5 ways) Β· pool dropped, fireplaceβ†’None, frontage imputed, garage flagged, target dropped Ch 20 Outliers Β· 4 big-but-cheap homes removed (r 0.47 β†’ 0.58) Ch 21 Transform Β· log(sale_price) pulled skew +1.15 β†’ +0.02 Ch 22 Encode by type Β· kitchen_qual ordinal (1–5); neighborhood & color one-hot (13 cols) Ch 24 610 Γ— 16 raw β†’ 593 Γ— 21 analysis-ready Β· every decision logged
πŸ€–
Why this matters for data science

The Ames data is a teaching favorite precisely because its missing values force you to think instead of reaching for a default. Drop, recode, impute, flag, or delete the row, the right answer changes column by column, and only a reason can choose. Master this dataset and you have mastered the single most common, and most consequential, judgment call in applied data work.

🐍

Run the whole case study in Python

The companion notebook is the full walkthrough: the first-contact audit, the price summary and the living-area scatter, the deduplication and date parsing, the five different missing-value treatments, the outlier removal (watch the correlation climb) and log transform, then a final exploration beat, a measurements table and the price-by-neighborhood box plots, ending at the analysis-ready table. Every cell explains its decision.

πŸ““ View Notebook (code & outputs) β–Ά Open in Colab ⬇ View / Download on GitHub

View opens the rendered notebook instantly (no setup). Open in Colab runs & edits it live in your browser. To run locally, install numpy, pandas, matplotlib and launch jupyter notebook.

πŸŽ“ Key Takeaways

  • βœ“Missingness is the headline: how much is missing is a clue, but why it is missing decides the fix.
  • βœ“Five gaps, five answers: drop pool_qc (99%), recode fireplace_qual to "None", median-impute lot_frontage, flag has_garage, drop rows missing the price target.
  • βœ“Parse messy dates with format="mixed" and always check the failure count (see Combining & Reshaping Data).
  • βœ“Outliers can be removed with reason: the 4 big-but-cheap homes were partial sales; dropping them lifted the size-price correlation 0.47 β†’ 0.58.
  • βœ“Log-transform a skewed price (+1.15 β†’ +0.02); then explore, and find that location beats size.
7

Practice Challenges

Five challenges on the Ames data, following the case study beat by beat. Try them in Python before checking the solutions.

1

Map the missingness

List every column with missing values and its percentage. For each, decide drop / recode-to-None / impute / flag / drop-rows, and justify it from why the value is missing.

Hint: 99% empty & no-signal β†’ drop; blank = "none" β†’ recode; ordinary gap β†’ impute; target β†’ drop rows.
2

Parse three date formats

Parse sale_date (which holds three layouts) into real datetimes, extract the sale year, and confirm none failed to parse.

Hint: pd.to_datetime(s, format="mixed"); check .isna().sum() for NaT.
3

Big houses, small prices

Find homes over 4,000 sq ft that sold cheaply. Remove them and show the living-area-to-price correlation before and after. Why is removing them justified?

Hint: filter gr_liv_area > 4000; correlation rises ~0.47 β†’ 0.58.
4

Straighten the price

Log-transform sale_price, report the skew before and after, and plot both distributions. Why do housing models prefer log(price)?

Hint: np.log; skew falls from ~+1.2 toward 0.
5

Size vs location

Compute the median price per neighborhood and draw box plots. Which is the bigger price driver, living area or neighborhood? Back it up with numbers.

Hint: compare the neighborhood median spread to the living-area correlation.
βœ…
Check your work

A fully-worked solutions notebook answers all five on the real data, in the same visual style. Try them yourself first, then compare.

πŸ““ View Solutions β–Ά Open Solutions in Colab ⬇ View / Download on GitHub
8

Quiz: Test Yourself

Eight questions on the Ames case study. Answer them, hit Check Answers, and keep refining until you score 100%. Your progress is saved, so you can hop back to the chapter and return anytime.