Yashwanth34567 commited on
Commit
f1b06d6
Β·
verified Β·
1 Parent(s): 50e148e

Upload folder using huggingface_hub

Browse files
Files changed (13) hide show
  1. Dockerfile +40 -0
  2. README.md +113 -10
  3. app.py +297 -0
  4. config.py +41 -0
  5. datasets.py +161 -0
  6. environment.py +363 -0
  7. graders.py +203 -0
  8. inference.py +0 -0
  9. models.py +63 -0
  10. openenv.yaml +129 -0
  11. requirements.txt +22 -0
  12. tasks.py +174 -0
  13. 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
- title: Data Cleaning Env
3
- emoji: 🐠
4
- colorFrom: purple
5
- colorTo: indigo
6
- sdk: docker
7
- pinned: false
8
- ---
9
-
10
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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}"