3.4 Data Cleaning Pipelines — Handle Missing Values, Duplicates & Text Data
3.4 Data Cleaning Pipelines with Pandas
Raw datasets are rarely ready for analysis. They may contain missing values, duplicate records, inconsistent text, unnecessary spaces, inconsistent capitalization, and invalid entries.
Data cleaning is the process of identifying and correcting or appropriately handling these problems before analysis.
- What a data-cleaning pipeline is
- How to detect missing values
- How to remove missing records using
dropna() - How to replace missing values using
fillna() - How to handle missing values column-wise
- How to detect duplicate records
- How to remove duplicates
- How to clean text using string methods
- How to standardize capitalization
- How to replace unwanted text
- How to validate a cleaned dataset
- How to build a reusable cleaning pipeline
1. Why Is Data Cleaning Important?
Poor-quality data can produce misleading statistics, incorrect visualizations, and unreliable machine-learning models.
Consider the following values in a City column:
London
london
London
LONDON
London
These values may refer to the same city but are represented differently.
A data-cleaning process can standardize them:
London
London
London
London
London
Data cleaning should not mean blindly deleting information. Every cleaning decision should be based on the meaning, quality, and intended use of the data.
2. Sample Dataset
We will use a deliberately imperfect dataset:
import pandas as pd
data = {
"Name": [
"Alice",
" Brian ",
"Clara",
"Alice",
None
],
"City": [
"London",
" london ",
"LONDON",
"London",
"Paris"
],
"Score": [
85,
None,
76,
85,
91
]
}
df = pd.DataFrame(data)
print(df)
This dataset contains:
- A missing name
- A missing score
- Extra spaces
- Inconsistent capitalization
- A duplicate record
3. Missing Values
A missing value means that information expected for a particular observation is unavailable or not recorded.
pandas commonly represents missing values using
NaN or None, depending on the data type
and context.
The first step is to identify missing values.
df.isna()
To count missing values in every column:
df.isna().sum()
Example:
Name 1
City 0
Score 1
dtype: int64
4. Calculating Missing-Value Percentage
Counting missing values is useful, but percentages can be more informative when datasets have different sizes.
missing_percentage = (
df.isna().mean() * 100
)
print(missing_percentage)
This calculates the percentage of missing observations in each column.
A column containing a small number of missing values may be handled differently from a column where most values are missing. Always examine the proportion before deciding what to do.
5. Removing Missing Values with dropna()
The dropna() method removes rows or columns containing
missing values according to the selected options.
The simplest form removes rows containing at least one missing value:
cleaned_df = df.dropna()
print(cleaned_df)
If a row contains a missing value, that row is removed.
6. Dropping Rows or Columns
The axis parameter controls whether rows or columns
are removed.
Remove rows containing missing values:
df.dropna(axis=0)
Remove columns containing missing values:
df.dropna(axis=1)
| Expression | Meaning |
|---|---|
axis=0 |
Operate along rows |
axis=1 |
Operate along columns |
7. how Parameter in dropna()
The how parameter controls when a row or column
should be removed.
how="any" removes the row if at least one selected
value is missing.
df.dropna(
how="any"
)
how="all" removes a row only when all selected values
are missing.
df.dropna(
how="all"
)
8. Dropping Rows Based on Specific Columns
Sometimes you do not want missing values in every column to cause a row to be removed.
Use subset to specify the columns that matter.
df.dropna(
subset=["Name"]
)
This removes rows where Name is missing, while
missing values in unrelated columns do not determine whether the
row is removed.
If every student must have a Student ID, you may decide to remove rows where Student ID is missing even if some other fields are incomplete.
9. Keeping Rows with Enough Data
The thresh parameter specifies the minimum number of
non-missing values required.
df.dropna(
thresh=3
)
A row is retained if it contains at least three non-missing values.
thresh is useful when you want to preserve records
that contain enough information rather than deleting every
incomplete record.
10. Replacing Missing Values with fillna()
Instead of deleting missing records, you can replace missing
values using fillna().
df.fillna(0)
This replaces missing values with 0.
Replacing every missing value with zero is not automatically correct. The replacement must make sense for the meaning of the column.
11. Filling Different Columns with Different Values
A dictionary can specify different replacement values for different columns.
df = df.fillna({
"Score": 0,
"City": "Unknown"
})
This is safer than applying the same replacement value to every column.
12. Filling Missing Numerical Values
Numerical missing values are sometimes replaced using a representative statistic such as the mean or median.
Fill with the mean:
df["Score"] = df["Score"].fillna(
df["Score"].mean()
)
Fill with the median:
df["Score"] = df["Score"].fillna(
df["Score"].median()
)
The median is often useful when the data contains extreme values because it is less sensitive to outliers than the mean.
13. Filling Categorical Missing Values
For categorical data, the most frequent category can sometimes be used as a replacement.
df["City"] = df["City"].fillna(
df["City"].mode()[0]
)
Another option is to explicitly mark the value:
df["City"] = df["City"].fillna(
"Unknown"
)
- Numerical data → mean, median, or another justified strategy.
-
Categorical data → mode, explicit category such as
"Unknown", or another domain-appropriate strategy.
The correct method depends on the meaning and distribution of the data.
14. Forward Fill
In sequential or time-series data, a missing value may sometimes be filled using the previous valid observation.
df["Value"] = df["Value"].ffill()
This is known as forward filling.
15. Backward Fill
Backward filling uses the next valid observation to fill a missing value.
df["Value"] = df["Value"].bfill()
Forward or backward filling should be used only when the underlying data logic justifies carrying information between observations.
16. dropna() vs fillna()
| Method | Purpose | Typical Use |
|---|---|---|
dropna() |
Removes missing observations. | When incomplete records are unusable or safely removable. |
fillna() |
Replaces missing values. | When the record is valuable and a justified replacement exists. |
ffill() |
Uses previous valid value. | Certain sequential or time-series situations. |
bfill() |
Uses next valid value. | Certain sequential or time-series situations. |
17. Identifying Duplicate Records
Duplicate rows can distort counts, averages, frequencies, and other analyses.
Use duplicated() to identify duplicate rows.
df.duplicated()
The result is a Boolean Series:
True→ duplicate rowFalse→ not identified as a duplicate
Count duplicate rows:
df.duplicated().sum()
18. Removing Duplicate Rows
Use drop_duplicates() to remove duplicate records.
cleaned_df = df.drop_duplicates()
The resulting DataFrame retains one copy of each duplicated row.
By default, pandas keeps the first occurrence and removes subsequent duplicate occurrences.
19. Finding Duplicates Based on Selected Columns
Sometimes two records should be considered duplicates based on specific identifying columns rather than every column.
df.duplicated(
subset=["StudentID"]
)
Remove duplicates based on Student ID:
df = df.drop_duplicates(
subset=["StudentID"]
)
This is particularly useful when a column such as Student ID, Product ID, or Transaction ID is expected to uniquely identify a record.
20. Controlling Which Duplicate to Keep
The keep parameter controls which duplicate occurrence
is retained.
Keep the first occurrence:
df.drop_duplicates(
keep="first"
)
Keep the last occurrence:
df.drop_duplicates(
keep="last"
)
Remove every occurrence belonging to a duplicate group:
df.drop_duplicates(
keep=False
)
21. String Cleaning with .str
pandas provides vectorized string operations through the
.str accessor.
Example:
df["Name"].str.strip()
This applies the string operation to every value in the selected Series.
.str allows you to apply many string operations
element-by-element across a pandas Series.
22. Removing Extra Spaces with strip()
Leading and trailing spaces can cause values that should be equal to appear different.
df["Name"] = df["Name"].str.strip()
Example:
" Alice " → "Alice"
" Brian " → "Brian"
This is one of the most common text-cleaning operations.
23. Standardizing Letter Case
Use lower(), upper(), or
title() through the .str accessor.
df["City"] = df["City"].str.lower()
df["City"] = df["City"].str.upper()
df["City"] = df["City"].str.title()
Example:
"london" → "London"
"LONDON" → "London"
" london " → "London"
A common normalization pattern is:
df["City"] = (
df["City"]
.str.strip()
.str.title()
)
24. Replacing Text with str.replace()
Use str.replace() to replace unwanted text patterns.
df["City"] = df["City"].str.replace(
"Ldn",
"London",
regex=False
)
The regex=False argument makes the replacement a
literal string replacement.
25. Finding Text Patterns with str.contains()
str.contains() checks whether a pattern occurs within
string values.
df["City"].str.contains(
"London",
case=False,
na=False
)
Here:
-
case=Falsemakes the search case-insensitive. -
na=Falsetreats missing values asFalse.
26. Checking Prefixes and Suffixes
Use startswith() and endswith() through
the .str accessor.
df["Name"].str.startswith(
"A",
na=False
)
df["Email"].str.endswith(
"@example.com",
na=False
)
These operations are useful when validating or filtering text fields.
27. Measuring Text Length
Use str.len() to determine the length of strings.
df["Name"].str.len()
This can help identify suspicious values such as unusually short or unusually long identifiers.
28. Splitting Text
str.split() can separate strings based on a delimiter.
df["Name"].str.split(
" "
)
For example, a full name can potentially be separated into components.
You can expand the result into separate columns:
df[
["FirstName", "LastName"]
] = df["FullName"].str.split(
" ",
n=1,
expand=True
)
29. Building a String-Cleaning Pipeline
Multiple string operations can be chained together.
df["City"] = (
df["City"]
.str.strip()
.str.lower()
.str.title()
)
The operations execute from top to bottom:
- Remove leading and trailing spaces.
- Convert text to lowercase.
- Convert it to title case.
This transforms values such as:
" LONDON "
" london "
"London"
into a consistent representation:
"London"
"London"
"London"
30. Handling Missing Values Before String Operations
String operations need to be handled carefully when a column contains missing values.
For example:
df["Name"].str.strip()
pandas string operations generally propagate missing values rather than treating them as ordinary text.
If you need missing values to be treated as empty strings for a particular operation, you can explicitly fill them:
df["Name"] = (
df["Name"]
.fillna("")
.str.strip()
)
Replacing missing names with an empty string changes the representation of the data. Use it only when that behavior is appropriate for the task.
31. Complete Cleaning Pipeline
A practical cleaning pipeline may combine several operations.
import pandas as pd
df = pd.read_csv(
"students.csv"
)
# Remove duplicate records
df = df.drop_duplicates()
# Clean text
df["Name"] = (
df["Name"]
.str.strip()
)
df["City"] = (
df["City"]
.str.strip()
.str.title()
)
# Fill missing numerical values
df["Score"] = df["Score"].fillna(
df["Score"].median()
)
# Fill missing categorical values
df["City"] = df["City"].fillna(
"Unknown"
)
print(df)
This is a simple example of a sequential data-cleaning pipeline.
32. Validate After Cleaning
Cleaning is not complete until you verify that the intended changes actually occurred.
print(df.shape)
print(df.isna().sum())
print(df.duplicated().sum())
print(df.head())
print(df.dtypes)
Validation answers questions such as:
- Did the number of rows change as expected?
- Are missing values still present?
- Are duplicate records still present?
- Are text values standardized?
- Did any data type unexpectedly change?
33. Before vs After Cleaning
| Problem | Before | After |
|---|---|---|
| Extra spaces | " Brian " |
"Brian" |
| Inconsistent case | "LONDON" |
"London" |
| Missing numerical value | NaN |
Justified replacement |
| Missing categorical value | NaN |
"Unknown" or another justified value |
| Duplicate record | Repeated row | Single retained record |
34. Understanding the Result of Cleaning Methods
Many pandas methods return a modified object rather than requiring the original DataFrame to be changed automatically.
For example:
cleaned_df = df.dropna()
The result is stored in cleaned_df.
Similarly:
df = df.drop_duplicates()
Here the cleaned result is assigned back to df.
Explicit assignment often makes data-cleaning pipelines easier to read, test, and debug.
35. Method Chaining for Cleaning
Pandas operations can be chained to create a compact cleaning workflow.
cleaned_df = (
df
.drop_duplicates()
.assign(
Name=lambda x: x["Name"].str.strip(),
City=lambda x: (
x["City"]
.str.strip()
.str.title()
)
)
)
Method chaining can make complex transformations easier to read when the pipeline is logically organized.
A short pipeline is not automatically better. For beginners and complex cleaning workflows, several clearly named steps may be easier to understand and debug.
36. Choosing the Right Cleaning Strategy
| Problem | Possible Approach | Typical Tool |
|---|---|---|
| Missing rows | Remove incomplete records | dropna() |
| Missing numerical values | Use a justified statistic | fillna() |
| Missing categorical values | Use a meaningful category or strategy | fillna() |
| Duplicate rows | Remove duplicate records | drop_duplicates() |
| Duplicate identifiers | Check selected identifying columns | duplicated(subset=...) |
| Leading/trailing spaces | Strip whitespace | str.strip() |
| Inconsistent capitalization | Standardize case | str.lower(), str.upper(), str.title() |
| Incorrect text values | Replace patterns | str.replace() |
37. Real-World Cleaning Workflow
A typical Data Science cleaning workflow may look like this:
import pandas as pd
# 1. Load
df = pd.read_csv(
"customer_data.csv"
)
# 2. Inspect
print(df.shape)
print(df.info())
print(df.isna().sum())
# 3. Remove exact duplicates
df = df.drop_duplicates()
# 4. Clean text fields
df["Name"] = (
df["Name"]
.str.strip()
)
df["City"] = (
df["City"]
.str.strip()
.str.title()
)
# 5. Handle missing values
df["Age"] = df["Age"].fillna(
df["Age"].median()
)
df["City"] = df["City"].fillna(
"Unknown"
)
# 6. Validate
print(df.shape)
print(df.isna().sum())
print(df.duplicated().sum())
# 7. Preview
print(df.head())
38. Data Cleaning Decisions That Require Care
Some cleaning decisions cannot be made safely using a universal rule.
Do not automatically:
- Delete every row containing a missing value.
- Replace every missing numerical value with zero.
- Replace every missing value with the mean.
- Remove every duplicate without checking its meaning.
- Convert every text value to lowercase without considering whether capitalization has meaning.
- Modify identifiers merely to make them look consistent.
Clean data according to its meaning, not merely according to its appearance.
39. Pandas Data Cleaning Interview Questions
Q1. What is the difference between dropna()
and fillna()?
View Answer
dropna() removes rows or columns containing
missing values according to its parameters, while
fillna() replaces missing values with
specified or calculated values.
Q2. How do you count missing values in every column?
View Answer
df.isna().sum()
Q3. How do you remove duplicate rows?
View Answer
df = df.drop_duplicates()
Q4. How can you identify duplicates based on a specific column?
View Answer
df.duplicated(
subset=["StudentID"]
)
Q5. How do you remove leading and trailing spaces from a pandas string column?
View Answer
df["Name"] = df["Name"].str.strip()
Q6. How do you convert a text column to lowercase?
View Answer
df["City"] = df["City"].str.lower()
Q7. How can you replace missing scores with the median?
View Answer
df["Score"] = df["Score"].fillna(
df["Score"].median()
)
Q8. What is the purpose of the .str accessor?
View Answer
It provides vectorized string operations for values in a pandas Series.
40. Examination Questions
Multiple Choice Questions
Q1. Which method is used to remove rows containing missing values?
remove_na()dropna()delete_na()clearna()
Answer: B — dropna()
Q2. Which method replaces missing values?
replace_na()fillna()insertna()setna()
Answer: B — fillna()
Q3. Which method identifies duplicate rows?
duplicate()duplicated()duplicates()find_duplicate()
Answer: B — duplicated()
Q4. Which method removes duplicate rows?
drop_duplicates()remove_duplicates()delete_duplicates()clear_duplicates()
Answer: A — drop_duplicates()
Q5. Which operation removes leading and trailing whitespace?
str.clean()str.strip()str.trim()str.space()
Answer: B — str.strip()
Q6. Which accessor provides vectorized string operations in pandas?
.text.string.str.chars
Answer: C — .str
Q7. Which argument makes dropna() remove a row
only when all selected values are missing?
how="any"how="all"mode="all"all=True
Answer: B — how="all"
Short Answer Questions
- Define data cleaning.
- Why is data cleaning important before analysis?
-
Differentiate between
dropna()andfillna(). -
What is the purpose of
duplicated()? -
What is the purpose of
drop_duplicates()? -
Explain the use of the
.straccessor. -
Write a statement to remove leading and trailing spaces from
the
Namecolumn. -
Write a statement to convert the
Citycolumn to title case.
41. Practical Examination Questions
Question 1 — Missing Values
Given a DataFrame named df, write statements to:
- Count missing values.
- Remove rows containing missing values.
- Fill missing values in
Scorewith its median.
Solution:
print(df.isna().sum())
df = df.dropna()
df["Score"] = df["Score"].fillna(
df["Score"].median()
)
Question 2 — Duplicate Records
Find and remove duplicate records.
Solution:
print(df.duplicated().sum())
df = df.drop_duplicates()
Question 3 — Text Cleaning
Clean the City column by removing unnecessary
spaces and standardizing capitalization.
Solution:
df["City"] = (
df["City"]
.str.strip()
.str.title()
)
Question 4 — Complete Pipeline
Write a basic cleaning pipeline that removes duplicates, cleans the City column, and handles missing Score values.
Solution:
df = df.drop_duplicates()
df["City"] = (
df["City"]
.str.strip()
.str.title()
)
df["Score"] = df["Score"].fillna(
df["Score"].median()
)
42. Common Data Cleaning Mistakes
- Deleting all incomplete rows: This may remove valuable observations.
- Replacing missing values blindly: Zero, mean, median, mode, and "Unknown" do not mean the same thing.
- Removing duplicates without checking: Some repeated records may be legitimate transactions or repeated events.
- Ignoring text inconsistencies: Spaces and capitalization can create apparently different categories.
- Cleaning without validation: Always inspect the result after transformation.
- Changing source data unnecessarily: Preserve the original dataset when possible and create a cleaned version for analysis.
43. Data Cleaning Checklist
| Check | Command / Technique | Question |
|---|---|---|
| Missing values | df.isna().sum() |
Which columns contain missing data? |
| Missing percentage | df.isna().mean() * 100 |
How much data is missing? |
| Remove missing | dropna() |
Can incomplete records be safely removed? |
| Replace missing | fillna() |
Can missing data be reasonably imputed? |
| Duplicates | duplicated() |
Are records repeated? |
| Remove duplicates | drop_duplicates() |
Which repeated records should remain? |
| Spaces | str.strip() |
Are text values padded with whitespace? |
| Capitalization | str.lower(), str.upper(), str.title() |
Are equivalent categories represented differently? |
| Text patterns | str.replace() |
Are known unwanted patterns present? |
| Validation | shape, info(), isna() |
Did cleaning produce the intended result? |
44. Quick Revision Cheatsheet
| Command | Purpose |
|---|---|
df.isna() |
Identify missing values. |
df.isna().sum() |
Count missing values per column. |
df.dropna() |
Remove rows with missing values. |
df.dropna(axis=1) |
Remove columns with missing values. |
df.dropna(subset=["A"]) |
Drop rows missing values in selected columns. |
df.fillna(value) |
Replace missing values. |
df["A"].fillna(df["A"].median()) |
Fill numerical missing values with median. |
df.duplicated() |
Identify duplicate rows. |
df.duplicated().sum() |
Count duplicate rows. |
df.drop_duplicates() |
Remove duplicate rows. |
df["A"].str.strip() |
Remove leading/trailing spaces. |
df["A"].str.lower() |
Convert text to lowercase. |
df["A"].str.upper() |
Convert text to uppercase. |
df["A"].str.title() |
Convert text to title case. |
df["A"].str.replace() |
Replace text patterns. |
df["A"].str.contains() |
Search for text patterns. |
df["A"].str.len() |
Measure string length. |
df["A"].str.split() |
Split text into components. |
df["A"].ffill() |
Forward-fill missing values. |
df["A"].bfill() |
Backward-fill missing values. |
45. Key Takeaways
- Data cleaning prepares raw data for reliable analysis.
-
Use
isna()andisna().sum()to identify missing values. -
Use
dropna()when incomplete records can be safely removed. -
Use
fillna()when missing information should be replaced using a justified strategy. -
Use
duplicated()to detect duplicate records. -
Use
drop_duplicates()to remove duplicate records. -
Use the
.straccessor for vectorized text operations. -
str.strip()removes leading and trailing whitespace. -
str.lower(),str.upper(), andstr.title()help standardize text case. -
str.replace()can correct known text patterns. - Always validate the dataset after cleaning.
A good data-cleaning pipeline does not simply make data look tidy; it makes the dataset more consistent, usable, and trustworthy while preserving meaningful information.