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
modelforge/data_audit.py
ADDED
|
@@ -0,0 +1,392 @@
|
|
|
1
|
+
from typing import Any
|
|
2
|
+
|
|
3
|
+
import pandas as pd
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class DataQualityAuditor:
|
|
7
|
+
"""
|
|
8
|
+
Detect common data-quality and leakage risks before model training.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
def __init__(
|
|
12
|
+
self,
|
|
13
|
+
high_missing_threshold: float = 0.50,
|
|
14
|
+
high_cardinality_threshold: float = 0.90,
|
|
15
|
+
id_like_threshold: float = 0.95,
|
|
16
|
+
):
|
|
17
|
+
self.high_missing_threshold = high_missing_threshold
|
|
18
|
+
self.high_cardinality_threshold = (
|
|
19
|
+
high_cardinality_threshold
|
|
20
|
+
)
|
|
21
|
+
self.id_like_threshold = id_like_threshold
|
|
22
|
+
|
|
23
|
+
self._validate_configuration()
|
|
24
|
+
|
|
25
|
+
def audit(
|
|
26
|
+
self,
|
|
27
|
+
data: pd.DataFrame,
|
|
28
|
+
target: str | None = None,
|
|
29
|
+
column_intelligence: dict[str, Any] | None = None,
|
|
30
|
+
) -> dict[str, Any]:
|
|
31
|
+
"""
|
|
32
|
+
Audit a dataset for common quality and leakage risks.
|
|
33
|
+
"""
|
|
34
|
+
|
|
35
|
+
self._validate_data(data, target)
|
|
36
|
+
|
|
37
|
+
duplicate_rows = int(data.duplicated().sum())
|
|
38
|
+
|
|
39
|
+
constant_columns = [
|
|
40
|
+
column
|
|
41
|
+
for column in data.columns
|
|
42
|
+
if data[column].nunique(dropna=False) <= 1
|
|
43
|
+
]
|
|
44
|
+
|
|
45
|
+
high_missing_columns = [
|
|
46
|
+
column
|
|
47
|
+
for column in data.columns
|
|
48
|
+
if data[column].isna().mean()
|
|
49
|
+
>= self.high_missing_threshold
|
|
50
|
+
]
|
|
51
|
+
|
|
52
|
+
high_cardinality_columns = (
|
|
53
|
+
self._detect_high_cardinality(data, target)
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
id_like_columns = self._detect_id_like_columns(
|
|
57
|
+
data,
|
|
58
|
+
target,
|
|
59
|
+
column_intelligence,
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
target_leakage_columns = (
|
|
63
|
+
self._detect_target_leakage(data, target)
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
issues = []
|
|
67
|
+
|
|
68
|
+
if duplicate_rows > 0:
|
|
69
|
+
issues.append(
|
|
70
|
+
{
|
|
71
|
+
"type": "duplicate_rows",
|
|
72
|
+
"severity": "low",
|
|
73
|
+
"message": "Dataset contains duplicate rows.",
|
|
74
|
+
"count": duplicate_rows,
|
|
75
|
+
}
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
if constant_columns:
|
|
79
|
+
issues.append(
|
|
80
|
+
{
|
|
81
|
+
"type": "constant_columns",
|
|
82
|
+
"severity": "medium",
|
|
83
|
+
"message": "Columns contain only one unique value.",
|
|
84
|
+
"columns": constant_columns,
|
|
85
|
+
}
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
if high_missing_columns:
|
|
89
|
+
issues.append(
|
|
90
|
+
{
|
|
91
|
+
"type": "high_missing",
|
|
92
|
+
"severity": "high",
|
|
93
|
+
"message": (
|
|
94
|
+
"Columns contain a high proportion "
|
|
95
|
+
"of missing values."
|
|
96
|
+
),
|
|
97
|
+
"columns": high_missing_columns,
|
|
98
|
+
}
|
|
99
|
+
)
|
|
100
|
+
|
|
101
|
+
if high_cardinality_columns:
|
|
102
|
+
issues.append(
|
|
103
|
+
{
|
|
104
|
+
"type": "high_cardinality",
|
|
105
|
+
"severity": "medium",
|
|
106
|
+
"message": (
|
|
107
|
+
"Categorical or text columns have "
|
|
108
|
+
"unusually high cardinality."
|
|
109
|
+
),
|
|
110
|
+
"columns": high_cardinality_columns,
|
|
111
|
+
}
|
|
112
|
+
)
|
|
113
|
+
|
|
114
|
+
if id_like_columns:
|
|
115
|
+
issues.append(
|
|
116
|
+
{
|
|
117
|
+
"type": "id_like_columns",
|
|
118
|
+
"severity": "medium",
|
|
119
|
+
"message": (
|
|
120
|
+
"Columns appear to contain identifiers."
|
|
121
|
+
),
|
|
122
|
+
"columns": id_like_columns,
|
|
123
|
+
}
|
|
124
|
+
)
|
|
125
|
+
|
|
126
|
+
if target_leakage_columns:
|
|
127
|
+
issues.append(
|
|
128
|
+
{
|
|
129
|
+
"type": "target_leakage",
|
|
130
|
+
"severity": "critical",
|
|
131
|
+
"message": (
|
|
132
|
+
"Columns may contain direct target leakage."
|
|
133
|
+
),
|
|
134
|
+
"columns": target_leakage_columns,
|
|
135
|
+
}
|
|
136
|
+
)
|
|
137
|
+
|
|
138
|
+
critical_issue_types = {
|
|
139
|
+
"target_leakage",
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
has_critical_issues = any(
|
|
143
|
+
issue["type"] in critical_issue_types
|
|
144
|
+
for issue in issues
|
|
145
|
+
)
|
|
146
|
+
|
|
147
|
+
return {
|
|
148
|
+
"target": target,
|
|
149
|
+
"duplicate_rows": duplicate_rows,
|
|
150
|
+
"constant_columns": constant_columns,
|
|
151
|
+
"high_missing_columns": high_missing_columns,
|
|
152
|
+
"high_cardinality_columns": high_cardinality_columns,
|
|
153
|
+
"id_like_columns": id_like_columns,
|
|
154
|
+
"target_leakage_columns": target_leakage_columns,
|
|
155
|
+
"issues": issues,
|
|
156
|
+
"issue_count": len(issues),
|
|
157
|
+
"has_critical_issues": has_critical_issues,
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
def _detect_high_cardinality(
|
|
161
|
+
self,
|
|
162
|
+
data: pd.DataFrame,
|
|
163
|
+
target: str | None,
|
|
164
|
+
) -> list[str]:
|
|
165
|
+
"""
|
|
166
|
+
Detect high-cardinality categorical/text columns.
|
|
167
|
+
|
|
168
|
+
Continuous numerical features are intentionally excluded.
|
|
169
|
+
"""
|
|
170
|
+
|
|
171
|
+
columns = []
|
|
172
|
+
|
|
173
|
+
for column in data.columns:
|
|
174
|
+
if column == target:
|
|
175
|
+
continue
|
|
176
|
+
|
|
177
|
+
series = data[column]
|
|
178
|
+
|
|
179
|
+
is_text_or_categorical = (
|
|
180
|
+
pd.api.types.is_object_dtype(series)
|
|
181
|
+
or pd.api.types.is_string_dtype(series)
|
|
182
|
+
or isinstance(
|
|
183
|
+
series.dtype,
|
|
184
|
+
pd.CategoricalDtype,
|
|
185
|
+
)
|
|
186
|
+
)
|
|
187
|
+
|
|
188
|
+
if not is_text_or_categorical:
|
|
189
|
+
continue
|
|
190
|
+
|
|
191
|
+
non_missing = series.dropna()
|
|
192
|
+
|
|
193
|
+
if non_missing.empty:
|
|
194
|
+
continue
|
|
195
|
+
|
|
196
|
+
unique_count = non_missing.nunique(
|
|
197
|
+
dropna=True
|
|
198
|
+
)
|
|
199
|
+
|
|
200
|
+
unique_ratio = (
|
|
201
|
+
unique_count / len(non_missing)
|
|
202
|
+
)
|
|
203
|
+
|
|
204
|
+
# High-cardinality categorical/text data is detected
|
|
205
|
+
# when most values are unique. A minimum of 5 unique
|
|
206
|
+
# values prevents tiny categorical columns from being
|
|
207
|
+
# flagged unnecessarily.
|
|
208
|
+
if (
|
|
209
|
+
unique_ratio
|
|
210
|
+
>= self.high_cardinality_threshold
|
|
211
|
+
and unique_count >= 5
|
|
212
|
+
):
|
|
213
|
+
columns.append(column)
|
|
214
|
+
|
|
215
|
+
return columns
|
|
216
|
+
|
|
217
|
+
def _detect_id_like_columns(
|
|
218
|
+
self,
|
|
219
|
+
data: pd.DataFrame,
|
|
220
|
+
target: str | None,
|
|
221
|
+
column_intelligence: dict[str, Any] | None = None,
|
|
222
|
+
) -> list[str]:
|
|
223
|
+
"""
|
|
224
|
+
Detect columns that appear to be identifiers.
|
|
225
|
+
"""
|
|
226
|
+
|
|
227
|
+
detected = []
|
|
228
|
+
|
|
229
|
+
if column_intelligence:
|
|
230
|
+
for key in (
|
|
231
|
+
"id_like_columns",
|
|
232
|
+
"identifier_columns",
|
|
233
|
+
"id_columns",
|
|
234
|
+
):
|
|
235
|
+
values = column_intelligence.get(key)
|
|
236
|
+
|
|
237
|
+
if isinstance(values, list):
|
|
238
|
+
for column in values:
|
|
239
|
+
if (
|
|
240
|
+
column in data.columns
|
|
241
|
+
and column != target
|
|
242
|
+
and column not in detected
|
|
243
|
+
):
|
|
244
|
+
detected.append(column)
|
|
245
|
+
|
|
246
|
+
for column in data.columns:
|
|
247
|
+
if column == target or column in detected:
|
|
248
|
+
continue
|
|
249
|
+
|
|
250
|
+
series = data[column]
|
|
251
|
+
|
|
252
|
+
non_missing = series.dropna()
|
|
253
|
+
|
|
254
|
+
if non_missing.empty:
|
|
255
|
+
continue
|
|
256
|
+
|
|
257
|
+
unique_ratio = (
|
|
258
|
+
non_missing.nunique(dropna=True)
|
|
259
|
+
/ len(non_missing)
|
|
260
|
+
)
|
|
261
|
+
|
|
262
|
+
normalized_name = (
|
|
263
|
+
str(column)
|
|
264
|
+
.strip()
|
|
265
|
+
.lower()
|
|
266
|
+
.replace("-", "_")
|
|
267
|
+
.replace(" ", "_")
|
|
268
|
+
)
|
|
269
|
+
|
|
270
|
+
name_tokens = {
|
|
271
|
+
token
|
|
272
|
+
for token in normalized_name.split("_")
|
|
273
|
+
if token
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
identifier_tokens = {
|
|
277
|
+
"id",
|
|
278
|
+
"identifier",
|
|
279
|
+
"uuid",
|
|
280
|
+
"guid",
|
|
281
|
+
"customerid",
|
|
282
|
+
"userid",
|
|
283
|
+
"user_id",
|
|
284
|
+
"recordid",
|
|
285
|
+
"record_id",
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
name_suggests_id = bool(
|
|
289
|
+
name_tokens.intersection(
|
|
290
|
+
identifier_tokens
|
|
291
|
+
)
|
|
292
|
+
) or normalized_name.endswith("id")
|
|
293
|
+
|
|
294
|
+
if (
|
|
295
|
+
name_suggests_id
|
|
296
|
+
and unique_ratio
|
|
297
|
+
>= self.id_like_threshold
|
|
298
|
+
):
|
|
299
|
+
detected.append(column)
|
|
300
|
+
|
|
301
|
+
return detected
|
|
302
|
+
|
|
303
|
+
def _detect_target_leakage(
|
|
304
|
+
self,
|
|
305
|
+
data: pd.DataFrame,
|
|
306
|
+
target: str | None,
|
|
307
|
+
) -> list[str]:
|
|
308
|
+
"""
|
|
309
|
+
Detect simple forms of direct target leakage.
|
|
310
|
+
"""
|
|
311
|
+
|
|
312
|
+
if target is None or target not in data.columns:
|
|
313
|
+
return []
|
|
314
|
+
|
|
315
|
+
target_series = data[target]
|
|
316
|
+
|
|
317
|
+
leaked_columns = []
|
|
318
|
+
|
|
319
|
+
for column in data.columns:
|
|
320
|
+
if column == target:
|
|
321
|
+
continue
|
|
322
|
+
|
|
323
|
+
series = data[column]
|
|
324
|
+
|
|
325
|
+
if len(series) != len(target_series):
|
|
326
|
+
continue
|
|
327
|
+
|
|
328
|
+
try:
|
|
329
|
+
comparison = series.eq(
|
|
330
|
+
target_series
|
|
331
|
+
)
|
|
332
|
+
|
|
333
|
+
comparable = (
|
|
334
|
+
series.notna()
|
|
335
|
+
& target_series.notna()
|
|
336
|
+
)
|
|
337
|
+
|
|
338
|
+
if comparable.any():
|
|
339
|
+
match_ratio = (
|
|
340
|
+
comparison[comparable].mean()
|
|
341
|
+
)
|
|
342
|
+
|
|
343
|
+
if match_ratio == 1.0:
|
|
344
|
+
leaked_columns.append(column)
|
|
345
|
+
continue
|
|
346
|
+
|
|
347
|
+
except (TypeError, ValueError):
|
|
348
|
+
continue
|
|
349
|
+
|
|
350
|
+
return leaked_columns
|
|
351
|
+
|
|
352
|
+
@staticmethod
|
|
353
|
+
def _validate_data(
|
|
354
|
+
data: pd.DataFrame,
|
|
355
|
+
target: str | None,
|
|
356
|
+
) -> None:
|
|
357
|
+
if not isinstance(data, pd.DataFrame):
|
|
358
|
+
raise TypeError(
|
|
359
|
+
"data must be a pandas DataFrame."
|
|
360
|
+
)
|
|
361
|
+
|
|
362
|
+
if data.empty:
|
|
363
|
+
raise ValueError(
|
|
364
|
+
"Cannot audit an empty dataset."
|
|
365
|
+
)
|
|
366
|
+
|
|
367
|
+
if target is not None and target not in data.columns:
|
|
368
|
+
raise ValueError(
|
|
369
|
+
f"Target column '{target}' does not exist."
|
|
370
|
+
)
|
|
371
|
+
|
|
372
|
+
def _validate_configuration(self) -> None:
|
|
373
|
+
if not (
|
|
374
|
+
0 < self.high_missing_threshold <= 1
|
|
375
|
+
):
|
|
376
|
+
raise ValueError(
|
|
377
|
+
"high_missing_threshold must be between 0 and 1."
|
|
378
|
+
)
|
|
379
|
+
|
|
380
|
+
if not (
|
|
381
|
+
0 < self.high_cardinality_threshold <= 1
|
|
382
|
+
):
|
|
383
|
+
raise ValueError(
|
|
384
|
+
"high_cardinality_threshold must be between 0 and 1."
|
|
385
|
+
)
|
|
386
|
+
|
|
387
|
+
if not (
|
|
388
|
+
0 < self.id_like_threshold <= 1
|
|
389
|
+
):
|
|
390
|
+
raise ValueError(
|
|
391
|
+
"id_like_threshold must be between 0 and 1."
|
|
392
|
+
)
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
from pathlib import Path
|
|
2
|
+
|
|
3
|
+
import pandas as pd
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class DatasetLoader:
|
|
7
|
+
"""Load and validate datasets for ModelForge."""
|
|
8
|
+
|
|
9
|
+
SUPPORTED_FORMATS = {
|
|
10
|
+
".csv",
|
|
11
|
+
".xlsx",
|
|
12
|
+
".xls",
|
|
13
|
+
".parquet",
|
|
14
|
+
".json",
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
def load(self, file_path: str) -> pd.DataFrame:
|
|
18
|
+
"""
|
|
19
|
+
Load a dataset from a local file.
|
|
20
|
+
|
|
21
|
+
Parameters
|
|
22
|
+
----------
|
|
23
|
+
file_path : str
|
|
24
|
+
Path to the dataset.
|
|
25
|
+
|
|
26
|
+
Returns
|
|
27
|
+
-------
|
|
28
|
+
pd.DataFrame
|
|
29
|
+
Loaded dataset.
|
|
30
|
+
|
|
31
|
+
Raises
|
|
32
|
+
------
|
|
33
|
+
FileNotFoundError
|
|
34
|
+
If the dataset does not exist.
|
|
35
|
+
|
|
36
|
+
ValueError
|
|
37
|
+
If the path is not a file or the format is unsupported.
|
|
38
|
+
"""
|
|
39
|
+
|
|
40
|
+
path = Path(file_path)
|
|
41
|
+
|
|
42
|
+
if not path.exists():
|
|
43
|
+
raise FileNotFoundError(
|
|
44
|
+
f"Dataset not found: {path}"
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
if not path.is_file():
|
|
48
|
+
raise ValueError(
|
|
49
|
+
f"Path is not a file: {path}"
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
extension = path.suffix.lower()
|
|
53
|
+
|
|
54
|
+
if extension not in self.SUPPORTED_FORMATS:
|
|
55
|
+
supported = ", ".join(sorted(self.SUPPORTED_FORMATS))
|
|
56
|
+
|
|
57
|
+
raise ValueError(
|
|
58
|
+
f"Unsupported file format: {extension}. "
|
|
59
|
+
f"Supported formats: {supported}"
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
if extension == ".csv":
|
|
63
|
+
return pd.read_csv(path)
|
|
64
|
+
|
|
65
|
+
if extension in {".xlsx", ".xls"}:
|
|
66
|
+
return pd.read_excel(path)
|
|
67
|
+
|
|
68
|
+
if extension == ".parquet":
|
|
69
|
+
return pd.read_parquet(path)
|
|
70
|
+
|
|
71
|
+
if extension == ".json":
|
|
72
|
+
return pd.read_json(path)
|
|
73
|
+
|
|
74
|
+
raise ValueError(
|
|
75
|
+
f"Unable to load dataset: {path}"
|
|
76
|
+
)
|