Skip to content

Exploratory Data Analysis & Correlations

What you’ll learn

  • why you explore a copy of the training set and nothing else
  • the Pearson correlation formula, computed by hand on five points
  • the two things correlation is blind to, and the plot that exposes both
  • how to spot data-quality artefacts that a summary statistic hides completely
  • three engineered ratios, two of which outrank every raw count in the dataset
  • the difference between a feature that correlates and a feature that helps

Intuition

The test set is locked away, so now you can be as aggressive as you like — but on a copy, so that experiments do not damage the training data itself.

Exploration has one goal: find the structure a model can use, and the defects that will mislead it. Both of those live in plots more often than in statistics. describe()describe() cannot tell you that two columns are latitude and longitude; a scatter plot draws California in three seconds.

explore_a_copy.py
housing = strat_train_set.copy()      # never explore the original, never touch the test set
explore_a_copy.py
housing = strat_train_set.copy()      # never explore the original, never touch the test set

The math: Pearson correlation

rxy=Cov(x,y)σxσy=i(xixˉ)(yiyˉ)i(xixˉ)2i(yiyˉ)2r_{xy} = \frac{\operatorname{Cov}(x, y)}{\sigma_x \sigma_y} = \frac{\sum_{i}(x_i - \bar{x})(y_i - \bar{y})} {\sqrt{\sum_{i}(x_i - \bar{x})^2}\sqrt{\sum_{i}(y_i - \bar{y})^2}}

The numerator is the covariance — how the two vary together. The denominator normalises it by both spreads, so rr is dimensionless and confined to [1,+1][-1, +1]. Because it is scale-free, the correlation between income and price is unchanged whether price is in dollars or thousands.

rr measures exactly one thing: the strength of a linear relationship. That precision is what makes it useful and what makes it dangerous.

Worked example by hand

Five districts. Income xx in tens of thousands, value yy in hundreds of thousands.

iixix_iyiy_ixixˉx_i - \bar{x}yiyˉy_i - \bar{y}product(xxˉ)2(x-\bar{x})^2(yyˉ)2(y-\bar{y})^2
121−2−2444
232−1−1111
3440+1001
454+1+1111
564+2+1241
xˉ=4\bar{x}=4yˉ=3\bar{y}=38108
r=8108=88.94430.8944r = \frac{8}{\sqrt{10}\sqrt{8}} = \frac{8}{8.9443} \approx 0.8944

A strong positive linear relationship. Note what the arithmetic did: it never looked at the shape of the relationship, only at whether xx and yy deviate from their means in the same direction.

And r2=0.80r^2 = 0.80. For a simple linear regression of yy on xx, the squared correlation is exactly the R2R^2 from Metrics R-Squared. Two chapters, one number.

Geography for free

geographic.py
housing.plot(
    kind="scatter", x="longitude", y="latitude", alpha=0.4,
    s=housing["population"] / 100, label="population",
    c="median_house_value", cmap="jet", colorbar=True, figsize=(10, 7),
)
geographic.py
housing.plot(
    kind="scatter", x="longitude", y="latitude", alpha=0.4,
    s=housing["population"] / 100, label="population",
    c="median_house_value", cmap="jet", colorbar=True, figsize=(10, 7),
)
figureTwo unlabelled numeric columns, plotted against each othermatplotlib
Scatter of longitude against latitude for 20,640 districts, forming the recognisable shape of California. Point size follows population and colour follows house value, with expensive districts concentrated along the coast.Scatter of longitude against latitude for 20,640 districts, forming the recognisable shape of California. Point size follows population and colour follows house value, with expensive districts concentrated along the coast.
Nothing told the plot this was a map. Price clearly follows the coastline and clusters around the Bay Area and Los Angeles — a feature no single column contains.

Reading the plot

  1. Price follows the coast. Density and coastal proximity together explain far more than either alone — which is why ocean_proximityocean_proximity, the text column, turns out to matter.
  2. Two bright clusters are the Bay Area and greater Los Angeles. A distance_to_nearest_citydistance_to_nearest_city feature is visible here and available nowhere in the raw columns.
  3. The interior is uniformly cheap. That is the INLANDINLAND category, and it becomes the second most important feature in the final model.
  4. Point size shows population, and the largest circles sit in the bright clusters. Correlated predictors, which matters when interpreting coefficients later.

What correlation cannot see

