vedastra commited on
Commit
7dcac70
·
verified ·
1 Parent(s): 1baabf9

Upload folder using huggingface_hub

Browse files
server/data_cleaning_env_environment.py CHANGED
@@ -149,6 +149,7 @@ def _grade_hard(rows: list[dict]) -> float:
149
 
150
  return round(sum(scores) / len(scores), 4)
151
 
 
152
  GRADERS = {
153
  "easy": _grade_easy,
154
  "medium": _grade_medium,
@@ -161,6 +162,7 @@ DATASETS = {
161
  "hard": _make_hard_dataset,
162
  }
163
 
 
164
  # ---------------------------------------------------------------------------
165
  # Environment
166
  # ---------------------------------------------------------------------------
@@ -171,27 +173,21 @@ MAX_STEPS = {
171
  "hard": 25,
172
  }
173
 
174
- VALID_OPERATIONS_PER_TASK = {
175
- "easy": {
176
- "impute_mean",
177
- "impute_mode",
178
- "drop_missing_rows",
179
- },
180
- "medium": {
181
- "remove_duplicates",
182
- "fix_type_errors",
183
- "drop_missing_rows",
184
- },
185
- "hard": {
186
- "drop_missing_rows",
187
- "fill_quantity_mean",
188
- "remove_duplicates",
189
- "fix_type_errors",
190
- "remove_outliers",
191
- "normalize_text",
192
- },
193
  }
194
 
 
195
  class DataCleaningEnvironment(Environment):
196
  """
197
  Data Cleaning RL Environment.
@@ -202,7 +198,9 @@ class DataCleaningEnvironment(Environment):
202
  Each task presents a different level of difficulty and requires
203
  different cleaning operations.
204
  """
 
205
  SUPPORTS_CONCURRENT_SESSIONS: bool = True
 
206
  # -----------------------------------------------------------------------
207
  # Lifecycle
208
  # -----------------------------------------------------------------------
@@ -268,13 +266,12 @@ class DataCleaningEnvironment(Environment):
268
  op = action.operation.strip().lower()
269
  col = getattr(action, "column", None)
270
 
271
- valid_ops = VALID_OPERATIONS_PER_TASK[self._task]
272
- if op not in valid_ops:
273
  # Invalid operation → small penalty, episode continues
274
  reward = -0.05
275
  self._last_reward = reward
276
  obs = self._make_observation(reward=reward, done=False)
277
- obs.metadata["error"] = f"Unknown operation '{op}'. Valid: {sorted(valid_ops)}"
278
  return obs
279
 
280
  before_score = GRADERS[self._task](self._rows)
@@ -283,17 +280,16 @@ class DataCleaningEnvironment(Environment):
283
 
284
  # Reward = improvement in quality score (partial progress signal)
285
  improvement = after_score - before_score
286
- # 0.01
287
  if improvement > 0:
288
- reward = round(improvement + 0.02, 4) # bonus for improvement
289
  elif improvement < 0:
290
  reward = round(improvement - 0.02, 4) # penalty for harming dataset
291
  else:
292
- reward = 0.0 # neutral if no change
293
-
294
  if after_score >= 1.0:
295
- reward += 1.0 # big bonus for perfect score (episode complete)
296
-
297
  self._last_reward = reward
298
  self._applied_ops.append(op)
299
 
@@ -327,7 +323,7 @@ class DataCleaningEnvironment(Environment):
327
  def grade(self) -> dict:
328
  """Return final grader score for the current episode."""
329
  raw = GRADERS[self._task](self._rows)
330
- score = max(0.001, min(0.999, raw))
331
  return {
332
  "task": self._task,
333
  "score": score,
@@ -379,7 +375,7 @@ class DataCleaningEnvironment(Environment):
379
  "action_schema": {
380
  "operation": {
381
  "type": "string",
382
- "enum": list(VALID_OPERATIONS_PER_TASK),
383
  },
384
  "column": {"type": "string", "description": "Target column (optional)"},
385
  },
@@ -403,7 +399,7 @@ class DataCleaningEnvironment(Environment):
403
  if not self._done:
404
  self.step(DataCleaningAction(operation=op))
405
  raw = GRADERS[task_id](self._rows)
406
- results[task_id] = max(0.001, min(0.999, raw))
407
  return {"baseline_scores": results}
408
 
409
  # -----------------------------------------------------------------------
@@ -415,7 +411,7 @@ class DataCleaningEnvironment(Environment):
415
  has_dupes = self._has_duplicates()
416
  has_outliers = self._has_outliers()
417
  score = GRADERS[self._task](self._rows)
418
- valid_ops = VALID_OPERATIONS_PER_TASK[self._task]
419
  return DataCleaningObservation(
420
  current_text=self._rows_to_text(),
421
  is_normalized=not has_outliers and missing_count == 0 and not has_dupes,
@@ -431,11 +427,13 @@ class DataCleaningEnvironment(Environment):
431
  "has_duplicates": has_dupes,
432
  "has_outliers": has_outliers,
433
  "quality_score": score,
434
- "valid_operations": sorted(valid_ops),
435
  "ops_already_applied": list(self._applied_ops),
 
 
 
436
  },
437
  )
438
-
439
  def _rows_to_text(self) -> str:
440
  if not self._rows:
441
  return "[]"
@@ -506,10 +504,10 @@ class DataCleaningEnvironment(Environment):
506
  for r in self._rows:
507
  val = r.get(c)
508
  if val is not None and not isinstance(val, (int, float)):
509
- val_str = str(val).strip()
510
- if re.fullmatch(r"-?\d+(\.\d+)?", val_str):
511
- r[c] = float(val_str)
512
- else:
513
  r[c] = None
514
 
515
  elif op == "remove_outliers":
@@ -533,6 +531,7 @@ class DataCleaningEnvironment(Environment):
533
  if r.get("quantity") is None:
534
  r["quantity"] = mean
535
 
 
536
  # ---------------------------------------------------------------------------
537
  # Baseline policies (deterministic rule-based agents)
538
  # ---------------------------------------------------------------------------
@@ -550,47 +549,49 @@ _BASELINE_POLICIES: dict[str, list[str]] = {
550
  ],
551
  }
