Skip to main content

๐Ÿงน Data Cleaning and Preprocessing

๐Ÿ“š What You'll Learn

By the end of this lesson, you will be able to:

  • Assess a dataset's quality by measuring missing values, duplicates, and incorrect data types
  • Choose and apply the right missing-data strategy โ€” dropping, filling, interpolation, or KNN imputation
  • Detect and remove exact and near-duplicate records with drop_duplicates and fuzzy matching
  • Convert and coerce columns safely, including parsing dates and numbers with errors='coerce'
  • Identify outliers using the Z-score and IQR methods, then remove, cap, or transform them
  • Standardize messy text with the pandas .str accessor and regular expressions

โฑ๏ธ Estimated Time: 45โ€“60 minutes

๐ŸŽฏ Project: Clean a messy e-commerce customer dataset end-to-end โ€” fixing IDs, names, emails, ages, dates, and purchase amounts into an analysis-ready table.

Transform messy, real-world data into pristine datasets ready for analysis

๐ŸŒŸ The Art of Data Cleaning

Imagine you're a detective examining evidence. Some clues are missing, others are misleading, and many are covered in dirt. Data cleaning is like forensic analysis - you must carefully examine, clean, and organize your evidence before solving the case!

In the real world, data scientists spend up to 80% of their time cleaning data. Raw data is like a rough diamond - valuable but needing careful cutting and polishing to reveal its true brilliance. Master these techniques, and you'll transform chaotic datasets into analytical gold!

๐Ÿ”„ The Data Cleaning Pipeline

graph TB A[Raw Data] --> B{Assess Quality} B --> C[Missing Values] B --> D[Duplicates] B --> E[Data Types] B --> F[Outliers] B --> G[Inconsistencies] C --> H[Handle Missing] D --> I[Remove Duplicates] E --> J[Convert Types] F --> K[Process Outliers] G --> L[Standardize] H --> M[Clean Data] I --> M J --> M K --> M L --> M M --> N[Validate] N --> O[Ready for Analysis] style A fill:#faa,stroke:#333,stroke-width:2px style O fill:#afa,stroke:#333,stroke-width:2px style M fill:#aaf,stroke:#333,stroke-width:2px

๐ŸŽฎ Data Quality Analyzer

Watch as we clean a messy dataset in real-time:

30% Clean

๐Ÿ” Detecting Missing Values

Like finding holes in Swiss cheese - you need to know where they are before you can fix them!

# Check for missing values
df.isnull().sum()  # Count per column
df.isnull().sum().sum()  # Total missing

# Visualize missing data pattern
import missingno as msno
msno.matrix(df)  # Visual matrix
msno.bar(df)     # Bar chart

# Find percentage of missing
missing_pct = (df.isnull().sum() / len(df)) * 100
print(missing_pct[missing_pct > 0].sort_values(ascending=False))
Age     Name    Salary
25      John    NaN
NaN     Alice   50000
30      NaN     60000
Age     Name    Salary
25      John    55000
28      Alice   50000
30      Bob     60000

๐ŸŽฏ Handling Missing Data

Choose your strategy wisely - like a surgeon deciding how to treat a wound!

# Strategy 1: Drop missing values
df.dropna()  # Drop rows with any NaN
df.dropna(how='all')  # Drop rows where ALL values are NaN
df.dropna(subset=['important_col'])  # Drop if specific column is NaN

# Strategy 2: Fill missing values
df.fillna(0)  # Fill with constant
df.fillna(df.mean())  # Fill with mean
df.fillna(method='ffill')  # Forward fill
df.fillna(method='bfill')  # Backward fill

# Strategy 3: Interpolation
df.interpolate(method='linear')  # Linear interpolation
df.interpolate(method='time')    # Time-based

# Strategy 4: Advanced imputation
from sklearn.impute import KNNImputer
imputer = KNNImputer(n_neighbors=5)
df_imputed = pd.DataFrame(imputer.fit_transform(df), columns=df.columns)

๐Ÿ”„ Removing Duplicates

Like finding twins in a crowd - sometimes they're exact copies, sometimes just very similar!

# Find duplicates
df.duplicated()  # Boolean mask
df.duplicated().sum()  # Count duplicates

# Show duplicate rows
df[df.duplicated(keep=False)]  # Show all duplicates

# Remove duplicates
df.drop_duplicates()  # Remove exact duplicates
df.drop_duplicates(subset=['id'])  # Based on specific columns
df.drop_duplicates(keep='first')  # Keep first occurrence
df.drop_duplicates(keep='last')   # Keep last occurrence

# Fuzzy matching for near-duplicates
from fuzzywuzzy import fuzz
def find_similar(name, names_list):
    return [(n, fuzz.ratio(name, n)) for n in names_list 
            if fuzz.ratio(name, n) > 80]

