thorodin103 commited on
Commit
06e2a53
Β·
verified Β·
1 Parent(s): 98c059c

Upload environment.py with huggingface_hub

Browse files
Files changed (1) hide show
  1. environment.py +404 -0
environment.py ADDED
@@ -0,0 +1,404 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+
2
+ import json
3
+ import pandas as pd
4
+ import numpy as np
5
+ from typing import Any, Dict, Optional, Tuple
6
+ from models import Action, Observation, Reward, StepResult, TaskInfo
7
+
8
+ AVAILABLE_OPERATIONS = [
9
+ "remove_duplicates",
10
+ "fill_missing",
11
+ "fix_dtype",
12
+ "remove_outliers",
13
+ "rename_columns",
14
+ "validate_schema",
15
+ "finish"
16
+ ]
17
+
18
+ class DataCleaningEnv:
19
+ """
20
+ OpenEnv-compliant Data Cleaning Environment.
21
+ Agent must clean dirty datasets step by step.
22
+ """
23
+
24
+ def __init__(self, task_id: str = "easy_dedup_rename"):
25
+ self.task_id = task_id
26
+ self.current_df: Optional[pd.DataFrame] = None
27
+ self.gold_df: Optional[pd.DataFrame] = None
28
+ self.task_meta: Dict = {}
29
+ self.step_count: int = 0
30
+ self.done: bool = False
31
+ self.max_steps: int = 10
32
+ self.reward_history = []
33
+ self._load_task_metadata()
34
+
35
+ # ─────────────────────────────────────────
36
+ # INTERNAL HELPERS
37
+ # ─────────────────────────────────────────
38
+
39
+ def _load_task_metadata(self):
40
+ with open("datasets/task_metadata.json", "r") as f:
41
+ all_meta = json.load(f)
42
+
43
+ mapping = {
44
+ "easy_dedup_rename": "easy",
45
+ "medium_missing_dtype": "medium",
46
+ "hard_full_pipeline": "hard"
47
+ }
48
+ key = mapping.get(self.task_id, "easy")
49
+ self.task_meta = all_meta[key]
50
+ self.max_steps = self.task_meta["max_steps"]
51
+
52
+ def _load_datasets(self):
53
+ mapping = {
54
+ "easy_dedup_rename": "easy",
55
+ "medium_missing_dtype": "medium",
56
+ "hard_full_pipeline": "hard"
57
+ }
58
+ folder = mapping.get(self.task_id, "easy")
59
+ self.current_df = pd.read_csv(f"datasets/{folder}/dirty.csv")
60
+ self.gold_df = pd.read_csv(f"datasets/{folder}/gold.csv")
61
+
62
+ def _get_observation(self, message: str = "") -> Observation:
63
+ df = self.current_df
64
+ missing = {col: int(df[col].isnull().sum()) for col in df.columns}
65
+ dtypes = {col: str(df[col].dtype) for col in df.columns}
66
+ sample = df.head(3).fillna("NULL").to_dict(orient="records")
67
+
68
+ return Observation(
69
+ task_id=self.task_id,
70
+ step=self.step_count,
71
+ dataset_info={
72
+ "total_rows": len(df),
73
+ "total_columns": len(df.columns),
74
+ "has_duplicates": bool(df.duplicated().any()),
75
+ "has_missing": bool(df.isnull().any().any()),
76
+ },
77
+ columns=list(df.columns),
78
+ shape=[len(df), len(df.columns)],
79
+ missing_values=missing,
80
+ dtypes=dtypes,
81
+ duplicate_count=int(df.duplicated().sum()),
82
+ sample_rows=sample,
83
+ available_operations=self.task_meta.get(
84
+ "operations_allowed", AVAILABLE_OPERATIONS
85
+ ),
86
+ task_description=self.task_meta.get("description", ""),
87
+ message=message
88
+ )
89
+
90
+ def _compute_reward(self) -> Reward:
91
+ df = self.current_df
92
+ gold = self.gold_df
93
+ scoring = self.task_meta.get("scoring", {})
94
+
95
+ dup_score = 0.0
96
+ missing_score = 0.0
97
+ dtype_score = 0.0
98
+ outlier_score = 0.0
99
+ schema_score = 0.0
100
+ penalty = 0.0
101
+
102
+ # ── Duplicate score ──────────────────────────────────────────
103
+ if "duplicate_score" in scoring:
104
+ gold_rows = len(gold)
105
+ curr_rows = len(df)
106
+ if curr_rows == gold_rows:
107
+ dup_score = 1.0
108
+ elif curr_rows < gold_rows:
109
+ dup_score = max(0.0, curr_rows / gold_rows)
110
+ else:
111
+ excess = curr_rows - gold_rows
112
+ dup_score = max(0.0, 1.0 - (excess / gold_rows))
113
+
114
+ # ── Missing value score ──────────────────────────────────────
115
+ if "missing_score" in scoring:
116
+ total_cells = df.shape[0] * df.shape[1]
117
+ missing_curr = int(df.isnull().sum().sum())
118
+ missing_gold = int(gold.isnull().sum().sum())
119
+ if total_cells > 0:
120
+ filled = max(0, missing_curr - missing_gold)
121
+ missing_score = 1.0 - (filled / total_cells)
122
+ missing_score = max(0.0, min(1.0, missing_score))
123
+
124
+ # ── Dtype score ──────────────────────────────────────────────
125
+ if "dtype_score" in scoring:
126
+ common_cols = [c for c in gold.columns if c in df.columns]
127
+ if common_cols:
128
+ matches = sum(
129
+ 1 for c in common_cols
130
+ if str(df[c].dtype) == str(gold[c].dtype)
131
+ )
132
+ dtype_score = matches / len(common_cols)
133
+
134
+ # ── Outlier score ────────────────────────────────────────────
135
+ if "outlier_score" in scoring:
136
+ num_cols = gold.select_dtypes(include=[np.number]).columns
137
+ scores = []
138
+ for col in num_cols:
139
+ if col not in df.columns:
140
+ continue
141
+ gold_mean = gold[col].mean()
142
+ gold_std = gold[col].std() + 1e-9
143
+ curr_col = pd.to_numeric(df[col], errors="coerce").dropna()
144
+ outliers = ((curr_col - gold_mean).abs() > 3 * gold_std).sum()
145
+ col_score = max(0.0, 1.0 - outliers / (len(curr_col) + 1e-9))
146
+ scores.append(col_score)
147
+ outlier_score = float(np.mean(scores)) if scores else 0.0
148
+
149
+ # ── Schema score ─────────────────────────────────────────────
150
+ if "schema_score" in scoring:
151
+ gold_cols = list(gold.columns)
152
+ curr_cols = list(df.columns)
153
+ matched = sum(1 for c in gold_cols if c in curr_cols)
154
+ schema_score = matched / len(gold_cols) if gold_cols else 0.0
155
+
156
+ # ── Penalty for too many steps ───────────────────────────────
157
+ step_ratio = self.step_count / self.max_steps
158
+ if step_ratio > 0.8:
159
+ penalty = 0.05
160
+
161
+ # ── Weighted total ───────────────────────────────────────────
162
+ weights = {
163
+ "duplicate_score": scoring.get("duplicate_score", 0.0),
164
+ "missing_score": scoring.get("missing_score", 0.0),
165
+ "dtype_score": scoring.get("dtype_score", 0.0),
166
+ "outlier_score": scoring.get("outlier_score", 0.0),
167
+ "schema_score": scoring.get("schema_score", 0.0),
168
+ }
169
+ component_scores = {
170
+ "duplicate_score": dup_score,
171
+ "missing_score": missing_score,
172
+ "dtype_score": dtype_score,
173
+ "outlier_score": outlier_score,
174
+ "schema_score": schema_score,
175
+ }
176
+ total = sum(
177
+ component_scores[k] * w
178
+ for k, w in weights.items()
179
+ ) - penalty
180
+ total = max(0.0, min(1.0, total))
181
+
182
+ return Reward(
183
+ total=round(total, 4),
184
+ duplicate_score=round(dup_score, 4),
185
+ missing_score=round(missing_score, 4),
186
+ dtype_score=round(dtype_score, 4),
187
+ outlier_score=round(outlier_score, 4),
188
+ schema_score=round(schema_score, 4),
189
+ penalty=round(penalty, 4)
190
+ )
191
+
192
+ # ─────────────────────────────────────────
193
+ # OPERATIONS
194
+ # ─────────────────────────────────────────
195
+
196
+ def _op_remove_duplicates(self, params: Dict) -> str:
197
+ before = len(self.current_df)
198
+ subset = params.get("subset", None)
199
+ self.current_df = self.current_df.drop_duplicates(subset=subset)
200
+ self.current_df = self.current_df.reset_index(drop=True)
201
+ removed = before - len(self.current_df)
202
+ return f"Removed {removed} duplicate rows. Rows: {before} β†’ {len(self.current_df)}"
203
+
204
+ def _op_fill_missing(self, params: Dict) -> str:
205
+ col = params.get("column")
206
+ strategy = params.get("strategy", "mean")
207
+ messages = []
208
+
209
+ cols_to_fill = [col] if col else list(self.current_df.columns)
210
+ for c in cols_to_fill:
211
+ if self.current_df[c].isnull().sum() == 0:
212
+ continue
213
+ if strategy == "mean":
214
+ numeric = pd.to_numeric(self.current_df[c], errors="coerce")
215
+ fill_val = numeric.mean()
216
+ self.current_df[c] = numeric.fillna(round(fill_val, 2))
217
+ elif strategy == "median":
218
+ numeric = pd.to_numeric(self.current_df[c], errors="coerce")
219
+ fill_val = numeric.median()
220
+ self.current_df[c] = numeric.fillna(fill_val)
221
+ elif strategy == "mode":
222
+ fill_val = self.current_df[c].mode()[0]
223
+ self.current_df[c] = self.current_df[c].fillna(fill_val)
224
+ elif strategy == "ffill":
225
+ self.current_df[c] = self.current_df[c].ffill()
226
+ else:
227
+ fill_val = strategy
228
+ self.current_df[c] = self.current_df[c].fillna(fill_val)
229
+ messages.append(f"{c}β†’{strategy}")
230
+
231
+ return f"Filled missing values: {', '.join(messages)}"
232
+
233
+ def _op_fix_dtype(self, params: Dict) -> str:
234
+ col = params.get("column")
235
+ dtype = params.get("dtype", "auto")
236
+ messages = []
237
+
238
+ cols_to_fix = [col] if col else list(self.current_df.columns)
239
+ for c in cols_to_fix:
240
+ try:
241
+ if dtype == "int" or dtype == "auto":
242
+ converted = pd.to_numeric(self.current_df[c], errors="coerce")
243
+ if converted.notna().all():
244
+ self.current_df[c] = converted.astype(int)
245
+ messages.append(f"{c}β†’int")
246
+ elif dtype == "float":
247
+ self.current_df[c] = pd.to_numeric(
248
+ self.current_df[c], errors="coerce"
249
+ )
250
+ messages.append(f"{c}β†’float")
251
+ elif dtype == "str":
252
+ self.current_df[c] = self.current_df[c].astype(str)
253
+ messages.append(f"{c}β†’str")
254
+ except Exception as e:
255
+ messages.append(f"{c}β†’failed({e})")
256
+
257
+ return f"Fixed dtypes: {', '.join(messages)}"
258
+
259
+ def _op_remove_outliers(self, params: Dict) -> str:
260
+ col = params.get("column")
261
+ method = params.get("method", "iqr")
262
+ before = len(self.current_df)
263
+ messages = []
264
+
265
+ cols = [col] if col else list(
266
+ self.current_df.select_dtypes(include=[np.number]).columns
267
+ )
268
+ for c in cols:
269
+ series = pd.to_numeric(self.current_df[c], errors="coerce")
270
+ if method == "iqr":
271
+ Q1 = series.quantile(0.25)
272
+ Q3 = series.quantile(0.75)
273
+ IQR = Q3 - Q1
274
+ mask = (series >= Q1 - 1.5 * IQR) & (series <= Q3 + 1.5 * IQR)
275
+ self.current_df = self.current_df[mask | series.isna()]
276
+ elif method == "zscore":
277
+ mean = series.mean()
278
+ std = series.std() + 1e-9
279
+ mask = ((series - mean).abs() <= 3 * std)
280
+ self.current_df = self.current_df[mask | series.isna()]
281
+ self.current_df = self.current_df.reset_index(drop=True)
282
+ messages.append(c)
283
+
284
+ removed = before - len(self.current_df)
285
+ return f"Removed {removed} outlier rows from: {', '.join(messages)}"
286
+
287
+ def _op_rename_columns(self, params: Dict) -> str:
288
+ mapping = params.get("mapping", {})
289
+ if not mapping:
290
+ # Auto snake_case
291
+ new_names = {
292
+ col: col.lower().replace(" ", "_")
293
+ for col in self.current_df.columns
294
+ }
295
+ self.current_df = self.current_df.rename(columns=new_names)
296
+ return f"Auto renamed columns to snake_case: {list(new_names.values())}"
297
+ self.current_df = self.current_df.rename(columns=mapping)
298
+ return f"Renamed columns: {mapping}"
299
+
300
+ def _op_validate_schema(self, params: Dict) -> str:
301
+ gold_cols = list(self.gold_df.columns)
302
+ curr_cols = list(self.current_df.columns)
303
+ missing = [c for c in gold_cols if c not in curr_cols]
304
+ extra = [c for c in curr_cols if c not in gold_cols]
305
+ if not missing and not extra:
306
+ return "Schema valid! All columns match gold standard."
307
+ msg = []
308
+ if missing:
309
+ msg.append(f"Missing columns: {missing}")
310
+ if extra:
311
+ msg.append(f"Extra columns: {extra}")
312
+ return "Schema issues: " + " | ".join(msg)
313
+
314
+ # ─────────────────────────────────────────
315
+ # OPENENV API
316
+ # ─────────────────────────────────────────
317
+
318
+ def reset(self) -> StepResult:
319
+ self._load_datasets()
320
+ self.step_count = 0
321
+ self.done = False
322
+ self.reward_history = []
323
+
324
+ obs = self._get_observation("Environment reset. Start cleaning!")
325
+ reward = Reward(total=0.0)
326
+
327
+ return StepResult(
328
+ observation=obs,
329
+ reward=reward,
330
+ done=False,
331
+ info={"task_id": self.task_id, "max_steps": self.max_steps}
332
+ )
333
+
334
+ def step(self, action: Action) -> StepResult:
335
+ if self.done:
336
+ obs = self._get_observation("Episode already done. Call reset().")
337
+ return StepResult(
338
+ observation=obs,
339
+ reward=Reward(total=0.0),
340
+ done=True,
341
+ info={"warning": "Episode already done"}
342
+ )
343
+
344
+ self.step_count += 1
345
+ op = action.operation
346
+ params = action.parameters
347
+ message = ""
348
+
349
+ # ── Route operation ──────────────────────────────────────────
350
+ try:
351
+ if op == "remove_duplicates":
352
+ message = self._op_remove_duplicates(params)
353
+ elif op == "fill_missing":
354
+ message = self._op_fill_missing(params)
355
+ elif op == "fix_dtype":
356
+ message = self._op_fix_dtype(params)
357
+ elif op == "remove_outliers":
358
+ message = self._op_remove_outliers(params)
359
+ elif op == "rename_columns":
360
+ message = self._op_rename_columns(params)
361
+ elif op == "validate_schema":
362
+ message = self._op_validate_schema(params)
363
+ elif op == "finish":
364
+ message = "Agent called finish."
365
+ self.done = True
366
+ else:
367
+ message = f"Unknown operation: {op}. No changes made."
368
+ except Exception as e:
369
+ message = f"Operation failed: {str(e)}"
370
+
371
+ # ── Check max steps ──────────────────────────────────────────
372
+ if self.step_count >= self.max_steps:
373
+ self.done = True
374
+ message += " | Max steps reached."
375
+
376
+ reward = self._compute_reward()
377
+ self.reward_history.append(reward.total)
378
+ obs = self._get_observation(message)
379
+
380
+ return StepResult(
381
+ observation=obs,
382
+ reward=reward,
383
+ done=self.done,
384
+ info={
385
+ "step": self.step_count,
386
+ "operation": op,
387
+ "reward_history": self.reward_history
388
+ }
389
+ )
390
+
391
+ def state(self) -> Dict[str, Any]:
392
+ if self.current_df is None:
393
+ return {"status": "not initialized β€” call reset() first"}
394
+ return {
395
+ "task_id": self.task_id,
396
+ "step": self.step_count,
397
+ "done": self.done,
398
+ "shape": list(self.current_df.shape),
399
+ "columns": list(self.current_df.columns),
400
+ "missing_values": self.current_df.isnull().sum().to_dict(),
401
+ "duplicate_count": int(self.current_df.duplicated().sum()),
402
+ "reward_history": self.reward_history,
403
+ "dtypes": {c: str(t) for c, t in self.current_df.dtypes.items()}
404
+ }