552
 
 
553
  # ---------------------------------------------------------------------------
554
  # Recommendation helper (guides the LLM toward correct next op)
555
  # ---------------------------------------------------------------------------
556
 
557
- # def _recommend_next(
558
- # task: str,
559
- # missing_count: int,
560
- # has_dupes: bool,
561
- # has_outliers: bool,
562
- # applied_ops: list,
563
- # ) -> str:
564
- # """Return a plain-English hint for the LLM about the best next operation."""
565
- # applied = set(applied_ops)
566
-
567
- # if task == "easy":
568
- # if missing_count > 0:
569
- # return "There are missing values. Use impute_mean (numeric) or impute_mode (text)."
570
- # return "No issues remain. Episode should be complete."
571
-
572
- # if task == "medium":
573
- # if has_dupes and "remove_duplicates" not in applied:
574
- # return "Duplicate rows exist. Use remove_duplicates."
575
- # if "fix_type_errors" not in applied:
576
- # return "Non-numeric values in numeric columns. Use fix_type_errors."
577
- # if missing_count > 0 and "drop_missing_rows" not in applied:
578
- # return "Some values still missing after type fix. Use drop_missing_rows."
579
- # return "No issues remain. Episode should be complete."
580
-
581
- # # hard
582
- # if missing_count > 0 and "fill_quantity_mean" not in applied:
583
- # return "Missing quantity values. Use fill_quantity_mean first."
584
- # if missing_count > 0 and "drop_missing_rows" not in applied:
585
- # return "Missing product/category values. Use drop_missing_rows."
586
- # if has_outliers and "remove_outliers" not in applied:
587
- # return "Price outliers detected (price<=0 or price>=500). Use remove_outliers."
588
- # if "normalize_text" not in applied:
589
- # return "String columns have inconsistent casing/whitespace. Use normalize_text."
590
- # if has_dupes and "remove_duplicates" not in applied:
591
- # return "Duplicate rows remain. Use remove_duplicates."
592
- # return "All issues fixed. Episode should be complete."
593
-
 