๐Ÿ”ง Data Type Conversion

Like translating between languages - ensuring everyone speaks the same data dialect!

# Check current data types
df.dtypes
df.info()

# Convert data types
df['price'] = df['price'].astype(float)
df['quantity'] = df['quantity'].astype(int)
df['category'] = df['category'].astype('category')

# Parse dates
df['date'] = pd.to_datetime(df['date'], format='%Y-%m-%d')
df['date'] = pd.to_datetime(df['date'], errors='coerce')  # Handle errors

# Convert numeric with error handling
df['amount'] = pd.to_numeric(df['amount'], errors='coerce')

# Memory optimization
df['category'] = df['category'].astype('category')  # For repeated values
df['small_int'] = df['small_int'].astype('int8')   # Use smaller int types

๐Ÿ“Š Outlier Detection

Finding the rebels in your data - values that march to their own beat!

# Statistical methods
# Z-score method
from scipy import stats
z_scores = stats.zscore(df['value'])
outliers = df[abs(z_scores) > 3]

# IQR method
Q1 = df['value'].quantile(0.25)
Q3 = df['value'].quantile(0.75)
IQR = Q3 - Q1
lower_bound = Q1 - 1.5 * IQR
upper_bound = Q3 + 1.5 * IQR
outliers = df[(df['value'] < lower_bound) | (df['value'] > upper_bound)]

# Visual detection
import matplotlib.pyplot as plt
df.boxplot(column='value')
plt.show()

# Handle outliers
# Option 1: Remove
df_clean = df[(df['value'] >= lower_bound) & (df['value'] <= upper_bound)]

# Option 2: Cap values
df['value'] = df['value'].clip(lower=lower_bound, upper=upper_bound)

# Option 3: Transform
df['value_log'] = np.log1p(df['value'])  # Log transformation

โœจ String Cleaning

Polishing text data until it sparkles - consistent, clean, and analysis-ready!

# Basic string cleaning
df['name'] = df['name'].str.strip()  # Remove whitespace
df['name'] = df['name'].str.lower()  # Lowercase
df['name'] = df['name'].str.title()  # Title case

# Remove special characters
df['phone'] = df['phone'].str.replace(r'[^0-9]', '', regex=True)

# Fix inconsistencies
df['category'] = df['category'].replace({
    'cat1': 'Category 1',
    'CAT1': 'Category 1',
    'category_1': 'Category 1'
})

# Extract information
df['domain'] = df['email'].str.extract(r'@([^.]+)')
df['year'] = df['date_string'].str.extract(r'(\d{4})')

# Split strings
df[['first_name', 'last_name']] = df['full_name'].str.split(' ', expand=True)

# Normalize text
import unicodedata
def normalize_text(text):
    return unicodedata.normalize('NFKD', text).encode('ascii', 'ignore').decode('utf-8')

๐ŸŒ Real-World Scenario: Customer Data Cleaning

Let's clean a messy customer dataset from an e-commerce platform:

# Load messy customer data
import pandas as pd
import numpy as np
from datetime import datetime

# Simulate real-world messy data
customers = pd.DataFrame({
    'customer_id': ['C001', 'C002', 'C002', 'C003', 'C004', np.nan],
    'name': ['John Smith', 'alice jones', 'Alice Jones', 'BOB WILSON', '  Sarah Lee  ', 'Mike Brown'],
    'email': ['john@email.com', 'ALICE@GMAIL.COM', 'alice@gmail.com', 'bob@', 'sarah@@email.com', 'mike@email'],
    'age': [25, 130, 35, -5, 42, 'thirty'],
    'signup_date': ['2024-01-15', '15/01/2024', '2024/01/15', '2024-13-45', '2024-02-30', '2024-03-10'],
    'purchase_amount': ['$1,234.56', '2345.67', '$3,456.78', 'N/A', '4567.89', '5678.90']
})

print("๐Ÿ”ด BEFORE CLEANING:")
print(customers)
print(f"\nData types:\n{customers.dtypes}")

# Step 1: Handle duplicates
customers = customers.drop_duplicates(subset=['customer_id'], keep='first')

# Step 2: Fix customer IDs
customers['customer_id'] = customers['customer_id'].fillna(
    'C' + customers.index.astype(str).str.zfill(3)
)

# Step 3: Clean names
customers['name'] = customers['name'].str.strip().str.title()

# Step 4: Validate and clean emails
import re
def validate_email(email):
    if pd.isna(email):
        return np.nan
    pattern = r'^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$'
    email = str(email).lower()
    if re.match(pattern, email):
        return email
    return np.nan

customers['email'] = customers['email'].apply(validate_email)

# Step 5: Clean age column
customers['age'] = pd.to_numeric(customers['age'], errors='coerce')
customers.loc[customers['age'] < 0, 'age'] = np.nan
customers.loc[customers['age'] > 120, 'age'] = np.nan
customers['age'] = customers['age'].fillna(customers['age'].median())

