Data Cleaning: Making Dirty Data Usable
After completing this topic, you will be able to:
Identify and handle missing values, duplicates, and outliers, which are common problems in real-world data.
Why is data cleaning necessary?
Real-world data is not clean. There are missing survey responses, the same person entered twice, and a row with an age of -5. If you analyze this data as is, you cannot trust the results.
It is said that 60-80% of data analysis work is spent on cleaning. This step is much more important than fancy analysis techniques.
Step 1: Understand the overall data
import pandas as pd
df = pd.read_csv("patients.csv")
# Basic informationprint(df.shape) # (1000, 8) - 1000 rows, 8 columnsprint(df.dtypes) # Data type of each columnprint(df.info()) # Summary, including missing value status
# Check the first/lastprint(df.head()) # Top 5 rowsprint(df.tail()) # Bottom 5 rows
# Statistical summaryprint(df.describe()) # Mean, standard deviation, minimum/maximum, etc. for numeric columnsWhen you run df.info(), you will see the Non-Null Count. If it is less than the total number of rows, it means there are missing values. In df.describe(), if min/max is outside the reasonable range, suspect outliers.
Step 2: Handling missing values
# Check for missing valuesprint(df.isnull().sum())# name 0# age 15# blood_type 3# weight 42
# Strategy 1: Deletion - when the number of missing rows is smalldf_clean = df.dropna(subset=["blood_type"]) # Delete 3 rows with missing blood_type
# Strategy 2: Imputation - for numeric columnsdf["age"] = df["age"].fillna(df["age"].median()) # Impute with the mediandf["weight"] = df["weight"].fillna(df["weight"].mean()) # Impute with the mean
# Strategy 3: Impute with a specific valuedf["blood_type"] = df["blood_type"].fillna("Unknown")Which strategy to use depends on the context.
- If the missing value ratio is less than 5%, deletion is the safest.
- For numeric values, impute with the median (robust to outliers) or the mean.
- For categorical values (blood type, gender), impute with the mode or "Unknown."
Step 3: Removing duplicates
# Check for duplicatesprint(df.duplicated().sum()) # 23 duplicate rows
# View duplicate entriesprint(df[df.duplicated(keep=False)]) # Show both original and duplicate
# Remove duplicates (keep the first)df = df.drop_duplicates()
# Remove duplicates based on a specific columndf = df.drop_duplicates(subset=["patient_id"], keep="last")keep="first" keeps the first row and deletes the rest. keep="last" keeps the last. If the data is time-series, last keeps the latest record.
Step 4: Detecting outliers
# Check with basic statisticsprint(df["age"].describe())# min: -5 <- Abnormal# max: 200 <- Abnormal
# Range filteringdf = df[(df["age"] >= 0) & (df["age"] <= 120)]
# IQR method - statistical outlier detectionQ1 = df["weight"].quantile(0.25)Q3 = df["weight"].quantile(0.75)IQR = Q3 - Q1lower = Q1 - 1.5 * IQRupper = Q3 + 1.5 * IQR
outliers = df[(df["weight"] < lower) | (df["weight"] > upper)]print(f"Found {len(outliers)} outliers")
# Remove outliersdf = df[(df["weight"] >= lower) & (df["weight"] <= upper)]You should not always delete outliers. An age of -5 is clearly an error and should be deleted, but a weight of 150kg may actually exist. Domain knowledge is key to judging outliers.
Step 5: Type conversion and format unification
# Convert numeric strings to numeric typesdf["age"] = pd.to_numeric(df["age"], errors="coerce")
# Convert date strings to datetimedf["visit_date"] = pd.to_datetime(df["visit_date"])
# Clean up strings - remove leading/trailing spaces, unify casedf["name"] = df["name"].str.strip()df["blood_type"] = df["blood_type"].str.upper()errors="coerce" converts values that cannot be converted (e.g., "N/A") to NaN. This allows you to proceed without errors, which is useful for large datasets.
Key takeaways
| Step | Check | Main tool |
|---|---|---|
| Overall understanding | Number of rows/columns, type, missing value status | info(), describe() |
| Missing values | Check ratio -> Delete or impute | isnull(), fillna(), dropna() |
| Duplicates | Duplicate rows or based on key | duplicated(), drop_duplicates() |
| Outliers | Reasonable range + IQR | describe(), range filtering |
| Type/Format | Unify numeric, date, and string | to_numeric(), to_datetime(), str.strip() |
Data cleaning is a repetitive and tedious task, but skipping this step will undermine all subsequent analysis. "Garbage in, garbage out" - if the input is garbage, the output will be garbage.