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.
@@ -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
+ )