Data Analysis · Tutorial 5 of 5
Data Cleaning: a checklist for messy datasets
By the FUTEAC Team · 6 November 2026 · 15 min read · Beginner
In short
Cleaning a dataset is six steps in order: inspect the mess, standardise text, drop duplicates, fix wrong types, fill missing values, then validate. Order matters, standardising " ali " to "Ali" before dropping duplicates catches hidden near-duplicates exact matching misses. The example below turns six messy customer rows into four clean ones with zero missing values.
- The checklist order is the lesson. Standardise text *before* dropping duplicates, or " ali " and "ali" survive as two different people.
- Inspect before you touch anything. Count the gaps with
isna().sum()and the duplicates withduplicated()first, so you know what you are fixing and can prove it afterwards. - Never clean the original. Work on a copy, so a wrong step costs you a re-run, not your data.
- Every fix needs a reason you could defend. Median for a missing number, a clear label for a missing name, not a guess dressed up as a decision.
- Validation is a step, not a feeling. Re-run the same checks at the end: zero missing values, zero duplicates, sensible types. If a check fails, the job is not done.
- Feeds the Spreadsheets for Data Analysis course (/course/spreadsheets-for-data-analysis/) lesson "Cleaning Data: Duplicates, Blanks and Bad Formats" and the Data Cleaning (/skill/data-cleaning/) and NumPy & pandas (/skill/numpy-pandas/) skills.
What does "clean" actually mean for a dataset?
A clean dataset is one where every value is what it claims to be. That sounds obvious until you look at a real export: a name with a leading space (" ali "), the same city spelled three ways ("Lahore", "lahore", "LAHORE"), an age column containing the word "unknown", a customer listed twice, a blank where someone forgot to type.
None of those rows is "wrong" in a way a spreadsheet complains about. Excel will happily average an age column containing "unknown" by silently ignoring it, and a pivot table will happily count " ali " and "ali" as two customers. A machine learning model will simply refuse to train, or worse, train on the mess and learn the wrong pattern.
So "clean" is a checklist, not a vibe. A dataset is clean when every row is a real, distinct record (no duplicates, hidden or exact); every column has one type and every value fits it; there are no missing values you have not deliberately decided about; and text that means the same thing is spelled the same way.
Notice what is *not* on that list: the data being correct. Cleaning cannot tell you whether Ali really is 25. Only whether the value is usable. Cleaning makes data usable; your judgement makes it trustworthy.
The checklist: six steps, in this order
- INSPECT. How dirty is it? Count missing values per column, count exact duplicates, look at the types.
- STANDARDISE TEXT. Strip spaces, fix casing, unify spellings. Do this *before* deduplication.
- DROP DUPLICATES, now that hidden near-duplicates have been exposed, remove them.
- FIX TYPES. Turn text that should be numbers into numbers, safely.
- FILL MISSING VALUES, replace each gap with a deliberate, defensible value.
- VALIDATE. Re-run the step-1 checks. Zero missing, zero duplicates, sensible types, or the job is not done.
The order is not a suggestion. Step 2 before step 3 is the whole point of this tutorial: if you drop duplicates first, " ali " and "ali" look like different people and both survive. Standardise first and the duplicate reveals itself. The same logic applies throughout. Each step makes the next step's job visible.
How do I inspect a dataset before cleaning it?
Inspection is two questions: *where are the gaps?* and *what repeats?*
df.isna().sum() counts the missing values in each column. df.duplicated().sum() counts rows that are exact copies of an earlier row. Run both before you change a single value, and write the numbers down. They are your "before" photograph, and the validation step at the end only means something if you can compare it with the start.
Also look at the data itself with print(df). A column you expect to be numeric that shows up as object (pandas' word for "text") is a warning: something in that column is not a number, and step 4 will find what it is.
One habit from the start: work on a copy. clean = df.copy() means every change below happens to clean, and the raw df stays exactly as it arrived. If a step goes wrong, you re-run from the copy. You never have to re-download the data. Professional analysts keep raw data untouched as a rule, not a preference.
Why standardise text before dropping duplicates?
Text is where mess hides. The same person can appear as " ali ", "Ali" and "ALI"; the same city as "Lahore", "lahore" and "LAHORE". To a human these are obviously the same. To pandas, they are three different strings, and duplicated() will not flag any of them.
Standardising means making one decision per column and applying it everywhere:
clean["name"] = clean["name"].str.strip().str.title()
clean["city"] = clean["city"].str.strip().str.title().str.strip() removes leading and trailing spaces. .str.title() capitalises the first letter of each word and lowercases the rest. After these two lines, " ali ", "ALI" and "ali" are all "Ali", and "karachi" and "KARACHI" are both "Karachi".
Only now is drop_duplicates() safe to run. Because the duplicates hiding behind spelling differences have been exposed. In the worked example below, the exact-duplicate count is 1 before standardising and 2 after it. That second duplicate is the entire lesson: it was there all along, invisible until the text was standardised.
How do I fix values with the wrong type?
A column should have one type. When a number column contains a word, an age of "unknown", a price of "TBC", pandas reads the whole column as text, and every calculation on it either fails or silently misbehaves.
The safe fix is pd.to_numeric(..., errors="coerce"):
clean["age"] = pd.to_numeric(clean["age"], errors="coerce")errors="coerce" is the important part: values that cannot become numbers turn into NaN (pandas' marker for "missing") instead of crashing your script. The alternative beginners reach for, astype(int), raises an error on the first non-numeric value and stops everything. Coercion keeps you moving and turns a type problem into a missing-value problem, which step 5 already knows how to handle. After coercion, check the type: the column should now be numeric (float64 once NaN is present). If it is still text, something else in the column is not a number. Look at the values, don't guess.
How do I handle missing values?
By now every gap is a deliberate, visible decision. Not an accident you have not noticed. There are three honest options, and the right one depends on the column: drop the row when it is useless without the value; fill with a sensible value, the median for a number, the most common value for a category, a clear label like "Unknown" for text; or leave it and say so, reporting "age unknown for 3% of customers" rather than inventing a number.
For this tutorial's small dataset, filling is the right call, because dropping rows would throw away a quarter of the data:
clean["name"] = clean["name"].fillna("Unknown")
clean["city"] = clean["city"].fillna("Unknown")
clean["age"] = clean["age"].fillna(clean["age"].median())A missing name becomes the label "Unknown", visible, honest, and easy to filter later. A missing age becomes the median of the ages that remain, which is less distorted by an extreme value than the average would be. What you must never do is fill a gap silently and forget it happened: note the fill in your analysis, so anyone reading your results knows which values are real and which are filled.
Two technical notes. Fill the median *after* fixing types: the median of a text column is meaningless, and it only exists once "unknown" has become NaN. And the .copy() after drop_duplicates() in the worked example is not decoration: without it, pandas may warn you (the SettingWithCopyWarning) that you are editing a slice of the original rather than a standalone table.
Worked example: the full checklist on one messy dataset
Six rows, four columns, every classic problem in miniature: a name with spaces, a name in capitals, a duplicated customer, a missing name and city, and the word "unknown" sitting in a numeric column.
import pandas as pd
df = pd.DataFrame({
"name": [" ali ", "Sara", "BILAL", "Sara", None, "ali"],
"age": [25, 30, "unknown", 30, 28, 25],
"city": ["Lahore", "karachi", "Lahore", "karachi", None, "lahore"],
"orders": [3, 5, 2, 5, 1, 3],
})
# 1. INSPECT - how dirty is it?
print(df.isna().sum())
print("Exact duplicate rows:", df.duplicated().sum())
# 2. STANDARDISE TEXT first - " ali " and "ali" are the same person
clean = df.copy()
clean["name"] = clean["name"].str.strip().str.title()
clean["city"] = clean["city"].str.strip().str.title()
print("Duplicates after standardising text:", clean.duplicated().sum())
# 3. DROP DUPLICATES (now the hidden ones are caught too)
clean = clean.drop_duplicates().copy()
# 4. FIX TYPES - "unknown" is not a number
clean["age"] = pd.to_numeric(clean["age"], errors="coerce")
# 5. HANDLE MISSING VALUES
clean["name"] = clean["name"].fillna("Unknown")
clean["city"] = clean["city"].fillna("Unknown")
clean["age"] = clean["age"].fillna(clean["age"].median())
# 6. VALIDATE
print(clean)
print(clean.isna().sum())
print("Rows: before", len(df), "| after", len(clean))Exact output, as run for this tutorial:
name 1
age 0
city 1
orders 0
dtype: int64
Exact duplicate rows: 1
Duplicates after standardising text: 2
name age city orders
0 Ali 25.0 Lahore 3
1 Sara 30.0 Karachi 5
2 Bilal 28.0 Lahore 2
4 Unknown 28.0 Unknown 1
name 0
age 0
city 0
orders 0
dtype: int64
Rows: before 6 | after 4Walk through what happened. Inspection found 1 missing name, 1 missing city and 1 exact duplicate. After standardising text, the duplicate count rose to 2. Row 5 ("ali", 25, "lahore", 3) only matches row 0 once whitespace and casing are normalised. Dropping duplicates removed both. Then "unknown" became NaN, and the median of the remaining ages (25, 30, 28) is 28.0, which is what Bilal's age becomes, since his was the "unknown" one. The final table has 4 rows, zero missing values, and every column the right type.
Two rows were removed and one value was filled, a third of the dataset gone, which is exactly why the checklist exists. On a six-row example the loss is visible; on a 60,000-row export the same mistakes would be invisible, and the wrong totals would go into a report nobody questions.
Try it yourself
Copy the script and add one more mess of your own: change one city to " Islamabad " (trailing spaces, capitalised), and change one orders value to the string "five". Run the checklist as written, where does it break, and which step needs to change? (Hint: orders now has a type problem like age did, and the city standardisation already handles the spaces.) Write down the before-and-after row counts and the final missing-value counts.
Continue in the free course: Spreadsheets for Data Analysis →
Common mistakes
- Dropping duplicates before standardising text. The hidden near-duplicate survives, and your counts are wrong by exactly the rows you thought you had cleaned.
- Cleaning the original instead of a copy. One wrong
dropand the raw data is gone. Alwaysdf.copy()first. - Using
astype(int)on a column with text in it. It crashes. Usepd.to_numeric(..., errors="coerce")and deal with the resultingNaNdeliberately. - Filling missing values with the average without thinking. The average is pulled by extreme values; the median is usually the safer fill for a number. And a filled value is still a guess. Note it in your analysis.
- Skipping validation. If you never re-run
isna().sum()andduplicated()at the end, you are assuming the cleaning worked. Check it. - Treating cleaning as a one-off. The next export from the same shop will have the same problems. Turn the six steps into a script you re-run, not a morning you repeat.
Frequently asked questions
How do I know when a dataset is "clean enough"?
When the validation step passes: zero missing values you have not decided about, zero duplicates, one type per column, and you can state, in one sentence each, what you did about every gap you found. "Clean enough" is a documented decision, not a feeling.
Should I drop rows with missing values, or fill them?
It depends on how much you would lose. Dropping is honest and simple when the gaps are few. Filling is better when dropping would throw away a large share of the data, but then the fill must be defensible (median for numbers, a clear label for text), and you must say so in your analysis.
Does this checklist work in Excel too, or only in pandas?
The checklist is the same everywhere; only the tools change. In Excel, TRIM and PROPER do the standardising, Remove Duplicates does step 3, and Find & Select → Go To Special → Blanks finds the gaps. The Spreadsheets for Data Analysis course covers the Excel version in "Cleaning Data: Duplicates, Blanks and Bad Formats".
Where does this sit in the FUTEAC Data Analyst roadmap?
This is the last tutorial in the data cluster's Doc 28 series, and it generalises the "Cleaning messy data" topic from Weeks 1–4 ("Spreadsheets like an analyst") of the Data Analyst roadmap (/roadmap/data-analyst/). The next step is the Data Cleaning skill (/skill/data-cleaning/) applied to a real dataset of your own, then visualisation, choosing the right chart for what you found.
Follow the Data Analyst Roadmap
Turn raw data into decisions with spreadsheets, SQL, Python and dashboards. Free, step by step, at your pace.
View the Roadmap