Drive an AI assistant through profiling, cleaning, and analyzing a 200,000-row dataset at full speed — and prove you can still catch every defect the data (and the assistant) tries to smuggle past you. The dataset carries three planted defects: a leaky column, corrupted dates, and duplicated customer entities.
Your job is not to find those defects by reading this page; it is to run an assistant-accelerated EDA that surfaces all three, then ship a findings memo a stakeholder could act on. This is the verification discipline that separates "uses AI" from "trusts AI" — the 2025 Stack Overflow survey's top frustration is AI output that is almost right.
~90 minutes hands-on · Core tier.
Works on a laptop, Colab, or Kaggle — no GPU, no API spend. You need Python 3.10+, pandas, numpy, scikit-learn, plus any AI assistant (Claude, ChatGPT, Copilot Chat, or an agentic tool like Claude Code/Cursor).
Save and run this generator. It is deterministic (seed 42), so your numbers will match the Verification section exactly.
"""make_messy_orders.py — deterministic 200k-row orders dataset with planted defects.
Requires: pandas, numpy. Run: python make_messy_orders.py -> orders_raw.csv"""
import numpy as np
import pandas as pd
rng = np.random.default_rng(42)
N_CUST, N_ORD = 40_000, 200_000
first = np.array("Asha Ravi Meera Karan Divya Arjun Sneha Vikram Priya Rahul".split())
last = np.array("Sharma Iyer Patel Reddy Nair Gupta Das Mehta Kulkarni Singh".split())
cust_id = np.array([f"C{100000+i}" for i in range(N_CUST)])
name = rng.choice(first, N_CUST) + " " + rng.choice(last, N_CUST)
email = np.char.add(np.char.replace(np.char.lower(name.astype(str)), " ", "."),
np.char.add(".", np.char.lower(cust_id.astype(str))))
email = np.char.add(email, "@example.com")
# 1,200 customers get a doppelganger id whose email differs only by
# case/whitespace and whose name has a trailing space.
dup_src = rng.choice(N_CUST, 1200, replace=False)
dup_id = np.array([f"C9{str(i).zfill(5)}" for i in range(1200)])
cust_id = np.concatenate([cust_id, dup_id])
name = np.concatenate([name, name[dup_src] + " "])
email = np.concatenate([email, np.char.add(np.char.upper(email[dup_src].astype(str)), " ")])
idx = rng.integers(0, len(cust_id), N_ORD)
cat = rng.choice(["electronics", "apparel", "home", "beauty", "sports"], N_ORD,
p=[.30, .25, .20, .15, .10])
price = np.round(rng.gamma(2.2, 900, N_ORD) + 99, 2)
qty = rng.integers(1, 6, N_ORD)
disc = rng.choice([0, 5, 10, 20, 30], N_ORD, p=[.45, .2, .15, .12, .08])
ship = rng.integers(1, 15, N_ORD).astype(float)
ship[rng.choice(N_ORD, 4000, replace=False)] = np.nan # ordinary missingness
day = rng.integers(0, 365, N_ORD)
dates = (pd.Timestamp("2025-01-01") + pd.to_timedelta(day, "D")).strftime("%Y-%m-%d").to_numpy(dtype=object)
p = .06 + .10 * (cat == "apparel") + .04 * (cat == "electronics") \
+ .003 * disc + .008 * np.nan_to_num(ship, nan=7)
returned = (rng.random(N_ORD) < p).astype(int)
# refund ticket is opened AFTER a return happens (2% noise)
leak = returned.copy()
flip = rng.choice(N_ORD, int(.02 * N_ORD), replace=False)
leak[flip] = 1 - leak[flip]
# 3,000 rows get unparseable date strings
bad = rng.choice(N_ORD, 3000, replace=False)
dates[bad[:1000]] = "2025-02-30"
dates[bad[1000:2000]] = "31/11/2025"
dates[bad[2000:]] = "20251399"
pd.DataFrame({
"order_id": [f"O{i:07d}" for i in range(N_ORD)],
"customer_id": cust_id[idx], "customer_name": name[idx], "email": email[idx],
"order_date": dates, "category": cat, "unit_price": price, "quantity": qty,
"discount_pct": disc, "ship_days": ship, "refund_ticket_opened": leak,
"returned": returned,
}).to_csv("orders_raw.csv", index=False)
print("wrote orders_raw.csv:", N_ORD, "rows")
Run it once, confirm the CSV exists, then do not re-read the generator. From here on you work only with orders_raw.csv, exactly as you would with a client extract of unknown provenance.
The generator source is this lab's answer key. Reading it mid-lab is the same as reading the answers.
returned at order time?" Prompt: "You are assisting with EDA on orders_raw.csv (200k rows, columns listed below). Before any modeling, produce a data-quality audit plan: what checks would you run on identifiers, dates, categoricals, numerics, and the target? Output a numbered checklist." Paste in df.dtypes and df.head(3). You want its plan, not its conclusions — you will execute the checks.pd.to_datetime(df.order_date, format='%Y-%m-%d', errors='coerce') and reports the count and distinct values of unparseable entries." A generic .describe() will not surface these; strict-format parsing will.returned (Cramer's V or point-biserial). Flag anything suspiciously high and explain why a feature could be leakage given that we predict at order time." Interrogate the top hit: could refund_ticket_opened exist before the return event exists? Ask the assistant to argue both sides, then decide yourself.Run every assistant-generated snippet before believing its narrative. The snippet is evidence; the narrative is a hypothesis.
Objective checks against the seeded ground truth — your numbers should match:
2025-02-30, 31/11/2025, 20251399).refund_ticket_opened agrees with returned on 98% of rows; AUC with the column is ≥0.97, without it lands near 0.55–0.60.ship_days.eda-findings.md — findings memo + defect log + link to your reproducible notebook/script. Self-grading rubric (pass = 8/10):
refund_ticket_opened looks like a legitimately great feature. Ask the temporal question: what is knowable at prediction time? Any feature that post-dates the outcome must go.dtype hints or use low_memory=False deliberately.On client data, run the identical protocol with the client-approved assistant (Bedrock/Azure-hosted, or a local model via Ollama) and never paste raw rows into a personal free-tier chat — schema, aggregates, and synthetic examples only. The planted-defect drill above is also a safe onboarding exercise for client teams: regenerate with a different seed and use it to calibrate how much your team over-trusts AI profiling.
refund_ticket_opened — created by copying returned and flipping 2% of rows. Detection: 0.98 agreement; AUC ~0.99 with it, ~0.55–0.60 without.2025-02-30 (impossible day), 31/11/2025 (wrong format + impossible day), 20251399 (unparseable). Detection: strict pd.to_datetime(..., format="%Y-%m-%d", errors="coerce").C9xxxxx IDs; emails differ only by upper-casing and a trailing space; names carry a trailing space. Detection: group by email.str.strip().str.lower() and count distinct customer_id.