Cleaning & Transforming Data
Real datasets arrive with wrong dtypes, missing values, and messy strings. Cleaning makes aggregates trustworthy before you merge or model.
Search across all documentation pages
Real datasets arrive with wrong dtypes, missing values, and messy strings. Cleaning makes aggregates trustworthy before you merge or model.
Quick-reference recipe card - copy-paste ready.
import pandas as pd
df = pd.read_csv("raw.csv", dtype={"region": "category"})
df["revenue"] = pd.to_numeric(df["revenue"], errors="coerce")
df["sku"] = df["sku"].str.strip().str.upper()
df = df.dropna(subset=["revenue"])When to reach for this:
import pandas as pd
import numpy as np
from io import StringIO
raw = """order_id,region,revenue,sku,ordered_at
1, east , $120 , abc-1 , 01/15/2025
2,WEST,340,def-2,2025-01-16
3,EAST,N/A,abc-1,2025-01-17
"""
df = pd.read_csv(StringIO(raw), dtype={"region": "string[pyarrow]"})
# Strip and normalize text keys
df["region"] = df["region"].str.strip().str.title()
df["sku"] = df["sku"].str.upper()
# Parse money and dates
df["revenue"] = (
df["revenue"]
.str.replace("$", "", regex=False)
.str.replace(",", "", regex=False)
.replace("N/A", np.nan)
)
df["revenue"] = pd.to_numeric(df["revenue"], errors="coerce")
df["ordered_at"] = pd.to_datetime(df["ordered_at"], format="mixed", utc=True)
# Impute with documented policy: median by region
df["revenue"] = df.groupby("region", observed=True)["revenue"].transform(
lambda s: s.fillna(s.median())
)
# Drop rows still missing critical keys
clean = df.dropna(subset=["order_id", "ordered_at"]).astype({"order_id": "int64"})
print(clean.dtypes)
print(clean)What this demonstrates:
str accessor chains for whitespace and casingto_numeric(errors="coerce") turning bad tokens into NANaN for floats, pd.NA for nullable dtypes, or None in object columns.astype may copy; convert_dtypes() upgrades to nullable pandas extension types.| Task | API |
|---|---|
| Parse numbers | pd.to_numeric(..., errors="coerce") |
| Parse dates | pd.to_datetime(..., utc=True) |
| Clip outliers | s.clip(lower, upper) |
| Rename columns | df.rename(columns={"old": "new"}) |
| Dedupe rows | df.drop_duplicates(subset=[...]) |
import pandas as pd
# Prefer nullable Int64 when NA is possible
df["units"] = pd.array([1, None, 3], dtype="Int64")
# map for small domain replacements
df["status"] = df["status"].map({"A": "active", "I": "inactive"})df.dropna(inplace=True) on a slice corrupts parent. Fix: assign back: df = df.dropna().$ and . need regex=False or escaping. Fix: pass regex=False for literal replacements.01/02/2025 parses differently by locale. Fix: enforce ISO "%Y-%m-%d" at source or use format="mixed" with audit."East" and "east " become different categories before strip. Fix: normalize strings before astype("category").| Alternative | Use When | Don't Use When |
|---|---|---|
| Pandera | Schema validation in CI | Quick notebook scrub |
| Great Expectations | Production data contracts | One-off CSV cleanup |
Polars with_columns | Large lazy pipelines | Team only knows pandas |
| OpenRefine / SQL | Human-in-loop cleanup at scale | Scripted repeatable ETL |
dupes = df[df.duplicated("order_id", keep=False)]NaN instead of raising.dropna or a quarantine table for bad rows.df.columns = df.columns.str.strip().str.lower().str.replace(" ", "_")df[["city", "state"]] = df["location"].str.split(",", expand=True)for chunk in pd.read_csv("huge.csv", chunksize=100_000):
process(chunk)Q3 + 1.5*IQR - flag, do not silently drop without review.pd.NA is pandas scalar missing for nullable extension dtypes.np.nan is float missing - propagates in float columns.df["revenue"].isna().sum() == 0 after policy.Stack versions: This page was written for Python 3.14.0 (stable 3.14, maintenance 3.13), FastAPI 0.115+, Django 5.2, Flask 3.1, Pydantic 2, PyTorch 2.6+, pandas 2.2+, Polars 1.x, ruff 0.9+, and uv 0.6+.
Reviewed by Chris St. John·Last updated Jul 16, 2026