Spaces:
Sleeping
Sleeping
| """ | |
| Task definitions for the CSV Cleaner Environment. | |
| Each task generates a deterministic messy dataset (given a seed) and defines | |
| a target clean dataset plus a grading function that returns a score in [0, 1]. | |
| """ | |
| import random | |
| from dataclasses import dataclass, field | |
| from typing import Callable, Dict, List, Optional | |
| import pandas as pd | |
| class TaskDefinition: | |
| """Definition of a single cleaning task.""" | |
| name: str | |
| description: str | |
| difficulty: str # easy, medium, hard | |
| max_steps: int | |
| generate_messy: Callable[[int], pd.DataFrame] | |
| generate_target: Callable[[int], pd.DataFrame] | |
| grade: Callable[[pd.DataFrame, pd.DataFrame], float] | |
| checklist: List[str] = field(default_factory=list) | |
| # --------------------------------------------------------------------------- | |
| # Helpers | |
| # --------------------------------------------------------------------------- | |
| def _score_column_types(current: pd.DataFrame, target: pd.DataFrame) -> float: | |
| """Score how many column types match the target.""" | |
| if current.empty or target.empty: | |
| return 0.0 | |
| matching = 0 | |
| total = 0 | |
| for col in target.columns: | |
| if col in current.columns: | |
| total += 1 | |
| # Compare dtype kind (i=int, f=float, O=object, M=datetime) | |
| if current[col].dtype.kind == target[col].dtype.kind: | |
| matching += 1 | |
| else: | |
| total += 1 | |
| return matching / total if total > 0 else 0.0 | |
| def _score_null_counts(current: pd.DataFrame, target: pd.DataFrame) -> float: | |
| """Score how close null counts are to target.""" | |
| if current.empty or target.empty: | |
| return 0.0 | |
| scores = [] | |
| for col in target.columns: | |
| if col in current.columns: | |
| target_nulls = target[col].isnull().sum() | |
| current_nulls = current[col].isnull().sum() | |
| if target_nulls == 0: | |
| scores.append(1.0 if current_nulls == 0 else max(0.0, 1.0 - current_nulls / max(len(current), 1))) | |
| else: | |
| scores.append(1.0 - min(1.0, abs(current_nulls - target_nulls) / max(len(current), 1))) | |
| return sum(scores) / len(scores) if scores else 0.0 | |
| def _score_duplicates(current: pd.DataFrame, target: pd.DataFrame) -> float: | |
| """Score duplicate removal progress.""" | |
| target_dups = target.duplicated().sum() | |
| current_dups = current.duplicated().sum() | |
| if target_dups == 0: | |
| if current_dups == 0: | |
| return 1.0 | |
| return max(0.0, 1.0 - current_dups / max(len(current), 1)) | |
| return 1.0 - min(1.0, abs(current_dups - target_dups) / max(len(current), 1)) | |
| def _score_row_count(current: pd.DataFrame, target: pd.DataFrame) -> float: | |
| """Score how close row count is to target.""" | |
| if len(target) == 0: | |
| return 1.0 if len(current) == 0 else 0.0 | |
| diff = abs(len(current) - len(target)) | |
| return max(0.0, 1.0 - diff / max(len(target), 1)) | |
| def _score_column_names(current: pd.DataFrame, target: pd.DataFrame) -> float: | |
| """Score how many column names match the target.""" | |
| target_cols = set(target.columns) | |
| current_cols = set(current.columns) | |
| if not target_cols: | |
| return 1.0 | |
| return len(target_cols & current_cols) / len(target_cols) | |
| # --------------------------------------------------------------------------- | |
| # Task 1: Easy — Fix Column Types | |
| # --------------------------------------------------------------------------- | |
| def _easy_generate_messy(seed: int) -> pd.DataFrame: | |
| """Generate a dataset with wrong column types.""" | |
| rng = random.Random(seed) | |
| n = 20 | |
| data = { | |
| "employee_id": [str(rng.randint(1000, 9999)) for _ in range(n)], | |
| "name": [rng.choice(["Alice", "Bob", "Charlie", "Diana", "Eve", "Frank", "Grace", "Hank"]) for _ in range(n)], | |
| "age": [str(rng.randint(22, 65)) for _ in range(n)], | |
| "salary": [f"{rng.uniform(30000, 120000):.2f}" for _ in range(n)], | |
| "join_date": [f"2{rng.randint(0, 0)}2{rng.randint(0, 4)}-{rng.randint(1, 12):02d}-{rng.randint(1, 28):02d}" for _ in range(n)], | |
| "department": [rng.choice(["Engineering", "Sales", "Marketing", "HR", "Finance"]) for _ in range(n)], | |
| } | |
| return pd.DataFrame(data) | |
| def _easy_generate_target(seed: int) -> pd.DataFrame: | |
| """Generate the target clean dataset for task 1.""" | |
| df = _easy_generate_messy(seed) | |
| df["employee_id"] = df["employee_id"].astype(int) | |
| df["age"] = df["age"].astype(int) | |
| df["salary"] = df["salary"].astype(float) | |
| df["join_date"] = pd.to_datetime(df["join_date"]) | |
| return df | |
| def _easy_grade(current: pd.DataFrame, target: pd.DataFrame) -> float: | |
| """Grade task 1: type matching is the primary objective.""" | |
| type_score = _score_column_types(current, target) | |
| row_score = _score_row_count(current, target) | |
| return 0.8 * type_score + 0.2 * row_score | |
| # --------------------------------------------------------------------------- | |
| # Task 2: Medium — Clean Missing Values + Remove Duplicates | |
| # --------------------------------------------------------------------------- | |
| def _medium_generate_messy(seed: int) -> pd.DataFrame: | |
| """Generate a dataset with missing values and duplicates.""" | |
| rng = random.Random(seed) | |
| n = 30 | |
| base_data = [] | |
| for i in range(n): | |
| row = { | |
| "product_id": i + 1, | |
| "product_name": rng.choice(["Widget A", "Widget B", "Gadget X", "Gadget Y", "Tool M", "Tool N"]), | |
| "category": rng.choice(["Electronics", "Hardware", "Software", "Accessories"]), | |
| "price": round(rng.uniform(5.0, 500.0), 2), | |
| "stock": rng.randint(0, 1000), | |
| } | |
| # Inject nulls | |
| if rng.random() < 0.2: | |
| row["price"] = None | |
| if rng.random() < 0.15: | |
| row["category"] = None | |
| if rng.random() < 0.1: | |
| row["stock"] = None | |
| base_data.append(row) | |
| # Inject duplicates (copy ~5 random rows) | |
| for _ in range(5): | |
| idx = rng.randint(0, len(base_data) - 1) | |
| base_data.append(base_data[idx].copy()) | |
| rng.shuffle(base_data) | |
| return pd.DataFrame(base_data) | |
| def _medium_generate_target(seed: int) -> pd.DataFrame: | |
| """Generate the target clean dataset for task 2.""" | |
| df = _medium_generate_messy(seed) | |
| # Fill missing price with median | |
| median_price = df["price"].median() | |
| df["price"] = df["price"].fillna(median_price) | |
| # Fill missing category with mode | |
| mode_cat = df["category"].mode()[0] if not df["category"].mode().empty else "Unknown" | |
| df["category"] = df["category"].fillna(mode_cat) | |
| # Fill missing stock with 0 | |
| df["stock"] = df["stock"].fillna(0).astype(int) | |
| # Drop duplicates | |
| df = df.drop_duplicates().reset_index(drop=True) | |
| return df | |
| def _medium_grade(current: pd.DataFrame, target: pd.DataFrame) -> float: | |
| """Grade task 2: null handling + duplicate removal.""" | |
| null_score = _score_null_counts(current, target) | |
| dup_score = _score_duplicates(current, target) | |
| row_score = _score_row_count(current, target) | |
| return 0.4 * null_score + 0.35 * dup_score + 0.25 * row_score | |
| # --------------------------------------------------------------------------- | |
| # Task 3: Hard — Full Pipeline | |
| # --------------------------------------------------------------------------- | |
| def _hard_generate_messy(seed: int) -> pd.DataFrame: | |
| """Generate a dataset needing the full cleaning pipeline.""" | |
| rng = random.Random(seed) | |
| n = 40 | |
| base_data = [] | |
| for i in range(n): | |
| row = { | |
| "cust_id": str(rng.randint(10000, 99999)), | |
| " Full Name ": rng.choice([ | |
| " John Smith ", "Alice Johnson", " Bob Williams ", | |
| "Charlie Brown", " Diana Ross", "Eve Davis ", | |
| "Frank Miller", " Grace Lee ", | |
| ]), | |
| "email_addr": rng.choice([ | |
| "john@example.com", "alice@test.com", "bob@demo.com", | |
| "charlie@sample.org", "diana@mail.com", "INVALID", | |
| "eve@test.com", "frank@example.com", | |
| ]), | |
| "purchase_amt": f"${rng.uniform(10, 5000):.2f}" if rng.random() > 0.15 else str(round(rng.uniform(10, 5000), 2)), | |
| "rating": str(rng.randint(1, 5)) if rng.random() > 0.1 else None, | |
| "signup_date": f"2{rng.randint(0, 0)}2{rng.randint(0, 4)}-{rng.randint(1, 12):02d}-{rng.randint(1, 28):02d}" if rng.random() > 0.1 else None, | |
| "status": rng.choice(["active", "Active", "ACTIVE", "inactive", "Inactive", "INACTIVE"]), | |
| } | |
| # Inject some nulls | |
| if rng.random() < 0.12: | |
| row["email_addr"] = None | |
| base_data.append(row) | |
| # Inject duplicates | |
| for _ in range(6): | |
| idx = rng.randint(0, len(base_data) - 1) | |
| base_data.append(base_data[idx].copy()) | |
| rng.shuffle(base_data) | |
| return pd.DataFrame(base_data) | |
| def _hard_generate_target(seed: int) -> pd.DataFrame: | |
| """Generate the target clean dataset for task 3.""" | |
| df = _hard_generate_messy(seed) | |
| # Rename columns | |
| df = df.rename(columns={ | |
| " Full Name ": "full_name", | |
| "email_addr": "email", | |
| "purchase_amt": "purchase_amount", | |
| "signup_date": "signup_date", | |
| "cust_id": "customer_id", | |
| }) | |
| # Strip whitespace from full_name | |
| df["full_name"] = df["full_name"].str.strip() | |
| # Cast customer_id to int | |
| df["customer_id"] = df["customer_id"].astype(int) | |
| # Clean purchase_amount: remove $ and cast to float | |
| df["purchase_amount"] = df["purchase_amount"].astype(str).str.replace("$", "", regex=False).astype(float) | |
| # Cast rating to int/float, fill missing with median | |
| df["rating"] = pd.to_numeric(df["rating"], errors="coerce") | |
| median_rating = df["rating"].median() | |
| df["rating"] = df["rating"].fillna(median_rating).astype(int) | |
| # Normalize status to lowercase | |
| df["status"] = df["status"].str.lower() | |
| # Fill missing signup_date with a sentinel | |
| df["signup_date"] = pd.to_datetime(df["signup_date"], errors="coerce") | |
| # Fill missing email | |
| df["email"] = df["email"].fillna("unknown@example.com") | |
| # Filter out INVALID emails | |
| df = df[df["email"] != "INVALID"].reset_index(drop=True) | |
| # Drop duplicates | |
| df = df.drop_duplicates().reset_index(drop=True) | |
| return df | |
| def _hard_grade(current: pd.DataFrame, target: pd.DataFrame) -> float: | |
| """Grade task 3: full pipeline.""" | |
| name_score = _score_column_names(current, target) | |
| type_score = _score_column_types(current, target) | |
| null_score = _score_null_counts(current, target) | |
| dup_score = _score_duplicates(current, target) | |
| row_score = _score_row_count(current, target) | |
| return (0.15 * name_score + 0.25 * type_score + 0.25 * null_score + | |
| 0.15 * dup_score + 0.20 * row_score) | |
| # --------------------------------------------------------------------------- | |
| # Task Registry | |
| # --------------------------------------------------------------------------- | |
| TASKS: Dict[str, TaskDefinition] = { | |
| "fix_column_types": TaskDefinition( | |
| name="fix_column_types", | |
| description=( | |
| "Fix column types in an employee dataset. Columns employee_id, age, " | |
| "salary, and join_date are stored as strings but should be int, int, " | |
| "float, and datetime respectively. Cast them to the correct types." | |
| ), | |
| difficulty="easy", | |
| max_steps=10, | |
| generate_messy=_easy_generate_messy, | |
| generate_target=_easy_generate_target, | |
| grade=_easy_grade, | |
| checklist=[ | |
| "Cast employee_id from string to int", | |
| "Cast age from string to int", | |
| "Cast salary from string to float", | |
| "Cast join_date from string to datetime", | |
| ], | |
| ), | |
| "clean_missing_duplicates": TaskDefinition( | |
| name="clean_missing_duplicates", | |
| description=( | |
| "Clean a product inventory dataset. Fill missing price values with the " | |
| "median, fill missing category with the mode, fill missing stock with 0, " | |
| "then remove all duplicate rows." | |
| ), | |
| difficulty="medium", | |
| max_steps=15, | |
| generate_messy=_medium_generate_messy, | |
| generate_target=_medium_generate_target, | |
| grade=_medium_grade, | |
| checklist=[ | |
| "Fill missing price with median", | |
| "Fill missing category with mode", | |
| "Fill missing stock with 0", | |
| "Remove duplicate rows", | |
| ], | |
| ), | |
| "full_pipeline": TaskDefinition( | |
| name="full_pipeline", | |
| description=( | |
| "Perform a full cleaning pipeline on a customer dataset: " | |
| "(1) Rename ' Full Name ' to 'full_name' and 'email_addr' to 'email' " | |
| "and 'purchase_amt' to 'purchase_amount' and 'cust_id' to 'customer_id'. " | |
| "(2) Strip whitespace from full_name. " | |
| "(3) Cast customer_id to int. " | |
| "(4) Remove '$' from purchase_amount and cast to float. " | |
| "(5) Cast rating to int, fill missing with median. " | |
| "(6) Normalize status to lowercase. " | |
| "(7) Fill missing email with 'unknown@example.com'. " | |
| "(8) Filter out rows where email is 'INVALID'. " | |
| "(9) Remove duplicate rows." | |
| ), | |
| difficulty="hard", | |
| max_steps=20, | |
| generate_messy=_hard_generate_messy, | |
| generate_target=_hard_generate_target, | |
| grade=_hard_grade, | |
| checklist=[ | |
| "Rename columns to clean names", | |
| "Strip whitespace from full_name", | |
| "Cast customer_id to int", | |
| "Clean and cast purchase_amount to float", | |
| "Cast rating to int, fill missing with median", | |
| "Normalize status to lowercase", | |
| "Fill missing email", | |
| "Filter out INVALID emails", | |
| "Remove duplicate rows", | |
| ], | |
| ), | |
| } | |