Spaces:
Sleeping
Sleeping
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 |
-
|
| 175 |
-
|
| 176 |
-
|
| 177 |
-
|
| 178 |
-
|
| 179 |
-
|
| 180 |
-
"
|
| 181 |
-
|
| 182 |
-
|
| 183 |
-
|
| 184 |
-
|
| 185 |
-
"
|
| 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 |
-
|
| 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(
|
| 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
|
| 293 |
-
|
| 294 |
if after_score >= 1.0:
|
| 295 |
-
reward += 1.0
|
| 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(
|
| 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 |
-
|
| 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(
|
| 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 |
-
|
| 510 |
-
|
| 511 |
-
r[c] = float(
|
| 512 |
-
|
| 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 |
-
|
| 558 |
-
|
| 559 |
-
|
| 560 |
-
|
| 561 |
-
|
| 562 |
-
|
| 563 |
-
|
| 564 |
-
|
| 565 |
-
|
| 566 |
-
|
| 567 |
-
|
| 568 |
-
|
| 569 |
-
|
| 570 |
-
|
| 571 |
-
|
| 572 |
-
|
| 573 |
-
|
| 574 |
-
|
| 575 |
-
|
| 576 |
-
|
| 577 |
-
|
| 578 |
-
|
| 579 |
-
|
| 580 |
-
|
| 581 |
-
#
|
| 582 |
-
|
| 583 |
-
|
| 584 |
-
|
| 585 |
-
|
| 586 |
-
|
| 587 |
-
|
| 588 |
-
|
| 589 |
-
|
| 590 |
-
|
| 591 |
-
|
| 592 |
-
|
| 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 []
|