The average customer was 100.4 years old and nobody noticed
You call describe() on a fresh customer extract. Two hundred rows, mean visits 17.3, mean revenue 24.4. Nothing jumps out. Into the model it goes.
The mean age in that table was 100.4. Thirteen records had stored "age unknown" as 999. Drop those and the mean is 37.9. No summary statistic will ever tell you this, because 999 is a perfectly good number and the mean does exactly what it is asked to do.
| How the age column was averaged | Rows used | Mean age | Believable? |
|---|---|---|---|
| 999 left in | 200 | 100.4 | no |
| 13 rows of 999 removed | 187 | 37.9 | yes |
Both rows came out of the same file, the same column, the same mean function. The only difference is whether anyone looked at the numbers before feeding them in. That one look was worth 62.5 years.
Exploratory data analysis is the habit of looking at what you actually have before you model it. John Tukey named it in 1977, and his argument was a simple one: let the data suggest the questions instead of arriving with them.
None of it is difficult. It gets skipped because it is tedious, produces no deliverable, and appears to cost nothing — right up until week three.
The key question
Is this the dataset I think it is?
Letting the data tell you what to ask
Statistical work runs in two opposite modes. One generates hypotheses; the other tests them. Run them at once and you lose both.
| Exploratory | Confirmatory | |
|---|---|---|
| Starts from | The data | A hypothesis written in advance |
| Produces | Questions and candidates | A p-value and a decision |
| Attitude | Look at everything | Look at one thing |
| p-values | Not to be reported as evidence | The point of the exercise |
| Danger | Seeing patterns in noise | Testing the wrong thing well |
This is why a p-value found while exploring cannot simply be reported. If you looked at a hundred relationships and picked the most striking one, that p-value has already been through a hundred rounds of selection. Exploration produces candidates; judgement has to come from somewhere else.
There is no fixed procedure, but the order matters — each pass is only worth doing once the pass before it has come back clean.
Structure
what is hereRows, types, duplicates, missing.
One variable
shape and tailsCentre, spread, skew, odd values.
Two variables
does it move togetherScatter, crosstab, grouped box.
With a group
who is it true forSplit by a third variable and check again.
| Pass | You look at | Tools | You are hunting for |
|---|---|---|---|
| Structure | The table as an object | shape, dtypes, head, duplicated | Wrong types, duplicates, missing blocks |
| One variable | Each column alone | Histogram, box plot, value counts | Skew, sentinels, impossible values |
| Two variables | Pairs of columns | Scatter, crosstab, grouped box | Relationships, separate clouds |
| With a group | Pairs split by a third | Colour, facets, correlation matrix | Confounders, effects in one segment only |
Why the order is not arbitrary
Plot a scatter before you have dealt with the 999s and one point pins itself to the far right while everything else collapses into the corner. Finish the univariate pass and the bivariate one becomes readable. Do it the other way round and you will draw every chart twice.
EDA is visual by necessity, not by taste. A summary statistic is a device for collapsing many values into one, and problems hide inside what got collapsed. Building datasets that share a mean, a standard deviation and a correlation while looking nothing alike is not even hard.
A plot, by contrast, shows you things you did not think to ask for: a second hump, values piling up on round numbers, a series that simply stops after some date. None of those can be requested from a statistic; all of them are obvious on sight.
The half hour you skip costs three weeks
Skip EDA and the code still runs, the model still trains, the numbers still come out. Whatever is wrong goes wrong quietly, and usually surfaces at the worst possible moment.
| What you missed | When it surfaces | What it costs then |
|---|---|---|
| A 999 sentinel | When the model behaves oddly | Redo the preprocessing |
| Duplicate rows | When validation looks too good | Rerun every experiment |
| Target leakage | After it ships | Throw the model away |
| A 99:1 class imbalance | After you reported 99% accuracy | Redesign the evaluation |
| "NY" and "New York" | When the totals refuse to reconcile | Recompute every aggregate |
| Collection method changed midway | In peer review | Retract the conclusion |
Nearly every technique comes with conditions attached, and there is no way to know whether they hold without inspecting the data.
EDA is not purely defensive. Which features will matter, what needs transforming, which interactions to include — most of that gets decided here. Expecting the model to work it out on its own is usually optimistic.
If you find a feature correlating at 0.99
That is not a discovery, it is almost certainly leakage. Predicting churn with a "cancellation reason" column in the frame; predicting order value with the sales tax still in there. Miss it during EDA and you find out when the model hits production and falls over.
Four passes over the same table
Run these in this order on any table you have not seen before. Most disasters get caught somewhere in these eight lines.
| Check | What a bad answer looks like | Act on it by |
|---|---|---|
| Row and column count | Not what the source system says | Find the filter that dropped rows |
| Duplicate rows | Any at all, on a keyed table | Trace the export, then de-duplicate |
| Data types | Dates or numbers stored as text | Cast before anything else |
| Min and max of every numeric column | 999, −1, 0 where 0 is impossible | Convert sentinels to missing |
| Distinct values of every text column | "Seoul", "seoul", "SEOUL " | Normalise before grouping |
| Missing rate per column | Concentrated in one segment | Ask why before imputing |
| Distribution of the target | 99 : 1 imbalance, unnoticed | Choose the metric accordingly |
| Correlation with the target | r above 0.95 | Suspect leakage, not a great feature |
For a numeric column these two numbers earn their keep faster than anything else. A mean can hide almost any problem; the extremes cannot.
| What you see | What to suspect |
|---|---|
| 999, 9999, −1, −99 | A sentinel standing in for missing |
| A maximum age over 120 | Data entry error, or another sentinel |
| A negative amount | Refunds mixed in, or a sign convention |
| A date of 1900-01-01 | A default value that was never replaced |
| Far too many zeros | Missing values filled with zero |
| A maximum of exactly 100 or 255 | Something got truncated upstream |
Categorical problems are almost always the same value spelled several ways, and a distinct count exposes them instantly. If a column that should hold fifty states holds a hundred and twelve, you already have your answer.
The usual ways one value becomes several
Whitespace: "NY" vs "NY "
Naming: "NY" vs "New York" vs "New York State"
Case: "Pro" vs "pro" vs "PRO"
Missing: "" vs "N/A" vs "unknown" vs NULL
"8% missing" is far less informative than where those 8% sit. Scattered evenly, most imputation methods are harmless. Concentrated in one segment, imputing manufactures facts that were never collected.
| The pattern | What it really means | What to do |
|---|---|---|
| Evenly scattered | Ordinary entry gaps | Mean or median imputation is fine |
| One whole period | It was not being collected yet | Handle that period separately |
| One segment only | That group was never asked | The group itself is the variable |
| Blank alongside other columns | A whole screen got skipped | Treat them as a block |
| More missing at higher values | High earners declined to answer | Imputing here amplifies the bias |
The better your EDA, the less entitled you are to test on the same rows. There is exactly one way out of that bind: split off a portion of the data before you start looking, and touch it only at the end.
Day one with a churn dataset
You have been handed churn prediction for a subscription product. The data team sends a CSV: fifty thousand rows, twenty-eight columns. Working through the checklist turns up the following.
| Check | Result | Read |
|---|---|---|
| Row count | 50,000 → 48,317 after de-duplication | The export ran twice |
| Churn rate | 3.2% | Accuracy is a useless metric here |
| Type of last_login | Text | Needs casting to a date |
| Minimum tenure_days | −7 | Signup dated after first payment |
| Distinct values of plan | 11, for a four-tier product | Spelling variants plus retired tiers |
| Missing rate of cancel_reason | 96.8% | Only churners have it — leakage |
| Correlation of monthly_fee with churn | r = 0.02 | Useless on its own |
| Churn rate by tier | 8.1% free, 1.4% paid | Everything has to be split by tier |
Row six is the day's real find. cancel_reason only gets populated after somebody churns, so including it pushes validation accuracy past 99% — and makes the model worthless in production, where that field is still empty at the moment you need a prediction.
The lesson here
Leakage does not announce itself by performing badly. It announces itself by performing suspiciously well. When a first model comes in better than you expected, go looking for the column that already knows the answer before you celebrate.
The same dataset showed that higher monthly fees came with lower churn. A proposal to raise prices was nearly written. Splitting the plot by tier told a different story.
Before and after the split
Pooled: fee ↑ → churn ↓ (r = −0.31)
Within free users: no relationship
Within paying users: no relationship
What was actually operating was not the fee but whether somebody pays at all. People who decided to pay stay; they do not stay because they pay more.
Read the correlation on its own and you land on a price increase — precisely the wrong move. Skipping the fourth pass, where you split by a third variable and look again, is how that happens.
Misconception 1
❌ EDA is what you do if there is time left over.
It works the other way round. Skipping it does not remove the cost, it defers it — and it gets steeper the longer it waits. Redoing preprocessing and pulling a deployed model are not the same size of problem.
Misconception 2
❌ An automated profiling report counts as EDA.
The tool will tell you the maximum of age is 999. It has no idea that this is strange. Profilers surface anomalies; only somebody who knows the domain can say what they mean.
Misconception 3
❌ It came out significant while exploring, so it is significant.
Look at twenty relationships in data with no signal in it and one of them, on average, comes back at p < 0.05. What exploration gives you is not a finding but the next thing worth testing.
Misconception 4
❌ Outliers should be removed once found.
The goal is to understand them, not to delete them. Sentinel? Convert it to missing. Typo? Fix it. A genuine extreme value? Keep it. Deleting without making that distinction throws away the most informative rows you have.
The same 200 rows, seen four ways
One customer table. The three numbers at the top — row count, mean visits, mean revenue — never change as you step through. Each pass, meanwhile, turns up something the previous one had no way of showing you.
Rows
200
Mean visits
17.3
Mean revenue
24.4
These three numbers are all you get from a summary call — and they do not move as you step through.
| Column | Stored as | Note |
|---|---|---|
| id | integer | identifier — never a quantity |
| signup | text | should be a date |
| age | integer | max is 999 |
| plan | text | 2 categories |
| visits | integer | count, ratio scale |
| revenue | decimal | ratio scale |
The export job ran twice: 6 rows are exact duplicates. "signup" arrived as text, so it sorts 2025-10 before 2025-2.
Every one of these was already in the table. None of them is in the three numbers above.
Learning points
The three summary numbers held steady through all four passes — which is to say they told you nothing.
Each pass can only see what the one before it cleared. Change the order and you miss things.
A pooled correlation describes no group at all while distinct groups are mixed together.
Look first
Plots before models
Question everything
Every column is guilty until checked
Stay honest
Exploring is not testing
The question is not "which chart should I draw?"
It is "is this the dataset I think it is?" EDA is less a list of techniques than a refusal to believe anything you have not checked.
EDA happens once when the data lands, and again every time you change the preprocessing. In SKARI you can check the following alongside.
Data profile
Column Types, Unique Counts, Missing Map
Univariate
Histogram, Box Plot, Frequency Table
Bivariate and beyond
Scatter Matrix, Correlation Heatmap, Crosstab
Quality checks
Duplicate Check, Outlier Scan, Leakage Check
Once this clicks, you can answer questions like these.