correlations.py
corr = housing.select_dtypes("number").corr()
print(corr["median_house_value"].sort_values(ascending=False).round(4).to_string())
 
# median_house_value    1.0000
# median_income         0.6881
# total_rooms           0.1342
# housing_median_age    0.1056
# households            0.0658
# total_bedrooms        0.0497
# population           -0.0246
# longitude            -0.0460
# latitude             -0.1442
correlations.py
corr = housing.select_dtypes("number").corr()
print(corr["median_house_value"].sort_values(ascending=False).round(4).to_string())
 
# median_house_value    1.0000
# median_income         0.6881
# total_rooms           0.1342
# housing_median_age    0.1056
# households            0.0658
# total_bedrooms        0.0497
# population           -0.0246
# longitude            -0.0460
# latitude             -0.1442
figureThe full numeric correlation matrixmatplotlib
A nine by nine correlation heatmap of the numeric housing columns, ordered by correlation with median house value. One strong positive cell at 0.69 and a block of near-1.0 correlations among total rooms, total bedrooms, population and households.A nine by nine correlation heatmap of the numeric housing columns, ordered by correlation with median house value. One strong positive cell at 0.69 and a block of near-1.0 correlations among total rooms, total bedrooms, population and households.
Only median income correlates strongly with price. The bright block in the middle is four count columns that are all essentially 'how big is this district' — correlations above 0.85 among themselves.

Two blind spots matter here:

It only sees straight lines. A perfect parabola y=x2y = x^2 over symmetric xx has r=0r = 0. Correlation zero does not mean “no relationship”, it means “no linear relationship”.

It only sees pairs. Latitude scores −0.14 and longitude −0.05, both apparently useless. The map above shows they are jointly among the strongest signals in the dataset. No pairwise statistic can find that, and in the final model latitude and longitude are the third and fourth most important features.

figureThe strongest relationship, at full resolutionmatplotlib
Dense scatter of median income against median house value showing a clear upward trend, a hard horizontal line of points at 500,001 dollars, and fainter horizontal lines at roughly 450,000, 350,000 and 280,000.Dense scatter of median income against median house value showing a clear upward trend, a hard horizontal line of points at 500,001 dollars, and fainter horizontal lines at roughly 450,000, 350,000 and 280,000.
The trend is real. So are four horizontal lines of points, which are artefacts of how the values were recorded — and which a model will happily learn to reproduce.

Reading the plot

  • The upward trend is genuine, and r=0.69r = 0.69 describes it fairly.
  • The line at 500,001500{,}001 is the cap: 965 districts.
  • Three fainter lines near 450k, 350k and 280k are also artefacts. Nobody documented these; only the scatter reveals them.
  • The cloud fans out as income rises — heteroscedasticity, which does not bias the fit but does make uniform confidence intervals wrong.

A model trained on this data will reproduce those horizontal lines, because they are in the labels. Removing the capped districts is the honest fix, and it changes what the model is for.

What to do with a correlation number

A coefficient on its own is never an instruction. The number tells you which plot to draw next, and the plot tells you what to do:

diagram Diagram mermaid

Every branch that ends in an action starts with a plot. That is the whole argument for EDA: the coefficient is a routing decision, not a conclusion.

See it move

The cloud below rotates and tightens while rr is recomputed live. Watch what happens when the relationship becomes a parabola: the points are perfectly determined and rr collapses to zero.

sketch What correlation can and cannot see p5.js
A point cloud cycles through positive, negative, uncorrelated and parabolic relationships while the Pearson coefficient is recomputed. The parabola is deterministic and still scores about zero.

Engineering features

Raw counts in this dataset are nearly meaningless: total_roomstotal_rooms mostly measures how large a district is, not how desirable. Ratios fix that by dividing out district size.

engineered.py
housing["rooms_per_household"] = housing["total_rooms"] / housing["households"]
housing["bedrooms_per_room"] = housing["total_bedrooms"] / housing["total_rooms"]
housing["population_per_household"] = housing["population"] / housing["households"]
 
corr = housing.select_dtypes("number").corr()
print(corr["median_house_value"].sort_values(ascending=False).round(4).to_string())
 