# Step 6: Parse dates
def parse_date(date_str):
    if pd.isna(date_str):
        return np.nan
    for fmt in ['%Y-%m-%d', '%d/%m/%Y', '%Y/%m/%d']:
        try:
            return pd.to_datetime(date_str, format=fmt)
        except:
            continue
    return np.nan

customers['signup_date'] = customers['signup_date'].apply(parse_date)

# Step 7: Clean purchase amounts
customers['purchase_amount'] = (customers['purchase_amount']
    .str.replace('$', '')
    .str.replace(',', '')
    .replace('N/A', np.nan)
    .astype(float))

# Step 8: Fill missing purchase amounts with median
customers['purchase_amount'] = customers['purchase_amount'].fillna(
    customers['purchase_amount'].median()
)

print("\nโœ… AFTER CLEANING:")
print(customers)
print(f"\nData types:\n{customers.dtypes}")
print(f"\nData Quality Report:")
print(f"- Missing values: {customers.isnull().sum().sum()}")
print(f"- Duplicate IDs: {customers['customer_id'].duplicated().sum()}")
print(f"- Invalid emails: {customers['email'].isnull().sum()}")
print(f"- Data is now clean and ready for analysis!")

๐Ÿ’ก Pro Tips for Data Cleaning

โš ๏ธ Common Pitfalls to Avoid

๐Ÿ“‹ Data Cleaning Checklist

graph TD A[Start] --> B{Missing Values?} B -->|Yes| C[Handle Missing] B -->|No| D{Duplicates?} C --> D D -->|Yes| E[Remove Duplicates] D -->|No| F{Wrong Types?} E --> F F -->|Yes| G[Convert Types] F -->|No| H{Outliers?} G --> H H -->|Yes| I[Process Outliers] H -->|No| J{Inconsistent?} I --> J J -->|Yes| K[Standardize] J -->|No| L[Validate] K --> L L --> M{Quality OK?} M -->|No| B M -->|Yes| N[Complete] style A fill:#aaf,stroke:#333,stroke-width:2px style N fill:#afa,stroke:#333,stroke-width:2px style M fill:#ffa,stroke:#333,stroke-width:2px

๐Ÿ““ Learning Journal

Keep a learning journal โ€” digital or physical. After this lesson, take a few minutes to write down:

  • Key concepts you learned
  • Techniques that clicked for you
  • Questions or confusion points to revisit
  • Ideas you want to try
  • Your progress and feelings about learning this

โœ๏ธ This lesson's prompt: Data scientists spend up to 80% of their time cleaning data. Think of a messy dataset (a spreadsheet, an export, a survey) you've encountered โ€” which cleaning step from this lesson would have saved you the most time, and why?

๐Ÿ“ Lesson Summary

๐ŸŽ“ Key Takeaways

  • Always assess quality first โ€” count missing values, duplicates, and wrong types before touching the data.
  • There is no single "right" fix for missing data; drop, fill, interpolate, or impute based on what the data means.
  • Use errors='coerce' when converting types so bad values become NaN instead of crashing your pipeline.
  • Work on a copy, validate after each step, and document your cleaning so results are reproducible.

๐ŸŽ‰ What You've Accomplished

You can now take a chaotic, real-world dataset โ€” with missing values, duplicates, mismatched types, outliers, and inconsistent text โ€” and turn it into a clean, validated table ready for analysis. That's the skill professionals spend most of their day using.

โ“ Common Questions at This Stage

Should I drop rows with missing values or fill them in?

It depends on how much is missing and why. Dropping is safe when only a few rows are affected and the loss won't bias results; filling (mean, median, forward-fill, or model-based imputation) preserves sample size but can introduce bias if done carelessly. Always ask what the missingness represents.

How do I know if an outlier is an error or a real extreme value?

Statistics (Z-score, IQR) flag candidates, but only domain knowledge decides. A 130-year-old age is clearly an error; a very large purchase might be a legitimate whale customer. Investigate before removing.

Why convert with pd.to_numeric(..., errors='coerce') instead of astype(float)?

astype(float) throws an error the moment it hits a value like 'thirty', halting your whole script. errors='coerce' turns those unparseable values into NaN, letting you handle them deliberately as missing data.

๐Ÿ”ญ Looking Ahead

With clean data in hand, the next step is combining datasets โ€” bringing together tables from different sources through merges and joins so you can answer richer questions.

โœ… Before the Next Lesson

๐ŸŒŸ Encouragement for the Journey

Cleaning data isn't glamorous, but it's where good analysis is won or lost. Every messy dataset you tame makes you a sharper, more trustworthy data scientist. Keep polishing those rough diamonds!