data-cleaning-openenv / server /environment.py
01ammu's picture
Sync from GitHub via hub-sync
fee2f22 verified
Raw History Blame Contribute Delete
6.57 kB
import uuid
import sys
import os
from typing import Any, Dict, List
from copy import deepcopy
# Fix path so 'tasks' module can be found
sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..'))
from tasks.graders import TASKS
class DataCleaningEnvironment:
"""
Data Cleaning RL Environment.
Agent receives a messy dataset row by row and must clean it.
"""
def __init__(self):
self.episode_id = None
self.task_name = None
self.dataset = []
self.cleaned_data = []
self.current_row_index = 0
self.step_count = 0
self.total_reward = 0.0
self.done = False
self.grader = None
def reset(self, task_name: str = "easy") -> Dict[str, Any]:
"""Start a new episode with the given task."""
if task_name not in TASKS:
raise ValueError(f"Unknown task: {task_name}. Choose from {list(TASKS.keys())}")
task = TASKS[task_name]
self.episode_id = str(uuid.uuid4())[:8]
self.task_name = task_name
self.dataset = task["generate"]()
self.cleaned_data = []
self.current_row_index = 0
self.step_count = 0
self.total_reward = 0.0
self.done = False
self.grader = task["grade"]
return self._make_observation(reward=0.0)
def step(self, action_type: str, column: str, value: Any = None) -> Dict[str, Any]:
"""Apply a cleaning action to the current row."""
if self.done:
raise ValueError("Episode done. Call reset() first.")
current_row = deepcopy(self.dataset[self.current_row_index])
reward = 0.0
issues = self._detect_issues(current_row)
if action_type == "fill_missing":
if current_row.get(column) is None:
current_row[column] = value
reward += 0.3
else:
reward -= 0.1
elif action_type == "fix_type":
try:
current_row[column] = float(current_row[column])
reward += 0.2
except (ValueError, TypeError):
reward -= 0.1
elif action_type == "remove_duplicate":
is_dup = any(
r.get("email") == current_row.get("email")
and r.get("name") == current_row.get("name")
for r in self.cleaned_data
)
if is_dup:
self.current_row_index += 1
reward += 0.4
self.step_count += 1
self.total_reward += reward
if self.current_row_index >= len(self.dataset):
self.done = True
return self._make_observation(reward=reward)
else:
reward -= 0.2
elif action_type == "fix_category":
category_map = {
"eng": "Engineering", "engineering": "Engineering",
"ENGINEERING": "Engineering",
"hr": "HR", "h.r.": "HR", "H.R.": "HR",
"finance": "Finance", "FINANCE": "Finance",
}
raw = str(current_row.get(column, ""))
if raw in category_map:
current_row[column] = category_map[raw]
reward += 0.3
else:
reward -= 0.1
elif action_type == "remove_outlier":
val = current_row.get(column)
if val is not None and (val > 200000 or val < 0):
current_row[column] = 70000
reward += 0.4
else:
reward -= 0.1
elif action_type == "fix_formatting":
val = current_row.get(column)
if isinstance(val, str):
current_row[column] = val.lower().strip()
reward += 0.2
else:
reward -= 0.1
elif action_type == "skip":
reward -= 0.2
else:
reward -= 0.3
self.cleaned_data.append(current_row)
self.current_row_index += 1
self.step_count += 1
self.total_reward += reward
if self.current_row_index >= len(self.dataset):
self.done = True
return self._make_observation(reward=reward)
def get_state(self) -> Dict[str, Any]:
score = 0.0
if self.done and self.grader:
score, _ = self.grader(self.cleaned_data)
return {
"episode_id": self.episode_id,
"task_name": self.task_name,
"step_count": self.step_count,
"total_reward": round(self.total_reward, 3),
"rows_cleaned": len(self.cleaned_data),
"score": score,
}
def _detect_issues(self, row: Dict[str, Any]) -> List[str]:
issues = []
for col, val in row.items():
if val is None:
issues.append(f"missing:{col}")
if isinstance(val, str):
try:
float(val)
issues.append(f"wrong_type:{col}")
except ValueError:
pass
if val != val.lower():
issues.append(f"formatting:{col}")
return issues
def _make_observation(self, reward: float) -> Dict[str, Any]:
if self.done or self.current_row_index >= len(self.dataset):
current_data = {}
issues = []
legal_actions = []
else:
current_data = self.dataset[self.current_row_index]
issues = self._detect_issues(current_data)
legal_actions = self._get_legal_actions(current_data, issues)
return {
"current_row": self.current_row_index,
"total_rows": len(self.dataset),
"current_data": current_data,
"issues_detected": issues,
"legal_actions": legal_actions,
"progress": round(self.current_row_index / max(len(self.dataset), 1), 2),
"reward": round(reward, 3),
"done": self.done,
}
def _get_legal_actions(self, row: Dict[str, Any], issues: List[str]) -> List[str]:
actions = ["skip"]
issue_types = [i.split(":")[0] for i in issues]
if "missing" in issue_types:
actions.append("fill_missing")
if "wrong_type" in issue_types:
actions.append("fix_type")
if "formatting" in issue_types:
actions.append("fix_formatting")
actions.extend(["remove_duplicate", "fix_category", "remove_outlier"])
return list(set(actions))