# median_house_value          1.0000
# median_income               0.6881
# rooms_per_household         0.1519
# total_rooms                 0.1342
# housing_median_age          0.1056
# households                  0.0658
# total_bedrooms              0.0497
# population_per_household   -0.0237
# population                 -0.0246
# longitude                  -0.0460
# latitude                   -0.1442
# bedrooms_per_room          -0.2559
engineered.py
housing["rooms_per_household"] = housing["total_rooms"] / housing["households"]
housing["bedrooms_per_room"] = housing["total_bedrooms"] / housing["total_rooms"]
housing["population_per_household"] = housing["population"] / housing["households"]
 
corr = housing.select_dtypes("number").corr()
print(corr["median_house_value"].sort_values(ascending=False).round(4).to_string())
 
# median_house_value          1.0000
# median_income               0.6881
# rooms_per_household         0.1519
# total_rooms                 0.1342
# housing_median_age          0.1056
# households                  0.0658
# total_bedrooms              0.0497
# population_per_household   -0.0237
# population                 -0.0246
# longitude                  -0.0460
# latitude                   -0.1442
# bedrooms_per_room          -0.2559
figureThree new columns, from divisionmatplotlib
Horizontal bar chart of every feature's correlation with median house value, with three engineered ratios highlighted in amber. bedrooms_per_room at -0.256 is the second strongest overall.Horizontal bar chart of every feature's correlation with median house value, with three engineered ratios highlighted in amber. bedrooms_per_room at -0.256 is the second strongest overall.
bedrooms_per_room reaches -0.256, stronger than any raw count and second only to median income. rooms_per_household at 0.152 beats total_rooms at 0.134.

bedrooms_per_roombedrooms_per_room is the win. A low ratio means larger, more expensive homes — the raw total_bedroomstotal_bedrooms correlation of 0.05 was measuring district size and hiding this entirely. Two columns that individually say almost nothing produce, on division, the second-strongest signal in the dataset.

This is what feature engineering is: not more data, but the same data in a form that exposes the relationship.

Pitfalls

quizCheck yourself
  1. A feature has correlation 0.00 with the target. What can you conclude?

    Show answer

    B — There is no linear relationship — a curved or threshold relationship may still be strong — Pearson r measures linear association only. A perfect parabola over symmetric x scores exactly zero while being fully deterministic.

  2. Latitude scores -0.14 and longitude -0.05 against price, yet both are among the most important features in the final model. Why?

    Show answer

    B — Correlation is pairwise; the signal lives in the interaction between the two, which is visible on a map and invisible to any single-column statistic — Neither coordinate alone predicts price. Together they specify a location, and location is what determines price. No pairwise measure can express that.

  3. bedrooms_per_room correlates -0.256 while its two source columns manage 0.05 and 0.13. What happened?

    Show answer

    B — Dividing out district size turned two size measurements into a measure of home layout, which is what actually relates to price — Both raw columns mostly measure how big a district is. Their ratio cancels that out and exposes a property of the housing stock itself.

  4. Why explore a copy of the training set rather than the training set itself?

    Show answer

    B — So that experimental transformations — dropped rows, new columns, imputed values — cannot corrupt the data you will actually train on — Exploration involves destructive experiments. A copy means you can be reckless without a silent mutation surviving into training.

🧪 Try It Yourself

Exercise 1 – Correlation by hand

Exercise 2 – Sort correlations against the target

Exercise 3 – Engineer the winning ratio

Exercise 4 – Correlation is blind to curves

Exercise 5 – Find the recording artefacts

Recap

  • Explore a copy of the training set. The test set stays closed.
  • r=Cov(x,y)/(σxσy)r = \operatorname{Cov}(x,y) / (\sigma_x\sigma_y), dimensionless and bounded by ±1\pm 1; worked by hand it gives 0.8944, and r2=0.80r^2 = 0.80 is the simple-regression R2R^2.
  • Correlation sees only straight lines and only pairs. Latitude and longitude prove both limitations at once.
  • Plotting two unlabelled columns drew a map, and the map showed that price follows the coast.
  • The income scatter exposes a hard cap at $500,001 and three fainter recording artefacts.
  • Three ratios: bedrooms_per_roombedrooms_per_room reaches −0.256, second only to income and stronger than every raw count.

Exercise 6 – The coefficient that sees curves, and the one that does not

Next

Continue to Data Cleaning & Handling Missing Values — deal with the 207 holes in total_bedroomstotal_bedrooms, and measure whether the choice of strategy actually changes anything.

If this helped you, consider buying me a coffee ☕

Buy me a coffee

Was this page helpful?

Let us know how we did