Spaces:
Running
Running
File size: 12,766 Bytes
09801ca | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 | """
βοΈ Preprocessing Agent
Autonomously selects encoding, scaling, and transformation techniques:
- Encoding: OneHot, Label, Target, Ordinal
- Scaling: Standard, Robust, MinMax, Power
- Transformations: Log, Box-Cox, Yeo-Johnson
Selection is based on data distribution and model compatibility.
"""
import pandas as pd
import numpy as np
from typing import Dict, List, Any, Tuple, Optional
from dataclasses import dataclass
import logging
from .base import BaseAgent, AgentResult, AgentStatus, Phase
logger = logging.getLogger(__name__)
@dataclass
class PreprocessingDecision:
"""A preprocessing decision made by the agent"""
column: str
technique: str
reason: str
params: Dict[str, Any]
class PreprocessingAgent(BaseAgent):
"""
Autonomous Preprocessing Agent
Analyzes data characteristics β Selects best techniques β Applies transformations
"""
name = "preprocessing"
description = "Auto-selects encoding, scaling, and transformations"
def __init__(self, memory=None):
super().__init__(memory)
self.decisions: List[PreprocessingDecision] = []
self.transformers: Dict[str, Any] = {}
def execute(self, **kwargs) -> AgentResult:
"""Main execution: analyze, decide, transform"""
# Get cleaned dataset
df = self.read_state("dataset_cleaned")
if df is None:
df = self.read_state("dataset")
target_col = self.read_state("target_column")
if df is None:
return AgentResult(
status=AgentStatus.FAILED,
agent_name=self.name,
phase=self.current_phase,
errors=["No dataset found"]
)
df = df.copy()
# Separate target
if target_col in df.columns:
y = df[target_col].copy()
X = df.drop(columns=[target_col])
else:
return AgentResult(
status=AgentStatus.FAILED,
agent_name=self.name,
phase=self.current_phase,
errors=[f"Target column '{target_col}' not found"]
)
self.logger.info(f"π Preprocessing {X.shape[1]} features")
# =====================================================================
# PHASE-AWARE PROCESSING
# =====================================================================
if self.is_fast_phase():
X_processed, feature_names = self._fast_preprocess(X, y)
else:
X_processed, feature_names = self._deep_preprocess(X, y)
# Process target
y_processed, task_type = self._process_target(y)
# Store results
self.write_state("features", X_processed, self.name)
self.write_state("target", y_processed, self.name)
self.write_state("feature_names", feature_names, self.name)
self.write_state("task_type", task_type, self.name)
self.write_state("preprocessing_decisions", [d.__dict__ for d in self.decisions], self.name)
return AgentResult(
status=AgentStatus.SUCCESS,
agent_name=self.name,
phase=self.current_phase,
data={
"n_features": len(feature_names),
"task_type": task_type,
"decisions": len(self.decisions)
},
metrics={
"feature_count": len(feature_names)
}
)
# =========================================================================
# FAST PHASE
# =========================================================================
def _fast_preprocess(self, X: pd.DataFrame, y: pd.Series) -> Tuple[np.ndarray, List[str]]:
"""Fast preprocessing with sensible defaults"""
feature_parts = []
feature_names = []
for col in X.columns:
try:
if pd.api.types.is_numeric_dtype(X[col]):
# Numeric: just scale
data, name = self._scale_numeric(X[col], col)
feature_parts.append(data)
feature_names.append(name)
else:
# Categorical: encode
data, names = self._encode_categorical(X[col], col)
feature_parts.append(data)
feature_names.extend(names)
except Exception as e:
self.logger.warning(f" β οΈ Skipped {col}: {str(e)[:30]}")
if not feature_parts:
return np.zeros((len(X), 1)), ["_empty_"]
X_processed = np.hstack(feature_parts)
self.logger.info(f" β
Generated {X_processed.shape[1]} features (fast mode)")
return X_processed, feature_names
# =========================================================================
# DEEP PHASE
# =========================================================================
def _deep_preprocess(self, X: pd.DataFrame, y: pd.Series) -> Tuple[np.ndarray, List[str]]:
"""Deep preprocessing with distribution-aware selection"""
feature_parts = []
feature_names = []
for col in X.columns:
try:
if pd.api.types.is_numeric_dtype(X[col]):
# Analyze distribution
skewness = X[col].skew()
if abs(skewness) > 2:
# Highly skewed: use log transform
data, name = self._transform_skewed(X[col], col)
else:
# Normal: use robust scaling
data, name = self._scale_numeric(X[col], col, method='robust')
feature_parts.append(data)
feature_names.append(name)
# Add polynomial features for important numerics
if len(X.columns) <= 10:
sq_data = data ** 2
feature_parts.append(sq_data)
feature_names.append(f"{col}^2")
else:
# Categorical: smart encoding
nunique = X[col].nunique()
if nunique <= 5:
data, names = self._encode_categorical(X[col], col, method='onehot')
elif nunique <= 50:
data, names = self._encode_target(X[col], col, y)
else:
data, names = self._encode_categorical(X[col], col, method='label')
feature_parts.append(data)
feature_names.extend(names)
except Exception as e:
self.logger.warning(f" β οΈ Skipped {col}: {str(e)[:30]}")
if not feature_parts:
return np.zeros((len(X), 1)), ["_empty_"]
X_processed = np.hstack(feature_parts)
self.logger.info(f" β
Generated {X_processed.shape[1]} features (deep mode)")
return X_processed, feature_names
# =========================================================================
# TRANSFORMATION METHODS
# =========================================================================
def _scale_numeric(self, series: pd.Series, col: str, method: str = 'robust') -> Tuple[np.ndarray, str]:
"""Scale numeric column"""
from sklearn.preprocessing import RobustScaler, StandardScaler
data = series.fillna(series.median()).values.reshape(-1, 1)
if method == 'robust':
scaler = RobustScaler()
else:
scaler = StandardScaler()
scaled = scaler.fit_transform(data)
self.transformers[f"{col}_scaler"] = scaler
self.decisions.append(PreprocessingDecision(col, f"{method}_scaling", "Numeric column", {}))
return scaled, col
def _transform_skewed(self, series: pd.Series, col: str) -> Tuple[np.ndarray, str]:
"""Transform skewed numeric column"""
data = series.fillna(0).values
# Use log1p for non-negative, else standard scaling
if data.min() >= 0:
transformed = np.log1p(data).reshape(-1, 1)
self.decisions.append(PreprocessingDecision(col, "log1p", f"Skewed (skew={series.skew():.1f})", {}))
else:
from sklearn.preprocessing import RobustScaler
scaler = RobustScaler()
transformed = scaler.fit_transform(data.reshape(-1, 1))
self.transformers[f"{col}_scaler"] = scaler
self.decisions.append(PreprocessingDecision(col, "robust_scaling", "Skewed but has negatives", {}))
return transformed, col
def _encode_categorical(self, series: pd.Series, col: str, method: str = 'auto') -> Tuple[np.ndarray, List[str]]:
"""Encode categorical column"""
series = series.fillna("_MISSING_").astype(str)
nunique = series.nunique()
if method == 'onehot' or (method == 'auto' and nunique <= 10):
# One-hot encoding
dummies = pd.get_dummies(series, prefix=col, drop_first=True)
self.decisions.append(PreprocessingDecision(col, "onehot", f"{nunique} categories", {}))
return dummies.values.astype(float), dummies.columns.tolist()
else:
# Label encoding
from sklearn.preprocessing import LabelEncoder
le = LabelEncoder()
encoded = le.fit_transform(series).reshape(-1, 1).astype(float)
self.transformers[f"{col}_encoder"] = le
self.decisions.append(PreprocessingDecision(col, "label", f"{nunique} categories", {}))
return encoded, [f"{col}_encoded"]
def _encode_target(self, series: pd.Series, col: str, y: pd.Series) -> Tuple[np.ndarray, List[str]]:
"""Target encoding for categorical column"""
series = series.fillna("_MISSING_").astype(str)
try:
y_numeric = pd.to_numeric(y, errors='coerce')
if y_numeric.notna().sum() < len(y) * 0.5:
# Target is not numeric, fall back to label encoding
return self._encode_categorical(series, col, method='label')
global_mean = y_numeric.mean()
target_means = {}
for cat in series.unique():
mask = series == cat
if mask.sum() >= 5:
target_means[cat] = y_numeric[mask].mean()
else:
target_means[cat] = global_mean
encoded = series.map(target_means).fillna(global_mean).values.reshape(-1, 1)
self.transformers[f"{col}_target_enc"] = target_means
self.decisions.append(PreprocessingDecision(col, "target_encoding", f"{series.nunique()} categories", {}))
return encoded.astype(float), [f"{col}_target_enc"]
except Exception as e:
return self._encode_categorical(series, col, method='label')
def _process_target(self, y: pd.Series) -> Tuple[np.ndarray, str]:
"""Process target variable and detect task type"""
y_clean = y.dropna()
# Try to convert to numeric
y_numeric = pd.to_numeric(y_clean, errors='coerce')
numeric_ratio = y_numeric.notna().sum() / len(y_clean)
if numeric_ratio > 0.9:
# Numeric target
nunique = y_numeric.nunique()
is_decimal = (y_numeric % 1 != 0).any()
if is_decimal or nunique > 20:
task_type = "regression"
y_processed = y_numeric.fillna(y_numeric.median()).values
else:
task_type = "classification"
from sklearn.preprocessing import LabelEncoder
le = LabelEncoder()
y_processed = le.fit_transform(y_clean.astype(str))
self.transformers["target_encoder"] = le
else:
# Categorical target
task_type = "classification"
from sklearn.preprocessing import LabelEncoder
le = LabelEncoder()
y_processed = le.fit_transform(y_clean.astype(str))
self.transformers["target_encoder"] = le
self.logger.info(f" π Task type: {task_type}")
return y_processed, task_type
|