Day 15: Data Cleaning: Missing, Duplicates, Types
✅ Today you will learn
- Build a data-quality report in 5 lines
- Missing values: drop, fill with median/mode, flag
- Duplicates: find and remove
- Fix text (case, spaces, M/F) and wrong types
- Handle impossible values and mixed date formats
🏢 In the real world
The BazaarKart warehouse exported 324 orders from an old system. Cities are written as “DELHI” and “ Delhi ”, quantity says “2 pcs”, some prices are −1 and a few customers are 250 years old. Data scientists spend up to 60% of their time on exactly this – today you learn to do it quickly and safely.
Step 1: a data-quality report
import pandas as pd
raw = pd.read_csv("sales_dirty.csv")
print("Shape:", raw.shape)
print("Duplicate rows:", raw.duplicated().sum())
quality = pd.DataFrame({"dtype": raw.dtypes.astype(str),
"missing": raw.isna().sum(),
"unique": raw.nunique()})
print(quality)quantity is object (text) – it should be a number. city has more than 11 unique values – case/space problems. customer_age and rating have missing values, and there are 12 duplicate rows.
- 1Copy raw data
- 2Remove duplicates
- 3Fix text
- 4Fix types
- 5Fix impossible values
- 6Fill missing
- 7Validate & save
Step 2: duplicates
import pandas as pd
raw = pd.read_csv("sales_dirty.csv")
df = raw.copy() # never edit the raw data
print(df[df.duplicated(keep=False)].sort_values("order_id").head(4)[["order_id", "city", "unit_price"]])
df = df.drop_duplicates()
print("After drop_duplicates:", len(df), "| repeated order_ids left:", df["order_id"].duplicated().sum())Step 3: text problems
import pandas as pd
df = pd.read_csv("sales_dirty.csv").drop_duplicates()
print("Before:", sorted(df["city"].unique())[:6])
df["city"] = df["city"].str.strip().str.title()
df["payment_mode"] = df["payment_mode"].str.strip().str.title().replace({"Upi": "UPI", "Cash On Delivery": "Cash on Delivery"})
df["customer_gender"] = df["customer_gender"].replace({"M": "Male", "F": "Female"})
print("After :", sorted(df["city"].unique()))
print(df["payment_mode"].value_counts().to_dict())
print(df["customer_gender"].value_counts().to_dict())Step 4: wrong data types
import pandas as pd
df = pd.read_csv("sales_dirty.csv").drop_duplicates()
print(df["quantity"].unique())
df["quantity"] = df["quantity"].str.replace("pcs", "").str.strip().astype(int)
print(df["quantity"].dtype, df["quantity"].sum())
print(df.loc[df["order_date"].str.contains("/"), "order_date"].head(3).tolist())
iso = pd.to_datetime(df["order_date"], format="%Y-%m-%d", errors="coerce")
dmy = pd.to_datetime(df["order_date"], format="%d/%m/%Y", errors="coerce")
df["order_date"] = iso.fillna(dmy)
print(df["order_date"].dtype, "| unparsed:", df["order_date"].isna().sum())pd.to_numeric(x, errors="coerce") and pd.to_datetime(x, errors="coerce") turn anything they cannot read into NaN/NaT instead of crashing – then you can count and inspect those rows.
Step 5: impossible values
import pandas as pd
import numpy as np
df = pd.read_csv("sales_dirty.csv").drop_duplicates()
print("Negative prices:", (df["unit_price"] <= 0).sum(), "| ages > 100:", (df["customer_age"] > 100).sum())
df.loc[df["unit_price"] <= 0, "unit_price"] = np.nan
df.loc[df["customer_age"] > 100, "customer_age"] = np.nan
# a missing price can be filled from the same product's median price
df["unit_price"] = df["unit_price"].fillna(df.groupby("product")["unit_price"].transform("median"))
print("Prices still missing:", df["unit_price"].isna().sum(), "| min price:", df["unit_price"].min())Step 6: missing values – three strategies
import pandas as pd
df = pd.read_csv("sales_dirty.csv").drop_duplicates()
df.loc[df["customer_age"] > 100, "customer_age"] = None
print(df[["customer_age", "rating"]].isna().sum().to_dict())
df["age_missing"] = df["customer_age"].isna().astype(int) # flag
df["customer_age"] = df["customer_age"].fillna(df["customer_age"].median()) # fill
df = df.dropna(subset=["rating"]) # drop (only 3 rows)
print(df[["customer_age", "rating"]].isna().sum().to_dict(), "| rows:", len(df), "| flagged:", df["age_missing"].sum())Putting it together: one reusable function
import pandas as pd
import numpy as np
def clean_sales(path):
df = pd.read_csv(path).drop_duplicates().copy()
df["city"] = df["city"].str.strip().str.title()
df["payment_mode"] = df["payment_mode"].str.strip().str.title().replace({"Upi": "UPI", "Cash On Delivery": "Cash on Delivery"})
df["customer_gender"] = df["customer_gender"].replace({"M": "Male", "F": "Female"})
df["quantity"] = pd.to_numeric(df["quantity"].str.replace("pcs", ""), errors="coerce")
d1 = pd.to_datetime(df["order_date"], format="%Y-%m-%d", errors="coerce")
df["order_date"] = d1.fillna(pd.to_datetime(df["order_date"], format="%d/%m/%Y", errors="coerce"))
df.loc[df["unit_price"] <= 0, "unit_price"] = np.nan
df["unit_price"] = df["unit_price"].fillna(df.groupby("product")["unit_price"].transform("median"))
df.loc[df["customer_age"] > 100, "customer_age"] = np.nan
df["age_missing"] = df["customer_age"].isna().astype(int)
df["customer_age"] = df["customer_age"].fillna(df["customer_age"].median())
df = df.dropna(subset=["rating"])
df["revenue"] = (df["quantity"] * df["unit_price"] * (1 - df["discount"])).round(2)
return df
clean = clean_sales("sales_dirty.csv")
assert clean.duplicated().sum() == 0 and clean["unit_price"].min() > 0
assert clean["city"].nunique() == 11 and clean["customer_age"].max() <= 100
print(clean.shape, "| total revenue ₹", round(clean["revenue"].sum()))
print(clean.dtypes.astype(str).to_dict())
clean.to_csv("sales_clean.csv", index=False)Next month the warehouse sends a new messy file. With clean_sales() you clean it in one line, the same way every time – and the assert lines stop you if something new is wrong.
🏅 Job skills unlocked today
Practice tasks
🏋️ Practice – do it yourself
telecom_churn.csv, total_charges has missing values. Fill them with monthly_charges × tenure_months and confirm none remain.Show solution
import pandas as pd
t = pd.read_csv("telecom_churn.csv")
print("Missing before:", t["total_charges"].isna().sum())
t["total_charges"] = t["total_charges"].fillna(t["monthly_charges"] * t["tenure_months"])
print("Missing after:", t["total_charges"].isna().sum())sales_data.csv, fill missing ratings with the median rating of the same category.Show solution
import pandas as pd
s = pd.read_csv("sales_data.csv")
s["rating"] = s["rating"].fillna(s.groupby("category")["rating"].transform("median"))
print(s["rating"].isna().sum(), s["rating"].mean().round(3))quality_report(df) that returns dtype, missing count, missing % and unique count for every column.Show solution
import pandas as pd
def quality_report(df):
return pd.DataFrame({"dtype": df.dtypes.astype(str), "missing": df.isna().sum(),
"missing_%": (df.isna().mean() * 100).round(1), "unique": df.nunique()})
print(quality_report(pd.read_csv("telecom_churn.csv")))Quick quiz
🧠 Quick Quiz – test yourself
Interview corner
💼 Interview Corner
How do you handle missing values?
First measure how many and whether they are random. Drop rows if very few and random; fill numbers with the median (or group median) and categories with the mode or "Unknown"; add a missing-flag column when missingness may carry information; for ML, do the filling inside a pipeline fitted only on training data.
How would you detect impossible values?
Use domain rules (price > 0, 0 < age < 100, dates within range), describe() to see min/max, value_counts() for categories, and assert statements in the cleaning code so future files are checked automatically.
Summary
- Always start with df = raw.copy() and a quality report.
- drop_duplicates, str.strip().str.title(), replace for codes like M/F.
- to_numeric / to_datetime with errors="coerce" fix types safely.
- Missing: drop, fill (median/mode/group) or flag – decide per column.
- Wrap cleaning in a function with assert checks.
Day 16: dates and text – extract month, weekday and festive season, and clean text with the .str toolkit.
