autoforge-engine 0.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.
- autoforge_engine-0.1.0.dist-info/METADATA +105 -0
- autoforge_engine-0.1.0.dist-info/RECORD +32 -0
- autoforge_engine-0.1.0.dist-info/WHEEL +5 -0
- autoforge_engine-0.1.0.dist-info/entry_points.txt +2 -0
- autoforge_engine-0.1.0.dist-info/licenses/LICENSE +0 -0
- autoforge_engine-0.1.0.dist-info/top_level.txt +1 -0
- modelforge/artifact_manager.py +485 -0
- modelforge/automl.py +1472 -0
- modelforge/cli.py +1258 -0
- modelforge/column_intelligence.py +404 -0
- modelforge/config.py +580 -0
- modelforge/cross_validation.py +749 -0
- modelforge/data_audit.py +392 -0
- modelforge/data_loader.py +76 -0
- modelforge/evaluation.py +397 -0
- modelforge/experiment_tracker.py +490 -0
- modelforge/explainability.py +346 -0
- modelforge/feature_engineering.py +393 -0
- modelforge/feature_selection.py +528 -0
- modelforge/hyperparameter_optimization.py +593 -0
- modelforge/model_registry.py +684 -0
- modelforge/model_screening.py +531 -0
- modelforge/persistence.py +456 -0
- modelforge/pipeline_generator.py +278 -0
- modelforge/prediction_validator.py +316 -0
- modelforge/preprocessing.py +179 -0
- modelforge/profiler.py +85 -0
- modelforge/ranking.py +351 -0
- modelforge/reproducibility.py +295 -0
- modelforge/reproducibility_integration.py +192 -0
- modelforge/run_manager.py +200 -0
- modelforge/target_selector.py +108 -0
|
@@ -0,0 +1,490 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import copy
|
|
4
|
+
import json
|
|
5
|
+
import uuid
|
|
6
|
+
from datetime import datetime, timezone
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
from typing import Any
|
|
9
|
+
|
|
10
|
+
import pandas as pd
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class ExperimentTracker:
|
|
14
|
+
"""
|
|
15
|
+
Local experiment tracking for ModelForge.
|
|
16
|
+
|
|
17
|
+
Experiments are stored as individual JSON files so that
|
|
18
|
+
users can inspect, copy, archive, or version them easily.
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
def __init__(
|
|
22
|
+
self,
|
|
23
|
+
directory: str | Path = ".modelforge/experiments",
|
|
24
|
+
experiment_directory: str | Path | None = None,
|
|
25
|
+
):
|
|
26
|
+
self.directory = Path(
|
|
27
|
+
experiment_directory
|
|
28
|
+
if experiment_directory is not None
|
|
29
|
+
else directory
|
|
30
|
+
)
|
|
31
|
+
self.directory.mkdir(
|
|
32
|
+
parents=True,
|
|
33
|
+
exist_ok=True,
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
def create_experiment_id(self) -> str:
|
|
37
|
+
"""
|
|
38
|
+
Generate a unique experiment identifier.
|
|
39
|
+
"""
|
|
40
|
+
|
|
41
|
+
timestamp = datetime.now(
|
|
42
|
+
timezone.utc
|
|
43
|
+
).strftime("%Y%m%d%H%M%S")
|
|
44
|
+
|
|
45
|
+
short_uuid = uuid.uuid4().hex[:8]
|
|
46
|
+
|
|
47
|
+
return f"exp_{timestamp}_{short_uuid}"
|
|
48
|
+
|
|
49
|
+
def record(
|
|
50
|
+
self,
|
|
51
|
+
result: dict[str, Any],
|
|
52
|
+
configuration: dict[str, Any] | None = None,
|
|
53
|
+
experiment_id: str | None = None,
|
|
54
|
+
) -> str:
|
|
55
|
+
"""
|
|
56
|
+
Save an AutoML result as an experiment.
|
|
57
|
+
|
|
58
|
+
Returns the experiment ID.
|
|
59
|
+
|
|
60
|
+
The reproducibility snapshot is copied exactly from
|
|
61
|
+
the AutoML result before serialization.
|
|
62
|
+
"""
|
|
63
|
+
|
|
64
|
+
if not isinstance(result, dict):
|
|
65
|
+
raise TypeError(
|
|
66
|
+
"result must be a dictionary."
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
if configuration is not None:
|
|
70
|
+
if not isinstance(configuration, dict):
|
|
71
|
+
raise TypeError(
|
|
72
|
+
"configuration must be a dictionary."
|
|
73
|
+
)
|
|
74
|
+
|
|
75
|
+
experiment_id = (
|
|
76
|
+
experiment_id
|
|
77
|
+
or self.create_experiment_id()
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
reproducibility = copy.deepcopy(
|
|
81
|
+
result.get("reproducibility")
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
run_summary = result.get(
|
|
85
|
+
"run_summary"
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
if not isinstance(run_summary, dict):
|
|
89
|
+
run_summary = {}
|
|
90
|
+
|
|
91
|
+
run_id = result.get(
|
|
92
|
+
"run_id"
|
|
93
|
+
) or run_summary.get(
|
|
94
|
+
"run_id"
|
|
95
|
+
)
|
|
96
|
+
|
|
97
|
+
status = result.get(
|
|
98
|
+
"status"
|
|
99
|
+
) or run_summary.get(
|
|
100
|
+
"status"
|
|
101
|
+
)
|
|
102
|
+
|
|
103
|
+
experiment = {
|
|
104
|
+
"experiment_id": experiment_id,
|
|
105
|
+
"timestamp": datetime.now(
|
|
106
|
+
timezone.utc
|
|
107
|
+
).isoformat(),
|
|
108
|
+
"target": self._safe_value(
|
|
109
|
+
result.get("target")
|
|
110
|
+
),
|
|
111
|
+
"profile": self._safe_value(
|
|
112
|
+
result.get("profile")
|
|
113
|
+
),
|
|
114
|
+
"column_intelligence": self._safe_value(
|
|
115
|
+
result.get("column_intelligence")
|
|
116
|
+
),
|
|
117
|
+
"audit": self._safe_value(
|
|
118
|
+
result.get("audit")
|
|
119
|
+
),
|
|
120
|
+
"models_evaluated": self._safe_value(
|
|
121
|
+
result.get("models_evaluated")
|
|
122
|
+
),
|
|
123
|
+
"screening_results": self._dataframe_to_records(
|
|
124
|
+
result.get("screening_results")
|
|
125
|
+
),
|
|
126
|
+
"cv_results": self._dataframe_to_records(
|
|
127
|
+
result.get("cv_results")
|
|
128
|
+
),
|
|
129
|
+
"initial_ranking": self._dataframe_to_records(
|
|
130
|
+
result.get("initial_ranking")
|
|
131
|
+
),
|
|
132
|
+
"optimization_enabled": self._safe_value(
|
|
133
|
+
result.get("optimization_enabled")
|
|
134
|
+
),
|
|
135
|
+
"optimization_results": self._safe_value(
|
|
136
|
+
result.get("optimization_results")
|
|
137
|
+
),
|
|
138
|
+
"ranking": self._dataframe_to_records(
|
|
139
|
+
result.get("ranking")
|
|
140
|
+
),
|
|
141
|
+
"best_model": self._safe_value(
|
|
142
|
+
result.get("best_model")
|
|
143
|
+
),
|
|
144
|
+
"feature_selection": self._safe_value(
|
|
145
|
+
result.get("feature_selection")
|
|
146
|
+
),
|
|
147
|
+
"configuration": self._safe_value(
|
|
148
|
+
configuration
|
|
149
|
+
),
|
|
150
|
+
"reproducibility": self._safe_value(
|
|
151
|
+
reproducibility
|
|
152
|
+
),
|
|
153
|
+
"run_id": self._safe_value(
|
|
154
|
+
run_id
|
|
155
|
+
),
|
|
156
|
+
"run_summary": self._safe_value(
|
|
157
|
+
run_summary
|
|
158
|
+
),
|
|
159
|
+
"status": self._safe_value(
|
|
160
|
+
status
|
|
161
|
+
),
|
|
162
|
+
"error": self._safe_value(
|
|
163
|
+
result.get("error")
|
|
164
|
+
),
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
path = self._experiment_path(
|
|
168
|
+
experiment_id
|
|
169
|
+
)
|
|
170
|
+
|
|
171
|
+
self._write_json(
|
|
172
|
+
path,
|
|
173
|
+
experiment,
|
|
174
|
+
)
|
|
175
|
+
|
|
176
|
+
return experiment_id
|
|
177
|
+
|
|
178
|
+
def get(
|
|
179
|
+
self,
|
|
180
|
+
experiment_id: str,
|
|
181
|
+
) -> dict[str, Any]:
|
|
182
|
+
"""
|
|
183
|
+
Load one experiment by ID.
|
|
184
|
+
"""
|
|
185
|
+
|
|
186
|
+
self._validate_experiment_id(
|
|
187
|
+
experiment_id
|
|
188
|
+
)
|
|
189
|
+
|
|
190
|
+
path = self._experiment_path(
|
|
191
|
+
experiment_id
|
|
192
|
+
)
|
|
193
|
+
|
|
194
|
+
if not path.exists():
|
|
195
|
+
raise FileNotFoundError(
|
|
196
|
+
f"Experiment '{experiment_id}' "
|
|
197
|
+
"does not exist."
|
|
198
|
+
)
|
|
199
|
+
|
|
200
|
+
with path.open(
|
|
201
|
+
"r",
|
|
202
|
+
encoding="utf-8",
|
|
203
|
+
) as file:
|
|
204
|
+
return json.load(file)
|
|
205
|
+
|
|
206
|
+
def list_experiments(
|
|
207
|
+
self,
|
|
208
|
+
) -> list[dict[str, Any]]:
|
|
209
|
+
"""
|
|
210
|
+
Return a compact list of tracked experiments.
|
|
211
|
+
|
|
212
|
+
Experiments are returned newest first.
|
|
213
|
+
"""
|
|
214
|
+
|
|
215
|
+
experiments = []
|
|
216
|
+
|
|
217
|
+
for path in self.directory.glob(
|
|
218
|
+
"exp_*.json"
|
|
219
|
+
):
|
|
220
|
+
try:
|
|
221
|
+
with path.open(
|
|
222
|
+
"r",
|
|
223
|
+
encoding="utf-8",
|
|
224
|
+
) as file:
|
|
225
|
+
experiment = json.load(file)
|
|
226
|
+
|
|
227
|
+
reproducibility = experiment.get(
|
|
228
|
+
"reproducibility"
|
|
229
|
+
)
|
|
230
|
+
|
|
231
|
+
experiments.append(
|
|
232
|
+
{
|
|
233
|
+
"experiment_id": experiment.get(
|
|
234
|
+
"experiment_id"
|
|
235
|
+
),
|
|
236
|
+
"timestamp": experiment.get(
|
|
237
|
+
"timestamp"
|
|
238
|
+
),
|
|
239
|
+
"target": experiment.get(
|
|
240
|
+
"target"
|
|
241
|
+
),
|
|
242
|
+
"task_type": self._extract_task_type(
|
|
243
|
+
experiment
|
|
244
|
+
),
|
|
245
|
+
"best_model": experiment.get(
|
|
246
|
+
"best_model"
|
|
247
|
+
),
|
|
248
|
+
"models_evaluated": experiment.get(
|
|
249
|
+
"models_evaluated"
|
|
250
|
+
),
|
|
251
|
+
"optimization_enabled": experiment.get(
|
|
252
|
+
"optimization_enabled"
|
|
253
|
+
),
|
|
254
|
+
"run_id": experiment.get(
|
|
255
|
+
"run_id"
|
|
256
|
+
),
|
|
257
|
+
"status": experiment.get(
|
|
258
|
+
"status"
|
|
259
|
+
),
|
|
260
|
+
"reproducibility": reproducibility,
|
|
261
|
+
"reproducibility_available": (
|
|
262
|
+
reproducibility is not None
|
|
263
|
+
),
|
|
264
|
+
}
|
|
265
|
+
)
|
|
266
|
+
|
|
267
|
+
except (
|
|
268
|
+
OSError,
|
|
269
|
+
json.JSONDecodeError,
|
|
270
|
+
):
|
|
271
|
+
continue
|
|
272
|
+
|
|
273
|
+
experiments.sort(
|
|
274
|
+
key=lambda item: item.get(
|
|
275
|
+
"timestamp",
|
|
276
|
+
"",
|
|
277
|
+
),
|
|
278
|
+
reverse=True,
|
|
279
|
+
)
|
|
280
|
+
|
|
281
|
+
return experiments
|
|
282
|
+
|
|
283
|
+
def delete(
|
|
284
|
+
self,
|
|
285
|
+
experiment_id: str,
|
|
286
|
+
) -> None:
|
|
287
|
+
"""
|
|
288
|
+
Delete an experiment by ID.
|
|
289
|
+
"""
|
|
290
|
+
|
|
291
|
+
self._validate_experiment_id(
|
|
292
|
+
experiment_id
|
|
293
|
+
)
|
|
294
|
+
|
|
295
|
+
path = self._experiment_path(
|
|
296
|
+
experiment_id
|
|
297
|
+
)
|
|
298
|
+
|
|
299
|
+
if not path.exists():
|
|
300
|
+
raise FileNotFoundError(
|
|
301
|
+
f"Experiment '{experiment_id}' "
|
|
302
|
+
"does not exist."
|
|
303
|
+
)
|
|
304
|
+
|
|
305
|
+
path.unlink()
|
|
306
|
+
|
|
307
|
+
def clear(self) -> int:
|
|
308
|
+
"""
|
|
309
|
+
Delete all tracked experiments.
|
|
310
|
+
|
|
311
|
+
Returns the number of deleted experiments.
|
|
312
|
+
"""
|
|
313
|
+
|
|
314
|
+
deleted = 0
|
|
315
|
+
|
|
316
|
+
for path in self.directory.glob(
|
|
317
|
+
"exp_*.json"
|
|
318
|
+
):
|
|
319
|
+
try:
|
|
320
|
+
path.unlink()
|
|
321
|
+
deleted += 1
|
|
322
|
+
except OSError:
|
|
323
|
+
continue
|
|
324
|
+
|
|
325
|
+
return deleted
|
|
326
|
+
|
|
327
|
+
def count(self) -> int:
|
|
328
|
+
"""
|
|
329
|
+
Return the number of tracked experiments.
|
|
330
|
+
"""
|
|
331
|
+
|
|
332
|
+
return len(
|
|
333
|
+
list(
|
|
334
|
+
self.directory.glob(
|
|
335
|
+
"exp_*.json"
|
|
336
|
+
)
|
|
337
|
+
)
|
|
338
|
+
)
|
|
339
|
+
|
|
340
|
+
def _experiment_path(
|
|
341
|
+
self,
|
|
342
|
+
experiment_id: str,
|
|
343
|
+
) -> Path:
|
|
344
|
+
return (
|
|
345
|
+
self.directory
|
|
346
|
+
/ f"{experiment_id}.json"
|
|
347
|
+
)
|
|
348
|
+
|
|
349
|
+
@staticmethod
|
|
350
|
+
def _validate_experiment_id(
|
|
351
|
+
experiment_id: str,
|
|
352
|
+
) -> None:
|
|
353
|
+
if not isinstance(
|
|
354
|
+
experiment_id,
|
|
355
|
+
str,
|
|
356
|
+
):
|
|
357
|
+
raise TypeError(
|
|
358
|
+
"experiment_id must be a string."
|
|
359
|
+
)
|
|
360
|
+
|
|
361
|
+
if not experiment_id:
|
|
362
|
+
raise ValueError(
|
|
363
|
+
"experiment_id cannot be empty."
|
|
364
|
+
)
|
|
365
|
+
|
|
366
|
+
if Path(experiment_id).name != experiment_id:
|
|
367
|
+
raise ValueError(
|
|
368
|
+
"Invalid experiment_id."
|
|
369
|
+
)
|
|
370
|
+
|
|
371
|
+
if not experiment_id.startswith(
|
|
372
|
+
"exp_"
|
|
373
|
+
):
|
|
374
|
+
raise ValueError(
|
|
375
|
+
"Invalid experiment_id."
|
|
376
|
+
)
|
|
377
|
+
|
|
378
|
+
@staticmethod
|
|
379
|
+
def _write_json(
|
|
380
|
+
path: Path,
|
|
381
|
+
data: dict[str, Any],
|
|
382
|
+
) -> None:
|
|
383
|
+
with path.open(
|
|
384
|
+
"w",
|
|
385
|
+
encoding="utf-8",
|
|
386
|
+
) as file:
|
|
387
|
+
json.dump(
|
|
388
|
+
data,
|
|
389
|
+
file,
|
|
390
|
+
indent=2,
|
|
391
|
+
ensure_ascii=False,
|
|
392
|
+
)
|
|
393
|
+
|
|
394
|
+
@staticmethod
|
|
395
|
+
def _dataframe_to_records(
|
|
396
|
+
value: Any,
|
|
397
|
+
) -> Any:
|
|
398
|
+
if isinstance(
|
|
399
|
+
value,
|
|
400
|
+
pd.DataFrame,
|
|
401
|
+
):
|
|
402
|
+
return value.to_dict(
|
|
403
|
+
orient="records"
|
|
404
|
+
)
|
|
405
|
+
|
|
406
|
+
return ExperimentTracker._safe_value(
|
|
407
|
+
value
|
|
408
|
+
)
|
|
409
|
+
|
|
410
|
+
@staticmethod
|
|
411
|
+
def _extract_task_type(
|
|
412
|
+
experiment: dict[str, Any],
|
|
413
|
+
) -> str | None:
|
|
414
|
+
target = experiment.get(
|
|
415
|
+
"target"
|
|
416
|
+
)
|
|
417
|
+
|
|
418
|
+
if isinstance(
|
|
419
|
+
target,
|
|
420
|
+
dict,
|
|
421
|
+
):
|
|
422
|
+
return target.get(
|
|
423
|
+
"task_type"
|
|
424
|
+
)
|
|
425
|
+
|
|
426
|
+
return None
|
|
427
|
+
|
|
428
|
+
@staticmethod
|
|
429
|
+
def _safe_value(
|
|
430
|
+
value: Any,
|
|
431
|
+
) -> Any:
|
|
432
|
+
if value is None:
|
|
433
|
+
return None
|
|
434
|
+
|
|
435
|
+
if isinstance(
|
|
436
|
+
value,
|
|
437
|
+
(str, int, float, bool),
|
|
438
|
+
):
|
|
439
|
+
return value
|
|
440
|
+
|
|
441
|
+
if isinstance(
|
|
442
|
+
value,
|
|
443
|
+
Path,
|
|
444
|
+
):
|
|
445
|
+
return str(value)
|
|
446
|
+
|
|
447
|
+
if isinstance(
|
|
448
|
+
value,
|
|
449
|
+
pd.DataFrame,
|
|
450
|
+
):
|
|
451
|
+
return value.to_dict(
|
|
452
|
+
orient="records"
|
|
453
|
+
)
|
|
454
|
+
|
|
455
|
+
if isinstance(
|
|
456
|
+
value,
|
|
457
|
+
pd.Series,
|
|
458
|
+
):
|
|
459
|
+
return value.to_list()
|
|
460
|
+
|
|
461
|
+
if isinstance(
|
|
462
|
+
value,
|
|
463
|
+
dict,
|
|
464
|
+
):
|
|
465
|
+
return {
|
|
466
|
+
str(key): ExperimentTracker._safe_value(
|
|
467
|
+
item
|
|
468
|
+
)
|
|
469
|
+
for key, item in value.items()
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
if isinstance(
|
|
473
|
+
value,
|
|
474
|
+
(list, tuple, set),
|
|
475
|
+
):
|
|
476
|
+
return [
|
|
477
|
+
ExperimentTracker._safe_value(
|
|
478
|
+
item
|
|
479
|
+
)
|
|
480
|
+
for item in value
|
|
481
|
+
]
|
|
482
|
+
|
|
483
|
+
try:
|
|
484
|
+
json.dumps(value)
|
|
485
|
+
return value
|
|
486
|
+
except (
|
|
487
|
+
TypeError,
|
|
488
|
+
ValueError,
|
|
489
|
+
):
|
|
490
|
+
return str(value)
|