594
  # ---------------------------------------------------------------------------
595
  # Column utility helpers
596
  # ---------------------------------------------------------------------------
@@ -603,6 +604,7 @@ def _numeric_cols(rows: list[dict]) -> list[str]:
603
  if any(isinstance(r.get(c), (int, float)) for r in rows)
604
  ]
605
 
 
606
  def _string_cols(rows: list[dict]) -> list[str]:
607
  if not rows:
608
  return []
 
149
 
150
  return round(sum(scores) / len(scores), 4)
151
 
152
+
153
  GRADERS = {
154
  "easy": _grade_easy,
155
  "medium": _grade_medium,
 
162
  "hard": _make_hard_dataset,
163
  }
164
 
165
+
166
  # ---------------------------------------------------------------------------
167
  # Environment
168
  # ---------------------------------------------------------------------------
 
173
  "hard": 25,
174
  }
175
 
176
+ VALID_OPERATIONS = {
177
+ # Easy task ops
178
+ "impute_mean", # fill numeric NaN with column mean
179
+ "impute_mode", # fill categorical NaN with column mode
180
+ "drop_missing_rows", # drop all rows that contain any None
181
+ # Medium task ops
182
+ "remove_duplicates", # drop exact duplicate rows
183
+ "fix_type_errors", # coerce non-numeric 'age' / numeric cols to float (NaN if fails)
184
+ # Hard task ops
185
+ "remove_outliers", # drop rows where price < 0 or price > 500
186
+ "normalize_text", # strip + title-case all string columns
187
+ "fill_quantity_mean", # fill missing quantity with column mean
 
 
 
 
 
 
 
188
  }
189
 
190
+
191
  class DataCleaningEnvironment(Environment):
192
  """
193
  Data Cleaning RL Environment.
 
198
  Each task presents a different level of difficulty and requires
199
  different cleaning operations.
200
  """
201
+
202
  SUPPORTS_CONCURRENT_SESSIONS: bool = True
203
+
204
  # -----------------------------------------------------------------------
205
  # Lifecycle
206
  # -----------------------------------------------------------------------
 
266
  op = action.operation.strip().lower()
267
  col = getattr(action, "column", None)
268
 
269
+ if op not in VALID_OPERATIONS:
 
270
  # Invalid operation → small penalty, episode continues
271
  reward = -0.05
272
  self._last_reward = reward
273
  obs = self._make_observation(reward=reward, done=False)
274
+ obs.metadata["error"] = f"Unknown operation '{op}'. Valid: {sorted(VALID_OPERATIONS)}"
275
  return obs
276
 
277
  before_score = GRADERS[self._task](self._rows)
 
280
 
281
  # Reward = improvement in quality score (partial progress signal)
282
  improvement = after_score - before_score
 
283
  if improvement > 0:
284
+ reward = round(improvement + 0.02, 4) # bonus for any improvement
285
  elif improvement < 0:
286
  reward = round(improvement - 0.02, 4) # penalty for harming dataset
287
  else:
288
+ reward = 0.0 # neutral if no change
289
+
290
  if after_score >= 1.0:
291
+ reward += 1.0 # big bonus for completing the task
292
+
293
  self._last_reward = reward
294
  self._applied_ops.append(op)
295
 
 
323
  def grade(self) -> dict:
324
  """Return final grader score for the current episode."""
325
  raw = GRADERS[self._task](self._rows)
