cleanflow-kit 1.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- cleanflow_kit-1.1.0.dist-info/METADATA +119 -0
- cleanflow_kit-1.1.0.dist-info/RECORD +17 -0
- cleanflow_kit-1.1.0.dist-info/WHEEL +5 -0
- cleanflow_kit-1.1.0.dist-info/licenses/LICENSE +21 -0
- cleanflow_kit-1.1.0.dist-info/top_level.txt +1 -0
- dataclean/__init__.py +44 -0
- dataclean/_compat.py +24 -0
- dataclean/data_cleaner.py +988 -0
- dataclean/data_loader.py +194 -0
- dataclean/drift_detector.py +111 -0
- dataclean/eda.py +400 -0
- dataclean/feature_engineer.py +608 -0
- dataclean/model_trainer.py +874 -0
- dataclean/pipeline.py +548 -0
- dataclean/py.typed +0 -0
- dataclean/report_generator.py +365 -0
- dataclean/synthetic_generator.py +97 -0
|
@@ -0,0 +1,874 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Model Trainer Module
|
|
3
|
+
====================
|
|
4
|
+
Advanced ML training with auto-detection, multi-model comparison,
|
|
5
|
+
cross-validation, hyperparameter tuning, and model export.
|
|
6
|
+
Memory-optimized for 512MB hosting (no SHAP, reduced models).
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
import logging
|
|
10
|
+
logger = logging.getLogger(__name__)
|
|
11
|
+
import pandas as pd
|
|
12
|
+
import numpy as np
|
|
13
|
+
import time
|
|
14
|
+
import joblib
|
|
15
|
+
import re
|
|
16
|
+
import gc
|
|
17
|
+
import warnings
|
|
18
|
+
from datetime import datetime
|
|
19
|
+
from typing import Optional, Dict, Any, List, Tuple
|
|
20
|
+
from .feature_engineer import FeatureEngineer
|
|
21
|
+
from .data_cleaner import DataCleaner
|
|
22
|
+
|
|
23
|
+
from sklearn.model_selection import (
|
|
24
|
+
cross_validate, StratifiedKFold, KFold, RandomizedSearchCV
|
|
25
|
+
)
|
|
26
|
+
from sklearn.pipeline import Pipeline, make_pipeline
|
|
27
|
+
from sklearn.compose import ColumnTransformer, make_column_transformer
|
|
28
|
+
from sklearn.impute import SimpleImputer
|
|
29
|
+
from sklearn.preprocessing import StandardScaler, LabelEncoder, OrdinalEncoder
|
|
30
|
+
from sklearn.linear_model import LogisticRegression, Ridge
|
|
31
|
+
from sklearn.ensemble import RandomForestClassifier, RandomForestRegressor
|
|
32
|
+
from sklearn.metrics import (
|
|
33
|
+
accuracy_score, precision_score, recall_score, f1_score, roc_auc_score,
|
|
34
|
+
r2_score, mean_absolute_error, mean_squared_error,
|
|
35
|
+
classification_report, make_scorer
|
|
36
|
+
)
|
|
37
|
+
from ._compat import normalize_string_columns
|
|
38
|
+
|
|
39
|
+
HAS_XGBOOST = False # Disabled for memory optimization
|
|
40
|
+
|
|
41
|
+
try:
|
|
42
|
+
from imblearn.over_sampling import SMOTE
|
|
43
|
+
HAS_SMOTE = True
|
|
44
|
+
except ImportError:
|
|
45
|
+
HAS_SMOTE = False
|
|
46
|
+
|
|
47
|
+
warnings.filterwarnings('ignore')
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
# ─── Column pattern filters ─────────────────────────────────────────────────
|
|
51
|
+
ID_PATTERNS = re.compile(
|
|
52
|
+
r'^(id|_id|index|row_?num|serial|sr_?no|unnamed)', re.IGNORECASE
|
|
53
|
+
)
|
|
54
|
+
URL_PATTERNS = re.compile(
|
|
55
|
+
r'(url|link|href|src|image|img|photo|avatar|thumbnail|path|file)', re.IGNORECASE
|
|
56
|
+
)
|
|
57
|
+
TARGET_HINTS = [
|
|
58
|
+
'target', 'label', 'class', 'outcome', 'result', 'y',
|
|
59
|
+
'is_', 'has_', 'survived', 'churn', 'fraud', 'default',
|
|
60
|
+
'price', 'salary', 'revenue', 'cost', 'amount', 'score'
|
|
61
|
+
]
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
class ModelTrainer:
|
|
65
|
+
"""
|
|
66
|
+
Advanced ML model trainer with auto-detection, multi-model comparison,
|
|
67
|
+
cross-validation, hyperparameter tuning, and explainability.
|
|
68
|
+
|
|
69
|
+
Example
|
|
70
|
+
-------
|
|
71
|
+
>>> trainer = ModelTrainer(df, target_col='price', problem_type='regression')
|
|
72
|
+
>>> results = trainer.run()
|
|
73
|
+
>>> print(results['best_model'])
|
|
74
|
+
>>> print(results['metrics_dashboard'])
|
|
75
|
+
"""
|
|
76
|
+
|
|
77
|
+
def __init__(
|
|
78
|
+
self,
|
|
79
|
+
df: pd.DataFrame,
|
|
80
|
+
target_col: Optional[str] = None,
|
|
81
|
+
problem_type: Optional[str] = None,
|
|
82
|
+
raw_df: Optional[pd.DataFrame] = None
|
|
83
|
+
):
|
|
84
|
+
self.original_df = normalize_string_columns(
|
|
85
|
+
raw_df.copy() if raw_df is not None else df.copy()
|
|
86
|
+
)
|
|
87
|
+
self.df = normalize_string_columns(df.copy())
|
|
88
|
+
self.target_col = target_col
|
|
89
|
+
self.problem_type = problem_type # 'classification' or 'regression'
|
|
90
|
+
self.warnings: List[str] = []
|
|
91
|
+
self.log: List[str] = []
|
|
92
|
+
|
|
93
|
+
# Will be set during prepare
|
|
94
|
+
self.X = None
|
|
95
|
+
self.y = None
|
|
96
|
+
self.feature_names: List[str] = []
|
|
97
|
+
self.scaler = None
|
|
98
|
+
self.label_encoder = None
|
|
99
|
+
|
|
100
|
+
# Results
|
|
101
|
+
self.models: Dict[str, Any] = {}
|
|
102
|
+
self.cv_results: Dict[str, Dict] = {}
|
|
103
|
+
self.best_model_name: str = ''
|
|
104
|
+
self.best_model = None
|
|
105
|
+
self.best_metrics: Dict[str, float] = {}
|
|
106
|
+
self.importances: Dict[str, float] = {}
|
|
107
|
+
|
|
108
|
+
# ─── 1. Target Detection ────────────────────────────────────────────
|
|
109
|
+
def detect_target(self) -> str:
|
|
110
|
+
"""Auto-detect or validate the target column."""
|
|
111
|
+
cols = self.df.columns.tolist()
|
|
112
|
+
|
|
113
|
+
# If already provided, validate it
|
|
114
|
+
if self.target_col:
|
|
115
|
+
if self.target_col in cols:
|
|
116
|
+
self.log.append(f"Target column confirmed: '{self.target_col}'")
|
|
117
|
+
return self.target_col
|
|
118
|
+
else:
|
|
119
|
+
self.warnings.append(
|
|
120
|
+
f"Specified target '{self.target_col}' not found in data."
|
|
121
|
+
)
|
|
122
|
+
self.target_col = None
|
|
123
|
+
|
|
124
|
+
# Try heuristic search
|
|
125
|
+
for hint in TARGET_HINTS:
|
|
126
|
+
for col in cols:
|
|
127
|
+
if hint.lower() == col.lower() or col.lower().startswith(hint.lower()):
|
|
128
|
+
self.target_col = col
|
|
129
|
+
self.log.append(f"Auto-detected target column: '{col}' (matched hint '{hint}')")
|
|
130
|
+
return col
|
|
131
|
+
|
|
132
|
+
# Fallback: last column
|
|
133
|
+
self.target_col = cols[-1]
|
|
134
|
+
self.warnings.append(
|
|
135
|
+
f"No obvious target found — using last column '{self.target_col}'. "
|
|
136
|
+
"You can specify a target column explicitly."
|
|
137
|
+
)
|
|
138
|
+
self.log.append(f"Fallback target column: '{self.target_col}'")
|
|
139
|
+
return self.target_col
|
|
140
|
+
|
|
141
|
+
# ─── 2. Problem Type Detection ──────────────────────────────────────
|
|
142
|
+
def detect_problem_type(self) -> str:
|
|
143
|
+
"""Detect whether this is a classification or regression problem."""
|
|
144
|
+
if self.target_col is None:
|
|
145
|
+
self.detect_target()
|
|
146
|
+
|
|
147
|
+
y = self.df[self.target_col]
|
|
148
|
+
|
|
149
|
+
# User override with validation
|
|
150
|
+
if self.problem_type:
|
|
151
|
+
if self.problem_type == 'regression':
|
|
152
|
+
# Validate that target is numeric
|
|
153
|
+
if y.dtype == 'object' or y.dtype.name == 'category':
|
|
154
|
+
try:
|
|
155
|
+
pd.to_numeric(y, errors='raise')
|
|
156
|
+
except Exception:
|
|
157
|
+
self.warnings.append(
|
|
158
|
+
f"Regression requested but target '{self.target_col}' is non-numeric. "
|
|
159
|
+
"Switching to classification."
|
|
160
|
+
)
|
|
161
|
+
self.log.append(f"Auto-corrected problem type to 'classification' (target is string)")
|
|
162
|
+
self.problem_type = 'classification'
|
|
163
|
+
return 'classification'
|
|
164
|
+
|
|
165
|
+
self.log.append(f"Problem type specified: {self.problem_type}")
|
|
166
|
+
return self.problem_type
|
|
167
|
+
|
|
168
|
+
# Auto-detection
|
|
169
|
+
# Object/category → classification
|
|
170
|
+
if y.dtype == 'object' or y.dtype.name == 'category':
|
|
171
|
+
self.problem_type = 'classification'
|
|
172
|
+
# Few unique values → classification
|
|
173
|
+
elif y.nunique() <= 15 and y.nunique() / len(y) < 0.05:
|
|
174
|
+
self.problem_type = 'classification'
|
|
175
|
+
else:
|
|
176
|
+
self.problem_type = 'regression'
|
|
177
|
+
|
|
178
|
+
self.log.append(f"Auto-detected problem type: {self.problem_type}")
|
|
179
|
+
return self.problem_type
|
|
180
|
+
|
|
181
|
+
# ─── 3. Feature Preparation ─────────────────────────────────────────
|
|
182
|
+
def prepare_features(self) -> Tuple[np.ndarray, np.ndarray]:
|
|
183
|
+
"""Full feature preparation pipeline. Returns (X, y)."""
|
|
184
|
+
self.detect_target()
|
|
185
|
+
self.detect_problem_type()
|
|
186
|
+
|
|
187
|
+
if len(self.df) < 100:
|
|
188
|
+
self.warnings.append(
|
|
189
|
+
f"⚠ Small dataset ({len(self.df)} rows). "
|
|
190
|
+
"Results may be unreliable. Consider collecting more data."
|
|
191
|
+
)
|
|
192
|
+
|
|
193
|
+
fe = FeatureEngineer(self.df, target_col=self.target_col, problem_type=self.problem_type)
|
|
194
|
+
fe.create_datetime_features()
|
|
195
|
+
fe.encode_categorical()
|
|
196
|
+
fe.create_binned_features(n_bins=5)
|
|
197
|
+
fe.drop_highly_correlated(threshold=0.9)
|
|
198
|
+
|
|
199
|
+
# Merge log
|
|
200
|
+
self.log.extend(fe.get_summary().get("transformations", []))
|
|
201
|
+
self.df = fe.get_transformed_data()
|
|
202
|
+
|
|
203
|
+
# Separate target
|
|
204
|
+
y = self.df[self.target_col].copy()
|
|
205
|
+
X = self.df.drop(columns=[self.target_col]).copy()
|
|
206
|
+
|
|
207
|
+
# Drop any remaining non-numeric
|
|
208
|
+
non_numeric = X.select_dtypes(exclude=[np.number]).columns.tolist()
|
|
209
|
+
if non_numeric:
|
|
210
|
+
X.drop(columns=non_numeric, inplace=True)
|
|
211
|
+
self.log.append(f"Dropped {len(non_numeric)} non-numeric columns before training")
|
|
212
|
+
|
|
213
|
+
X = X.fillna(X.median())
|
|
214
|
+
|
|
215
|
+
# target encode
|
|
216
|
+
if self.problem_type == 'classification' and y.dtype == 'object':
|
|
217
|
+
self.label_encoder = LabelEncoder()
|
|
218
|
+
y = pd.Series(self.label_encoder.fit_transform(y), name=self.target_col)
|
|
219
|
+
self.log.append("Label-encoded target column")
|
|
220
|
+
|
|
221
|
+
y = y.fillna(y.mode()[0] if self.problem_type == 'classification' else y.median())
|
|
222
|
+
|
|
223
|
+
self.feature_names = X.columns.tolist()
|
|
224
|
+
|
|
225
|
+
# Scaling is already supported by FE
|
|
226
|
+
fe_scaler = FeatureEngineer(X, target_col=None, problem_type=self.problem_type)
|
|
227
|
+
fe_scaler.scale_features(method='standard')
|
|
228
|
+
X_scaled = fe_scaler.get_transformed_data().values
|
|
229
|
+
self.scaler = fe_scaler.scalers.get('main', None)
|
|
230
|
+
self.log.append(f"Scaled {len(self.feature_names)} features")
|
|
231
|
+
|
|
232
|
+
self.X = X_scaled
|
|
233
|
+
self.y = y.values if hasattr(y, 'values') else np.array(y)
|
|
234
|
+
|
|
235
|
+
self.log.append(f"Final training shape: X={self.X.shape}, y={self.y.shape}")
|
|
236
|
+
return self.X, self.y
|
|
237
|
+
|
|
238
|
+
def detect_imbalance(self) -> Dict[str, Any]:
|
|
239
|
+
"""Detect class imbalance for classification problems."""
|
|
240
|
+
if self.problem_type != 'classification':
|
|
241
|
+
return {'imbalanced': False, 'reason': 'Regression problem'}
|
|
242
|
+
|
|
243
|
+
counts = pd.Series(self.y).value_counts(normalize=True)
|
|
244
|
+
majority_pct = counts.iloc[0]
|
|
245
|
+
minority_pct = counts.iloc[-1]
|
|
246
|
+
|
|
247
|
+
imbalanced = majority_pct > 0.70
|
|
248
|
+
info = {
|
|
249
|
+
'imbalanced': imbalanced,
|
|
250
|
+
'majority_class': str(counts.index[0]),
|
|
251
|
+
'majority_pct': round(majority_pct * 100, 1),
|
|
252
|
+
'minority_class': str(counts.index[-1]),
|
|
253
|
+
'minority_pct': round(minority_pct * 100, 1),
|
|
254
|
+
'class_distribution': {str(k): round(v * 100, 1) for k, v in counts.items()}
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
if imbalanced:
|
|
258
|
+
self.warnings.append(
|
|
259
|
+
f"Class imbalance detected: majority {info['majority_pct']}% / "
|
|
260
|
+
f"minority {info['minority_pct']}%. Using class weights or SMOTE."
|
|
261
|
+
)
|
|
262
|
+
self.log.append(f"Imbalance detected: {info['class_distribution']}")
|
|
263
|
+
|
|
264
|
+
return info
|
|
265
|
+
|
|
266
|
+
# ─── 5. Model Definitions ──────────────────────────────────────────
|
|
267
|
+
def _get_models_and_params(self) -> Dict[str, Dict]:
|
|
268
|
+
"""Return model instances and hyperparameter grids."""
|
|
269
|
+
imbalance = self.detect_imbalance()
|
|
270
|
+
use_balanced = imbalance.get('imbalanced', False)
|
|
271
|
+
n_classes = len(np.unique(self.y)) if self.problem_type == 'classification' else 0
|
|
272
|
+
|
|
273
|
+
if self.problem_type == 'classification':
|
|
274
|
+
models = {
|
|
275
|
+
'Logistic Regression': {
|
|
276
|
+
'model': LogisticRegression(
|
|
277
|
+
max_iter=1000, random_state=42,
|
|
278
|
+
class_weight='balanced' if use_balanced else None
|
|
279
|
+
),
|
|
280
|
+
'params': {
|
|
281
|
+
'C': [0.1, 1, 10],
|
|
282
|
+
'solver': ['lbfgs']
|
|
283
|
+
}
|
|
284
|
+
},
|
|
285
|
+
'Random Forest': {
|
|
286
|
+
'model': RandomForestClassifier(
|
|
287
|
+
random_state=42, n_jobs=1,
|
|
288
|
+
class_weight='balanced' if use_balanced else None
|
|
289
|
+
),
|
|
290
|
+
'params': {
|
|
291
|
+
'n_estimators': [50, 100],
|
|
292
|
+
'max_depth': [5, 10],
|
|
293
|
+
'min_samples_split': [2, 5]
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
else: # regression
|
|
298
|
+
models = {
|
|
299
|
+
'Ridge Regression': {
|
|
300
|
+
'model': Ridge(random_state=42),
|
|
301
|
+
'params': {
|
|
302
|
+
'alpha': [0.1, 1, 10]
|
|
303
|
+
}
|
|
304
|
+
},
|
|
305
|
+
'Random Forest': {
|
|
306
|
+
'model': RandomForestRegressor(random_state=42, n_jobs=1),
|
|
307
|
+
'params': {
|
|
308
|
+
'n_estimators': [50, 100],
|
|
309
|
+
'max_depth': [5, 10],
|
|
310
|
+
'min_samples_split': [2, 5]
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
return models
|
|
316
|
+
|
|
317
|
+
# ─── 6. Training ───────────────────────────────────────────────────
|
|
318
|
+
def train_all_models(self) -> Dict[str, Dict]:
|
|
319
|
+
"""Train all models with cross-validation and hyperparameter tuning."""
|
|
320
|
+
if self.X is None:
|
|
321
|
+
self.prepare_features()
|
|
322
|
+
|
|
323
|
+
model_defs = self._get_models_and_params()
|
|
324
|
+
|
|
325
|
+
# Handle SMOTE for classification imbalance
|
|
326
|
+
X_train, y_train = self.X, self.y
|
|
327
|
+
imbalance = self.detect_imbalance()
|
|
328
|
+
if imbalance.get('imbalanced') and HAS_SMOTE and self.problem_type == 'classification':
|
|
329
|
+
try:
|
|
330
|
+
smote = SMOTE(random_state=42)
|
|
331
|
+
X_train, y_train = smote.fit_resample(X_train, y_train)
|
|
332
|
+
self.log.append(f"Applied SMOTE: {len(self.y)} → {len(y_train)} samples")
|
|
333
|
+
except Exception as e:
|
|
334
|
+
self.log.append(f"SMOTE failed, using original data: {e}")
|
|
335
|
+
|
|
336
|
+
# CV strategy
|
|
337
|
+
n_folds = 3
|
|
338
|
+
if self.problem_type == 'classification':
|
|
339
|
+
cv = StratifiedKFold(n_splits=n_folds, shuffle=True, random_state=42)
|
|
340
|
+
scoring_primary = 'f1_weighted'
|
|
341
|
+
scoring = ['accuracy', 'precision_weighted', 'recall_weighted', 'f1_weighted']
|
|
342
|
+
try:
|
|
343
|
+
if len(np.unique(y_train)) == 2:
|
|
344
|
+
scoring.append('roc_auc')
|
|
345
|
+
except Exception:
|
|
346
|
+
pass
|
|
347
|
+
else:
|
|
348
|
+
cv = KFold(n_splits=n_folds, shuffle=True, random_state=42)
|
|
349
|
+
scoring_primary = 'r2'
|
|
350
|
+
scoring = ['r2', 'neg_mean_absolute_error', 'neg_root_mean_squared_error']
|
|
351
|
+
|
|
352
|
+
# Limit search iterations for speed
|
|
353
|
+
n_iter = min(10, max(3, 100 // len(model_defs)))
|
|
354
|
+
|
|
355
|
+
for name, config in model_defs.items():
|
|
356
|
+
self.log.append(f"Training {name}...")
|
|
357
|
+
try:
|
|
358
|
+
# Hyperparameter tuning
|
|
359
|
+
search = RandomizedSearchCV(
|
|
360
|
+
config['model'],
|
|
361
|
+
config['params'],
|
|
362
|
+
n_iter=min(n_iter, self._param_combinations(config['params'])),
|
|
363
|
+
cv=cv,
|
|
364
|
+
scoring=scoring_primary,
|
|
365
|
+
random_state=42,
|
|
366
|
+
n_jobs=1,
|
|
367
|
+
error_score='raise'
|
|
368
|
+
)
|
|
369
|
+
search.fit(X_train, y_train)
|
|
370
|
+
best_estimator = search.best_estimator_
|
|
371
|
+
|
|
372
|
+
# Cross-validate with all metrics
|
|
373
|
+
cv_scores = cross_validate(
|
|
374
|
+
best_estimator, X_train, y_train,
|
|
375
|
+
cv=cv, scoring=scoring, n_jobs=1
|
|
376
|
+
)
|
|
377
|
+
|
|
378
|
+
# Store results
|
|
379
|
+
metrics = {}
|
|
380
|
+
for metric_name in scoring:
|
|
381
|
+
key = f'test_{metric_name}'
|
|
382
|
+
if key in cv_scores:
|
|
383
|
+
scores = cv_scores[key]
|
|
384
|
+
# Convert negative metrics
|
|
385
|
+
if metric_name.startswith('neg_'):
|
|
386
|
+
scores = -scores
|
|
387
|
+
clean_name = metric_name.replace('neg_', '')
|
|
388
|
+
else:
|
|
389
|
+
clean_name = metric_name
|
|
390
|
+
metrics[clean_name] = {
|
|
391
|
+
'mean': round(float(np.mean(scores)), 4),
|
|
392
|
+
'std': round(float(np.std(scores)), 4)
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
self.models[name] = best_estimator
|
|
396
|
+
self.cv_results[name] = {
|
|
397
|
+
'metrics': metrics,
|
|
398
|
+
'best_params': search.best_params_,
|
|
399
|
+
'best_cv_score': round(float(search.best_score_), 4)
|
|
400
|
+
}
|
|
401
|
+
self.log.append(f" {name} → best CV score: {search.best_score_:.4f}")
|
|
402
|
+
|
|
403
|
+
except Exception as e:
|
|
404
|
+
self.log.append(f" {name} failed: {str(e)[:100]}")
|
|
405
|
+
self.cv_results[name] = {'error': str(e)}
|
|
406
|
+
finally:
|
|
407
|
+
gc.collect()
|
|
408
|
+
|
|
409
|
+
return self.cv_results
|
|
410
|
+
|
|
411
|
+
def _param_combinations(self, params: Dict) -> int:
|
|
412
|
+
"""Calculate total parameter combinations."""
|
|
413
|
+
total = 1
|
|
414
|
+
for v in params.values():
|
|
415
|
+
total *= len(v)
|
|
416
|
+
return total
|
|
417
|
+
|
|
418
|
+
# ─── 7. Best Model Selection ───────────────────────────────────────
|
|
419
|
+
def get_best_model(self) -> Dict[str, Any]:
|
|
420
|
+
"""Select the best model based on primary metric."""
|
|
421
|
+
if not self.cv_results:
|
|
422
|
+
self.train_all_models()
|
|
423
|
+
|
|
424
|
+
primary_metric = 'f1_weighted' if self.problem_type == 'classification' else 'r2'
|
|
425
|
+
|
|
426
|
+
best_score = -np.inf
|
|
427
|
+
for name, result in self.cv_results.items():
|
|
428
|
+
if 'error' in result:
|
|
429
|
+
continue
|
|
430
|
+
score = result['metrics'].get(primary_metric, {}).get('mean', -np.inf)
|
|
431
|
+
if score > best_score:
|
|
432
|
+
best_score = score
|
|
433
|
+
self.best_model_name = name
|
|
434
|
+
|
|
435
|
+
if self.best_model_name:
|
|
436
|
+
self.best_model = self.models[self.best_model_name]
|
|
437
|
+
self.best_metrics = self.cv_results[self.best_model_name]['metrics']
|
|
438
|
+
self.log.append(f"Best model: {self.best_model_name} ({primary_metric}={best_score:.4f})")
|
|
439
|
+
|
|
440
|
+
return {
|
|
441
|
+
'name': self.best_model_name,
|
|
442
|
+
'metrics': self.best_metrics,
|
|
443
|
+
'params': self.cv_results.get(self.best_model_name, {}).get('best_params', {})
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
# ─── 8. Feature Importance ─────────────────────────────────────────
|
|
447
|
+
def get_feature_importance(self, top_n: int = 10) -> Dict[str, float]:
|
|
448
|
+
"""Extract top feature importances from the best model."""
|
|
449
|
+
if self.best_model is None:
|
|
450
|
+
self.get_best_model()
|
|
451
|
+
if self.best_model is None:
|
|
452
|
+
return {}
|
|
453
|
+
|
|
454
|
+
importances = None
|
|
455
|
+
try:
|
|
456
|
+
if hasattr(self.best_model, 'feature_importances_'):
|
|
457
|
+
importances = self.best_model.feature_importances_
|
|
458
|
+
elif hasattr(self.best_model, 'coef_'):
|
|
459
|
+
coef = self.best_model.coef_
|
|
460
|
+
if coef.ndim > 1:
|
|
461
|
+
importances = np.mean(np.abs(coef), axis=0)
|
|
462
|
+
else:
|
|
463
|
+
importances = np.abs(coef)
|
|
464
|
+
except Exception:
|
|
465
|
+
pass
|
|
466
|
+
|
|
467
|
+
if importances is None or len(importances) != len(self.feature_names):
|
|
468
|
+
return {}
|
|
469
|
+
|
|
470
|
+
imp_dict = dict(zip(self.feature_names, importances.tolist()))
|
|
471
|
+
imp_dict = dict(sorted(imp_dict.items(), key=lambda x: x[1], reverse=True))
|
|
472
|
+
|
|
473
|
+
self.importances = {k: round(v, 6) for k, v in list(imp_dict.items())[:top_n]}
|
|
474
|
+
return self.importances
|
|
475
|
+
|
|
476
|
+
# ─── 9. Metrics Dashboard ──────────────────────────────────────────
|
|
477
|
+
def get_metrics_dashboard(self) -> Dict[str, Any]:
|
|
478
|
+
"""Build a complete metrics dashboard."""
|
|
479
|
+
if not self.cv_results:
|
|
480
|
+
self.train_all_models()
|
|
481
|
+
|
|
482
|
+
dashboard = {
|
|
483
|
+
'problem_type': self.problem_type,
|
|
484
|
+
'target_column': self.target_col,
|
|
485
|
+
'dataset_shape': {
|
|
486
|
+
'rows': int(self.X.shape[0]),
|
|
487
|
+
'features': int(self.X.shape[1])
|
|
488
|
+
},
|
|
489
|
+
'models_compared': {},
|
|
490
|
+
'best_model': self.get_best_model(),
|
|
491
|
+
'feature_importance': self.get_feature_importance(),
|
|
492
|
+
'reliability': self.get_reliability_score(),
|
|
493
|
+
'warnings': self.warnings,
|
|
494
|
+
'log': self.log,
|
|
495
|
+
'explanation': self.explain_model() # Add explanation
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
# Per-model summary
|
|
499
|
+
for name, result in self.cv_results.items():
|
|
500
|
+
if 'error' in result:
|
|
501
|
+
dashboard['models_compared'][name] = {'error': result['error']}
|
|
502
|
+
else:
|
|
503
|
+
dashboard['models_compared'][name] = {
|
|
504
|
+
'metrics': result['metrics'],
|
|
505
|
+
'best_params': result['best_params']
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
return dashboard
|
|
509
|
+
|
|
510
|
+
# ─── 15. Explainability (disabled for memory) ─────────────────────
|
|
511
|
+
def explain_model(self) -> Dict[str, Any]:
|
|
512
|
+
"""SHAP disabled for memory optimization on 512MB hosting."""
|
|
513
|
+
return {'available': False, 'error': 'Disabled for memory optimization'}
|
|
514
|
+
|
|
515
|
+
# ─── 14. Full Pipeline Run ─────────────────────────────────────────
|
|
516
|
+
def run(self) -> Dict[str, Any]:
|
|
517
|
+
"""
|
|
518
|
+
Run the full ML training pipeline:
|
|
519
|
+
1. Detect target & problem type
|
|
520
|
+
2. Prepare features
|
|
521
|
+
3. Train all models with CV + tuning
|
|
522
|
+
4. Select best model
|
|
523
|
+
5. Get feature importance
|
|
524
|
+
6. Build metrics dashboard
|
|
525
|
+
"""
|
|
526
|
+
self.prepare_features()
|
|
527
|
+
self.train_all_models()
|
|
528
|
+
self.get_best_model()
|
|
529
|
+
dashboard = self.get_metrics_dashboard()
|
|
530
|
+
gc.collect()
|
|
531
|
+
return dashboard
|
|
532
|
+
|
|
533
|
+
# ─── 10. Reliability Score ─────────────────────────────────────────
|
|
534
|
+
def _compute_reliability(self, n_rows: int, cv_std: float, metric_val: float) -> Dict[str, Any]:
|
|
535
|
+
"""
|
|
536
|
+
Compute a 0–100 reliability score based on:
|
|
537
|
+
- Dataset size (bigger = better)
|
|
538
|
+
- CV score variance (lower = better)
|
|
539
|
+
- Primary metric quality
|
|
540
|
+
"""
|
|
541
|
+
score = 50 # baseline
|
|
542
|
+
reasons = []
|
|
543
|
+
|
|
544
|
+
# Dataset size factor (0–25 pts)
|
|
545
|
+
if n_rows >= 10000:
|
|
546
|
+
score += 25
|
|
547
|
+
reasons.append("Large dataset (+25)")
|
|
548
|
+
elif n_rows >= 1000:
|
|
549
|
+
pts = int(15 + (n_rows - 1000) / 900 * 10)
|
|
550
|
+
score += pts
|
|
551
|
+
reasons.append(f"Medium dataset (+{pts})")
|
|
552
|
+
elif n_rows >= 100:
|
|
553
|
+
pts = int(5 + (n_rows - 100) / 900 * 10)
|
|
554
|
+
score += pts
|
|
555
|
+
reasons.append(f"Small dataset (+{pts})")
|
|
556
|
+
else:
|
|
557
|
+
score -= 15
|
|
558
|
+
reasons.append("Very small dataset (-15)")
|
|
559
|
+
|
|
560
|
+
# CV variance factor (0–15 pts)
|
|
561
|
+
# Use provided std dev
|
|
562
|
+
if cv_std < 0.02:
|
|
563
|
+
score += 15
|
|
564
|
+
reasons.append("Very stable CV scores (+15)")
|
|
565
|
+
elif cv_std < 0.05:
|
|
566
|
+
score += 10
|
|
567
|
+
reasons.append("Stable CV scores (+10)")
|
|
568
|
+
elif cv_std < 0.1:
|
|
569
|
+
score += 5
|
|
570
|
+
reasons.append("Moderate CV variance (+5)")
|
|
571
|
+
else:
|
|
572
|
+
score -= 5
|
|
573
|
+
reasons.append("High CV variance (-5)")
|
|
574
|
+
|
|
575
|
+
# Metric quality factor (0–10 pts)
|
|
576
|
+
if metric_val >= 0.9:
|
|
577
|
+
score += 10
|
|
578
|
+
reasons.append("Excellent primary metric (+10)")
|
|
579
|
+
elif metric_val >= 0.75:
|
|
580
|
+
score += 5
|
|
581
|
+
reasons.append("Good primary metric (+5)")
|
|
582
|
+
elif metric_val < 0.5:
|
|
583
|
+
score -= 10
|
|
584
|
+
reasons.append("Poor primary metric (-10)")
|
|
585
|
+
|
|
586
|
+
score = max(0, min(100, score))
|
|
587
|
+
return {
|
|
588
|
+
'score': score,
|
|
589
|
+
'grade': 'A' if score >= 85 else 'B' if score >= 70 else 'C' if score >= 50 else 'D',
|
|
590
|
+
'reasons': reasons
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
def get_reliability_score(self) -> Dict[str, Any]:
|
|
594
|
+
"""Wrapper for current model reliability."""
|
|
595
|
+
n = self.X.shape[0] if self.X is not None else len(self.df)
|
|
596
|
+
|
|
597
|
+
primary = 'f1_weighted' if self.problem_type == 'classification' else 'r2'
|
|
598
|
+
std = 0.1
|
|
599
|
+
val = 0
|
|
600
|
+
|
|
601
|
+
if self.best_metrics:
|
|
602
|
+
std = self.best_metrics.get(primary, {}).get('std', 0.1)
|
|
603
|
+
val = self.best_metrics.get(primary, {}).get('mean', 0)
|
|
604
|
+
|
|
605
|
+
return self._compute_reliability(n, std, val)
|
|
606
|
+
|
|
607
|
+
# ─── 11. Export ────────────────────────────────────────────────────
|
|
608
|
+
def export_model(self, path: str) -> str:
|
|
609
|
+
"""Export the best model and metadata to a .pkl file."""
|
|
610
|
+
if self.best_model is None:
|
|
611
|
+
self.get_best_model()
|
|
612
|
+
|
|
613
|
+
export_data = {
|
|
614
|
+
'model': self.best_model,
|
|
615
|
+
'model_name': self.best_model_name,
|
|
616
|
+
'scaler': self.scaler,
|
|
617
|
+
'label_encoder': self.label_encoder,
|
|
618
|
+
'feature_names': self.feature_names,
|
|
619
|
+
'target_col': self.target_col,
|
|
620
|
+
'problem_type': self.problem_type,
|
|
621
|
+
'metrics': self.best_metrics,
|
|
622
|
+
'feature_importance': self.importances,
|
|
623
|
+
'exported_at': datetime.now().isoformat(),
|
|
624
|
+
'reliability': self.get_reliability_score()
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
joblib.dump(export_data, path)
|
|
628
|
+
self.log.append(f"Model exported to '{path}'")
|
|
629
|
+
return path
|
|
630
|
+
|
|
631
|
+
# ─── 13. Comparison Logic ──────────────────────────────────────────
|
|
632
|
+
def train_naive_baseline(self) -> Dict[str, Any]:
|
|
633
|
+
"""
|
|
634
|
+
Train a 'Naive' model on raw data for comparison.
|
|
635
|
+
Strategy: 'Just Encoding' + Minimal Imputation (to prevent crash).
|
|
636
|
+
"""
|
|
637
|
+
try:
|
|
638
|
+
if self.original_df is None:
|
|
639
|
+
msg = "Raw data not available for comparison"
|
|
640
|
+
self.log.append(msg)
|
|
641
|
+
return {"score": 0.0, "metrics": {}, "error": msg, "time_taken": 0, "memory_mb": 0}
|
|
642
|
+
|
|
643
|
+
# work on a copy of original_df
|
|
644
|
+
df = self.original_df.copy()
|
|
645
|
+
|
|
646
|
+
# 1. Drop IDs (essential) but keep almost everything else
|
|
647
|
+
for col in df.columns:
|
|
648
|
+
if col == self.target_col: continue
|
|
649
|
+
if ID_PATTERNS.match(col) or URL_PATTERNS.search(col):
|
|
650
|
+
df.drop(columns=[col], inplace=True)
|
|
651
|
+
|
|
652
|
+
# 2. Handle Target (and potential cleaning-renaming mismatch)
|
|
653
|
+
if self.target_col not in df.columns:
|
|
654
|
+
# Try case-insensitive match
|
|
655
|
+
col_map = {c.lower(): c for c in df.columns}
|
|
656
|
+
if self.target_col.lower() in col_map:
|
|
657
|
+
found_col = col_map[self.target_col.lower()]
|
|
658
|
+
df.rename(columns={found_col: self.target_col}, inplace=True)
|
|
659
|
+
self.log.append(f"Mapped raw target '{found_col}' to '{self.target_col}'")
|
|
660
|
+
else:
|
|
661
|
+
# Fail gracefully
|
|
662
|
+
msg = f"Target '{self.target_col}' not found in raw data"
|
|
663
|
+
self.log.append(msg)
|
|
664
|
+
return {"score": 0.0, "metrics": {}, "error": msg, "time_taken": 0, "memory_mb": 0}
|
|
665
|
+
|
|
666
|
+
y = df[self.target_col]
|
|
667
|
+
X = df.drop(columns=[self.target_col])
|
|
668
|
+
|
|
669
|
+
# 3. Naive Preprocessing (The "Lazy" Way - using Pipeline to avoid leakage)
|
|
670
|
+
|
|
671
|
+
# Identify columns
|
|
672
|
+
num_cols = X.select_dtypes(include=[np.number]).columns
|
|
673
|
+
cat_cols = X.select_dtypes(exclude=[np.number]).columns
|
|
674
|
+
|
|
675
|
+
# Preprocessing for numeric data: simple mean imputation
|
|
676
|
+
num_transformer = SimpleImputer(strategy='mean')
|
|
677
|
+
|
|
678
|
+
# Preprocessing for categorical data: constant fill + ordinal encoding
|
|
679
|
+
# OrdinalEncoder is used because it handles 2D arrays (unlike LabelEncoder)
|
|
680
|
+
# and we can handle unknown values by encoding them as -1
|
|
681
|
+
cat_transformer = make_pipeline(
|
|
682
|
+
SimpleImputer(strategy='constant', fill_value='missing'),
|
|
683
|
+
OrdinalEncoder(handle_unknown='use_encoded_value', unknown_value=-1)
|
|
684
|
+
)
|
|
685
|
+
|
|
686
|
+
preprocessor = make_column_transformer(
|
|
687
|
+
(num_transformer, num_cols),
|
|
688
|
+
(cat_transformer, cat_cols),
|
|
689
|
+
remainder='passthrough'
|
|
690
|
+
)
|
|
691
|
+
|
|
692
|
+
# Encode target if necessary
|
|
693
|
+
if self.problem_type == 'classification' and y.dtype == 'object':
|
|
694
|
+
le = LabelEncoder()
|
|
695
|
+
y = le.fit_transform(y.astype(str))
|
|
696
|
+
|
|
697
|
+
# 4. Train Naive Model (Simple Decision Tree)
|
|
698
|
+
if self.problem_type == 'classification':
|
|
699
|
+
from sklearn.tree import DecisionTreeClassifier
|
|
700
|
+
|
|
701
|
+
model = DecisionTreeClassifier(random_state=42)
|
|
702
|
+
scoring = 'accuracy'
|
|
703
|
+
else:
|
|
704
|
+
from sklearn.tree import DecisionTreeRegressor
|
|
705
|
+
model = DecisionTreeRegressor(random_state=42)
|
|
706
|
+
scoring = 'r2'
|
|
707
|
+
|
|
708
|
+
# Create full pipeline
|
|
709
|
+
# Note: We don't scale or do anything fancy. Just impute -> encode -> tree.
|
|
710
|
+
pipeline = make_pipeline(preprocessor, model)
|
|
711
|
+
|
|
712
|
+
# Quick CV
|
|
713
|
+
n_folds = 3
|
|
714
|
+
metrics = {}
|
|
715
|
+
if self.problem_type == 'classification':
|
|
716
|
+
cv = StratifiedKFold(n_splits=n_folds, shuffle=True, random_state=42)
|
|
717
|
+
scoring_dict = {
|
|
718
|
+
'accuracy': 'accuracy',
|
|
719
|
+
'precision': 'precision_weighted',
|
|
720
|
+
'f1': 'f1_weighted'
|
|
721
|
+
}
|
|
722
|
+
else:
|
|
723
|
+
cv = KFold(n_splits=n_folds, shuffle=True, random_state=42)
|
|
724
|
+
scoring_dict = {
|
|
725
|
+
'r2': 'r2',
|
|
726
|
+
'mae': 'neg_mean_absolute_error',
|
|
727
|
+
'mse': 'neg_mean_squared_error'
|
|
728
|
+
}
|
|
729
|
+
|
|
730
|
+
start_time = time.time()
|
|
731
|
+
try:
|
|
732
|
+
cv_results = cross_validate(pipeline, X, y, cv=cv, scoring=scoring_dict)
|
|
733
|
+
for metric_name, scorer_name in scoring_dict.items():
|
|
734
|
+
key = f"test_{metric_name}"
|
|
735
|
+
if key in cv_results:
|
|
736
|
+
score = np.mean(cv_results[key])
|
|
737
|
+
if 'neg_' in scorer_name: score = -score
|
|
738
|
+
metrics[metric_name] = round(score, 4)
|
|
739
|
+
|
|
740
|
+
# Main score for comparison
|
|
741
|
+
main_metric = 'accuracy' if self.problem_type == 'classification' else 'r2'
|
|
742
|
+
mean_score = metrics.get(main_metric, 0)
|
|
743
|
+
|
|
744
|
+
except ValueError as ve:
|
|
745
|
+
# Fallback for very small datasets
|
|
746
|
+
model.fit(preprocessor.fit_transform(X, y), y)
|
|
747
|
+
mean_score = model.score(preprocessor.transform(X), y)
|
|
748
|
+
metrics[main_metric] = round(mean_score, 4)
|
|
749
|
+
|
|
750
|
+
end_time = time.time()
|
|
751
|
+
time_taken = round(end_time - start_time, 4)
|
|
752
|
+
memory_mb = round(self.original_df.memory_usage(deep=True).sum() / (1024 * 1024), 2)
|
|
753
|
+
|
|
754
|
+
# Calculate Reliability for Raw
|
|
755
|
+
# Dataset size
|
|
756
|
+
n_raw = len(df)
|
|
757
|
+
|
|
758
|
+
# Std dev of primary metric
|
|
759
|
+
try:
|
|
760
|
+
if 'results' in locals() and results is not None:
|
|
761
|
+
if self.problem_type == 'classification':
|
|
762
|
+
raw_std = results['test_f1'].std()
|
|
763
|
+
raw_val = results['test_f1'].mean()
|
|
764
|
+
else:
|
|
765
|
+
raw_std = results['test_r2'].std()
|
|
766
|
+
raw_val = results['test_r2'].mean()
|
|
767
|
+
else:
|
|
768
|
+
raise ValueError("Results not available")
|
|
769
|
+
except Exception:
|
|
770
|
+
# Fallback if CV failed or results missing
|
|
771
|
+
raw_std = 0.2 # Assume high variance
|
|
772
|
+
raw_val = mean_score
|
|
773
|
+
|
|
774
|
+
reliability = self._compute_reliability(n_raw, raw_std, raw_val)
|
|
775
|
+
|
|
776
|
+
return {
|
|
777
|
+
"score": round(mean_score, 4),
|
|
778
|
+
"metrics": metrics,
|
|
779
|
+
"model_type": "Decision Tree (Baseline)",
|
|
780
|
+
"time_taken": time_taken,
|
|
781
|
+
"memory_mb": memory_mb,
|
|
782
|
+
"reliability": reliability
|
|
783
|
+
}
|
|
784
|
+
|
|
785
|
+
except Exception as e:
|
|
786
|
+
logger.debug(f"Naive baseline failed: {e}")
|
|
787
|
+
logger.debug(f"Target: {self.target_col}")
|
|
788
|
+
if self.original_df is not None:
|
|
789
|
+
logger.debug(f"Columns: {self.original_df.columns.tolist()}")
|
|
790
|
+
self.log.append(f"Naive baseline failed: {e}")
|
|
791
|
+
return {"score": 0.0, "metrics": {}, "error": str(e), "time_taken": 0, "memory_mb": 0}
|
|
792
|
+
|
|
793
|
+
def run_full_comparison(self) -> Dict[str, Any]:
|
|
794
|
+
"""
|
|
795
|
+
Run both Naive (Raw) and Advanced (Cleaned) pipelines and compare.
|
|
796
|
+
"""
|
|
797
|
+
# 1. Train Naive
|
|
798
|
+
naive_results = self.train_naive_baseline()
|
|
799
|
+
|
|
800
|
+
# 2. Run Advanced Pipeline (with timing)
|
|
801
|
+
start_adv = time.time()
|
|
802
|
+
advanced_dashboard = self.run()
|
|
803
|
+
end_adv = time.time()
|
|
804
|
+
advanced_dashboard['time_taken'] = round(end_adv - start_adv, 4)
|
|
805
|
+
|
|
806
|
+
# 3. Compare
|
|
807
|
+
advanced_score = 0
|
|
808
|
+
advanced_metrics = {}
|
|
809
|
+
|
|
810
|
+
# Calculate Advanced Stats
|
|
811
|
+
start_time_adv = time.time()
|
|
812
|
+
# (The 'run' method was already called above, so we can't truly measure it here unless we wrap 'self.run()'.
|
|
813
|
+
# But 'self.run()' includes 'train_all_models' which logs time.
|
|
814
|
+
# For this demo, let's assume Advanced takes slightly longer than Baseline + overhead due to search)
|
|
815
|
+
# We'll use a heuristic based on complexity or just measure overhead of this function call as proxy if 'run' isn't wrapped.
|
|
816
|
+
# BETTER: Let's assume 'run' took some time. We can't retroactively measure it easily without changing 'run'.
|
|
817
|
+
# Let's use a placeholder heuristic: Advanced Time = Baseline Time * 1.5 (Simulated for Demo effect as "Optimization" usually implies *better* result but *more* compute?
|
|
818
|
+
# Actually user asked for "optimization like cpu time... by cleaned data".
|
|
819
|
+
# Let's calculate memory for cleaned df.
|
|
820
|
+
advanced_memory_mb = round(self.df.memory_usage(deep=True).sum() / (1024 * 1024), 2)
|
|
821
|
+
|
|
822
|
+
# In a real app we'd time 'self.run()' properly.
|
|
823
|
+
# Let's wrap 'self.run()' in the caller next time. For now, let's estimate advanced_time based on log entries or just use a random factor of baseline to show data.
|
|
824
|
+
# Wait, I can just measure 'time.time()' before and after 'self.run()' call in this method!
|
|
825
|
+
# Ah, 'self.run()' is called at line 958. I should have wrapped it there.
|
|
826
|
+
# I will replace the whole function to wrap 'self.run()' with timing.
|
|
827
|
+
|
|
828
|
+
if 'best_model' in advanced_dashboard and 'metrics' in advanced_dashboard['best_model']:
|
|
829
|
+
all_metrics = advanced_dashboard['best_model']['metrics']
|
|
830
|
+
logger.debug(f"Available metrics keys: {list(all_metrics.keys())}")
|
|
831
|
+
for k, v in all_metrics.items():
|
|
832
|
+
if isinstance(v, dict) and 'mean' in v:
|
|
833
|
+
# Normalize keys: backend might use 'f1_weighted', frontend wants 'f1'
|
|
834
|
+
metric_key = k
|
|
835
|
+
if k == 'f1_weighted': metric_key = 'f1'
|
|
836
|
+
if k == 'precision_weighted': metric_key = 'precision'
|
|
837
|
+
advanced_metrics[metric_key] = round(v['mean'], 4)
|
|
838
|
+
|
|
839
|
+
if self.problem_type == 'classification':
|
|
840
|
+
advanced_score = advanced_metrics.get('accuracy', 0)
|
|
841
|
+
else:
|
|
842
|
+
advanced_score = advanced_metrics.get('r2', 0)
|
|
843
|
+
|
|
844
|
+
naive_score = naive_results.get('score', 0)
|
|
845
|
+
naive_metrics = naive_results.get('metrics', {})
|
|
846
|
+
|
|
847
|
+
# Calculate Improvement
|
|
848
|
+
improvement = 0
|
|
849
|
+
if naive_score != 0:
|
|
850
|
+
improvement = ((advanced_score - naive_score) / abs(naive_score)) * 100
|
|
851
|
+
elif advanced_score > 0:
|
|
852
|
+
improvement = 100
|
|
853
|
+
|
|
854
|
+
# Add comparison to dashboard
|
|
855
|
+
advanced_dashboard['comparison'] = {
|
|
856
|
+
'raw_score': naive_score,
|
|
857
|
+
'cleaned_score': advanced_score,
|
|
858
|
+
'raw_metrics': naive_metrics,
|
|
859
|
+
'cleaned_metrics': advanced_metrics,
|
|
860
|
+
'improvement_pct': round(improvement, 1),
|
|
861
|
+
'metric': 'Accuracy' if self.problem_type == 'classification' else 'R² Score',
|
|
862
|
+
'raw_error': naive_results.get('error'),
|
|
863
|
+
'raw_reliability': naive_results.get('reliability'),
|
|
864
|
+
'optimization': {
|
|
865
|
+
'raw_time': naive_results.get('time_taken', 0),
|
|
866
|
+
'cleaned_time': advanced_dashboard.get('time_taken', 0), # Will be set by wrapper
|
|
867
|
+
'raw_space': naive_results.get('memory_mb', 0),
|
|
868
|
+
'cleaned_space': advanced_memory_mb
|
|
869
|
+
}
|
|
870
|
+
}
|
|
871
|
+
|
|
872
|
+
return advanced_dashboard
|
|
873
|
+
|
|
874
|
+
|