What is Data Cleaning?
Data Cleaning, also called data cleansing, is the process of finding and fixing inaccurate, incomplete, inconsistent, duplicate, or incorrectly formatted data.
Real-world data is rarely perfect. A customer dataset may contain missing phone numbers, duplicate customers, inconsistent names, invalid ages, incorrect dates, or extra spaces. Python and Pandas provide tools to clean this data efficiently.
๐ Find Problems
Inspect rows, columns, data types, missing values, duplicates, and unusual values.
๐งน Clean Data
Remove duplicates, handle missing values, standardize text, and fix formatting problems.
โ Validate Data
Check ranges, formats, data types, and business rules after cleaning.
๐ Prepare for Analysis
Produce a reliable dataset that can be used for analysis, reporting, visualization, or machine learning.
A good cleaning process does not simply delete problematic rows. First understand why the problem exists, then choose an appropriate treatment.
Pandas is one of the most commonly used Python libraries for cleaning tabular data.
import pandas as pd
The following example represents customer information collected from an application. The dataset contains several common data-quality problems.
| Name | Age | City | Salary | |
|---|---|---|---|---|
| Rahul | 25 | Pune | rahul@gmail.com | 45000 |
| Priya | 30 | mumbai | priya@gmail.com | 55000 |
| Rahul | 25 | Pune | rahul@gmail.com | 45000 |
| Amit | None | Delhi | amit@gmail.com | 60000 |
| Sneha | 28 | delhi | 52000 |
import pandas as pd
data = {
"Name": [
"Rahul",
" Priya ",
"Rahul",
"Amit",
"Sneha"
],
"Age": [
25,
30,
25,
None,
28
],
"City": [
"Pune",
"mumbai",
"Pune",
"Delhi",
"delhi"
],
"Email": [
"rahul@gmail.com",
"priya@gmail.com",
"rahul@gmail.com",
"amit@gmail.com",
" "
],
"Salary": [
45000,
55000,
45000,
60000,
52000
]
}
df = pd.DataFrame(data)
print(df)
Before cleaning anything, inspect the dataset.
print(df.head())
print(df.shape)
print(df.columns)
print(df.info())
Missing values are one of the most common data-quality problems.
print(df.isnull())
To count missing values in each column:
print(df.isnull().sum())
Percentages can make missing-data problems easier to understand.
missing_percentage = (
df.isnull().sum() / len(df)
) * 100
print(missing_percentage)
dropna() removes rows containing missing values.
clean_df = df.dropna()
print(clean_df)
Numeric missing values can sometimes be replaced with a meaningful statistic such as the mean or median.
median_age = df["Age"].median()
df["Age"] = df["Age"].fillna(median_age)
print(df)
The median is often useful when the data contains extreme values.
mean_salary = df["Salary"].mean()
df["Salary"] = df["Salary"].fillna(mean_salary)
print(df)
Text columns can be filled with a known label when appropriate.
df["City"] = df["City"].fillna("Unknown")
print(df)
Duplicate records can lead to incorrect totals and misleading analysis.
duplicates = df.duplicated()
print(duplicates)
Count duplicate rows:
print(df.duplicated().sum())
df = df.drop_duplicates()
print(df)
You can also remove duplicates based on selected columns.
df = df.drop_duplicates(
subset=["Email"]
)
print(df)
User-entered text frequently contains unnecessary spaces at the beginning or end.
df["Name"] = df["Name"].str.strip()
print(df["Name"])
Apply the same technique to other text columns:
df["Name"] = df["Name"].str.strip()
df["City"] = df["City"].str.strip()
df["Email"] = df["Email"].str.strip()
Values such as mumbai, Mumbai,
and MUMBAI may represent the same city.
df["City"] = df["City"].str.title()
print(df["City"])
Other useful text transformations include:
df["Name"] = df["Name"].str.upper()
df["City"] = df["City"].str.lower()
df["Email"] = df["Email"].str.lower()
Correct data types are essential for reliable calculations.
df["Age"] = df["Age"].astype(int)
print(df.dtypes)
For numeric values stored as text:
df["Salary"] = pd.to_numeric(
df["Salary"],
errors="coerce"
)
Real datasets may contain values such as
"45,000" or "โน50000".
df["Salary"] = (
df["Salary"]
.astype(str)
.str.replace(",", "", regex=False)
)
df["Salary"] = pd.to_numeric(
df["Salary"],
errors="coerce"
)
df["Salary"] = (
df["Salary"]
.astype(str)
.str.replace("โน", "", regex=False)
.str.replace(",", "", regex=False)
.str.strip()
)
df["Salary"] = pd.to_numeric(
df["Salary"],
errors="coerce"
)
print(df["Salary"])
Dates should be converted to Pandas datetime values before performing date-based analysis.
df["Date"] = pd.to_datetime(
df["Date"],
errors="coerce"
)
print(df.dtypes)
errors="coerce"?
Invalid date strings are converted to missing datetime
values instead of causing the entire conversion to fail.
Data can be non-missing but still incorrect. For example,
an age of -5 or 250 is invalid
for a normal customer dataset.
invalid_age = df[
(df["Age"] < 0) |
(df["Age"] > 120)
]
print(invalid_age)
Once invalid values are identified, they can be replaced with a missing value and handled using an appropriate strategy.
df.loc[
(df["Age"] < 0) |
(df["Age"] > 120),
"Age"
] = pd.NA
print(df)
Regular expressions can be used for basic format validation.
email_pattern = r"^[^@\s]+@[^@\s]+\.[^@\s]+$"
valid_email = df["Email"].str.match(
email_pattern,
na=False
)
print(valid_email)
1 True
2 False
3 True
4 False
Name: Email, dtype: bool
An empty string is not necessarily represented as a Pandas missing value. Convert blank strings when appropriate.
df["Email"] = df["Email"].replace(
r"^\s*$",
pd.NA,
regex=True
)
print(df)
| Name | ||
|---|---|---|
| 0 | Rahul | rahul@gmail.com |
| 1 | Priya | priya@gmail.com |
| 2 | Sneha | <NA> |
Standardized column names make data processing easier.
df = df.rename(columns={
"Name": "customer_name",
"Age": "age",
"City": "city",
"Email": "email",
"Salary": "salary"
})
print(df.columns)
For larger datasets, column names can be normalized automatically.
df.columns = (
df.columns
.str.strip()
.str.lower()
.str.replace(" ", "_")
)
print(df.columns)
An outlier is a value that is unusually far from most other observations. Outliers may be legitimate or may indicate data-entry problems.
Q1 = df["Salary"].quantile(0.25)
Q3 = df["Salary"].quantile(0.75)
IQR = Q3 - Q1
lower = Q1 - 1.5 * IQR
upper = Q3 + 1.5 * IQR
outliers = df[
(df["Salary"] < lower) |
(df["Salary"] > upper)
]
print(outliers)
| Name | Department | Salary | |
|---|---|---|---|
| 4 | Vikram Singh | Executive | 550000 |
Business rules can be used to identify records that need review.
invalid_records = df[
(df["Age"] < 18) |
(df["Salary"] < 0)
]
print(invalid_records)
| Name | Age | Salary | |
|---|---|---|---|
| 2 | Rahul | 16 | -5000 |
The replace() function is useful when a
dataset contains different labels for the same value.
df["City"] = df["City"].replace({
"Bombay": "Mumbai",
"mumbai": "Mumbai",
"MUMBAI": "Mumbai",
"delhi": "Delhi"
})
print(df["City"])
1 Mumbai
2 Delhi
3 Mumbai
4 Delhi
Name: City, dtype: object
A common real-world workflow starts by loading a CSV file.
import pandas as pd
df = pd.read_csv("customers.csv")
print("Before Cleaning:")
print(df.shape)
# Remove duplicates
df = df.drop_duplicates()
# Remove extra spaces
df["Name"] = df["Name"].str.strip()
# Standardize city
df["City"] = df["City"].str.title()
# Convert blank email values
df["Email"] = df["Email"].replace(
r"^\s*$",
pd.NA,
regex=True
)
print("After Cleaning:")
print(df.shape)
(5, 4)
After Cleaning:
(3, 4)
Imagine an e-commerce company has collected customer information from several registration forms. The data contains inconsistent names, cities, duplicate records, missing ages, and blank email addresses.
import pandas as pd
data = {
"Name": [
" Rahul ",
"Priya",
"Rahul",
"AMIT",
"Sneha "
],
"Age": [
25,
30,
25,
None,
28
],
"City": [
"pune",
"MUMBAI",
"Pune",
"delhi",
"Delhi"
],
"Email": [
"rahul@gmail.com",
"priya@gmail.com",
"rahul@gmail.com",
"amit@gmail.com",
" "
]
}
df = pd.DataFrame(data)
print("Original Data:")
print(df)
# 1. Remove extra spaces
df["Name"] = df["Name"].str.strip()
df["City"] = df["City"].str.strip()
df["Email"] = df["Email"].str.strip()
# 2. Standardize text
df["Name"] = df["Name"].str.title()
df["City"] = df["City"].str.title()
df["Email"] = df["Email"].str.lower()
# 3. Convert blank emails to missing values
df["Email"] = df["Email"].replace(
r"^\s*$",
pd.NA,
regex=True
)
# 4. Fill missing age with median
df["Age"] = df["Age"].fillna(
df["Age"].median()
)
# 5. Remove duplicate customers
df = df.drop_duplicates(
subset=["Email"],
keep="first"
)
print("\nCleaned Data:")
print(df)
| Name | Age | City | ||
|---|---|---|---|---|
| 0 | Rahul | 25.0 | pune | rahul@gmail.com |
| 1 | Priya | 30.0 | MUMBAI | priya@gmail.com |
| 2 | Rahul | 25.0 | Pune | rahul@gmail.com |
| 3 | AMIT | NaN | delhi | amit@gmail.com |
| 4 | Sneha | 28.0 | Delhi |
| Name | Age | City | ||
|---|---|---|---|---|
| 0 | Rahul | 25.0 | Pune | rahul@gmail.com |
| 1 | Priya | 30.0 | Mumbai | priya@gmail.com |
| 3 | Amit | 26.5 | Delhi | amit@gmail.com |
| 4 | Sneha | 28.0 | Delhi | <NA> |
A sales department receives transaction data where prices contain commas, product names have inconsistent capitalization, and some quantities are missing.
import pandas as pd
data = {
"Product": [
"Laptop",
" laptop ",
"MOBILE",
"Mobile",
"Tablet"
],
"Quantity": [
10,
None,
25,
20,
15
],
"Price": [
"60,000",
"60,000",
"25,000",
"25,000",
"18,000"
]
}
df = pd.DataFrame(data)
# Clean product names
df["Product"] = (
df["Product"]
.str.strip()
.str.title()
)
# Convert price to numeric
df["Price"] = (
df["Price"]
.str.replace(",", "", regex=False)
)
df["Price"] = pd.to_numeric(
df["Price"],
errors="coerce"
)
# Fill missing quantity
df["Quantity"] = df["Quantity"].fillna(0)
# Calculate sales
df["Total"] = (
df["Quantity"] *
df["Price"]
)
print(df)
| Product | Quantity | Price | Total | |
|---|---|---|---|---|
| 0 | Laptop | 10.0 | 60000 | 600000.0 |
| 1 | Laptop | 0.0 | 60000 | 0.0 |
| 2 | Mobile | 25.0 | 25000 | 625000.0 |
| 3 | Mobile | 20.0 | 25000 | 500000.0 |
| 4 | Tablet | 15.0 | 18000 | 270000.0 |
An HR department wants to verify that employee records satisfy basic business rules.
import pandas as pd
data = {
"Employee": [
"Amit",
"Priya",
"Rahul",
"Sneha"
],
"Age": [
29,
31,
150,
27
],
"Salary": [
60000,
75000,
-5000,
65000
]
}
df = pd.DataFrame(data)
# Find invalid ages
invalid_age = df[
(df["Age"] < 18) |
(df["Age"] > 100)
]
# Find invalid salaries
invalid_salary = df[
df["Salary"] < 0
]
print("Invalid Age Records:")
print(invalid_age)
print("\nInvalid Salary Records:")
print(invalid_salary)
| Employee | Age | Salary | |
|---|---|---|---|
| 2 | Rahul | 150 | -5000 |
| Employee | Age | Salary | |
|---|---|---|---|
| 2 | Rahul | 150 | -5000 |
Inventory systems can contain inconsistent product names, negative stock values, and missing prices.
import pandas as pd
data = {
"Product": [
"Laptop",
" laptop ",
"Mobile",
"MOBILE",
"Monitor"
],
"Stock": [
20,
15,
-5,
30,
None
],
"Price": [
60000,
60000,
25000,
25000,
22000
]
}
df = pd.DataFrame(data)
# Standardize names
df["Product"] = (
df["Product"]
.str.strip()
.str.title()
)
# Replace negative stock with missing
df.loc[
df["Stock"] < 0,
"Stock"
] = pd.NA
# Fill missing stock
df["Stock"] = df["Stock"].fillna(0)
print(df)
| Product | Stock | Price | |
|---|---|---|---|
| 0 | Laptop | 20.0 | 60000 |
| 1 | Laptop | 15.0 | 60000 |
| 2 | Mobile | 0.0 | 25000 |
| 3 | Mobile | 30.0 | 25000 |
| 4 | Monitor | 0.0 | 22000 |
print("Rows before cleaning:", len(df))
df = df.drop_duplicates()
print("Rows after cleaning:", len(df))
print("Missing values:")
print(df.isnull().sum())
print("Data types:")
print(df.dtypes)
Rows after cleaning: 3
Missing values:
Name 0
City 0
Email 0
Age 1
dtype: int64
Data types:
Name object
City object
Email object
Age float64
dtype: object
If the same type of dataset arrives regularly, put cleaning steps into a reusable function.
import pandas as pd
def clean_customer_data(df):
df = df.copy()
df["Name"] = (
df["Name"]
.str.strip()
.str.title()
)
df["City"] = (
df["City"]
.str.strip()
.str.title()
)
df["Email"] = (
df["Email"]
.str.strip()
.str.lower()
)
df = df.drop_duplicates()
return df
clean_df = clean_customer_data(df)
print(clean_df)
| Name | City | Age | ||
|---|---|---|---|---|
| 0 | Rahul Sharma | Mumbai | rahul@gmail.com | 28 |
| 1 | Priya Patel | Delhi | priya@yahoo.com | 34 |
| 2 | Amit Kumar | Bangalore | amit@outlook.com | NaN |
This example demonstrates a complete CSV cleaning pipeline.
import pandas as pd
# Load data
df = pd.read_csv("customers.csv")
# Inspect
print(df.head())
print(df.info())
# Remove duplicates
df = df.drop_duplicates()
# Clean text
df["Name"] = (
df["Name"]
.str.strip()
.str.title()
)
df["City"] = (
df["City"]
.str.strip()
.str.title()
)
# Clean email
df["Email"] = (
df["Email"]
.str.strip()
.str.lower()
)
# Handle missing values
df["Age"] = df["Age"].fillna(
df["Age"].median()
)
# Validate
print("\nMissing Values:")
print(df.isnull().sum())
print("\nFinal Dataset:")
print(df)
City 0
Email 0
Age 0
dtype: int64
| Name | City | Age | ||
|---|---|---|---|---|
| 0 | Rahul Sharma | Mumbai | rahul@gmail.com | 28.0 |
| 1 | Priya Patel | Delhi | priya@yahoo.com | 34.0 |
| 2 | Amit Kumar | Bangalore | amit@outlook.com | 31.0 |
After cleaning, save the processed dataset separately from the original raw data.
df.to_csv(
"customers_cleaned.csv",
index=False
)
df.to_excel(
"customers_cleaned.xlsx",
index=False
)
| Problem | Example | Common Solution |
|---|---|---|
| Missing values | Age = NaN | fillna() or dropna() |
| Duplicates | Same customer twice | drop_duplicates() |
| Extra spaces | " Rahul " | str.strip() |
| Inconsistent case | "mumbai", "Mumbai" | str.title() |
| Wrong data type | "50000" instead of 50000 | pd.to_numeric() |
| Invalid dates | "unknown" | pd.to_datetime() |
| Invalid values | Age = -10 | Business-rule validation |
| Outliers | Extremely large salary | Investigate using statistical methods |
| Inconsistent labels | "M", "Male", "male" | replace() / standardization |
- Always keep the original raw dataset unchanged.
- Inspect data before modifying it.
- Understand why a value is missing before filling it.
- Do not remove rows simply because they look unusual.
- Use consistent text formatting.
- Validate data types before calculations.
- Use business rules to identify invalid values.
- Track how many records are removed or changed.
- Separate raw, cleaned, and final datasets when practical.
- Create reusable cleaning functions for recurring workflows.
- Validate the final dataset after every major transformation.
- Document important assumptions used during cleaning.
This example combines the most important cleaning techniques into one practical e-commerce workflow.
import pandas as pd
# ---------------------------------
# 1. Load customer data
# ---------------------------------
data = {
"Customer": [
" Rahul ",
"Priya",
"Rahul",
"AMIT",
"Sneha ",
"Vikas"
],
"Age": [
25,
30,
25,
None,
28,
150
],
"City": [
"pune",
"MUMBAI",
"Pune",
"delhi",
"Delhi",
"mumbai"
],
"Email": [
"rahul@gmail.com",
"priya@gmail.com",
"rahul@gmail.com",
"amit@gmail.com",
" ",
"vikas@gmail.com"
],
"Orders": [
5,
8,
5,
10,
4,
7
],
"Amount": [
15000,
28000,
15000,
42000,
12000,
25000
]
}
df = pd.DataFrame(data)
print("Original Data")
print(df)
# ---------------------------------
# 2. Remove extra spaces
# ---------------------------------
df["Customer"] = (
df["Customer"]
.str.strip()
)
df["City"] = (
df["City"]
.str.strip()
)
df["Email"] = (
df["Email"]
.str.strip()
)
# ---------------------------------
# 3. Standardize text
# ---------------------------------
df["Customer"] = (
df["Customer"]
.str.title()
)
df["City"] = (
df["City"]
.str.title()
)
df["Email"] = (
df["Email"]
.str.lower()
)
# ---------------------------------
# 4. Convert blank email to missing
# ---------------------------------
df["Email"] = df["Email"].replace(
r"^\s*$",
pd.NA,
regex=True
)
# ---------------------------------
# 5. Validate age
# ---------------------------------
df.loc[
(df["Age"] < 18) |
(df["Age"] > 100),
"Age"
] = pd.NA
# ---------------------------------
# 6. Fill missing age
# ---------------------------------
df["Age"] = df["Age"].fillna(
df["Age"].median()
)
# ---------------------------------
# 7. Remove duplicate customers
# ---------------------------------
df = df.drop_duplicates(
subset=["Email"],
keep="first"
)
# ---------------------------------
# 8. Validate order values
# ---------------------------------
df = df[df["Orders"] >= 0]
df = df[df["Amount"] >= 0]
# ---------------------------------
# 9. Final validation
# ---------------------------------
print("\nMissing Values:")
print(df.isnull().sum())
print("\nFinal Data:")
print(df)
# ---------------------------------
# 10. Save cleaned data
# ---------------------------------
df.to_csv(
"clean_customers.csv",
index=False
)
- Never assume raw data is already clean.
- Inspect the dataset before making changes.
- Check missing values in every important column.
- Identify duplicate records.
- Remove unnecessary spaces from text.
- Standardize text capitalization.
- Convert columns to appropriate data types.
- Validate numeric ranges.
- Validate dates and other structured values.
- Investigate outliers before removing them.
- Use business rules for validation.
- Keep raw and cleaned datasets separate.
- Track important changes made during cleaning.
- Validate the final dataset again after cleaning.
- Only use cleaned data for downstream analysis when appropriate.
Python Data Cleaning
Clean, validate, and prepare real-world data for reliable analysis.