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,192 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
from typing import Any
|
|
5
|
+
|
|
6
|
+
import pandas as pd
|
|
7
|
+
|
|
8
|
+
from modelforge.reproducibility import ReproducibilityManager
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class ReproducibilityIntegration:
|
|
12
|
+
"""
|
|
13
|
+
Integrates reproducibility metadata with ModelForge runs,
|
|
14
|
+
experiments, and saved model artifacts.
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
def __init__(self, random_state: int = 42) -> None:
|
|
18
|
+
self.manager = ReproducibilityManager(
|
|
19
|
+
random_state=random_state
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
def create_run_snapshot(
|
|
23
|
+
self,
|
|
24
|
+
data: pd.DataFrame,
|
|
25
|
+
configuration: dict[str, Any],
|
|
26
|
+
target: str | None = None,
|
|
27
|
+
task_type: str | None = None,
|
|
28
|
+
run_id: str | None = None,
|
|
29
|
+
extra_metadata: dict[str, Any] | None = None,
|
|
30
|
+
) -> dict[str, Any]:
|
|
31
|
+
"""
|
|
32
|
+
Create reproducibility information for a ModelForge run.
|
|
33
|
+
"""
|
|
34
|
+
|
|
35
|
+
metadata = dict(extra_metadata or {})
|
|
36
|
+
|
|
37
|
+
if run_id is not None:
|
|
38
|
+
metadata["run_id"] = run_id
|
|
39
|
+
|
|
40
|
+
return self.manager.create_snapshot(
|
|
41
|
+
data=data,
|
|
42
|
+
configuration=configuration,
|
|
43
|
+
target=target,
|
|
44
|
+
task_type=task_type,
|
|
45
|
+
extra_metadata=metadata,
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
def attach_to_result(
|
|
49
|
+
self,
|
|
50
|
+
result: dict[str, Any],
|
|
51
|
+
snapshot: dict[str, Any],
|
|
52
|
+
) -> dict[str, Any]:
|
|
53
|
+
"""
|
|
54
|
+
Attach reproducibility information to a ModelForge result.
|
|
55
|
+
"""
|
|
56
|
+
|
|
57
|
+
if not isinstance(result, dict):
|
|
58
|
+
raise TypeError("result must be a dictionary.")
|
|
59
|
+
|
|
60
|
+
if not isinstance(snapshot, dict):
|
|
61
|
+
raise TypeError("snapshot must be a dictionary.")
|
|
62
|
+
|
|
63
|
+
result["reproducibility"] = snapshot
|
|
64
|
+
|
|
65
|
+
return result
|
|
66
|
+
|
|
67
|
+
def save_snapshot(
|
|
68
|
+
self,
|
|
69
|
+
snapshot: dict[str, Any],
|
|
70
|
+
directory: str | Path,
|
|
71
|
+
run_id: str,
|
|
72
|
+
overwrite: bool = False,
|
|
73
|
+
) -> str:
|
|
74
|
+
"""
|
|
75
|
+
Save a run reproducibility snapshot inside an experiment directory.
|
|
76
|
+
"""
|
|
77
|
+
|
|
78
|
+
if not isinstance(run_id, str) or not run_id.strip():
|
|
79
|
+
raise ValueError(
|
|
80
|
+
"run_id must be a non-empty string."
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
directory = Path(directory)
|
|
84
|
+
|
|
85
|
+
path = (
|
|
86
|
+
directory
|
|
87
|
+
/ run_id
|
|
88
|
+
/ "reproducibility.json"
|
|
89
|
+
)
|
|
90
|
+
|
|
91
|
+
return self.manager.save_snapshot(
|
|
92
|
+
snapshot=snapshot,
|
|
93
|
+
path=path,
|
|
94
|
+
overwrite=overwrite,
|
|
95
|
+
)
|
|
96
|
+
|
|
97
|
+
def load_snapshot(
|
|
98
|
+
self,
|
|
99
|
+
directory: str | Path,
|
|
100
|
+
run_id: str,
|
|
101
|
+
) -> dict[str, Any]:
|
|
102
|
+
"""
|
|
103
|
+
Load a saved run reproducibility snapshot.
|
|
104
|
+
"""
|
|
105
|
+
|
|
106
|
+
if not isinstance(run_id, str) or not run_id.strip():
|
|
107
|
+
raise ValueError(
|
|
108
|
+
"run_id must be a non-empty string."
|
|
109
|
+
)
|
|
110
|
+
|
|
111
|
+
path = (
|
|
112
|
+
Path(directory)
|
|
113
|
+
/ run_id
|
|
114
|
+
/ "reproducibility.json"
|
|
115
|
+
)
|
|
116
|
+
|
|
117
|
+
return self.manager.load_snapshot(path)
|
|
118
|
+
|
|
119
|
+
def attach_artifact_integrity(
|
|
120
|
+
self,
|
|
121
|
+
metadata: dict[str, Any],
|
|
122
|
+
artifact_path: str | Path,
|
|
123
|
+
) -> dict[str, Any]:
|
|
124
|
+
"""
|
|
125
|
+
Attach a SHA-256 fingerprint for a saved model artifact.
|
|
126
|
+
"""
|
|
127
|
+
|
|
128
|
+
if not isinstance(metadata, dict):
|
|
129
|
+
raise TypeError(
|
|
130
|
+
"metadata must be a dictionary."
|
|
131
|
+
)
|
|
132
|
+
|
|
133
|
+
fingerprint = self.manager.artifact_fingerprint(
|
|
134
|
+
artifact_path
|
|
135
|
+
)
|
|
136
|
+
|
|
137
|
+
updated = dict(metadata)
|
|
138
|
+
|
|
139
|
+
updated["artifact"] = {
|
|
140
|
+
"path": str(artifact_path),
|
|
141
|
+
"sha256": fingerprint,
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
return updated
|
|
145
|
+
|
|
146
|
+
def verify_artifact(
|
|
147
|
+
self,
|
|
148
|
+
artifact_path: str | Path,
|
|
149
|
+
expected_fingerprint: str,
|
|
150
|
+
) -> bool:
|
|
151
|
+
"""
|
|
152
|
+
Verify the integrity of a saved artifact.
|
|
153
|
+
"""
|
|
154
|
+
|
|
155
|
+
return self.manager.verify_artifact(
|
|
156
|
+
artifact_path,
|
|
157
|
+
expected_fingerprint,
|
|
158
|
+
)
|
|
159
|
+
|
|
160
|
+
def verify_dataset(
|
|
161
|
+
self,
|
|
162
|
+
data: pd.DataFrame,
|
|
163
|
+
snapshot: dict[str, Any],
|
|
164
|
+
) -> bool:
|
|
165
|
+
"""
|
|
166
|
+
Verify that a dataset matches the snapshot fingerprint.
|
|
167
|
+
"""
|
|
168
|
+
|
|
169
|
+
if not isinstance(snapshot, dict):
|
|
170
|
+
raise TypeError(
|
|
171
|
+
"snapshot must be a dictionary."
|
|
172
|
+
)
|
|
173
|
+
|
|
174
|
+
dataset = snapshot.get("dataset")
|
|
175
|
+
|
|
176
|
+
if not isinstance(dataset, dict):
|
|
177
|
+
raise ValueError(
|
|
178
|
+
"Snapshot does not contain dataset metadata."
|
|
179
|
+
)
|
|
180
|
+
|
|
181
|
+
expected = dataset.get("fingerprint")
|
|
182
|
+
|
|
183
|
+
if not isinstance(expected, str):
|
|
184
|
+
raise ValueError(
|
|
185
|
+
"Snapshot does not contain a valid "
|
|
186
|
+
"dataset fingerprint."
|
|
187
|
+
)
|
|
188
|
+
|
|
189
|
+
return self.manager.verify_dataset(
|
|
190
|
+
data,
|
|
191
|
+
expected,
|
|
192
|
+
)
|
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import time
|
|
4
|
+
import uuid
|
|
5
|
+
from datetime import datetime, timezone
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class RunManager:
|
|
10
|
+
"""
|
|
11
|
+
Manage the lifecycle and metadata of a ModelForge run.
|
|
12
|
+
|
|
13
|
+
A run represents one execution of an AutoML workflow.
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
def __init__(self) -> None:
|
|
17
|
+
self.run_id: str | None = None
|
|
18
|
+
self.status: str = "not_started"
|
|
19
|
+
self.started_at: str | None = None
|
|
20
|
+
self.finished_at: str | None = None
|
|
21
|
+
self.duration_seconds: float | None = None
|
|
22
|
+
self.metadata: dict[str, Any] = {}
|
|
23
|
+
self._start_time: float | None = None
|
|
24
|
+
|
|
25
|
+
def start(
|
|
26
|
+
self,
|
|
27
|
+
metadata: dict[str, Any] | None = None,
|
|
28
|
+
) -> str:
|
|
29
|
+
"""
|
|
30
|
+
Start a new run.
|
|
31
|
+
|
|
32
|
+
Returns the generated run ID.
|
|
33
|
+
"""
|
|
34
|
+
|
|
35
|
+
if self.status == "running":
|
|
36
|
+
raise RuntimeError(
|
|
37
|
+
"A run is already in progress."
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
self.run_id = self._create_run_id()
|
|
41
|
+
self.status = "running"
|
|
42
|
+
self.started_at = (
|
|
43
|
+
datetime.now(timezone.utc).isoformat()
|
|
44
|
+
)
|
|
45
|
+
self.finished_at = None
|
|
46
|
+
self.duration_seconds = None
|
|
47
|
+
self._start_time = time.perf_counter()
|
|
48
|
+
|
|
49
|
+
self.metadata = dict(
|
|
50
|
+
metadata or {}
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
return self.run_id
|
|
54
|
+
|
|
55
|
+
def complete(
|
|
56
|
+
self,
|
|
57
|
+
metadata: dict[str, Any] | None = None,
|
|
58
|
+
) -> dict[str, Any]:
|
|
59
|
+
"""
|
|
60
|
+
Mark the current run as completed.
|
|
61
|
+
"""
|
|
62
|
+
|
|
63
|
+
self._require_running()
|
|
64
|
+
|
|
65
|
+
self._finish(
|
|
66
|
+
status="completed",
|
|
67
|
+
metadata=metadata,
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
return self.summary()
|
|
71
|
+
|
|
72
|
+
def fail(
|
|
73
|
+
self,
|
|
74
|
+
error: Exception | str,
|
|
75
|
+
metadata: dict[str, Any] | None = None,
|
|
76
|
+
) -> dict[str, Any]:
|
|
77
|
+
"""
|
|
78
|
+
Mark the current run as failed.
|
|
79
|
+
"""
|
|
80
|
+
|
|
81
|
+
self._require_running()
|
|
82
|
+
|
|
83
|
+
error_message = str(error)
|
|
84
|
+
|
|
85
|
+
failure_metadata = dict(
|
|
86
|
+
metadata or {}
|
|
87
|
+
)
|
|
88
|
+
|
|
89
|
+
failure_metadata["error"] = (
|
|
90
|
+
error_message
|
|
91
|
+
)
|
|
92
|
+
|
|
93
|
+
self._finish(
|
|
94
|
+
status="failed",
|
|
95
|
+
metadata=failure_metadata,
|
|
96
|
+
)
|
|
97
|
+
|
|
98
|
+
return self.summary()
|
|
99
|
+
|
|
100
|
+
def update(
|
|
101
|
+
self,
|
|
102
|
+
metadata: dict[str, Any],
|
|
103
|
+
) -> None:
|
|
104
|
+
"""
|
|
105
|
+
Add or update run metadata.
|
|
106
|
+
"""
|
|
107
|
+
|
|
108
|
+
if not isinstance(
|
|
109
|
+
metadata,
|
|
110
|
+
dict,
|
|
111
|
+
):
|
|
112
|
+
raise TypeError(
|
|
113
|
+
"metadata must be a dictionary."
|
|
114
|
+
)
|
|
115
|
+
|
|
116
|
+
if self.status != "running":
|
|
117
|
+
raise RuntimeError(
|
|
118
|
+
"Run metadata can only be "
|
|
119
|
+
"updated while a run is running."
|
|
120
|
+
)
|
|
121
|
+
|
|
122
|
+
self.metadata.update(
|
|
123
|
+
metadata
|
|
124
|
+
)
|
|
125
|
+
|
|
126
|
+
def summary(self) -> dict[str, Any]:
|
|
127
|
+
"""
|
|
128
|
+
Return the current run summary.
|
|
129
|
+
"""
|
|
130
|
+
|
|
131
|
+
return {
|
|
132
|
+
"run_id": self.run_id,
|
|
133
|
+
"status": self.status,
|
|
134
|
+
"started_at": self.started_at,
|
|
135
|
+
"finished_at": self.finished_at,
|
|
136
|
+
"duration_seconds": (
|
|
137
|
+
self.duration_seconds
|
|
138
|
+
),
|
|
139
|
+
"metadata": dict(
|
|
140
|
+
self.metadata
|
|
141
|
+
),
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
def is_running(self) -> bool:
|
|
145
|
+
"""
|
|
146
|
+
Return whether a run is currently active.
|
|
147
|
+
"""
|
|
148
|
+
|
|
149
|
+
return self.status == "running"
|
|
150
|
+
|
|
151
|
+
def reset(self) -> None:
|
|
152
|
+
"""
|
|
153
|
+
Reset the manager to its initial state.
|
|
154
|
+
"""
|
|
155
|
+
|
|
156
|
+
self.run_id = None
|
|
157
|
+
self.status = "not_started"
|
|
158
|
+
self.started_at = None
|
|
159
|
+
self.finished_at = None
|
|
160
|
+
self.duration_seconds = None
|
|
161
|
+
self.metadata = {}
|
|
162
|
+
self._start_time = None
|
|
163
|
+
|
|
164
|
+
def _finish(
|
|
165
|
+
self,
|
|
166
|
+
status: str,
|
|
167
|
+
metadata: dict[str, Any] | None,
|
|
168
|
+
) -> None:
|
|
169
|
+
self.status = status
|
|
170
|
+
|
|
171
|
+
self.finished_at = (
|
|
172
|
+
datetime.now(timezone.utc).isoformat()
|
|
173
|
+
)
|
|
174
|
+
|
|
175
|
+
if self._start_time is not None:
|
|
176
|
+
self.duration_seconds = (
|
|
177
|
+
time.perf_counter()
|
|
178
|
+
- self._start_time
|
|
179
|
+
)
|
|
180
|
+
|
|
181
|
+
if metadata:
|
|
182
|
+
self.metadata.update(
|
|
183
|
+
metadata
|
|
184
|
+
)
|
|
185
|
+
|
|
186
|
+
def _require_running(self) -> None:
|
|
187
|
+
if self.status != "running":
|
|
188
|
+
raise RuntimeError(
|
|
189
|
+
"No active run is currently running."
|
|
190
|
+
)
|
|
191
|
+
|
|
192
|
+
@staticmethod
|
|
193
|
+
def _create_run_id() -> str:
|
|
194
|
+
timestamp = datetime.now(
|
|
195
|
+
timezone.utc
|
|
196
|
+
).strftime("%Y%m%d%H%M%S")
|
|
197
|
+
|
|
198
|
+
short_uuid = uuid.uuid4().hex[:8]
|
|
199
|
+
|
|
200
|
+
return f"run_{timestamp}_{short_uuid}"
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import pandas as pd
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class TargetSelector:
|
|
5
|
+
"""Validate and analyze the target column selected by the user."""
|
|
6
|
+
|
|
7
|
+
def select(
|
|
8
|
+
self,
|
|
9
|
+
data: pd.DataFrame,
|
|
10
|
+
target: str,
|
|
11
|
+
task_type: str | None = None,
|
|
12
|
+
) -> dict:
|
|
13
|
+
"""
|
|
14
|
+
Validate the selected target and determine the ML task type.
|
|
15
|
+
|
|
16
|
+
Parameters
|
|
17
|
+
----------
|
|
18
|
+
data : pd.DataFrame
|
|
19
|
+
Input dataset.
|
|
20
|
+
|
|
21
|
+
target : str
|
|
22
|
+
Name of the target column selected by the user.
|
|
23
|
+
|
|
24
|
+
task_type : str | None
|
|
25
|
+
Optional explicit task type:
|
|
26
|
+
'regression' or 'classification'.
|
|
27
|
+
|
|
28
|
+
Returns
|
|
29
|
+
-------
|
|
30
|
+
dict
|
|
31
|
+
Target information and task type.
|
|
32
|
+
"""
|
|
33
|
+
|
|
34
|
+
if not isinstance(data, pd.DataFrame):
|
|
35
|
+
raise TypeError("data must be a pandas DataFrame.")
|
|
36
|
+
|
|
37
|
+
if data.empty:
|
|
38
|
+
raise ValueError("Dataset is empty.")
|
|
39
|
+
|
|
40
|
+
if not target:
|
|
41
|
+
raise ValueError("Target column must be provided.")
|
|
42
|
+
|
|
43
|
+
if target not in data.columns:
|
|
44
|
+
raise ValueError(
|
|
45
|
+
f"Target column '{target}' does not exist in the dataset."
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
target_series = data[target]
|
|
49
|
+
|
|
50
|
+
if target_series.isna().all():
|
|
51
|
+
raise ValueError(
|
|
52
|
+
f"Target column '{target}' contains only missing values."
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
if task_type is not None:
|
|
56
|
+
task_type = task_type.lower()
|
|
57
|
+
|
|
58
|
+
if task_type not in {"regression", "classification"}:
|
|
59
|
+
raise ValueError(
|
|
60
|
+
"task_type must be 'regression' or 'classification'."
|
|
61
|
+
)
|
|
62
|
+
else:
|
|
63
|
+
task_type = self._detect_task_type(target_series)
|
|
64
|
+
|
|
65
|
+
return {
|
|
66
|
+
"target": target,
|
|
67
|
+
"task_type": task_type,
|
|
68
|
+
"dtype": str(target_series.dtype),
|
|
69
|
+
"unique_values": int(target_series.nunique(dropna=True)),
|
|
70
|
+
"missing_values": int(target_series.isna().sum()),
|
|
71
|
+
"rows": int(len(target_series)),
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
@staticmethod
|
|
75
|
+
def _detect_task_type(target: pd.Series) -> str:
|
|
76
|
+
"""Detect the likely ML task from the target column."""
|
|
77
|
+
|
|
78
|
+
if pd.api.types.is_bool_dtype(target):
|
|
79
|
+
return "classification"
|
|
80
|
+
|
|
81
|
+
if pd.api.types.is_object_dtype(target):
|
|
82
|
+
return "classification"
|
|
83
|
+
|
|
84
|
+
if isinstance(target.dtype, pd.CategoricalDtype):
|
|
85
|
+
return "classification"
|
|
86
|
+
|
|
87
|
+
if pd.api.types.is_numeric_dtype(target):
|
|
88
|
+
unique_values = target.nunique(dropna=True)
|
|
89
|
+
total_values = target.notna().sum()
|
|
90
|
+
|
|
91
|
+
if total_values == 0:
|
|
92
|
+
raise ValueError("Target contains no valid values.")
|
|
93
|
+
|
|
94
|
+
unique_ratio = unique_values / total_values
|
|
95
|
+
|
|
96
|
+
# A small number of repeated integer values is likely
|
|
97
|
+
# classification. High-cardinality numeric targets are
|
|
98
|
+
# treated as regression.
|
|
99
|
+
if (
|
|
100
|
+
pd.api.types.is_integer_dtype(target)
|
|
101
|
+
and unique_values <= 10
|
|
102
|
+
and unique_ratio <= 0.20
|
|
103
|
+
):
|
|
104
|
+
return "classification"
|
|
105
|
+
|
|
106
|
+
return "regression"
|
|
107
|
+
|
|
108
|
+
return "classification"
|