ds-agent-cli 0.1.0
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.
- package/bin/ds-agent.js +451 -0
- package/ds_agent/__init__.py +8 -0
- package/package.json +28 -0
- package/requirements.txt +126 -0
- package/setup.py +35 -0
- package/src/__init__.py +7 -0
- package/src/_compress_tool_result.py +118 -0
- package/src/api/__init__.py +4 -0
- package/src/api/app.py +1626 -0
- package/src/cache/__init__.py +5 -0
- package/src/cache/cache_manager.py +561 -0
- package/src/cli.py +2886 -0
- package/src/dynamic_prompts.py +281 -0
- package/src/orchestrator.py +4799 -0
- package/src/progress_manager.py +139 -0
- package/src/reasoning/__init__.py +332 -0
- package/src/reasoning/business_summary.py +431 -0
- package/src/reasoning/data_understanding.py +356 -0
- package/src/reasoning/model_explanation.py +383 -0
- package/src/reasoning/reasoning_trace.py +239 -0
- package/src/registry/__init__.py +3 -0
- package/src/registry/tools_registry.py +3 -0
- package/src/session_memory.py +448 -0
- package/src/session_store.py +370 -0
- package/src/storage/__init__.py +19 -0
- package/src/storage/artifact_store.py +620 -0
- package/src/storage/helpers.py +116 -0
- package/src/storage/huggingface_storage.py +694 -0
- package/src/storage/r2_storage.py +0 -0
- package/src/storage/user_files_service.py +288 -0
- package/src/tools/__init__.py +335 -0
- package/src/tools/advanced_analysis.py +823 -0
- package/src/tools/advanced_feature_engineering.py +708 -0
- package/src/tools/advanced_insights.py +578 -0
- package/src/tools/advanced_preprocessing.py +549 -0
- package/src/tools/advanced_training.py +906 -0
- package/src/tools/agent_tool_mapping.py +326 -0
- package/src/tools/auto_pipeline.py +420 -0
- package/src/tools/autogluon_training.py +1480 -0
- package/src/tools/business_intelligence.py +860 -0
- package/src/tools/cloud_data_sources.py +581 -0
- package/src/tools/code_interpreter.py +390 -0
- package/src/tools/computer_vision.py +614 -0
- package/src/tools/data_cleaning.py +614 -0
- package/src/tools/data_profiling.py +593 -0
- package/src/tools/data_type_conversion.py +268 -0
- package/src/tools/data_wrangling.py +433 -0
- package/src/tools/eda_reports.py +284 -0
- package/src/tools/enhanced_feature_engineering.py +241 -0
- package/src/tools/feature_engineering.py +302 -0
- package/src/tools/matplotlib_visualizations.py +1327 -0
- package/src/tools/model_training.py +520 -0
- package/src/tools/nlp_text_analytics.py +761 -0
- package/src/tools/plotly_visualizations.py +497 -0
- package/src/tools/production_mlops.py +852 -0
- package/src/tools/time_series.py +507 -0
- package/src/tools/tools_registry.py +2133 -0
- package/src/tools/visualization_engine.py +559 -0
- package/src/utils/__init__.py +42 -0
- package/src/utils/error_recovery.py +313 -0
- package/src/utils/parallel_executor.py +402 -0
- package/src/utils/polars_helpers.py +248 -0
- package/src/utils/schema_extraction.py +132 -0
- package/src/utils/semantic_layer.py +392 -0
- package/src/utils/token_budget.py +411 -0
- package/src/utils/validation.py +377 -0
- package/src/workflow_state.py +154 -0
|
@@ -0,0 +1,520 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Model Training Tools
|
|
3
|
+
Tools for training machine learning models and generating reports.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
import polars as pl
|
|
7
|
+
import numpy as np
|
|
8
|
+
from typing import Dict, Any, List, Optional
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
import sys
|
|
11
|
+
import os
|
|
12
|
+
import joblib
|
|
13
|
+
import json
|
|
14
|
+
import tempfile
|
|
15
|
+
|
|
16
|
+
# Add parent directory to path for imports
|
|
17
|
+
sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
|
18
|
+
|
|
19
|
+
# Import artifact store
|
|
20
|
+
try:
|
|
21
|
+
from storage.helpers import save_model_with_store
|
|
22
|
+
ARTIFACT_STORE_AVAILABLE = True
|
|
23
|
+
except ImportError:
|
|
24
|
+
ARTIFACT_STORE_AVAILABLE = False
|
|
25
|
+
print("⚠️ Artifact store not available, using local paths")
|
|
26
|
+
|
|
27
|
+
from sklearn.model_selection import train_test_split
|
|
28
|
+
from sklearn.linear_model import LogisticRegression, Ridge, Lasso
|
|
29
|
+
from sklearn.ensemble import RandomForestClassifier, RandomForestRegressor
|
|
30
|
+
from xgboost import XGBClassifier, XGBRegressor
|
|
31
|
+
from lightgbm import LGBMClassifier, LGBMRegressor
|
|
32
|
+
from catboost import CatBoostClassifier, CatBoostRegressor
|
|
33
|
+
from sklearn.metrics import (
|
|
34
|
+
accuracy_score, precision_score, recall_score, f1_score,
|
|
35
|
+
confusion_matrix, classification_report,
|
|
36
|
+
mean_squared_error, mean_absolute_error, r2_score
|
|
37
|
+
)
|
|
38
|
+
import shap
|
|
39
|
+
|
|
40
|
+
try:
|
|
41
|
+
from .visualization_engine import (
|
|
42
|
+
generate_model_performance_plots,
|
|
43
|
+
generate_feature_importance_plot
|
|
44
|
+
)
|
|
45
|
+
VISUALIZATION_AVAILABLE = True
|
|
46
|
+
except ImportError as e:
|
|
47
|
+
VISUALIZATION_AVAILABLE = False
|
|
48
|
+
print(f"⚠️ Visualization engine not available: {e}")
|
|
49
|
+
|
|
50
|
+
from ds_agent.utils.polars_helpers import (
|
|
51
|
+
load_dataframe,
|
|
52
|
+
get_numeric_columns,
|
|
53
|
+
split_features_target,
|
|
54
|
+
)
|
|
55
|
+
from ds_agent.utils.validation import (
|
|
56
|
+
validate_file_exists,
|
|
57
|
+
validate_file_format,
|
|
58
|
+
validate_dataframe,
|
|
59
|
+
validate_column_exists,
|
|
60
|
+
validate_target_column,
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def train_baseline_models(file_path: str, target_col: str,
|
|
65
|
+
task_type: str = "auto",
|
|
66
|
+
test_size: float = 0.2,
|
|
67
|
+
random_state: int = 42) -> Dict[str, Any]:
|
|
68
|
+
"""
|
|
69
|
+
Train multiple baseline models and compare performance.
|
|
70
|
+
|
|
71
|
+
Args:
|
|
72
|
+
file_path: Path to prepared dataset
|
|
73
|
+
target_col: Name of target column
|
|
74
|
+
task_type: 'classification', 'regression', or 'auto'
|
|
75
|
+
test_size: Proportion for test split
|
|
76
|
+
random_state: Random seed
|
|
77
|
+
|
|
78
|
+
Returns:
|
|
79
|
+
Dictionary with training results and best model
|
|
80
|
+
"""
|
|
81
|
+
# Validation
|
|
82
|
+
validate_file_exists(file_path)
|
|
83
|
+
validate_file_format(file_path)
|
|
84
|
+
|
|
85
|
+
# Load data
|
|
86
|
+
df = load_dataframe(file_path)
|
|
87
|
+
validate_dataframe(df)
|
|
88
|
+
validate_column_exists(df, target_col)
|
|
89
|
+
|
|
90
|
+
# Infer task type if auto
|
|
91
|
+
if task_type == "auto":
|
|
92
|
+
task_type = validate_target_column(df, target_col)
|
|
93
|
+
|
|
94
|
+
# Split features and target
|
|
95
|
+
X, y = split_features_target(df, target_col)
|
|
96
|
+
|
|
97
|
+
# Convert to numpy for sklearn
|
|
98
|
+
# Only keep numeric columns for X
|
|
99
|
+
numeric_cols = get_numeric_columns(X)
|
|
100
|
+
if len(numeric_cols) == 0:
|
|
101
|
+
return {
|
|
102
|
+
"status": "error",
|
|
103
|
+
"message": "No numeric features found. Please encode categorical variables first."
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
X_numeric = X.select(numeric_cols)
|
|
107
|
+
X_np = X_numeric.to_numpy()
|
|
108
|
+
y_np = y.to_numpy()
|
|
109
|
+
|
|
110
|
+
# Handle missing values (simple imputation with mean)
|
|
111
|
+
from sklearn.impute import SimpleImputer
|
|
112
|
+
imputer = SimpleImputer(strategy='mean')
|
|
113
|
+
X_np = imputer.fit_transform(X_np)
|
|
114
|
+
|
|
115
|
+
# Train-test split
|
|
116
|
+
X_train, X_test, y_train, y_test = train_test_split(
|
|
117
|
+
X_np, y_np, test_size=test_size, random_state=random_state
|
|
118
|
+
)
|
|
119
|
+
|
|
120
|
+
results = {
|
|
121
|
+
"task_type": task_type,
|
|
122
|
+
"n_features": X_np.shape[1],
|
|
123
|
+
"n_samples": len(X_np),
|
|
124
|
+
"train_size": len(X_train),
|
|
125
|
+
"test_size": len(X_test),
|
|
126
|
+
"feature_names": numeric_cols,
|
|
127
|
+
"models": {}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
# Train models based on task type
|
|
131
|
+
import sys
|
|
132
|
+
print(f"\n🚀 Training {5 if task_type == 'classification' else 5} baseline models...", flush=True)
|
|
133
|
+
print(f" 📊 Training set: {len(X_train):,} samples × {X_train.shape[1]} features", flush=True)
|
|
134
|
+
print(f" 📊 Test set: {len(X_test):,} samples", flush=True)
|
|
135
|
+
print(f" ⚡ Note: Random Forest excluded to optimize compute resources", flush=True)
|
|
136
|
+
sys.stdout.flush()
|
|
137
|
+
|
|
138
|
+
if task_type == "classification":
|
|
139
|
+
models = {
|
|
140
|
+
"logistic_regression": LogisticRegression(max_iter=1000, random_state=random_state),
|
|
141
|
+
"xgboost": XGBClassifier(n_estimators=100, random_state=random_state, n_jobs=-1),
|
|
142
|
+
"lightgbm": LGBMClassifier(n_estimators=100, random_state=random_state, n_jobs=-1, verbose=-1),
|
|
143
|
+
"catboost": CatBoostClassifier(iterations=100, random_state=random_state, verbose=0, allow_writing_files=False)
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
for idx, (model_name, model) in enumerate(models.items(), 1):
|
|
147
|
+
try:
|
|
148
|
+
# Train
|
|
149
|
+
print(f"\n [{idx}/{len(models)}] Training {model_name}...", flush=True)
|
|
150
|
+
sys.stdout.flush()
|
|
151
|
+
import time
|
|
152
|
+
start_time = time.time()
|
|
153
|
+
model.fit(X_train, y_train)
|
|
154
|
+
elapsed = time.time() - start_time
|
|
155
|
+
print(f" ✓ {model_name} trained in {elapsed:.1f}s", flush=True)
|
|
156
|
+
sys.stdout.flush()
|
|
157
|
+
|
|
158
|
+
# Predict
|
|
159
|
+
y_pred_train = model.predict(X_train)
|
|
160
|
+
y_pred_test = model.predict(X_test)
|
|
161
|
+
|
|
162
|
+
# Metrics
|
|
163
|
+
results["models"][model_name] = {
|
|
164
|
+
"train_metrics": {
|
|
165
|
+
"accuracy": float(accuracy_score(y_train, y_pred_train)),
|
|
166
|
+
"precision": float(precision_score(y_train, y_pred_train, average='weighted', zero_division=0)),
|
|
167
|
+
"recall": float(recall_score(y_train, y_pred_train, average='weighted', zero_division=0)),
|
|
168
|
+
"f1": float(f1_score(y_train, y_pred_train, average='weighted', zero_division=0))
|
|
169
|
+
},
|
|
170
|
+
"test_metrics": {
|
|
171
|
+
"accuracy": float(accuracy_score(y_test, y_pred_test)),
|
|
172
|
+
"precision": float(precision_score(y_test, y_pred_test, average='weighted', zero_division=0)),
|
|
173
|
+
"recall": float(recall_score(y_test, y_pred_test, average='weighted', zero_division=0)),
|
|
174
|
+
"f1": float(f1_score(y_test, y_pred_test, average='weighted', zero_division=0))
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
# Save model using artifact store
|
|
179
|
+
if ARTIFACT_STORE_AVAILABLE:
|
|
180
|
+
model_path = save_model_with_store(
|
|
181
|
+
model_data={
|
|
182
|
+
"model": model,
|
|
183
|
+
"imputer": imputer,
|
|
184
|
+
"feature_names": numeric_cols
|
|
185
|
+
},
|
|
186
|
+
filename=f"{model_name}.pkl",
|
|
187
|
+
metadata={
|
|
188
|
+
"model_name": model_name,
|
|
189
|
+
"task_type": "classification",
|
|
190
|
+
"train_accuracy": float(accuracy_score(y_train, y_pred_train)),
|
|
191
|
+
"test_accuracy": float(accuracy_score(y_test, y_pred_test)),
|
|
192
|
+
"features": numeric_cols
|
|
193
|
+
}
|
|
194
|
+
)
|
|
195
|
+
else:
|
|
196
|
+
model_path = f"./outputs/models/{model_name}.pkl"
|
|
197
|
+
Path(model_path).parent.mkdir(parents=True, exist_ok=True)
|
|
198
|
+
joblib.dump({
|
|
199
|
+
"model": model,
|
|
200
|
+
"imputer": imputer,
|
|
201
|
+
"feature_names": numeric_cols
|
|
202
|
+
}, model_path)
|
|
203
|
+
|
|
204
|
+
results["models"][model_name]["model_path"] = model_path
|
|
205
|
+
|
|
206
|
+
except Exception as e:
|
|
207
|
+
results["models"][model_name] = {
|
|
208
|
+
"status": "error",
|
|
209
|
+
"message": str(e)
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
else: # regression
|
|
213
|
+
models = {
|
|
214
|
+
"ridge": Ridge(random_state=random_state),
|
|
215
|
+
"lasso": Lasso(random_state=random_state),
|
|
216
|
+
"xgboost": XGBRegressor(n_estimators=100, random_state=random_state, n_jobs=-1),
|
|
217
|
+
"lightgbm": LGBMRegressor(n_estimators=100, random_state=random_state, n_jobs=-1, verbose=-1),
|
|
218
|
+
"catboost": CatBoostRegressor(iterations=100, random_state=random_state, verbose=0, allow_writing_files=False)
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
for idx, (model_name, model) in enumerate(models.items(), 1):
|
|
222
|
+
try:
|
|
223
|
+
# Train
|
|
224
|
+
import sys
|
|
225
|
+
print(f"\n [{idx}/{len(models)}] Training {model_name}...", flush=True)
|
|
226
|
+
sys.stdout.flush()
|
|
227
|
+
import time
|
|
228
|
+
start_time = time.time()
|
|
229
|
+
model.fit(X_train, y_train)
|
|
230
|
+
elapsed = time.time() - start_time
|
|
231
|
+
print(f" ✓ {model_name} trained in {elapsed:.1f}s", flush=True)
|
|
232
|
+
sys.stdout.flush()
|
|
233
|
+
|
|
234
|
+
# Predict
|
|
235
|
+
y_pred_train = model.predict(X_train)
|
|
236
|
+
y_pred_test = model.predict(X_test)
|
|
237
|
+
|
|
238
|
+
# Metrics
|
|
239
|
+
results["models"][model_name] = {
|
|
240
|
+
"train_metrics": {
|
|
241
|
+
"mse": float(mean_squared_error(y_train, y_pred_train)),
|
|
242
|
+
"rmse": float(np.sqrt(mean_squared_error(y_train, y_pred_train))),
|
|
243
|
+
"mae": float(mean_absolute_error(y_train, y_pred_train)),
|
|
244
|
+
"r2": float(r2_score(y_train, y_pred_train))
|
|
245
|
+
},
|
|
246
|
+
"test_metrics": {
|
|
247
|
+
"mse": float(mean_squared_error(y_test, y_pred_test)),
|
|
248
|
+
"rmse": float(np.sqrt(mean_squared_error(y_test, y_pred_test))),
|
|
249
|
+
"mae": float(mean_absolute_error(y_test, y_pred_test)),
|
|
250
|
+
"r2": float(r2_score(y_test, y_pred_test))
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
# Save model using artifact store
|
|
255
|
+
if ARTIFACT_STORE_AVAILABLE:
|
|
256
|
+
model_path = save_model_with_store(
|
|
257
|
+
model_data={
|
|
258
|
+
"model": model,
|
|
259
|
+
"imputer": imputer,
|
|
260
|
+
"feature_names": numeric_cols
|
|
261
|
+
},
|
|
262
|
+
filename=f"{model_name}.pkl",
|
|
263
|
+
metadata={
|
|
264
|
+
"model_name": model_name,
|
|
265
|
+
"task_type": "regression",
|
|
266
|
+
"train_r2": float(r2_score(y_train, y_pred_train)),
|
|
267
|
+
"test_r2": float(r2_score(y_test, y_pred_test)),
|
|
268
|
+
"features": numeric_cols
|
|
269
|
+
}
|
|
270
|
+
)
|
|
271
|
+
else:
|
|
272
|
+
model_path = f"./outputs/models/{model_name}.pkl"
|
|
273
|
+
Path(model_path).parent.mkdir(parents=True, exist_ok=True)
|
|
274
|
+
joblib.dump({
|
|
275
|
+
"model": model,
|
|
276
|
+
"imputer": imputer,
|
|
277
|
+
"feature_names": numeric_cols
|
|
278
|
+
}, model_path)
|
|
279
|
+
|
|
280
|
+
results["models"][model_name]["model_path"] = model_path
|
|
281
|
+
|
|
282
|
+
except Exception as e:
|
|
283
|
+
results["models"][model_name] = {
|
|
284
|
+
"status": "error",
|
|
285
|
+
"message": str(e)
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
# Determine best model
|
|
289
|
+
best_model_name = None
|
|
290
|
+
best_score = -float('inf')
|
|
291
|
+
|
|
292
|
+
for model_name, model_results in results["models"].items():
|
|
293
|
+
if "test_metrics" in model_results:
|
|
294
|
+
if task_type == "classification":
|
|
295
|
+
score = model_results["test_metrics"]["f1"]
|
|
296
|
+
else:
|
|
297
|
+
score = model_results["test_metrics"]["r2"]
|
|
298
|
+
|
|
299
|
+
if score > best_score:
|
|
300
|
+
best_score = score
|
|
301
|
+
best_model_name = model_name
|
|
302
|
+
|
|
303
|
+
results["best_model"] = {
|
|
304
|
+
"name": best_model_name,
|
|
305
|
+
"score": best_score,
|
|
306
|
+
"model_path": results["models"][best_model_name]["model_path"] if best_model_name else None
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
# ⚠️ Add guidance for hyperparameter tuning on large datasets
|
|
310
|
+
if results["n_samples"] > 100000:
|
|
311
|
+
# Recommend faster models for large datasets
|
|
312
|
+
fast_models = ["xgboost", "lightgbm"]
|
|
313
|
+
if best_model_name in fast_models:
|
|
314
|
+
results["tuning_recommendation"] = {
|
|
315
|
+
"suggested_model": best_model_name,
|
|
316
|
+
"reason": f"{best_model_name} is optimal for large datasets - fast training and good performance"
|
|
317
|
+
}
|
|
318
|
+
elif best_model_name == "random_forest_legacy": # Disabled for compute optimization
|
|
319
|
+
# Find next best fast model
|
|
320
|
+
fast_model_scores = {name: results["models"][name]["test_metrics"].get("r2" if task_type == "regression" else "f1", 0)
|
|
321
|
+
for name in fast_models if name in results["models"]}
|
|
322
|
+
if fast_model_scores:
|
|
323
|
+
alt_model = max(fast_model_scores, key=fast_model_scores.get)
|
|
324
|
+
alt_score = fast_model_scores[alt_model]
|
|
325
|
+
score_diff = abs(best_score - alt_score)
|
|
326
|
+
if score_diff < 0.05: # Less than 5% difference
|
|
327
|
+
results["tuning_recommendation"] = {
|
|
328
|
+
"suggested_model": alt_model,
|
|
329
|
+
"reason": f"For large datasets, {alt_model} is 5-10x faster than {best_model_name} with similar performance (score difference: {score_diff:.4f})"
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
# Generate visualizations for best model
|
|
333
|
+
if VISUALIZATION_AVAILABLE and best_model_name:
|
|
334
|
+
try:
|
|
335
|
+
print(f"\n🎨 Generating visualizations for {best_model_name}...")
|
|
336
|
+
|
|
337
|
+
# Load best model
|
|
338
|
+
model_data = joblib.dump({
|
|
339
|
+
"model": models[best_model_name],
|
|
340
|
+
"imputer": imputer,
|
|
341
|
+
"feature_names": numeric_cols
|
|
342
|
+
}, f"./outputs/models/{best_model_name}_temp.pkl")
|
|
343
|
+
|
|
344
|
+
# Get predictions for visualization
|
|
345
|
+
best_model = models[best_model_name]
|
|
346
|
+
y_pred_test = best_model.predict(X_test)
|
|
347
|
+
y_pred_proba = None
|
|
348
|
+
if hasattr(best_model, "predict_proba") and task_type == "classification":
|
|
349
|
+
y_pred_proba = best_model.predict_proba(X_test)
|
|
350
|
+
|
|
351
|
+
# Generate model performance plots
|
|
352
|
+
plot_dir = "./outputs/plots/model_performance"
|
|
353
|
+
perf_plots = generate_model_performance_plots(
|
|
354
|
+
y_true=y_test,
|
|
355
|
+
y_pred=y_pred_test,
|
|
356
|
+
y_pred_proba=y_pred_proba,
|
|
357
|
+
task_type=task_type,
|
|
358
|
+
model_name=best_model_name,
|
|
359
|
+
output_dir=plot_dir
|
|
360
|
+
)
|
|
361
|
+
results["performance_plots"] = perf_plots["plot_paths"]
|
|
362
|
+
|
|
363
|
+
# Generate feature importance plot if available
|
|
364
|
+
if hasattr(best_model, "feature_importances_"):
|
|
365
|
+
feature_importance = dict(zip(numeric_cols, best_model.feature_importances_))
|
|
366
|
+
importance_plot = generate_feature_importance_plot(
|
|
367
|
+
feature_importances=feature_importance,
|
|
368
|
+
output_path=f"{plot_dir}/feature_importance_{best_model_name}.png"
|
|
369
|
+
)
|
|
370
|
+
results["feature_importance_plot"] = importance_plot
|
|
371
|
+
|
|
372
|
+
print(f" ✓ Generated {len(perf_plots.get('plot_paths', []))} performance plots")
|
|
373
|
+
results["visualization_generated"] = True
|
|
374
|
+
|
|
375
|
+
except Exception as e:
|
|
376
|
+
print(f" ⚠️ Could not generate visualizations: {str(e)}")
|
|
377
|
+
results["visualization_generated"] = False
|
|
378
|
+
else:
|
|
379
|
+
results["visualization_generated"] = False
|
|
380
|
+
|
|
381
|
+
# Print final summary
|
|
382
|
+
print(f"\n{'='*60}")
|
|
383
|
+
print(f"✅ TRAINING COMPLETE")
|
|
384
|
+
print(f"{'='*60}")
|
|
385
|
+
print(f"📊 Best Model: {best_model_name}")
|
|
386
|
+
if task_type == "regression":
|
|
387
|
+
print(f"📈 Test R²: {best_score:.4f}")
|
|
388
|
+
print(f"📉 Test RMSE: {results['models'][best_model_name]['test_metrics']['rmse']:.4f}")
|
|
389
|
+
else:
|
|
390
|
+
print(f"📈 Test F1: {best_score:.4f}")
|
|
391
|
+
print(f"📉 Test Accuracy: {results['models'][best_model_name]['test_metrics']['accuracy']:.4f}")
|
|
392
|
+
print(f"💾 Model saved: {results['best_model']['model_path']}")
|
|
393
|
+
print(f"{'='*60}\\n")
|
|
394
|
+
|
|
395
|
+
return results
|
|
396
|
+
|
|
397
|
+
|
|
398
|
+
def generate_model_report(model_path: str, test_data_path: str,
|
|
399
|
+
target_col: str, output_path: str) -> Dict[str, Any]:
|
|
400
|
+
"""
|
|
401
|
+
Generate comprehensive model evaluation report.
|
|
402
|
+
|
|
403
|
+
Args:
|
|
404
|
+
model_path: Path to saved model file
|
|
405
|
+
test_data_path: Path to test dataset
|
|
406
|
+
target_col: Name of target column
|
|
407
|
+
output_path: Path to save report JSON
|
|
408
|
+
|
|
409
|
+
Returns:
|
|
410
|
+
Dictionary with model report
|
|
411
|
+
"""
|
|
412
|
+
# Validation
|
|
413
|
+
validate_file_exists(model_path)
|
|
414
|
+
validate_file_exists(test_data_path)
|
|
415
|
+
|
|
416
|
+
# Load model
|
|
417
|
+
model_data = joblib.load(model_path)
|
|
418
|
+
model = model_data["model"]
|
|
419
|
+
imputer = model_data["imputer"]
|
|
420
|
+
feature_names = model_data["feature_names"]
|
|
421
|
+
|
|
422
|
+
# Load test data
|
|
423
|
+
df = load_dataframe(test_data_path)
|
|
424
|
+
validate_dataframe(df)
|
|
425
|
+
validate_column_exists(df, target_col)
|
|
426
|
+
|
|
427
|
+
# Prepare features
|
|
428
|
+
X = df.select(feature_names)
|
|
429
|
+
y = df[target_col].to_numpy()
|
|
430
|
+
X_np = imputer.transform(X.to_numpy())
|
|
431
|
+
|
|
432
|
+
# Predict
|
|
433
|
+
y_pred = model.predict(X_np)
|
|
434
|
+
|
|
435
|
+
# Determine task type
|
|
436
|
+
if hasattr(model, "predict_proba"):
|
|
437
|
+
task_type = "classification"
|
|
438
|
+
else:
|
|
439
|
+
task_type = "regression"
|
|
440
|
+
|
|
441
|
+
report = {
|
|
442
|
+
"model_path": model_path,
|
|
443
|
+
"task_type": task_type,
|
|
444
|
+
"n_features": len(feature_names),
|
|
445
|
+
"n_samples": len(X_np)
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
# Calculate metrics
|
|
449
|
+
if task_type == "classification":
|
|
450
|
+
report["metrics"] = {
|
|
451
|
+
"accuracy": float(accuracy_score(y, y_pred)),
|
|
452
|
+
"precision": float(precision_score(y, y_pred, average='weighted', zero_division=0)),
|
|
453
|
+
"recall": float(recall_score(y, y_pred, average='weighted', zero_division=0)),
|
|
454
|
+
"f1": float(f1_score(y, y_pred, average='weighted', zero_division=0))
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
# Confusion matrix
|
|
458
|
+
cm = confusion_matrix(y, y_pred)
|
|
459
|
+
report["confusion_matrix"] = cm.tolist()
|
|
460
|
+
|
|
461
|
+
# Classification report
|
|
462
|
+
class_report = classification_report(y, y_pred, output_dict=True, zero_division=0)
|
|
463
|
+
report["classification_report"] = class_report
|
|
464
|
+
|
|
465
|
+
else: # regression
|
|
466
|
+
report["metrics"] = {
|
|
467
|
+
"mse": float(mean_squared_error(y, y_pred)),
|
|
468
|
+
"rmse": float(np.sqrt(mean_squared_error(y, y_pred))),
|
|
469
|
+
"mae": float(mean_absolute_error(y, y_pred)),
|
|
470
|
+
"r2": float(r2_score(y, y_pred))
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
# Feature importance
|
|
474
|
+
if hasattr(model, "feature_importances_"):
|
|
475
|
+
importances = model.feature_importances_
|
|
476
|
+
feature_importance = [
|
|
477
|
+
{"feature": name, "importance": float(imp)}
|
|
478
|
+
for name, imp in zip(feature_names, importances)
|
|
479
|
+
]
|
|
480
|
+
feature_importance.sort(key=lambda x: x["importance"], reverse=True)
|
|
481
|
+
report["feature_importance"] = feature_importance[:20] # Top 20
|
|
482
|
+
|
|
483
|
+
# SHAP values (for top 10 features)
|
|
484
|
+
try:
|
|
485
|
+
# Use TreeExplainer for tree-based models
|
|
486
|
+
if hasattr(model, "feature_importances_"):
|
|
487
|
+
explainer = shap.TreeExplainer(model)
|
|
488
|
+
else:
|
|
489
|
+
# Use KernelExplainer for other models (sample for speed)
|
|
490
|
+
sample_size = min(100, len(X_np))
|
|
491
|
+
explainer = shap.KernelExplainer(
|
|
492
|
+
model.predict,
|
|
493
|
+
X_np[:sample_size]
|
|
494
|
+
)
|
|
495
|
+
|
|
496
|
+
shap_values = explainer.shap_values(X_np[:100]) # First 100 samples
|
|
497
|
+
|
|
498
|
+
# Calculate mean absolute SHAP values
|
|
499
|
+
if isinstance(shap_values, list): # Multi-class
|
|
500
|
+
shap_values = shap_values[0]
|
|
501
|
+
|
|
502
|
+
mean_shap = np.abs(shap_values).mean(axis=0)
|
|
503
|
+
shap_importance = [
|
|
504
|
+
{"feature": name, "shap_value": float(val)}
|
|
505
|
+
for name, val in zip(feature_names, mean_shap)
|
|
506
|
+
]
|
|
507
|
+
shap_importance.sort(key=lambda x: x["shap_value"], reverse=True)
|
|
508
|
+
report["shap_feature_importance"] = shap_importance[:10] # Top 10
|
|
509
|
+
|
|
510
|
+
except Exception as e:
|
|
511
|
+
report["shap_error"] = f"Could not compute SHAP values: {str(e)}"
|
|
512
|
+
|
|
513
|
+
# Save report
|
|
514
|
+
Path(output_path).parent.mkdir(parents=True, exist_ok=True)
|
|
515
|
+
with open(output_path, 'w') as f:
|
|
516
|
+
json.dump(report, f, indent=2)
|
|
517
|
+
|
|
518
|
+
report["output_path"] = output_path
|
|
519
|
+
|
|
520
|
+
return report
|