326
+ score = max(0.001, min(0.999, raw)) # strictly (0, 1) as required
327
  return {
328
  "task": self._task,
329
  "score": score,
 
375
  "action_schema": {
376
  "operation": {
377
  "type": "string",
378
+ "enum": list(VALID_OPERATIONS),
379
  },
380
  "column": {"type": "string", "description": "Target column (optional)"},
381
  },
 
399
  if not self._done:
400
  self.step(DataCleaningAction(operation=op))
401
  raw = GRADERS[task_id](self._rows)
402
+ results[task_id] = max(0.001, min(0.999, raw)) # strictly (0, 1)
403
  return {"baseline_scores": results}
404
 
405
  # -----------------------------------------------------------------------
 
411
  has_dupes = self._has_duplicates()
412
  has_outliers = self._has_outliers()
413
  score = GRADERS[self._task](self._rows)
414
+
415
  return DataCleaningObservation(
416
  current_text=self._rows_to_text(),
417
  is_normalized=not has_outliers and missing_count == 0 and not has_dupes,
 
427
  "has_duplicates": has_dupes,
428
  "has_outliers": has_outliers,
429
  "quality_score": score,
430
+ "valid_operations": sorted(VALID_OPERATIONS),
431
  "ops_already_applied": list(self._applied_ops),
432
+ "recommended_next": _recommend_next(
433
+ self._task, missing_count, has_dupes, has_outliers, self._applied_ops
434
+ ),
435
  },
436
  )
 
437
  def _rows_to_text(self) -> str:
438
  if not self._rows:
439
  return "[]"
 
504
  for r in self._rows:
505
  val = r.get(c)
506
  if val is not None and not isinstance(val, (int, float)):
507
+ # Try to coerce to float; set None if it fails
508
+ try:
509
+ r[c] = float(re.sub(r"[^\d.\-]", "", str(val)))
510
+ except ValueError:
511
  r[c] = None
512
 
513
  elif op == "remove_outliers":
 
531
  if r.get("quantity") is None:
532
  r["quantity"] = mean
533
 
534
+
535
  # ---------------------------------------------------------------------------
536
  # Baseline policies (deterministic rule-based agents)
537
  # ---------------------------------------------------------------------------
 
549
  ],
550
  }
551
 
552
+
553
  # ---------------------------------------------------------------------------
554
  # Recommendation helper (guides the LLM toward correct next op)
555
  # ---------------------------------------------------------------------------
556
 
557
+ def _recommend_next(
558
+ task: str,
559
+ missing_count: int,
560
+ has_dupes: bool,
561
+ has_outliers: bool,
562
+ applied_ops: list,
563
+ ) -> str:
564
+ """Return a plain-English hint for the LLM about the best next operation."""
565
+ applied = set(applied_ops)
566
+
567
+ if task == "easy":
568
+ if missing_count > 0:
569
+ return "There are missing values. Use impute_mean (numeric) or impute_mode (text)."
570
+ return "No issues remain. Episode should be complete."
571
+
572
+ if task == "medium":
573
+ if has_dupes and "remove_duplicates" not in applied:
574
+ return "Duplicate rows exist. Use remove_duplicates."
575
+ if "fix_type_errors" not in applied:
576
+ return "Non-numeric values in numeric columns. Use fix_type_errors."
577
+ if missing_count > 0 and "drop_missing_rows" not in applied:
578
+ return "Some values still missing after type fix. Use drop_missing_rows."
579
+ return "No issues remain. Episode should be complete."
580
+
581
+ # hard
582
+ if missing_count > 0 and "fill_quantity_mean" not in applied:
583
+ return "Missing quantity values. Use fill_quantity_mean first."
584
+ if missing_count > 0 and "drop_missing_rows" not in applied:
585
+ return "Missing product/category values. Use drop_missing_rows."
586
+ if has_outliers and "remove_outliers" not in applied:
587
+ return "Price outliers detected (price<=0 or price>=500). Use remove_outliers."
588
+ if "normalize_text" not in applied:
589
+ return "String columns have inconsistent casing/whitespace. Use normalize_text."
590
+ if has_dupes and "remove_duplicates" not in applied:
591
+ return "Duplicate rows remain. Use remove_duplicates."
592
+ return "All issues fixed. Episode should be complete."
593
+
594
+ \
595
  # ---------------------------------------------------------------------------
596
  # Column utility helpers
597
  # ---------------------------------------------------------------------------
 
604
  if any(isinstance(r.get(c), (int, float)) for r in rows)
605
  ]
606
 
607
+
608
  def _string_cols(rows: list[dict]) -> list[str]:
609
  if not rows:
610
  return []