Spaces:
Sleeping
Sleeping
Upload folder using huggingface_hub
Browse files- Dockerfile +40 -0
- README.md +113 -10
- app.py +297 -0
- config.py +41 -0
- datasets.py +161 -0
- environment.py +363 -0
- graders.py +203 -0
- inference.py +0 -0
- models.py +63 -0
- openenv.yaml +129 -0
- requirements.txt +22 -0
- tasks.py +174 -0
- utils.py +142 -0
Dockerfile
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Dockerfile
|
| 2 |
+
# βββββββββββββββββββββββββββββββββββββββββββββ
|
| 3 |
+
# Data Cleaning OpenEnv β Container Setup
|
| 4 |
+
# βββββββββββββββββββββββββββββββββββββββββββββ
|
| 5 |
+
|
| 6 |
+
FROM python:3.11-slim
|
| 7 |
+
|
| 8 |
+
# Set working directory
|
| 9 |
+
WORKDIR /app
|
| 10 |
+
|
| 11 |
+
# Environment variables
|
| 12 |
+
ENV PYTHONDONTWRITEBYTECODE=1 \
|
| 13 |
+
PYTHONUNBUFFERED=1 \
|
| 14 |
+
PORT=7860
|
| 15 |
+
|
| 16 |
+
# Install system dependencies
|
| 17 |
+
RUN apt-get update && apt-get install -y \
|
| 18 |
+
build-essential \
|
| 19 |
+
curl \
|
| 20 |
+
&& rm -rf /var/lib/apt/lists/*
|
| 21 |
+
|
| 22 |
+
# Copy requirements first (for caching)
|
| 23 |
+
COPY requirements.txt .
|
| 24 |
+
|
| 25 |
+
# Install Python dependencies
|
| 26 |
+
RUN pip install --no-cache-dir --upgrade pip && \
|
| 27 |
+
pip install --no-cache-dir -r requirements.txt
|
| 28 |
+
|
| 29 |
+
# Copy all project files
|
| 30 |
+
COPY . .
|
| 31 |
+
|
| 32 |
+
# Expose port
|
| 33 |
+
EXPOSE 7860
|
| 34 |
+
|
| 35 |
+
# Health check
|
| 36 |
+
HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \
|
| 37 |
+
CMD curl -f http://localhost:7860/ || exit 1
|
| 38 |
+
|
| 39 |
+
# Run the app
|
| 40 |
+
CMD ["python", "app.py"]
|
README.md
CHANGED
|
@@ -1,10 +1,113 @@
|
|
| 1 |
-
|
| 2 |
-
|
| 3 |
-
|
| 4 |
-
|
| 5 |
-
|
| 6 |
-
|
| 7 |
-
|
| 8 |
-
|
| 9 |
-
|
| 10 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Data Cleaning Environment β OpenEnv
|
| 2 |
+
|
| 3 |
+
An OpenEnv-compliant environment where an AI agent cleans messy CSV datasets step by step.
|
| 4 |
+
|
| 5 |
+
## Environment Description
|
| 6 |
+
|
| 7 |
+
The agent receives a messy CSV dataset and must issue cleaning actions to fix issues like missing values, duplicates, outliers, bad column names, and inconsistent values.
|
| 8 |
+
|
| 9 |
+
## Action Space
|
| 10 |
+
|
| 11 |
+
| Action | Parameters | Description |
|
| 12 |
+
|---|---|---|
|
| 13 |
+
| `fill_missing` | column, strategy (mean/median/mode/drop) | Fill null values |
|
| 14 |
+
| `drop_duplicates` | β | Remove duplicate rows |
|
| 15 |
+
| `fix_dtype` | column, target_type (int/float/str) | Convert column dtype |
|
| 16 |
+
| `rename_column` | old_name, new_name | Rename to snake_case |
|
| 17 |
+
| `remove_outliers` | column, method (iqr/zscore) | Remove outlier rows |
|
| 18 |
+
| `standardize_values` | column, mapping (JSON) | Unify inconsistent values |
|
| 19 |
+
| `submit` | β | Finalize and trigger grader |
|
| 20 |
+
|
| 21 |
+
## Observation Space
|
| 22 |
+
|
| 23 |
+
| Field | Type | Description |
|
| 24 |
+
|---|---|---|
|
| 25 |
+
| `task_id` | string | Current task identifier |
|
| 26 |
+
| `difficulty` | string | easy / medium / hard |
|
| 27 |
+
| `step` | int | Current step number |
|
| 28 |
+
| `max_steps` | int | Maximum steps allowed |
|
| 29 |
+
| `dataframe` | array | Current dataset as list of dicts |
|
| 30 |
+
| `columns` | array | Column names |
|
| 31 |
+
| `dtypes` | object | Column dtype mapping |
|
| 32 |
+
| `null_counts` | object | Null count per column |
|
| 33 |
+
| `duplicate_rows` | int | Number of duplicate rows |
|
| 34 |
+
| `issues` | array | Detected issues list |
|
| 35 |
+
| `done` | bool | Episode over? |
|
| 36 |
+
|
| 37 |
+
## Tasks
|
| 38 |
+
|
| 39 |
+
### Task 1 β Easy (score: 0.0β1.0)
|
| 40 |
+
Fix a 10-row employee dataset:
|
| 41 |
+
- Fill missing values in `age` and `salary`
|
| 42 |
+
- Convert `age` from string to integer
|
| 43 |
+
|
| 44 |
+
Baseline score: **0.72**
|
| 45 |
+
|
| 46 |
+
### Task 2 β Medium (score: 0.0β1.0)
|
| 47 |
+
Fix a 20-row dataset:
|
| 48 |
+
- Remove duplicate rows
|
| 49 |
+
- Handle salary outliers (IQR method)
|
| 50 |
+
- Standardize country values β `United States` or `UK`
|
| 51 |
+
|
| 52 |
+
Baseline score: **0.65**
|
| 53 |
+
|
| 54 |
+
### Task 3 β Hard (score: 0.0β1.0)
|
| 55 |
+
Fix a 30-row dataset with all issues:
|
| 56 |
+
- Fix bad column names (trailing spaces, uppercase, special chars)
|
| 57 |
+
- Fill missing values, remove duplicates and outliers
|
| 58 |
+
- Standardize country values
|
| 59 |
+
- Fix invalid `dept_id` values (referential integrity)
|
| 60 |
+
|
| 61 |
+
Baseline score: **0.48**
|
| 62 |
+
|
| 63 |
+
## Reward Function
|
| 64 |
+
|
| 65 |
+
| Event | Reward |
|
| 66 |
+
|---|---|
|
| 67 |
+
| Issue fixed | +0.10 |
|
| 68 |
+
| Clean submit bonus | +0.30 |
|
| 69 |
+
| Wrong/redundant action | -0.05 |
|
| 70 |
+
| Destructive action | -0.10 |
|
| 71 |
+
|
| 72 |
+
## Setup Instructions
|
| 73 |
+
|
| 74 |
+
### Local
|
| 75 |
+
```bash
|
| 76 |
+
git clone https://huggingface.co/spaces/Yashwanth34567/data-cleaning-env
|
| 77 |
+
cd data-cleaning-env
|
| 78 |
+
pip install -r requirements.txt
|
| 79 |
+
python app.py
|
| 80 |
+
```
|
| 81 |
+
|
| 82 |
+
### Docker
|
| 83 |
+
```bash
|
| 84 |
+
docker build -t data-cleaning-env .
|
| 85 |
+
docker run -p 7860:7860 data-cleaning-env
|
| 86 |
+
```
|
| 87 |
+
|
| 88 |
+
### Environment Variables
|
| 89 |
+
```bash
|
| 90 |
+
API_BASE_URL=https://api-inference.huggingface.co/v1
|
| 91 |
+
MODEL_NAME=mistralai/Mistral-7B-Instruct-v0.3
|
| 92 |
+
OPENAI_API_KEY=your_hf_token
|
| 93 |
+
HF_TOKEN=your_hf_token
|
| 94 |
+
```
|
| 95 |
+
|
| 96 |
+
## Run Inference
|
| 97 |
+
```bash
|
| 98 |
+
python inference.py
|
| 99 |
+
```
|
| 100 |
+
|
| 101 |
+
## API Endpoints
|
| 102 |
+
|
| 103 |
+
| Endpoint | Method | Description |
|
| 104 |
+
|---|---|---|
|
| 105 |
+
| `/reset` | POST | Start fresh episode |
|
| 106 |
+
| `/step` | POST | Execute action |
|
| 107 |
+
| `/state` | GET | Current environment state |
|
| 108 |
+
| `/tasks` | GET | List all tasks |
|
| 109 |
+
| `/openenv.yaml` | GET | OpenEnv spec |
|
| 110 |
+
|
| 111 |
+
## Author
|
| 112 |
+
|
| 113 |
+
**Yashwanth R (Yashwanth34567)** & **Mohammed Ayaan** β OpenEnv Hackathon Γ Scaler School of Technology
|
app.py
ADDED
|
@@ -0,0 +1,297 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# app.py
|
| 2 |
+
# βββββββββββββββββββββββββββββββββββββββββββββ
|
| 3 |
+
# Gradio UI for Data Cleaning Environment
|
| 4 |
+
# Beautiful dashboard with Playfair Display
|
| 5 |
+
# βββββββββββββββββββββββββββββββββββββββββββββ
|
| 6 |
+
|
| 7 |
+
import gradio as gr
|
| 8 |
+
import pandas as pd
|
| 9 |
+
import json
|
| 10 |
+
from environment import DataCleaningEnv
|
| 11 |
+
from models import Action
|
| 12 |
+
from config import TASK_EASY, TASK_MEDIUM, TASK_HARD
|
| 13 |
+
|
| 14 |
+
# ββ Global State ββββββββββββββββββββββββββββββ
|
| 15 |
+
envs = {
|
| 16 |
+
TASK_EASY: DataCleaningEnv(TASK_EASY),
|
| 17 |
+
TASK_MEDIUM: DataCleaningEnv(TASK_MEDIUM),
|
| 18 |
+
TASK_HARD: DataCleaningEnv(TASK_HARD),
|
| 19 |
+
}
|
| 20 |
+
current_difficulty = TASK_EASY
|
| 21 |
+
step_logs = {TASK_EASY: [], TASK_MEDIUM: [], TASK_HARD: []}
|
| 22 |
+
|
| 23 |
+
# ββ Helpers βββββββββββββββββββββββββββββββββββ
|
| 24 |
+
|
| 25 |
+
def get_env():
|
| 26 |
+
return envs[current_difficulty]
|
| 27 |
+
|
| 28 |
+
def df_to_html(df: pd.DataFrame, null_counts: dict) -> str:
|
| 29 |
+
"""Render DataFrame as styled HTML table"""
|
| 30 |
+
if df is None or len(df) == 0:
|
| 31 |
+
return "<p style='color:#64748B;font-size:13px'>No data</p>"
|
| 32 |
+
|
| 33 |
+
rows_html = ""
|
| 34 |
+
for _, row in df.iterrows():
|
| 35 |
+
cells = ""
|
| 36 |
+
for col in df.columns:
|
| 37 |
+
val = row[col]
|
| 38 |
+
is_null = val is None or (
|
| 39 |
+
isinstance(val, float) and pd.isna(val)
|
| 40 |
+
)
|
| 41 |
+
if is_null:
|
| 42 |
+
cells += f"<td style='color:#EF4444;font-style:italic;background:#FFF5F5;padding:7px 12px;border-bottom:1px solid #F1F5F9'>null</td>"
|
| 43 |
+
elif isinstance(val, (int, float)) and col in ["salary", "salary$"]:
|
| 44 |
+
if abs(float(val)) > 500000 or float(val) < 0:
|
| 45 |
+
cells += f"<td style='color:#D97706;background:#FFFBEB;padding:7px 12px;border-bottom:1px solid #F1F5F9'>{val}</td>"
|
| 46 |
+
else:
|
| 47 |
+
cells += f"<td style='padding:7px 12px;border-bottom:1px solid #F1F5F9;color:#1E293B'>{val}</td>"
|
| 48 |
+
else:
|
| 49 |
+
cells += f"<td style='padding:7px 12px;border-bottom:1px solid #F1F5F9;color:#1E293B'>{val}</td>"
|
| 50 |
+
rows_html += f"<tr>{cells}</tr>"
|
| 51 |
+
|
| 52 |
+
headers = "".join([
|
| 53 |
+
f"<th style='background:#F8FAFC;padding:8px 12px;text-align:left;font-size:11px;color:#64748B;font-weight:500;border-bottom:1px solid #E2E8F0'>{col}</th>"
|
| 54 |
+
for col in df.columns
|
| 55 |
+
])
|
| 56 |
+
|
| 57 |
+
return f"""
|
| 58 |
+
<div style='background:#fff;border:1px solid #E2E8F0;border-radius:10px;overflow:hidden;font-family:DM Sans,sans-serif'>
|
| 59 |
+
<div style='padding:10px 14px;border-bottom:1px solid #E2E8F0;display:flex;justify-content:space-between;align-items:center'>
|
| 60 |
+
<span style='font-size:13px;font-weight:500;color:#1E293B'>Live dataset</span>
|
| 61 |
+
<span style='background:#EFF6FF;color:#2563EB;font-size:11px;padding:3px 10px;border-radius:20px;border:1px solid #BFDBFE'>{len(df)} rows Β· {len(df.columns)} cols</span>
|
| 62 |
+
</div>
|
| 63 |
+
<div style='overflow-x:auto'>
|
| 64 |
+
<table style='width:100%;border-collapse:collapse;font-size:12px;color:#1E293B'>
|
| 65 |
+
<thead><tr>{headers}</tr></thead>
|
| 66 |
+
<tbody>{rows_html}</tbody>
|
| 67 |
+
</table>
|
| 68 |
+
</div>
|
| 69 |
+
</div>
|
| 70 |
+
"""
|
| 71 |
+
|
| 72 |
+
def issues_to_html(issues: list) -> str:
|
| 73 |
+
"""Render issues as styled pills"""
|
| 74 |
+
if not issues:
|
| 75 |
+
return "<div style='color:#10B981;font-size:13px;font-weight:500'>No issues detected!</div>"
|
| 76 |
+
|
| 77 |
+
pills = ""
|
| 78 |
+
for issue in issues:
|
| 79 |
+
if "missing" in issue:
|
| 80 |
+
color = "#FFF5F5"; text = "#B91C1C"; border = "#FECACA"
|
| 81 |
+
elif "duplicate" in issue:
|
| 82 |
+
color = "#FFFBEB"; text = "#92400E"; border = "#FDE68A"
|
| 83 |
+
elif "outlier" in issue:
|
| 84 |
+
color = "#FFFBEB"; text = "#92400E"; border = "#FDE68A"
|
| 85 |
+
elif "bad_column" in issue:
|
| 86 |
+
color = "#EFF6FF"; text = "#1D4ED8"; border = "#BFDBFE"
|
| 87 |
+
else:
|
| 88 |
+
color = "#F8FAFC"; text = "#475569"; border = "#E2E8F0"
|
| 89 |
+
|
| 90 |
+
pills += f"<div style='background:{color};color:{text};border:1px solid {border};font-size:11px;padding:5px 10px;border-radius:6px;margin-bottom:6px'>{issue}</div>"
|
| 91 |
+
|
| 92 |
+
return pills
|
| 93 |
+
|
| 94 |
+
def log_to_html(logs: list) -> str:
|
| 95 |
+
"""Render step log as terminal-style HTML"""
|
| 96 |
+
if not logs:
|
| 97 |
+
return "<div style='background:#0F172A;border-radius:8px;padding:12px;font-size:11px;font-family:monospace;color:#475569'>No steps yet...</div>"
|
| 98 |
+
|
| 99 |
+
lines = ""
|
| 100 |
+
for entry in logs[-10:]:
|
| 101 |
+
reward_color = "#34D399" if entry["reward"] > 0 else "#F87171"
|
| 102 |
+
lines += f"""
|
| 103 |
+
<div style='margin-bottom:6px'>
|
| 104 |
+
<span style='color:#60A5FA'>[step {entry['step']}]</span>
|
| 105 |
+
<span style='color:#E2E8F0'> {entry['action']}</span>
|
| 106 |
+
<br>
|
| 107 |
+
<span style='color:{reward_color};padding-left:12px'>{entry['reward']:+.3f} Β· {entry['reason'][:50]}</span>
|
| 108 |
+
</div>"""
|
| 109 |
+
|
| 110 |
+
return f"<div style='background:#0F172A;border-radius:8px;padding:12px;font-size:11px;font-family:monospace;line-height:1.7;max-height:220px;overflow-y:auto'>{lines}</div>"
|
| 111 |
+
|
| 112 |
+
|
| 113 |
+
# ββ Actions βββββββββββββββββββββββββββββββββββ
|
| 114 |
+
|
| 115 |
+
def reset_task(difficulty):
|
| 116 |
+
global current_difficulty
|
| 117 |
+
current_difficulty = difficulty
|
| 118 |
+
env = envs[difficulty]
|
| 119 |
+
obs = env.reset()
|
| 120 |
+
step_logs[difficulty] = []
|
| 121 |
+
|
| 122 |
+
df = pd.DataFrame(obs.dataframe)
|
| 123 |
+
score, _ = env.task.grade()
|
| 124 |
+
|
| 125 |
+
return (
|
| 126 |
+
df_to_html(df, obs.null_counts),
|
| 127 |
+
issues_to_html(obs.issues),
|
| 128 |
+
log_to_html([]),
|
| 129 |
+
f"<div style='font-family:Playfair Display,serif;font-size:28px;font-weight:700;color:#2563EB'>0.000</div>",
|
| 130 |
+
f"<div style='font-family:Playfair Display,serif;font-size:28px;font-weight:700;color:#1E293B'>0</div>",
|
| 131 |
+
f"<div style='font-family:Playfair Display,serif;font-size:28px;font-weight:700;color:#EF4444'>{sum(obs.null_counts.values())}</div>",
|
| 132 |
+
f"<div style='font-family:Playfair Display,serif;font-size:28px;font-weight:700;color:#F59E0B'>0</div>",
|
| 133 |
+
)
|
| 134 |
+
|
| 135 |
+
|
| 136 |
+
def run_action(difficulty, action_type, column, strategy,
|
| 137 |
+
target_type, old_name, new_name, mapping_json):
|
| 138 |
+
global current_difficulty
|
| 139 |
+
current_difficulty = difficulty
|
| 140 |
+
env = envs[difficulty]
|
| 141 |
+
|
| 142 |
+
if not env._initialized:
|
| 143 |
+
env.reset()
|
| 144 |
+
|
| 145 |
+
# Build parameters
|
| 146 |
+
params = {}
|
| 147 |
+
if action_type == "fill_missing":
|
| 148 |
+
params = {"column": column, "strategy": strategy or "mean"}
|
| 149 |
+
elif action_type == "drop_duplicates":
|
| 150 |
+
params = {}
|
| 151 |
+
elif action_type == "fix_dtype":
|
| 152 |
+
params = {"column": column, "target_type": target_type or "float"}
|
| 153 |
+
elif action_type == "rename_column":
|
| 154 |
+
params = {"old_name": old_name, "new_name": new_name}
|
| 155 |
+
elif action_type == "remove_outliers":
|
| 156 |
+
params = {"column": column, "method": strategy or "iqr"}
|
| 157 |
+
elif action_type == "standardize_values":
|
| 158 |
+
try:
|
| 159 |
+
params = {"column": column, "mapping": json.loads(mapping_json)}
|
| 160 |
+
except Exception:
|
| 161 |
+
params = {"column": column, "mapping": {}}
|
| 162 |
+
elif action_type == "submit":
|
| 163 |
+
params = {}
|
| 164 |
+
|
| 165 |
+
action = Action(action_type=action_type, parameters=params)
|
| 166 |
+
result = env.step(action)
|
| 167 |
+
obs = result.observation
|
| 168 |
+
|
| 169 |
+
# Log step
|
| 170 |
+
step_logs[difficulty].append({
|
| 171 |
+
"step": env.step_count,
|
| 172 |
+
"action": action_type,
|
| 173 |
+
"reward": result.reward.value,
|
| 174 |
+
"reason": result.reward.reason,
|
| 175 |
+
})
|
| 176 |
+
|
| 177 |
+
df = pd.DataFrame(obs.dataframe)
|
| 178 |
+
score, _ = env.task.grade()
|
| 179 |
+
|
| 180 |
+
return (
|
| 181 |
+
df_to_html(df, obs.null_counts),
|
| 182 |
+
issues_to_html(obs.issues),
|
| 183 |
+
log_to_html(step_logs[difficulty]),
|
| 184 |
+
f"<div style='font-family:Playfair Display,serif;font-size:28px;font-weight:700;color:#2563EB'>{score:.3f}</div>",
|
| 185 |
+
f"<div style='font-family:Playfair Display,serif;font-size:28px;font-weight:700;color:#1E293B'>{env.step_count}</div>",
|
| 186 |
+
f"<div style='font-family:Playfair Display,serif;font-size:28px;font-weight:700;color:#EF4444'>{sum(obs.null_counts.values())}</div>",
|
| 187 |
+
f"<div style='font-family:Playfair Display,serif;font-size:28px;font-weight:700;color:{'#10B981' if result.reward.value > 0 else '#EF4444'}'>{result.reward.value:+.3f}</div>",
|
| 188 |
+
)
|
| 189 |
+
|
| 190 |
+
|
| 191 |
+
# ββ UI ββββββββββββββββββββββββββββββββββββββββ
|
| 192 |
+
|
| 193 |
+
CSS = """
|
| 194 |
+
@import url('https://fonts.googleapis.com/css2?family=Playfair+Display:wght@700;900&family=DM+Sans:wght@300;400;500&display=swap');
|
| 195 |
+
body, .gradio-container { background: #F8FAFC !important; font-family: 'DM Sans', sans-serif !important; }
|
| 196 |
+
.logo { font-family: 'Playfair Display', serif !important; }
|
| 197 |
+
h1, h2, h3 { font-family: 'Playfair Display', serif !important; }
|
| 198 |
+
.gr-button-primary { background: #2563EB !important; border: none !important; }
|
| 199 |
+
.gr-button { border-radius: 8px !important; font-family: 'DM Sans', sans-serif !important; }
|
| 200 |
+
"""
|
| 201 |
+
|
| 202 |
+
with gr.Blocks(css=CSS, title="DataClean β OpenEnv") as demo:
|
| 203 |
+
|
| 204 |
+
gr.HTML("""
|
| 205 |
+
<div style='background:#fff;border-bottom:1px solid #E2E8F0;padding:16px 24px;display:flex;align-items:center;justify-content:space-between;margin-bottom:0'>
|
| 206 |
+
<div style='font-family:Playfair Display,serif;font-size:26px;font-weight:900;color:#2563EB;letter-spacing:-0.5px'>
|
| 207 |
+
Data<span style='color:#1E293B'>Clean</span>
|
| 208 |
+
</div>
|
| 209 |
+
<div style='background:#EFF6FF;color:#2563EB;font-size:12px;font-weight:500;padding:4px 14px;border-radius:20px;border:1px solid #BFDBFE'>
|
| 210 |
+
OpenEnv v1.0 Β· Yashwanth34567
|
| 211 |
+
</div>
|
| 212 |
+
</div>
|
| 213 |
+
""")
|
| 214 |
+
|
| 215 |
+
with gr.Row():
|
| 216 |
+
|
| 217 |
+
# ββ Left Sidebar ββ
|
| 218 |
+
with gr.Column(scale=1):
|
| 219 |
+
gr.HTML("<div style='font-size:11px;font-weight:500;color:#64748B;letter-spacing:0.08em;text-transform:uppercase;margin-bottom:8px'>Select Task</div>")
|
| 220 |
+
difficulty = gr.Radio(
|
| 221 |
+
choices=[TASK_EASY, TASK_MEDIUM, TASK_HARD],
|
| 222 |
+
value=TASK_EASY,
|
| 223 |
+
label="",
|
| 224 |
+
)
|
| 225 |
+
reset_btn = gr.Button("Reset Task", variant="primary")
|
| 226 |
+
|
| 227 |
+
gr.HTML("<div style='font-size:11px;font-weight:500;color:#64748B;letter-spacing:0.08em;text-transform:uppercase;margin:16px 0 8px'>Issues</div>")
|
| 228 |
+
issues_display = gr.HTML()
|
| 229 |
+
|
| 230 |
+
# ββ Center ββ
|
| 231 |
+
with gr.Column(scale=3):
|
| 232 |
+
with gr.Row():
|
| 233 |
+
score_display = gr.HTML("<div style='font-family:Playfair Display,serif;font-size:28px;font-weight:700;color:#2563EB'>β</div>")
|
| 234 |
+
step_display = gr.HTML("<div style='font-family:Playfair Display,serif;font-size:28px;font-weight:700;color:#1E293B'>β</div>")
|
| 235 |
+
null_display = gr.HTML("<div style='font-family:Playfair Display,serif;font-size:28px;font-weight:700;color:#EF4444'>β</div>")
|
| 236 |
+
reward_display = gr.HTML("<div style='font-family:Playfair Display,serif;font-size:28px;font-weight:700;color:#F59E0B'>β</div>")
|
| 237 |
+
|
| 238 |
+
gr.HTML("<div style='font-size:11px;font-weight:500;color:#64748B;letter-spacing:0.08em;text-transform:uppercase;margin:8px 0'>Live Dataset</div>")
|
| 239 |
+
table_display = gr.HTML()
|
| 240 |
+
|
| 241 |
+
gr.HTML("<div style='font-size:11px;font-weight:500;color:#64748B;letter-spacing:0.08em;text-transform:uppercase;margin:16px 0 8px'>Step Log</div>")
|
| 242 |
+
log_display = gr.HTML()
|
| 243 |
+
|
| 244 |
+
# ββ Right Panel ββ
|
| 245 |
+
with gr.Column(scale=1):
|
| 246 |
+
gr.HTML("<div style='font-size:11px;font-weight:500;color:#64748B;letter-spacing:0.08em;text-transform:uppercase;margin-bottom:8px'>Take Action</div>")
|
| 247 |
+
|
| 248 |
+
action_type = gr.Dropdown(
|
| 249 |
+
choices=["fill_missing", "drop_duplicates", "fix_dtype",
|
| 250 |
+
"rename_column", "remove_outliers",
|
| 251 |
+
"standardize_values", "submit"],
|
| 252 |
+
value="fill_missing",
|
| 253 |
+
label="Action",
|
| 254 |
+
)
|
| 255 |
+
column = gr.Textbox(label="Column", placeholder="e.g. age")
|
| 256 |
+
strategy = gr.Textbox(label="Strategy / Method", placeholder="mean | median | mode | iqr")
|
| 257 |
+
target_type = gr.Textbox(label="Target Type", placeholder="int | float | str")
|
| 258 |
+
old_name = gr.Textbox(label="Old Column Name", placeholder="Full Name ")
|
| 259 |
+
new_name = gr.Textbox(label="New Column Name", placeholder="full_name")
|
| 260 |
+
mapping = gr.Textbox(label='Mapping (JSON)', placeholder='{"USA": "United States"}')
|
| 261 |
+
|
| 262 |
+
run_btn = gr.Button("Run Action", variant="primary")
|
| 263 |
+
submit_btn = gr.Button("Submit Task", variant="secondary")
|
| 264 |
+
|
| 265 |
+
# ββ Events βββββββββββββββββββββββββββββββ
|
| 266 |
+
outputs = [
|
| 267 |
+
table_display, issues_display, log_display,
|
| 268 |
+
score_display, step_display, null_display, reward_display
|
| 269 |
+
]
|
| 270 |
+
|
| 271 |
+
reset_btn.click(
|
| 272 |
+
fn=reset_task,
|
| 273 |
+
inputs=[difficulty],
|
| 274 |
+
outputs=outputs,
|
| 275 |
+
)
|
| 276 |
+
|
| 277 |
+
run_btn.click(
|
| 278 |
+
fn=run_action,
|
| 279 |
+
inputs=[difficulty, action_type, column, strategy,
|
| 280 |
+
target_type, old_name, new_name, mapping],
|
| 281 |
+
outputs=outputs,
|
| 282 |
+
)
|
| 283 |
+
|
| 284 |
+
submit_btn.click(
|
| 285 |
+
fn=lambda d, c, s, t, o, n, m: run_action(
|
| 286 |
+
d, "submit", c, s, t, o, n, m),
|
| 287 |
+
inputs=[difficulty, column, strategy,
|
| 288 |
+
target_type, old_name, new_name, mapping],
|
| 289 |
+
outputs=outputs,
|
| 290 |
+
)
|
| 291 |
+
|
| 292 |
+
if __name__ == "__main__":
|
| 293 |
+
demo.launch(
|
| 294 |
+
server_name="0.0.0.0",
|
| 295 |
+
server_port=7860,
|
| 296 |
+
inbrowser=True,
|
| 297 |
+
)
|
config.py
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# config.py
|
| 2 |
+
# βββββββββββββββββββββββββββββββββββββββββββββ
|
| 3 |
+
# Central configuration for Data Cleaning Env
|
| 4 |
+
# βββββββββββββββββββββββββββββββββββββββββββββ
|
| 5 |
+
|
| 6 |
+
import os
|
| 7 |
+
|
| 8 |
+
# ββ Environment Metadata ββββββββββββββββββββββ
|
| 9 |
+
ENV_NAME = "data-cleaning-env"
|
| 10 |
+
ENV_VERSION = "1.0.0"
|
| 11 |
+
ENV_DESCRIPTION = "An OpenEnv environment where an AI agent cleans messy CSV datasets"
|
| 12 |
+
AUTHOR = "Yashwanth34567"
|
| 13 |
+
|
| 14 |
+
# ββ Task Difficulty Levels ββββββββββββββββββββ
|
| 15 |
+
TASK_EASY = "easy"
|
| 16 |
+
TASK_MEDIUM = "medium"
|
| 17 |
+
TASK_HARD = "hard"
|
| 18 |
+
|
| 19 |
+
TASK_IDS = {
|
| 20 |
+
TASK_EASY: "task_1_easy",
|
| 21 |
+
TASK_MEDIUM: "task_2_medium",
|
| 22 |
+
TASK_HARD: "task_3_hard",
|
| 23 |
+
}
|
| 24 |
+
|
| 25 |
+
# ββ Reward Values βββββββββββββββββββββββββββββ
|
| 26 |
+
REWARD_ISSUE_FIXED = 0.10 # each real issue fixed
|
| 27 |
+
REWARD_SUBMIT_BONUS = 0.30 # bonus for clean submit
|
| 28 |
+
PENALTY_WRONG_ACTION = -0.05 # redundant / wrong action
|
| 29 |
+
PENALTY_DESTRUCTIVE = -0.10 # dropping valid data
|
| 30 |
+
|
| 31 |
+
# ββ Episode Settings ββββββββββββββββββββββββββ
|
| 32 |
+
MAX_STEPS = 20 # max actions per episode
|
| 33 |
+
|
| 34 |
+
# ββ API Settings ββββββββββββββββββββββββββββββ
|
| 35 |
+
API_BASE_URL = os.getenv("API_BASE_URL", "https://api.openai.com/v1")
|
| 36 |
+
MODEL_NAME = os.getenv("MODEL_NAME", "gpt-4o-mini")
|
| 37 |
+
HF_TOKEN = os.getenv("HF_TOKEN", "")
|
| 38 |
+
|
| 39 |
+
# ββ Server Settings βββββββββββββββββββββββββββ
|
| 40 |
+
HOST = "0.0.0.0"
|
| 41 |
+
PORT = 8000
|
datasets.py
ADDED
|
@@ -0,0 +1,161 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# datasets.py
|
| 2 |
+
# βββββββββββββββββββββββββββββββββββββββββββββ
|
| 3 |
+
# CSV dataset generators for each difficulty
|
| 4 |
+
# Easy / Medium / Hard messy datasets
|
| 5 |
+
# βββββββββββββββββββββββββββββββββββββββββββββ
|
| 6 |
+
|
| 7 |
+
import pandas as pd
|
| 8 |
+
import numpy as np
|
| 9 |
+
import random
|
| 10 |
+
|
| 11 |
+
random.seed(42)
|
| 12 |
+
np.random.seed(42)
|
| 13 |
+
|
| 14 |
+
# ββ Task 1 β Easy βββββββββββββββββββββββββββββ
|
| 15 |
+
def generate_easy_dataset() -> pd.DataFrame:
|
| 16 |
+
"""
|
| 17 |
+
10 rows, issues:
|
| 18 |
+
- Missing values in age & salary
|
| 19 |
+
- Wrong dtype (age as string)
|
| 20 |
+
"""
|
| 21 |
+
data = {
|
| 22 |
+
"name": ["Alice", "Bob", "Charlie", "Diana", "Eve",
|
| 23 |
+
"Frank", "Grace", "Hank", "Ivy", "Jack"],
|
| 24 |
+
"age": ["25", "30", None, "22", "28",
|
| 25 |
+
"35", None, "40", "27", "33"],
|
| 26 |
+
"salary": [50000, 60000, None, 45000, 55000,
|
| 27 |
+
None, 70000, 80000, None, 65000],
|
| 28 |
+
"email": [
|
| 29 |
+
"alice@mail.com", "bob@mail.com", "charlie@mail.com",
|
| 30 |
+
"diana@mail.com", "eve@mail.com", "frank@mail.com",
|
| 31 |
+
"grace@mail.com", "hank@mail.com", "ivy@mail.com",
|
| 32 |
+
"jack@mail.com"
|
| 33 |
+
]
|
| 34 |
+
}
|
| 35 |
+
return pd.DataFrame(data)
|
| 36 |
+
|
| 37 |
+
|
| 38 |
+
# ββ Task 2 β Medium βββββββββββββββββββββββββββ
|
| 39 |
+
def generate_medium_dataset() -> pd.DataFrame:
|
| 40 |
+
"""
|
| 41 |
+
20 rows, issues:
|
| 42 |
+
- Duplicate rows
|
| 43 |
+
- Outliers in salary
|
| 44 |
+
- Inconsistent country values
|
| 45 |
+
"""
|
| 46 |
+
data = {
|
| 47 |
+
"name": [
|
| 48 |
+
"Alice", "Bob", "Charlie", "Diana", "Eve",
|
| 49 |
+
"Frank", "Grace", "Hank", "Ivy", "Jack",
|
| 50 |
+
"Alice", "Bob", "Karen", "Leo", "Mia",
|
| 51 |
+
"Nina", "Oscar", "Paul", "Quinn", "Rita"
|
| 52 |
+
],
|
| 53 |
+
"age": [25, 30, 35, 22, 28, 40, 27, 33, 29, 31,
|
| 54 |
+
25, 30, 26, 38, 24, 32, 36, 28, 30, 27],
|
| 55 |
+
"salary": [
|
| 56 |
+
50000, 60000, 55000, 45000, 58000,
|
| 57 |
+
62000, 48000, 70000, 999999, 65000,
|
| 58 |
+
50000, 60000, 47000, 72000, 43000,
|
| 59 |
+
-5000, 68000, 54000, 61000, 49000
|
| 60 |
+
],
|
| 61 |
+
"country": [
|
| 62 |
+
"USA", "United States", "US", "USA", "America",
|
| 63 |
+
"UK", "United Kingdom", "UK", "USA", "US",
|
| 64 |
+
"USA", "United States", "UK", "USA", "US",
|
| 65 |
+
"USA", "UK", "United Kingdom", "USA", "US"
|
| 66 |
+
]
|
| 67 |
+
}
|
| 68 |
+
return pd.DataFrame(data)
|
| 69 |
+
|
| 70 |
+
|
| 71 |
+
# ββ Task 3 β Hard βββββββββββββββββββββββββββββ
|
| 72 |
+
def generate_hard_dataset() -> tuple:
|
| 73 |
+
"""
|
| 74 |
+
30 rows, issues:
|
| 75 |
+
- All of easy + medium issues
|
| 76 |
+
- Bad column names
|
| 77 |
+
- Mixed dtype columns
|
| 78 |
+
Returns (main_df, lookup_df) for referential integrity check
|
| 79 |
+
"""
|
| 80 |
+
main_data = {
|
| 81 |
+
"Full Name ": [ # trailing space in col name
|
| 82 |
+
"Alice", "Bob", "Charlie", "Diana", "Eve",
|
| 83 |
+
"Frank", "Grace", "Hank", "Ivy", "Jack",
|
| 84 |
+
"Karen", "Leo", "Mia", "Nina", "Oscar",
|
| 85 |
+
"Paul", "Quinn", "Rita", "Sam", "Tina",
|
| 86 |
+
"Uma", "Victor", "Wendy", "Xander", "Yara",
|
| 87 |
+
"Zane", "Alice", "Bob", "Charlie", "Diana"
|
| 88 |
+
],
|
| 89 |
+
"AGE": [ # should be lowercase
|
| 90 |
+
"25", "30", None, "22", "28",
|
| 91 |
+
"35", "27", "40", "abc", "33",
|
| 92 |
+
"26", "38", "24", "32", "36",
|
| 93 |
+
"28", "30", "27", "29", "31",
|
| 94 |
+
"25", "30", None, "22", "28",
|
| 95 |
+
"35", "25", "30", "35", "22"
|
| 96 |
+
],
|
| 97 |
+
"salary$": [ # special char in col name
|
| 98 |
+
50000, 60000, None, 45000, 55000,
|
| 99 |
+
None, 70000, 80000, None, 65000,
|
| 100 |
+
47000, 72000, 43000, -5000, 68000,
|
| 101 |
+
54000, 61000, 49000, 999999, 58000,
|
| 102 |
+
50000, 60000, 55000, 45000, 58000,
|
| 103 |
+
62000, 50000, 60000, 55000, 45000
|
| 104 |
+
],
|
| 105 |
+
"COUNTRY": [
|
| 106 |
+
"USA", "United States", "US", "USA", "America",
|
| 107 |
+
"UK", "United Kingdom", "UK", "USA", "US",
|
| 108 |
+
"USA", "United States", "UK", "USA", "US",
|
| 109 |
+
"USA", "UK", "United Kingdom", "USA", "US",
|
| 110 |
+
"USA", "United States", "US", "USA", "America",
|
| 111 |
+
"UK", "USA", "US", "UK", "USA"
|
| 112 |
+
],
|
| 113 |
+
"dept_id": [
|
| 114 |
+
1, 2, 3, 1, 2, 3, 1, 2, 3, 1,
|
| 115 |
+
2, 3, 1, 2, 99, 1, 2, 3, 1, 2, # 99 is invalid
|
| 116 |
+
3, 1, 2, 3, 1, 2, 3, 1, 2, 3
|
| 117 |
+
]
|
| 118 |
+
}
|
| 119 |
+
|
| 120 |
+
# Lookup table for referential integrity
|
| 121 |
+
lookup_data = {
|
| 122 |
+
"dept_id": [1, 2, 3],
|
| 123 |
+
"dept_name": ["Engineering", "Marketing", "Sales"]
|
| 124 |
+
}
|
| 125 |
+
|
| 126 |
+
return pd.DataFrame(main_data), pd.DataFrame(lookup_data)
|
| 127 |
+
|
| 128 |
+
|
| 129 |
+
# ββ Issue Detection βββββββββββββββββββββββββββ
|
| 130 |
+
def detect_issues(df: pd.DataFrame, task_id: str) -> list:
|
| 131 |
+
"""Detect all issues in a dataframe"""
|
| 132 |
+
issues = []
|
| 133 |
+
|
| 134 |
+
# Null values
|
| 135 |
+
null_counts = df.isnull().sum()
|
| 136 |
+
for col, count in null_counts.items():
|
| 137 |
+
if count > 0:
|
| 138 |
+
issues.append(f"missing_values::{col}::{count}")
|
| 139 |
+
|
| 140 |
+
# Duplicates
|
| 141 |
+
dup_count = df.duplicated().sum()
|
| 142 |
+
if dup_count > 0:
|
| 143 |
+
issues.append(f"duplicate_rows::{dup_count}")
|
| 144 |
+
|
| 145 |
+
# Outliers (numeric columns)
|
| 146 |
+
for col in df.select_dtypes(include=[np.number]).columns:
|
| 147 |
+
Q1 = df[col].quantile(0.25)
|
| 148 |
+
Q3 = df[col].quantile(0.75)
|
| 149 |
+
IQR = Q3 - Q1
|
| 150 |
+
outliers = ((df[col] < Q1 - 1.5 * IQR) |
|
| 151 |
+
(df[col] > Q3 + 1.5 * IQR)).sum()
|
| 152 |
+
if outliers > 0:
|
| 153 |
+
issues.append(f"outliers::{col}::{outliers}")
|
| 154 |
+
|
| 155 |
+
# Bad column names
|
| 156 |
+
for col in df.columns:
|
| 157 |
+
if col != col.strip() or col != col.lower() or \
|
| 158 |
+
any(c in col for c in ["$", "@", "#", "!"]):
|
| 159 |
+
issues.append(f"bad_column_name::{col}")
|
| 160 |
+
|
| 161 |
+
return issues
|
environment.py
ADDED
|
@@ -0,0 +1,363 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# environment.py
|
| 2 |
+
# βββββββββββββββββββββββββββββββββββββββββββββ
|
| 3 |
+
# Core OpenEnv class β step / reset / state
|
| 4 |
+
# Full OpenEnv spec compliant
|
| 5 |
+
# βββββββββββββββββββββββββββββββββββββββββββββ
|
| 6 |
+
|
| 7 |
+
import pandas as pd
|
| 8 |
+
import numpy as np
|
| 9 |
+
from typing import Any, Dict, Optional, Tuple
|
| 10 |
+
|
| 11 |
+
from config import (
|
| 12 |
+
MAX_STEPS,
|
| 13 |
+
REWARD_ISSUE_FIXED,
|
| 14 |
+
REWARD_SUBMIT_BONUS,
|
| 15 |
+
PENALTY_WRONG_ACTION,
|
| 16 |
+
PENALTY_DESTRUCTIVE,
|
| 17 |
+
TASK_EASY, TASK_MEDIUM, TASK_HARD,
|
| 18 |
+
)
|
| 19 |
+
from models import Observation, Action, Reward, StepResult
|
| 20 |
+
from tasks import Task, get_task
|
| 21 |
+
from utils import (
|
| 22 |
+
df_to_records,
|
| 23 |
+
get_null_counts,
|
| 24 |
+
get_duplicate_count,
|
| 25 |
+
detect_outliers_iqr,
|
| 26 |
+
clean_column_name,
|
| 27 |
+
standardize_column,
|
| 28 |
+
clamp,
|
| 29 |
+
)
|
| 30 |
+
from datasets import detect_issues
|
| 31 |
+
|
| 32 |
+
|
| 33 |
+
class DataCleaningEnv:
|
| 34 |
+
"""
|
| 35 |
+
OpenEnv-compliant Data Cleaning Environment.
|
| 36 |
+
|
| 37 |
+
The agent receives a messy CSV dataset and must
|
| 38 |
+
issue cleaning actions to fix it step by step.
|
| 39 |
+
|
| 40 |
+
Actions:
|
| 41 |
+
fill_missing β fill null values
|
| 42 |
+
drop_duplicates β remove duplicate rows
|
| 43 |
+
fix_dtype β convert column dtype
|
| 44 |
+
rename_column β rename a column
|
| 45 |
+
remove_outliers β remove outlier rows
|
| 46 |
+
standardize_values β replace values via mapping
|
| 47 |
+
submit β finalize and trigger grader
|
| 48 |
+
"""
|
| 49 |
+
|
| 50 |
+
def __init__(self, difficulty: str = TASK_EASY):
|
| 51 |
+
self.difficulty = difficulty
|
| 52 |
+
self.task: Task = get_task(difficulty)
|
| 53 |
+
self.step_count = 0
|
| 54 |
+
self.total_reward = 0.0
|
| 55 |
+
self.done = False
|
| 56 |
+
self._prev_score = 0.0
|
| 57 |
+
self._initialized = False
|
| 58 |
+
|
| 59 |
+
# ββ OpenEnv API βββββββββββββββββββββββββββ
|
| 60 |
+
|
| 61 |
+
def reset(self) -> Observation:
|
| 62 |
+
"""Start a fresh episode"""
|
| 63 |
+
self.task.reset()
|
| 64 |
+
self.step_count = 0
|
| 65 |
+
self.total_reward = 0.0
|
| 66 |
+
self.done = False
|
| 67 |
+
self._prev_score = 0.0
|
| 68 |
+
self._initialized = True
|
| 69 |
+
return self._make_observation()
|
| 70 |
+
|
| 71 |
+
def step(self, action: Action) -> StepResult:
|
| 72 |
+
"""Execute one action and return result"""
|
| 73 |
+
if not self._initialized:
|
| 74 |
+
self.reset()
|
| 75 |
+
|
| 76 |
+
if self.done:
|
| 77 |
+
obs = self._make_observation()
|
| 78 |
+
reward = Reward(value=0.0, total=self.total_reward,
|
| 79 |
+
reason="Episode already done")
|
| 80 |
+
return StepResult(observation=obs, reward=reward,
|
| 81 |
+
done=True, info={})
|
| 82 |
+
|
| 83 |
+
self.step_count += 1
|
| 84 |
+
reward_value, reason, info = self._execute_action(action)
|
| 85 |
+
|
| 86 |
+
# Clamp and accumulate
|
| 87 |
+
reward_value = clamp(reward_value, -1.0, 1.0)
|
| 88 |
+
self.total_reward = round(self.total_reward + reward_value, 4)
|
| 89 |
+
|
| 90 |
+
# Check done conditions
|
| 91 |
+
if self.step_count >= MAX_STEPS:
|
| 92 |
+
self.done = True
|
| 93 |
+
reason += " | Max steps reached"
|
| 94 |
+
|
| 95 |
+
reward = Reward(
|
| 96 |
+
value = round(reward_value, 4),
|
| 97 |
+
total = self.total_reward,
|
| 98 |
+
reason = reason,
|
| 99 |
+
)
|
| 100 |
+
obs = self._make_observation()
|
| 101 |
+
obs.done = self.done
|
| 102 |
+
|
| 103 |
+
return StepResult(
|
| 104 |
+
observation = obs,
|
| 105 |
+
reward = reward,
|
| 106 |
+
done = self.done,
|
| 107 |
+
info = info,
|
| 108 |
+
)
|
| 109 |
+
|
| 110 |
+
def state(self) -> Dict[str, Any]:
|
| 111 |
+
"""Return current environment state as dict"""
|
| 112 |
+
if not self._initialized:
|
| 113 |
+
return {"status": "not initialized, call reset() first"}
|
| 114 |
+
|
| 115 |
+
score, details = self.task.grade()
|
| 116 |
+
return {
|
| 117 |
+
"task_id": self.task.task_id,
|
| 118 |
+
"difficulty": self.difficulty,
|
| 119 |
+
"step": self.step_count,
|
| 120 |
+
"max_steps": MAX_STEPS,
|
| 121 |
+
"total_reward": self.total_reward,
|
| 122 |
+
"current_score": score,
|
| 123 |
+
"done": self.done,
|
| 124 |
+
"issues": self.task.issues,
|
| 125 |
+
"grade_details": details,
|
| 126 |
+
"dataframe": df_to_records(self.task.current_df),
|
| 127 |
+
"columns": list(self.task.current_df.columns),
|
| 128 |
+
"null_counts": get_null_counts(self.task.current_df),
|
| 129 |
+
"duplicate_rows": get_duplicate_count(self.task.current_df),
|
| 130 |
+
}
|
| 131 |
+
|
| 132 |
+
# ββ Action Executor βββββββββββββββββββββββ
|
| 133 |
+
|
| 134 |
+
def _execute_action(self, action: Action
|
| 135 |
+
) -> Tuple[float, str, Dict]:
|
| 136 |
+
"""Route action to handler, return (reward, reason, info)"""
|
| 137 |
+
df = self.task.current_df
|
| 138 |
+
p = action.parameters
|
| 139 |
+
info = {}
|
| 140 |
+
|
| 141 |
+
try:
|
| 142 |
+
if action.action_type == "fill_missing":
|
| 143 |
+
df, reward, reason = self._fill_missing(df, p)
|
| 144 |
+
|
| 145 |
+
elif action.action_type == "drop_duplicates":
|
| 146 |
+
df, reward, reason = self._drop_duplicates(df)
|
| 147 |
+
|
| 148 |
+
elif action.action_type == "fix_dtype":
|
| 149 |
+
df, reward, reason = self._fix_dtype(df, p)
|
| 150 |
+
|
| 151 |
+
elif action.action_type == "rename_column":
|
| 152 |
+
df, reward, reason = self._rename_column(df, p)
|
| 153 |
+
|
| 154 |
+
elif action.action_type == "remove_outliers":
|
| 155 |
+
df, reward, reason = self._remove_outliers(df, p)
|
| 156 |
+
|
| 157 |
+
elif action.action_type == "standardize_values":
|
| 158 |
+
df, reward, reason = self._standardize_values(df, p)
|
| 159 |
+
|
| 160 |
+
elif action.action_type == "submit":
|
| 161 |
+
df, reward, reason, info = self._submit(df)
|
| 162 |
+
|
| 163 |
+
else:
|
| 164 |
+
reward = PENALTY_WRONG_ACTION
|
| 165 |
+
reason = f"Unknown action: {action.action_type}"
|
| 166 |
+
|
| 167 |
+
except Exception as e:
|
| 168 |
+
reward = PENALTY_WRONG_ACTION
|
| 169 |
+
reason = f"Action failed: {str(e)}"
|
| 170 |
+
info = {"error": str(e)}
|
| 171 |
+
|
| 172 |
+
self.task.current_df = df
|
| 173 |
+
self.task.update_issues()
|
| 174 |
+
return reward, reason, info
|
| 175 |
+
|
| 176 |
+
# ββ Action Handlers βββββββββββββββββββββββ
|
| 177 |
+
|
| 178 |
+
def _fill_missing(self, df: pd.DataFrame,
|
| 179 |
+
params: Dict) -> Tuple[pd.DataFrame, float, str]:
|
| 180 |
+
column = params.get("column")
|
| 181 |
+
strategy = params.get("strategy", "mean")
|
| 182 |
+
|
| 183 |
+
if column not in df.columns:
|
| 184 |
+
return df, PENALTY_WRONG_ACTION, f"Column '{column}' not found"
|
| 185 |
+
|
| 186 |
+
null_before = df[column].isnull().sum()
|
| 187 |
+
if null_before == 0:
|
| 188 |
+
return df, PENALTY_WRONG_ACTION, \
|
| 189 |
+
f"No nulls in '{column}' β redundant action"
|
| 190 |
+
|
| 191 |
+
df = df.copy()
|
| 192 |
+
if strategy == "mean":
|
| 193 |
+
numeric = pd.to_numeric(df[column], errors="coerce")
|
| 194 |
+
df[column] = df[column].fillna(numeric.mean())
|
| 195 |
+
elif strategy == "median":
|
| 196 |
+
numeric = pd.to_numeric(df[column], errors="coerce")
|
| 197 |
+
df[column] = df[column].fillna(numeric.median())
|
| 198 |
+
elif strategy == "mode":
|
| 199 |
+
df[column].fillna(df[column].mode()[0], inplace=True)
|
| 200 |
+
elif strategy == "drop":
|
| 201 |
+
before = len(df)
|
| 202 |
+
df.dropna(subset=[column], inplace=True)
|
| 203 |
+
dropped = before - len(df)
|
| 204 |
+
if dropped > len(df) * 0.3:
|
| 205 |
+
return df, PENALTY_DESTRUCTIVE, \
|
| 206 |
+
f"Dropped {dropped} rows β too destructive"
|
| 207 |
+
else:
|
| 208 |
+
df[column].fillna(strategy, inplace=True)
|
| 209 |
+
|
| 210 |
+
null_after = df[column].isnull().sum()
|
| 211 |
+
fixed = null_before - null_after
|
| 212 |
+
reward = REWARD_ISSUE_FIXED * (fixed / max(null_before, 1))
|
| 213 |
+
return df, reward, f"Filled {fixed} nulls in '{column}' via {strategy}"
|
| 214 |
+
|
| 215 |
+
def _drop_duplicates(self, df: pd.DataFrame
|
| 216 |
+
) -> Tuple[pd.DataFrame, float, str]:
|
| 217 |
+
dup_before = df.duplicated().sum()
|
| 218 |
+
if dup_before == 0:
|
| 219 |
+
return df, PENALTY_WRONG_ACTION, "No duplicates β redundant action"
|
| 220 |
+
|
| 221 |
+
df = df.drop_duplicates().reset_index(drop=True)
|
| 222 |
+
reward = REWARD_ISSUE_FIXED
|
| 223 |
+
return df, reward, f"Removed {dup_before} duplicate rows"
|
| 224 |
+
|
| 225 |
+
def _fix_dtype(self, df: pd.DataFrame,
|
| 226 |
+
params: Dict) -> Tuple[pd.DataFrame, float, str]:
|
| 227 |
+
column = params.get("column")
|
| 228 |
+
target_type = params.get("target_type", "float")
|
| 229 |
+
|
| 230 |
+
if column not in df.columns:
|
| 231 |
+
return df, PENALTY_WRONG_ACTION, f"Column '{column}' not found"
|
| 232 |
+
|
| 233 |
+
df = df.copy()
|
| 234 |
+
try:
|
| 235 |
+
if target_type == "int":
|
| 236 |
+
df[column] = pd.to_numeric(
|
| 237 |
+
df[column], errors="coerce").fillna(0).astype(int)
|
| 238 |
+
elif target_type == "float":
|
| 239 |
+
df[column] = pd.to_numeric(df[column], errors="coerce")
|
| 240 |
+
elif target_type == "str":
|
| 241 |
+
df[column] = df[column].astype(str)
|
| 242 |
+
elif target_type == "datetime":
|
| 243 |
+
df[column] = pd.to_datetime(df[column], errors="coerce")
|
| 244 |
+
else:
|
| 245 |
+
return df, PENALTY_WRONG_ACTION, \
|
| 246 |
+
f"Unknown target type: {target_type}"
|
| 247 |
+
except Exception as e:
|
| 248 |
+
return df, PENALTY_WRONG_ACTION, f"Dtype fix failed: {e}"
|
| 249 |
+
|
| 250 |
+
return df, REWARD_ISSUE_FIXED, \
|
| 251 |
+
f"Converted '{column}' to {target_type}"
|
| 252 |
+
|
| 253 |
+
def _rename_column(self, df: pd.DataFrame,
|
| 254 |
+
params: Dict) -> Tuple[pd.DataFrame, float, str]:
|
| 255 |
+
old_name = params.get("old_name")
|
| 256 |
+
new_name = params.get("new_name")
|
| 257 |
+
|
| 258 |
+
if old_name not in df.columns:
|
| 259 |
+
return df, PENALTY_WRONG_ACTION, \
|
| 260 |
+
f"Column '{old_name}' not found"
|
| 261 |
+
|
| 262 |
+
expected = clean_column_name(old_name)
|
| 263 |
+
if new_name != expected:
|
| 264 |
+
return df, PENALTY_WRONG_ACTION, \
|
| 265 |
+
f"Suggested name '{new_name}' β expected '{expected}'"
|
| 266 |
+
|
| 267 |
+
df = df.rename(columns={old_name: new_name})
|
| 268 |
+
return df, REWARD_ISSUE_FIXED, \
|
| 269 |
+
f"Renamed '{old_name}' to '{new_name}'"
|
| 270 |
+
|
| 271 |
+
def _remove_outliers(self, df: pd.DataFrame,
|
| 272 |
+
params: Dict) -> Tuple[pd.DataFrame, float, str]:
|
| 273 |
+
column = params.get("column")
|
| 274 |
+
method = params.get("method", "iqr")
|
| 275 |
+
|
| 276 |
+
if column not in df.columns:
|
| 277 |
+
return df, PENALTY_WRONG_ACTION, f"Column '{column}' not found"
|
| 278 |
+
|
| 279 |
+
df = df.copy()
|
| 280 |
+
try:
|
| 281 |
+
numeric = pd.to_numeric(df[column], errors="coerce")
|
| 282 |
+
df[column] = numeric
|
| 283 |
+
|
| 284 |
+
if method == "iqr":
|
| 285 |
+
mask = detect_outliers_iqr(df, column)
|
| 286 |
+
else:
|
| 287 |
+
from utils import detect_outliers_zscore
|
| 288 |
+
mask = detect_outliers_zscore(df, column)
|
| 289 |
+
|
| 290 |
+
count = mask.sum()
|
| 291 |
+
if count == 0:
|
| 292 |
+
return df, PENALTY_WRONG_ACTION, \
|
| 293 |
+
f"No outliers in '{column}' β redundant"
|
| 294 |
+
|
| 295 |
+
if count > len(df) * 0.4:
|
| 296 |
+
return df, PENALTY_DESTRUCTIVE, \
|
| 297 |
+
f"Would remove {count} rows β too destructive"
|
| 298 |
+
|
| 299 |
+
df = df[~mask].reset_index(drop=True)
|
| 300 |
+
return df, REWARD_ISSUE_FIXED, \
|
| 301 |
+
f"Removed {count} outliers from '{column}'"
|
| 302 |
+
|
| 303 |
+
except Exception as e:
|
| 304 |
+
return df, PENALTY_WRONG_ACTION, f"Outlier removal failed: {e}"
|
| 305 |
+
|
| 306 |
+
def _standardize_values(self, df: pd.DataFrame,
|
| 307 |
+
params: Dict
|
| 308 |
+
) -> Tuple[pd.DataFrame, float, str]:
|
| 309 |
+
column = params.get("column")
|
| 310 |
+
mapping = params.get("mapping", {})
|
| 311 |
+
|
| 312 |
+
if column not in df.columns:
|
| 313 |
+
return df, PENALTY_WRONG_ACTION, f"Column '{column}' not found"
|
| 314 |
+
|
| 315 |
+
if not mapping:
|
| 316 |
+
return df, PENALTY_WRONG_ACTION, "Empty mapping provided"
|
| 317 |
+
|
| 318 |
+
before = df[column].value_counts().to_dict()
|
| 319 |
+
df = standardize_column(df, column, mapping)
|
| 320 |
+
after = df[column].value_counts().to_dict()
|
| 321 |
+
|
| 322 |
+
changed = sum(1 for k, v in before.items()
|
| 323 |
+
if after.get(mapping.get(k, k), 0) != v)
|
| 324 |
+
if changed == 0:
|
| 325 |
+
return df, PENALTY_WRONG_ACTION, "No values changed"
|
| 326 |
+
|
| 327 |
+
return df, REWARD_ISSUE_FIXED, \
|
| 328 |
+
f"Standardized {len(mapping)} values in '{column}'"
|
| 329 |
+
|
| 330 |
+
def _submit(self, df: pd.DataFrame
|
| 331 |
+
) -> Tuple[pd.DataFrame, float, str, Dict]:
|
| 332 |
+
score, details = self.task.grade()
|
| 333 |
+
self.done = True
|
| 334 |
+
|
| 335 |
+
if score >= 0.9:
|
| 336 |
+
reward = REWARD_SUBMIT_BONUS + score
|
| 337 |
+
reason = f"Excellent! Score: {score:.3f} β dataset is clean!"
|
| 338 |
+
elif score >= 0.6:
|
| 339 |
+
reward = score
|
| 340 |
+
reason = f"Good effort. Score: {score:.3f} β some issues remain"
|
| 341 |
+
else:
|
| 342 |
+
reward = score * 0.5
|
| 343 |
+
reason = f"Submitted early. Score: {score:.3f} β many issues remain"
|
| 344 |
+
|
| 345 |
+
return df, reward, reason, {"final_score": score, "details": details}
|
| 346 |
+
|
| 347 |
+
# ββ Observation Builder βββββββββββββββββββ
|
| 348 |
+
|
| 349 |
+
def _make_observation(self) -> Observation:
|
| 350 |
+
df = self.task.current_df
|
| 351 |
+
return Observation(
|
| 352 |
+
task_id = self.task.task_id,
|
| 353 |
+
difficulty = self.difficulty,
|
| 354 |
+
step = self.step_count,
|
| 355 |
+
max_steps = MAX_STEPS,
|
| 356 |
+
dataframe = df_to_records(df),
|
| 357 |
+
columns = list(df.columns),
|
| 358 |
+
dtypes = {c: str(t) for c, t in df.dtypes.items()},
|
| 359 |
+
null_counts = get_null_counts(df),
|
| 360 |
+
duplicate_rows = get_duplicate_count(df),
|
| 361 |
+
issues = self.task.issues,
|
| 362 |
+
done = self.done,
|
| 363 |
+
)
|
graders.py
ADDED
|
@@ -0,0 +1,203 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# graders.py
|
| 2 |
+
# βββββββββββββββββββββββββββββββββββββββββββββ
|
| 3 |
+
# Grading logic for all 3 tasks
|
| 4 |
+
# Returns scores between 0.0 and 1.0
|
| 5 |
+
# βββββββββββββββββββββββββββββββββββββββββββββ
|
| 6 |
+
|
| 7 |
+
import pandas as pd
|
| 8 |
+
import numpy as np
|
| 9 |
+
from typing import Dict, Tuple
|
| 10 |
+
from utils import (
|
| 11 |
+
compute_null_score,
|
| 12 |
+
compute_duplicate_score,
|
| 13 |
+
compute_dtype_score,
|
| 14 |
+
detect_outliers_iqr,
|
| 15 |
+
has_bad_column_names,
|
| 16 |
+
clamp,
|
| 17 |
+
)
|
| 18 |
+
|
| 19 |
+
|
| 20 |
+
# ββ Task 1 Grader β Easy ββββββββββββββββββββββ
|
| 21 |
+
|
| 22 |
+
def grade_task1(original_df: pd.DataFrame,
|
| 23 |
+
current_df: pd.DataFrame) -> Tuple[float, Dict]:
|
| 24 |
+
"""
|
| 25 |
+
Scores:
|
| 26 |
+
- 40% null values fixed
|
| 27 |
+
- 40% correct dtypes (age=int, salary=float)
|
| 28 |
+
- 20% no data loss (row count preserved)
|
| 29 |
+
"""
|
| 30 |
+
details = {}
|
| 31 |
+
|
| 32 |
+
# Null score (40%)
|
| 33 |
+
null_score = compute_null_score(original_df, current_df)
|
| 34 |
+
details["null_score"] = null_score
|
| 35 |
+
|
| 36 |
+
# Dtype score (40%)
|
| 37 |
+
expected_dtypes = {"age": "int64", "salary": "float64"}
|
| 38 |
+
dtype_score = compute_dtype_score(current_df, expected_dtypes)
|
| 39 |
+
details["dtype_score"] = dtype_score
|
| 40 |
+
|
| 41 |
+
# Row preservation score (20%)
|
| 42 |
+
expected_rows = len(original_df)
|
| 43 |
+
actual_rows = len(current_df)
|
| 44 |
+
row_score = 1.0 if actual_rows == expected_rows else \
|
| 45 |
+
clamp(actual_rows / expected_rows)
|
| 46 |
+
details["row_score"] = row_score
|
| 47 |
+
|
| 48 |
+
# Final weighted score
|
| 49 |
+
final = (
|
| 50 |
+
0.40 * null_score +
|
| 51 |
+
0.40 * dtype_score +
|
| 52 |
+
0.20 * row_score
|
| 53 |
+
)
|
| 54 |
+
details["final"] = round(clamp(final), 4)
|
| 55 |
+
return details["final"], details
|
| 56 |
+
|
| 57 |
+
|
| 58 |
+
# ββ Task 2 Grader β Medium ββββββββββββββββββββ
|
| 59 |
+
|
| 60 |
+
def grade_task2(original_df: pd.DataFrame,
|
| 61 |
+
current_df: pd.DataFrame) -> Tuple[float, Dict]:
|
| 62 |
+
"""
|
| 63 |
+
Scores:
|
| 64 |
+
- 35% duplicates removed
|
| 65 |
+
- 35% outliers handled in salary
|
| 66 |
+
- 30% country values standardized
|
| 67 |
+
"""
|
| 68 |
+
details = {}
|
| 69 |
+
|
| 70 |
+
# Duplicate score (35%)
|
| 71 |
+
dup_score = compute_duplicate_score(original_df, current_df)
|
| 72 |
+
details["duplicate_score"] = dup_score
|
| 73 |
+
|
| 74 |
+
# Outlier score (35%)
|
| 75 |
+
if "salary" in current_df.columns:
|
| 76 |
+
try:
|
| 77 |
+
salary_col = pd.to_numeric(current_df["salary"], errors="coerce")
|
| 78 |
+
outlier_mask = detect_outliers_iqr(
|
| 79 |
+
current_df.assign(salary=salary_col), "salary"
|
| 80 |
+
)
|
| 81 |
+
remaining_outliers = outlier_mask.sum()
|
| 82 |
+
original_salary = pd.to_numeric(
|
| 83 |
+
original_df["salary"], errors="coerce"
|
| 84 |
+
)
|
| 85 |
+
original_outliers = detect_outliers_iqr(
|
| 86 |
+
original_df.assign(salary=original_salary), "salary"
|
| 87 |
+
).sum()
|
| 88 |
+
if original_outliers == 0:
|
| 89 |
+
outlier_score = 1.0
|
| 90 |
+
else:
|
| 91 |
+
fixed = original_outliers - remaining_outliers
|
| 92 |
+
outlier_score = clamp(fixed / original_outliers)
|
| 93 |
+
except Exception:
|
| 94 |
+
outlier_score = 0.0
|
| 95 |
+
else:
|
| 96 |
+
outlier_score = 0.0
|
| 97 |
+
details["outlier_score"] = outlier_score
|
| 98 |
+
|
| 99 |
+
# Country standardization score (30%)
|
| 100 |
+
if "country" in current_df.columns:
|
| 101 |
+
standard_values = {"United States", "UK"}
|
| 102 |
+
unique_vals = set(current_df["country"].dropna().unique())
|
| 103 |
+
non_standard = unique_vals - standard_values
|
| 104 |
+
country_score = 1.0 if not non_standard else \
|
| 105 |
+
clamp(1 - len(non_standard) / len(unique_vals))
|
| 106 |
+
else:
|
| 107 |
+
country_score = 0.0
|
| 108 |
+
details["country_score"] = country_score
|
| 109 |
+
|
| 110 |
+
# Final weighted score
|
| 111 |
+
final = (
|
| 112 |
+
0.35 * dup_score +
|
| 113 |
+
0.35 * outlier_score +
|
| 114 |
+
0.30 * country_score
|
| 115 |
+
)
|
| 116 |
+
details["final"] = round(clamp(final), 4)
|
| 117 |
+
return details["final"], details
|
| 118 |
+
|
| 119 |
+
|
| 120 |
+
# ββ Task 3 Grader β Hard ββββββββββββββββββββββ
|
| 121 |
+
|
| 122 |
+
def grade_task3(original_df: pd.DataFrame,
|
| 123 |
+
current_df: pd.DataFrame,
|
| 124 |
+
lookup_df: pd.DataFrame) -> Tuple[float, Dict]:
|
| 125 |
+
"""
|
| 126 |
+
Scores:
|
| 127 |
+
- 25% null values fixed
|
| 128 |
+
- 20% column names cleaned
|
| 129 |
+
- 20% outliers handled
|
| 130 |
+
- 20% country standardized
|
| 131 |
+
- 15% referential integrity (dept_id valid)
|
| 132 |
+
"""
|
| 133 |
+
details = {}
|
| 134 |
+
|
| 135 |
+
# Null score (25%)
|
| 136 |
+
null_score = compute_null_score(original_df, current_df)
|
| 137 |
+
details["null_score"] = null_score
|
| 138 |
+
|
| 139 |
+
# Column name score (20%)
|
| 140 |
+
bad_cols = has_bad_column_names(current_df)
|
| 141 |
+
col_score = 1.0 if not bad_cols else \
|
| 142 |
+
clamp(1 - len(bad_cols) / len(current_df.columns))
|
| 143 |
+
details["column_name_score"] = col_score
|
| 144 |
+
|
| 145 |
+
# Outlier score (20%)
|
| 146 |
+
numeric_cols = current_df.select_dtypes(include=[np.number]).columns
|
| 147 |
+
salary_like = [c for c in numeric_cols if "salary" in c.lower()]
|
| 148 |
+
if salary_like:
|
| 149 |
+
col = salary_like[0]
|
| 150 |
+
try:
|
| 151 |
+
outlier_mask = detect_outliers_iqr(current_df, col)
|
| 152 |
+
orig_col = [c for c in original_df.columns
|
| 153 |
+
if "salary" in c.lower()]
|
| 154 |
+
if orig_col:
|
| 155 |
+
orig_mask = detect_outliers_iqr(original_df, orig_col[0])
|
| 156 |
+
orig_count = orig_mask.sum()
|
| 157 |
+
if orig_count == 0:
|
| 158 |
+
outlier_score = 1.0
|
| 159 |
+
else:
|
| 160 |
+
fixed = orig_count - outlier_mask.sum()
|
| 161 |
+
outlier_score = clamp(fixed / orig_count)
|
| 162 |
+
else:
|
| 163 |
+
outlier_score = 1.0
|
| 164 |
+
except Exception:
|
| 165 |
+
outlier_score = 0.0
|
| 166 |
+
else:
|
| 167 |
+
outlier_score = 0.0
|
| 168 |
+
details["outlier_score"] = outlier_score
|
| 169 |
+
|
| 170 |
+
# Country standardization (20%)
|
| 171 |
+
country_cols = [c for c in current_df.columns if "country" in c.lower()]
|
| 172 |
+
if country_cols:
|
| 173 |
+
standard = {"United States", "UK"}
|
| 174 |
+
unique_vals = set(current_df[country_cols[0]].dropna().unique())
|
| 175 |
+
non_standard = unique_vals - standard
|
| 176 |
+
country_score = 1.0 if not non_standard else \
|
| 177 |
+
clamp(1 - len(non_standard) / len(unique_vals))
|
| 178 |
+
else:
|
| 179 |
+
country_score = 0.0
|
| 180 |
+
details["country_score"] = country_score
|
| 181 |
+
|
| 182 |
+
# Referential integrity (15%)
|
| 183 |
+
dept_cols = [c for c in current_df.columns if "dept_id" in c.lower()]
|
| 184 |
+
if dept_cols and lookup_df is not None:
|
| 185 |
+
valid_ids = set(lookup_df["dept_id"].unique())
|
| 186 |
+
actual_ids = set(current_df[dept_cols[0]].dropna().unique())
|
| 187 |
+
invalid = actual_ids - valid_ids
|
| 188 |
+
ref_score = 1.0 if not invalid else \
|
| 189 |
+
clamp(1 - len(invalid) / len(actual_ids))
|
| 190 |
+
else:
|
| 191 |
+
ref_score = 0.0
|
| 192 |
+
details["referential_integrity_score"] = ref_score
|
| 193 |
+
|
| 194 |
+
# Final weighted score
|
| 195 |
+
final = (
|
| 196 |
+
0.25 * null_score +
|
| 197 |
+
0.20 * col_score +
|
| 198 |
+
0.20 * outlier_score +
|
| 199 |
+
0.20 * country_score +
|
| 200 |
+
0.15 * ref_score
|
| 201 |
+
)
|
| 202 |
+
details["final"] = round(clamp(final), 4)
|
| 203 |
+
return details["final"], details
|
inference.py
ADDED
|
File without changes
|
models.py
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# models.py
|
| 2 |
+
# βββββββββββββββββββββββββββββββββββββββββββββ
|
| 3 |
+
# Pydantic models for OpenEnv spec compliance
|
| 4 |
+
# Observation, Action, Reward typed models
|
| 5 |
+
# βββββββββββββββββββββββββββββββββββββββββββββ
|
| 6 |
+
|
| 7 |
+
from pydantic import BaseModel, Field
|
| 8 |
+
from typing import Any, Dict, List, Optional
|
| 9 |
+
|
| 10 |
+
# ββ Observation βββββββββββββββββββββββββββββββ
|
| 11 |
+
class Observation(BaseModel):
|
| 12 |
+
"""What the agent sees at each step"""
|
| 13 |
+
|
| 14 |
+
task_id: str = Field(..., description="Current task identifier")
|
| 15 |
+
difficulty: str = Field(..., description="easy / medium / hard")
|
| 16 |
+
step: int = Field(..., description="Current step number")
|
| 17 |
+
max_steps: int = Field(..., description="Maximum steps allowed")
|
| 18 |
+
dataframe: List[Dict[str, Any]] = Field(..., description="Current data as list of row dicts")
|
| 19 |
+
columns: List[str] = Field(..., description="Column names")
|
| 20 |
+
dtypes: Dict[str, str] = Field(..., description="Column -> dtype mapping")
|
| 21 |
+
null_counts: Dict[str, int] = Field(..., description="Column -> null count")
|
| 22 |
+
duplicate_rows: int = Field(..., description="Number of duplicate rows")
|
| 23 |
+
issues: List[str] = Field(..., description="List of detected issues")
|
| 24 |
+
done: bool = Field(False, description="Is episode over?")
|
| 25 |
+
|
| 26 |
+
# ββ Action ββββββββββββββββββββββββββββββββββββ
|
| 27 |
+
class Action(BaseModel):
|
| 28 |
+
"""What the agent can do"""
|
| 29 |
+
|
| 30 |
+
action_type: str = Field(..., description="""
|
| 31 |
+
One of:
|
| 32 |
+
fill_missing | drop_duplicates | fix_dtype |
|
| 33 |
+
rename_column | remove_outliers |
|
| 34 |
+
standardize_values | submit
|
| 35 |
+
""")
|
| 36 |
+
parameters: Dict[str, Any] = Field(default_factory=dict, description="Action parameters")
|
| 37 |
+
|
| 38 |
+
# ββ Reward ββββββββββββββββββββββββββββββββββββ
|
| 39 |
+
class Reward(BaseModel):
|
| 40 |
+
"""Score returned after each step"""
|
| 41 |
+
|
| 42 |
+
value: float = Field(..., description="Reward value for this step")
|
| 43 |
+
total: float = Field(..., description="Cumulative reward so far")
|
| 44 |
+
reason: str = Field(..., description="Why this reward was given")
|
| 45 |
+
|
| 46 |
+
# ββ Step Result βββββββββββββββββββββββββββββββ
|
| 47 |
+
class StepResult(BaseModel):
|
| 48 |
+
"""Full return value of step()"""
|
| 49 |
+
|
| 50 |
+
observation: Observation
|
| 51 |
+
reward: Reward
|
| 52 |
+
done: bool
|
| 53 |
+
info: Dict[str, Any] = Field(default_factory=dict)
|
| 54 |
+
|
| 55 |
+
# ββ Task Info βββββββββββββββββββββββββββββββββ
|
| 56 |
+
class TaskInfo(BaseModel):
|
| 57 |
+
"""Metadata about a task"""
|
| 58 |
+
|
| 59 |
+
task_id: str
|
| 60 |
+
difficulty: str
|
| 61 |
+
description: str
|
| 62 |
+
max_steps: int
|
| 63 |
+
score: Optional[float] = None
|
openenv.yaml
ADDED
|
@@ -0,0 +1,129 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# openenv.yaml
|
| 2 |
+
# βββββββββββββββββββββββββββββββββββββββββββββ
|
| 3 |
+
# OpenEnv metadata specification
|
| 4 |
+
# βββββββββββββββββββββββββββββββββββββββββββββ
|
| 5 |
+
|
| 6 |
+
name: data-cleaning-env
|
| 7 |
+
version: 1.0.0
|
| 8 |
+
description: >
|
| 9 |
+
An OpenEnv environment where an AI agent cleans messy CSV datasets.
|
| 10 |
+
The agent receives a dataset with issues like missing values, duplicates,
|
| 11 |
+
outliers, bad column names, and inconsistent values, and must fix them
|
| 12 |
+
step by step using structured actions.
|
| 13 |
+
|
| 14 |
+
author: Yashwanth34567
|
| 15 |
+
repository: https://huggingface.co/spaces/Yashwanth34567/data-cleaning-env
|
| 16 |
+
tags:
|
| 17 |
+
- openenv
|
| 18 |
+
- data-cleaning
|
| 19 |
+
- csv
|
| 20 |
+
- real-world
|
| 21 |
+
- tabular
|
| 22 |
+
|
| 23 |
+
# ββ Action Space ββββββββββββββββββββββββββββββ
|
| 24 |
+
actions:
|
| 25 |
+
- name: fill_missing
|
| 26 |
+
description: Fill null values in a column
|
| 27 |
+
parameters:
|
| 28 |
+
column: string
|
| 29 |
+
strategy: "mean | median | mode | drop"
|
| 30 |
+
|
| 31 |
+
- name: drop_duplicates
|
| 32 |
+
description: Remove duplicate rows
|
| 33 |
+
parameters: {}
|
| 34 |
+
|
| 35 |
+
- name: fix_dtype
|
| 36 |
+
description: Convert column to target dtype
|
| 37 |
+
parameters:
|
| 38 |
+
column: string
|
| 39 |
+
target_type: "int | float | str | datetime"
|
| 40 |
+
|
| 41 |
+
- name: rename_column
|
| 42 |
+
description: Rename a column to clean snake_case
|
| 43 |
+
parameters:
|
| 44 |
+
old_name: string
|
| 45 |
+
new_name: string
|
| 46 |
+
|
| 47 |
+
- name: remove_outliers
|
| 48 |
+
description: Remove outlier rows from a numeric column
|
| 49 |
+
parameters:
|
| 50 |
+
column: string
|
| 51 |
+
method: "iqr | zscore"
|
| 52 |
+
|
| 53 |
+
- name: standardize_values
|
| 54 |
+
description: Replace inconsistent values using a mapping
|
| 55 |
+
parameters:
|
| 56 |
+
column: string
|
| 57 |
+
mapping: object
|
| 58 |
+
|
| 59 |
+
- name: submit
|
| 60 |
+
description: Finalize episode and trigger grader
|
| 61 |
+
parameters: {}
|
| 62 |
+
|
| 63 |
+
# ββ Observation Space βββββββββββββββββββββββββ
|
| 64 |
+
observation:
|
| 65 |
+
task_id: string
|
| 66 |
+
difficulty: "easy | medium | hard"
|
| 67 |
+
step: integer
|
| 68 |
+
max_steps: integer
|
| 69 |
+
dataframe: array
|
| 70 |
+
columns: array
|
| 71 |
+
dtypes: object
|
| 72 |
+
null_counts: object
|
| 73 |
+
duplicate_rows: integer
|
| 74 |
+
issues: array
|
| 75 |
+
done: boolean
|
| 76 |
+
|
| 77 |
+
# ββ Reward ββββββββββββββββββββββββββββββββββββ
|
| 78 |
+
reward:
|
| 79 |
+
type: float
|
| 80 |
+
range: [-1.0, 1.0]
|
| 81 |
+
description: >
|
| 82 |
+
Partial progress rewards at each step.
|
| 83 |
+
+0.10 per issue fixed, +0.30 bonus on clean submit,
|
| 84 |
+
-0.05 for wrong/redundant actions,
|
| 85 |
+
-0.10 for destructive actions.
|
| 86 |
+
|
| 87 |
+
# ββ Tasks βββββββββββββββββββββββββββββββββββββ
|
| 88 |
+
tasks:
|
| 89 |
+
- id: task_1_easy
|
| 90 |
+
difficulty: easy
|
| 91 |
+
description: >
|
| 92 |
+
Fix a 10-row employee dataset.
|
| 93 |
+
Fill missing values in age and salary columns.
|
| 94 |
+
Convert age from string to integer dtype.
|
| 95 |
+
score_range: [0.0, 1.0]
|
| 96 |
+
|
| 97 |
+
- id: task_2_medium
|
| 98 |
+
difficulty: medium
|
| 99 |
+
description: >
|
| 100 |
+
Fix a 20-row dataset.
|
| 101 |
+
Remove duplicate rows.
|
| 102 |
+
Handle salary outliers using IQR method.
|
| 103 |
+
Standardize country values to United States or UK.
|
| 104 |
+
score_range: [0.0, 1.0]
|
| 105 |
+
|
| 106 |
+
- id: task_3_hard
|
| 107 |
+
difficulty: hard
|
| 108 |
+
description: >
|
| 109 |
+
Fix a 30-row dataset with all issues combined.
|
| 110 |
+
Fix bad column names with trailing spaces, uppercase, special chars.
|
| 111 |
+
Fill missing values, remove duplicates and outliers.
|
| 112 |
+
Standardize country values.
|
| 113 |
+
Fix invalid dept_id values for referential integrity.
|
| 114 |
+
score_range: [0.0, 1.0]
|
| 115 |
+
|
| 116 |
+
# ββ Environment Settings ββββββββββββββββββββββ
|
| 117 |
+
settings:
|
| 118 |
+
max_steps: 20
|
| 119 |
+
render_mode: gradio
|
| 120 |
+
server_host: 0.0.0.0
|
| 121 |
+
server_port: 7860
|
| 122 |
+
|
| 123 |
+
# ββ API Endpoints βββββββββββββββββββββββββββββ
|
| 124 |
+
endpoints:
|
| 125 |
+
reset: POST /reset
|
| 126 |
+
step: POST /step
|
| 127 |
+
state: GET /state
|
| 128 |
+
tasks: GET /tasks
|
| 129 |
+
spec: GET /openenv.yaml
|
requirements.txt
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# requirements.txt
|
| 2 |
+
# βββββββββββββββββββββββββββββββββββββββββββββ
|
| 3 |
+
# Python dependencies for data-cleaning-env
|
| 4 |
+
# βββββββββββββββββββββββββββββββββββββββββββββ
|
| 5 |
+
|
| 6 |
+
# Core
|
| 7 |
+
fastapi==0.115.0
|
| 8 |
+
uvicorn==0.30.6
|
| 9 |
+
gradio==4.44.1
|
| 10 |
+
pydantic==2.9.0
|
| 11 |
+
|
| 12 |
+
# Data
|
| 13 |
+
pandas==2.2.3
|
| 14 |
+
numpy==1.26.4
|
| 15 |
+
|
| 16 |
+
# LLM
|
| 17 |
+
openai==2.30.0
|
| 18 |
+
|
| 19 |
+
# Utils
|
| 20 |
+
pyyaml==6.0.2
|
| 21 |
+
python-multipart==0.0.9
|
| 22 |
+
huggingface-hub==0.25.0
|
tasks.py
ADDED
|
@@ -0,0 +1,174 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# tasks.py
|
| 2 |
+
# βββββββββββββββββββββββββββββββββββββββββββββ
|
| 3 |
+
# Task definitions for all 3 difficulty levels
|
| 4 |
+
# Each task has a description, dataset, grader
|
| 5 |
+
# βββββββββββββββββββββββββββββββββββββββββββββ
|
| 6 |
+
|
| 7 |
+
import pandas as pd
|
| 8 |
+
from typing import Dict, Any, Tuple, Optional
|
| 9 |
+
from config import (
|
| 10 |
+
TASK_EASY, TASK_MEDIUM, TASK_HARD, TASK_IDS, MAX_STEPS
|
| 11 |
+
)
|
| 12 |
+
from datasets import (
|
| 13 |
+
generate_easy_dataset,
|
| 14 |
+
generate_medium_dataset,
|
| 15 |
+
generate_hard_dataset,
|
| 16 |
+
detect_issues,
|
| 17 |
+
)
|
| 18 |
+
from graders import grade_task1, grade_task2, grade_task3
|
| 19 |
+
from models import TaskInfo
|
| 20 |
+
|
| 21 |
+
|
| 22 |
+
# ββ Task Registry βββββββββββββββββββββββββββββ
|
| 23 |
+
|
| 24 |
+
TASK_REGISTRY = {
|
| 25 |
+
TASK_EASY: {
|
| 26 |
+
"task_id": TASK_IDS[TASK_EASY],
|
| 27 |
+
"difficulty": TASK_EASY,
|
| 28 |
+
"description": (
|
| 29 |
+
"Fix a 10-row employee dataset: "
|
| 30 |
+
"fill missing values in 'age' and 'salary', "
|
| 31 |
+
"convert 'age' from string to integer."
|
| 32 |
+
),
|
| 33 |
+
"max_steps": MAX_STEPS,
|
| 34 |
+
"issues": [
|
| 35 |
+
"missing_values in age and salary columns",
|
| 36 |
+
"wrong dtype: age should be int not string",
|
| 37 |
+
],
|
| 38 |
+
"hints": [
|
| 39 |
+
"Use fill_missing to handle null values",
|
| 40 |
+
"Use fix_dtype to convert age to int",
|
| 41 |
+
"Call submit when the dataset looks clean",
|
| 42 |
+
],
|
| 43 |
+
},
|
| 44 |
+
TASK_MEDIUM: {
|
| 45 |
+
"task_id": TASK_IDS[TASK_MEDIUM],
|
| 46 |
+
"difficulty": TASK_MEDIUM,
|
| 47 |
+
"description": (
|
| 48 |
+
"Fix a 20-row dataset: "
|
| 49 |
+
"remove duplicate rows, "
|
| 50 |
+
"handle salary outliers, "
|
| 51 |
+
"standardize country values to 'United States' or 'UK'."
|
| 52 |
+
),
|
| 53 |
+
"max_steps": MAX_STEPS,
|
| 54 |
+
"issues": [
|
| 55 |
+
"duplicate rows present",
|
| 56 |
+
"salary column has extreme outliers",
|
| 57 |
+
"country has inconsistent values (USA, US, America, etc.)",
|
| 58 |
+
],
|
| 59 |
+
"hints": [
|
| 60 |
+
"Use drop_duplicates first",
|
| 61 |
+
"Use remove_outliers on salary column",
|
| 62 |
+
"Use standardize_values to unify country names",
|
| 63 |
+
],
|
| 64 |
+
},
|
| 65 |
+
TASK_HARD: {
|
| 66 |
+
"task_id": TASK_IDS[TASK_HARD],
|
| 67 |
+
"difficulty": TASK_HARD,
|
| 68 |
+
"description": (
|
| 69 |
+
"Fix a 30-row dataset with all issues: "
|
| 70 |
+
"nulls, duplicates, outliers, "
|
| 71 |
+
"bad column names (trailing spaces, uppercase, special chars), "
|
| 72 |
+
"inconsistent country values, "
|
| 73 |
+
"and invalid dept_id values (referential integrity)."
|
| 74 |
+
),
|
| 75 |
+
"max_steps": MAX_STEPS,
|
| 76 |
+
"issues": [
|
| 77 |
+
"missing values in multiple columns",
|
| 78 |
+
"bad column names: 'Full Name ', 'AGE', 'salary$', 'COUNTRY'",
|
| 79 |
+
"salary outliers and negative values",
|
| 80 |
+
"inconsistent country values",
|
| 81 |
+
"invalid dept_id: 99 does not exist in lookup table",
|
| 82 |
+
],
|
| 83 |
+
"hints": [
|
| 84 |
+
"Start by renaming bad column names",
|
| 85 |
+
"Then fix nulls, duplicates, and outliers",
|
| 86 |
+
"Standardize country values",
|
| 87 |
+
"Fix invalid dept_id values last",
|
| 88 |
+
],
|
| 89 |
+
},
|
| 90 |
+
}
|
| 91 |
+
|
| 92 |
+
|
| 93 |
+
# ββ Task Class ββββββββββββββββββββββββββββββββ
|
| 94 |
+
|
| 95 |
+
class Task:
|
| 96 |
+
"""Represents a single task with its dataset and grader"""
|
| 97 |
+
|
| 98 |
+
def __init__(self, difficulty: str):
|
| 99 |
+
if difficulty not in TASK_REGISTRY:
|
| 100 |
+
raise ValueError(f"Unknown difficulty: {difficulty}")
|
| 101 |
+
|
| 102 |
+
self.difficulty = difficulty
|
| 103 |
+
self.meta = TASK_REGISTRY[difficulty]
|
| 104 |
+
self.task_id = self.meta["task_id"]
|
| 105 |
+
self.description = self.meta["description"]
|
| 106 |
+
self.max_steps = self.meta["max_steps"]
|
| 107 |
+
self.hints = self.meta["hints"]
|
| 108 |
+
|
| 109 |
+
# Dataset state
|
| 110 |
+
self.original_df: Optional[pd.DataFrame] = None
|
| 111 |
+
self.current_df: Optional[pd.DataFrame] = None
|
| 112 |
+
self.lookup_df: Optional[pd.DataFrame] = None
|
| 113 |
+
self.issues: list = []
|
| 114 |
+
|
| 115 |
+
def reset(self) -> pd.DataFrame:
|
| 116 |
+
"""Generate a fresh dataset and return it"""
|
| 117 |
+
if self.difficulty == TASK_EASY:
|
| 118 |
+
self.original_df = generate_easy_dataset()
|
| 119 |
+
|
| 120 |
+
elif self.difficulty == TASK_MEDIUM:
|
| 121 |
+
self.original_df = generate_medium_dataset()
|
| 122 |
+
|
| 123 |
+
elif self.difficulty == TASK_HARD:
|
| 124 |
+
self.original_df, self.lookup_df = generate_hard_dataset()
|
| 125 |
+
|
| 126 |
+
self.current_df = self.original_df.copy()
|
| 127 |
+
self.issues = detect_issues(self.current_df, self.task_id)
|
| 128 |
+
return self.current_df
|
| 129 |
+
|
| 130 |
+
def grade(self) -> Tuple[float, Dict[str, Any]]:
|
| 131 |
+
"""Grade the current state of the dataset"""
|
| 132 |
+
if self.current_df is None or self.original_df is None:
|
| 133 |
+
return 0.0, {"error": "Task not initialized, call reset() first"}
|
| 134 |
+
|
| 135 |
+
if self.difficulty == TASK_EASY:
|
| 136 |
+
return grade_task1(self.original_df, self.current_df)
|
| 137 |
+
|
| 138 |
+
elif self.difficulty == TASK_MEDIUM:
|
| 139 |
+
return grade_task2(self.original_df, self.current_df)
|
| 140 |
+
|
| 141 |
+
elif self.difficulty == TASK_HARD:
|
| 142 |
+
return grade_task3(
|
| 143 |
+
self.original_df, self.current_df, self.lookup_df
|
| 144 |
+
)
|
| 145 |
+
|
| 146 |
+
return 0.0, {}
|
| 147 |
+
|
| 148 |
+
def update_issues(self):
|
| 149 |
+
"""Refresh issue list based on current state"""
|
| 150 |
+
if self.current_df is not None:
|
| 151 |
+
self.issues = detect_issues(self.current_df, self.task_id)
|
| 152 |
+
|
| 153 |
+
def get_info(self) -> TaskInfo:
|
| 154 |
+
"""Return TaskInfo model"""
|
| 155 |
+
score, _ = self.grade() if self.current_df is not None else (None, {})
|
| 156 |
+
return TaskInfo(
|
| 157 |
+
task_id = self.task_id,
|
| 158 |
+
difficulty = self.difficulty,
|
| 159 |
+
description = self.description,
|
| 160 |
+
max_steps = self.max_steps,
|
| 161 |
+
score = score,
|
| 162 |
+
)
|
| 163 |
+
|
| 164 |
+
|
| 165 |
+
# ββ Task Factory ββββββββββββββββββββββββββββββ
|
| 166 |
+
|
| 167 |
+
def get_task(difficulty: str) -> Task:
|
| 168 |
+
"""Create and return a Task instance"""
|
| 169 |
+
return Task(difficulty)
|
| 170 |
+
|
| 171 |
+
|
| 172 |
+
def get_all_tasks() -> Dict[str, Task]:
|
| 173 |
+
"""Return all tasks as a dict"""
|
| 174 |
+
return {d: Task(d) for d in [TASK_EASY, TASK_MEDIUM, TASK_HARD]}
|
utils.py
ADDED
|
@@ -0,0 +1,142 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# utils.py
|
| 2 |
+
# βββββββββββββββββββββββββββββββββββββββββββββ
|
| 3 |
+
# Helper functions used across the project
|
| 4 |
+
# βββββββββββββββββββββββββββββββββββββββββββββ
|
| 5 |
+
|
| 6 |
+
import pandas as pd
|
| 7 |
+
import numpy as np
|
| 8 |
+
from typing import Any, Dict, List, Tuple
|
| 9 |
+
|
| 10 |
+
|
| 11 |
+
# ββ DataFrame Helpers βββββββββββββββββββββββββ
|
| 12 |
+
|
| 13 |
+
def df_to_records(df: pd.DataFrame) -> List[Dict[str, Any]]:
|
| 14 |
+
"""Convert DataFrame to list of dicts (for JSON serialization)"""
|
| 15 |
+
return df.where(pd.notnull(df), None).to_dict(orient="records")
|
| 16 |
+
|
| 17 |
+
|
| 18 |
+
def df_summary(df: pd.DataFrame) -> Dict[str, Any]:
|
| 19 |
+
"""Return a quick summary of a DataFrame"""
|
| 20 |
+
return {
|
| 21 |
+
"shape": list(df.shape),
|
| 22 |
+
"columns": list(df.columns),
|
| 23 |
+
"dtypes": {col: str(dtype) for col, dtype in df.dtypes.items()},
|
| 24 |
+
"null_counts": df.isnull().sum().to_dict(),
|
| 25 |
+
"duplicate_rows": int(df.duplicated().sum()),
|
| 26 |
+
}
|
| 27 |
+
|
| 28 |
+
|
| 29 |
+
def get_null_counts(df: pd.DataFrame) -> Dict[str, int]:
|
| 30 |
+
"""Return null count per column"""
|
| 31 |
+
return {col: int(count) for col, count in df.isnull().sum().items()}
|
| 32 |
+
|
| 33 |
+
|
| 34 |
+
def get_duplicate_count(df: pd.DataFrame) -> int:
|
| 35 |
+
"""Return number of duplicate rows"""
|
| 36 |
+
return int(df.duplicated().sum())
|
| 37 |
+
|
| 38 |
+
|
| 39 |
+
# ββ Outlier Helpers βββββββββββββββββββββββββββ
|
| 40 |
+
|
| 41 |
+
def detect_outliers_iqr(df: pd.DataFrame, column: str) -> pd.Series:
|
| 42 |
+
"""Return boolean mask of outliers using IQR method"""
|
| 43 |
+
Q1 = df[column].quantile(0.25)
|
| 44 |
+
Q3 = df[column].quantile(0.75)
|
| 45 |
+
IQR = Q3 - Q1
|
| 46 |
+
lower = Q1 - 1.5 * IQR
|
| 47 |
+
upper = Q3 + 1.5 * IQR
|
| 48 |
+
return (df[column] < lower) | (df[column] > upper)
|
| 49 |
+
|
| 50 |
+
|
| 51 |
+
def detect_outliers_zscore(df: pd.DataFrame, column: str,
|
| 52 |
+
threshold: float = 3.0) -> pd.Series:
|
| 53 |
+
"""Return boolean mask of outliers using Z-score method"""
|
| 54 |
+
mean = df[column].mean()
|
| 55 |
+
std = df[column].std()
|
| 56 |
+
if std == 0:
|
| 57 |
+
return pd.Series([False] * len(df))
|
| 58 |
+
return ((df[column] - mean) / std).abs() > threshold
|
| 59 |
+
|
| 60 |
+
|
| 61 |
+
# ββ Column Name Helpers βββββββββββββββββββββββ
|
| 62 |
+
|
| 63 |
+
def clean_column_name(name: str) -> str:
|
| 64 |
+
"""Normalize a column name to snake_case"""
|
| 65 |
+
import re
|
| 66 |
+
name = name.strip().lower()
|
| 67 |
+
name = re.sub(r"[^a-z0-9_]", "_", name)
|
| 68 |
+
name = re.sub(r"_+", "_", name)
|
| 69 |
+
return name.strip("_")
|
| 70 |
+
|
| 71 |
+
|
| 72 |
+
def has_bad_column_names(df: pd.DataFrame) -> List[str]:
|
| 73 |
+
"""Return list of columns with bad names"""
|
| 74 |
+
bad = []
|
| 75 |
+
for col in df.columns:
|
| 76 |
+
if col != clean_column_name(col):
|
| 77 |
+
bad.append(col)
|
| 78 |
+
return bad
|
| 79 |
+
|
| 80 |
+
|
| 81 |
+
# ββ Value Standardization βββββββββββββββββββββ
|
| 82 |
+
|
| 83 |
+
def standardize_column(df: pd.DataFrame, column: str,
|
| 84 |
+
mapping: Dict[str, str]) -> pd.DataFrame:
|
| 85 |
+
"""Replace values in a column using a mapping dict"""
|
| 86 |
+
df = df.copy()
|
| 87 |
+
df[column] = df[column].replace(mapping)
|
| 88 |
+
return df
|
| 89 |
+
|
| 90 |
+
|
| 91 |
+
def get_value_counts(df: pd.DataFrame, column: str) -> Dict[str, int]:
|
| 92 |
+
"""Return value counts for a column"""
|
| 93 |
+
return df[column].value_counts().to_dict()
|
| 94 |
+
|
| 95 |
+
|
| 96 |
+
# ββ Scoring Helpers βββββββββββββββββββββββββββ
|
| 97 |
+
|
| 98 |
+
def compute_null_score(original_df: pd.DataFrame,
|
| 99 |
+
current_df: pd.DataFrame) -> float:
|
| 100 |
+
"""Score based on how many nulls have been fixed (0.0 to 1.0)"""
|
| 101 |
+
original_nulls = original_df.isnull().sum().sum()
|
| 102 |
+
if original_nulls == 0:
|
| 103 |
+
return 1.0
|
| 104 |
+
current_nulls = current_df.isnull().sum().sum()
|
| 105 |
+
fixed = original_nulls - current_nulls
|
| 106 |
+
return round(max(0.0, fixed / original_nulls), 4)
|
| 107 |
+
|
| 108 |
+
|
| 109 |
+
def compute_duplicate_score(original_df: pd.DataFrame,
|
| 110 |
+
current_df: pd.DataFrame) -> float:
|
| 111 |
+
"""Score based on how many duplicates have been removed (0.0 to 1.0)"""
|
| 112 |
+
original_dups = original_df.duplicated().sum()
|
| 113 |
+
if original_dups == 0:
|
| 114 |
+
return 1.0
|
| 115 |
+
current_dups = current_df.duplicated().sum()
|
| 116 |
+
fixed = original_dups - current_dups
|
| 117 |
+
return round(max(0.0, fixed / original_dups), 4)
|
| 118 |
+
|
| 119 |
+
|
| 120 |
+
def compute_dtype_score(df: pd.DataFrame,
|
| 121 |
+
expected: Dict[str, str]) -> float:
|
| 122 |
+
"""Score based on how many columns have correct dtype"""
|
| 123 |
+
if not expected:
|
| 124 |
+
return 1.0
|
| 125 |
+
correct = sum(
|
| 126 |
+
1 for col, dtype in expected.items()
|
| 127 |
+
if col in df.columns and str(df[col].dtype) == dtype
|
| 128 |
+
)
|
| 129 |
+
return round(correct / len(expected), 4)
|
| 130 |
+
|
| 131 |
+
|
| 132 |
+
# ββ Misc ββββββββββββββββββββββββββββββββββββββ
|
| 133 |
+
|
| 134 |
+
def clamp(value: float, min_val: float = 0.0,
|
| 135 |
+
max_val: float = 1.0) -> float:
|
| 136 |
+
"""Clamp a float between min and max"""
|
| 137 |
+
return max(min_val, min(max_val, value))
|
| 138 |
+
|
| 139 |
+
|
| 140 |
+
def format_score(score: float) -> str:
|
| 141 |
+
"""Format score for display"""
|
| 142 |
+
return f"{score:.3f}"
|