df.isna().sum() # missing count per column
df.isna().mean() * 100 # percentage per column
df[df["email"].isna()] # rows where email is missing
df.dropna() # drop any row with any NaN
df.dropna(subset=["email", "customer"]) # only drop if those cols are NaN
df.dropna(axis=1) # drop columns with NaN
df.dropna(thresh=5) # keep rows with ≥5 non-null values
df["age"] = df["age"].fillna(df["age"].median())
df["country"] = df["country"].fillna("Unknown")
# Fill different columns differently
df = df.fillna({"age": 0, "country": "Unknown", "score": df["score"].mean()})
df["price"] = df["price"].ffill() # carry last known forward
df["price"] = df["price"].bfill() # use the next known backward
df.isna().sum().sort_values(ascending=False).head(10)
df = df.dropna(subset=["respondent_id", "submitted_at"])
df["age"] = df["age"].fillna(df["age"].median())
df["region"] = df["region"].fillna("Unknown")
df["score"] = df["score"].fillna(0)
isna().sum() shows you where the holes are.dropna(subset=[...]) is safer than blanket dropna.fillna() can take a single value, a dict, or a forward/back fill.Load any messy CSV. Print a table of column name, missing count, and missing percentage, sorted by worst first.