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,393 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import numpy as np
|
|
4
|
+
import pandas as pd
|
|
5
|
+
|
|
6
|
+
from sklearn.base import BaseEstimator, TransformerMixin
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class DateTimeFeatureExtractor(
|
|
10
|
+
BaseEstimator,
|
|
11
|
+
TransformerMixin,
|
|
12
|
+
):
|
|
13
|
+
"""
|
|
14
|
+
Extract useful features from datetime columns.
|
|
15
|
+
|
|
16
|
+
Datetime columns can be supplied explicitly. When they are not supplied,
|
|
17
|
+
the transformer detects native datetime columns and datetime-like
|
|
18
|
+
string/object columns during fit().
|
|
19
|
+
|
|
20
|
+
The original datetime columns are removed after their derived features
|
|
21
|
+
are created.
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
def __init__(self, datetime_columns=None):
|
|
25
|
+
self.datetime_columns = datetime_columns
|
|
26
|
+
|
|
27
|
+
def fit(self, X, y=None):
|
|
28
|
+
"""Fit the transformer and identify datetime columns."""
|
|
29
|
+
|
|
30
|
+
if not isinstance(X, pd.DataFrame):
|
|
31
|
+
raise TypeError(
|
|
32
|
+
"X must be a pandas DataFrame."
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
if self.datetime_columns is None:
|
|
36
|
+
self.datetime_columns_ = []
|
|
37
|
+
|
|
38
|
+
for column in X.columns:
|
|
39
|
+
series = X[column]
|
|
40
|
+
|
|
41
|
+
if pd.api.types.is_datetime64_any_dtype(series):
|
|
42
|
+
self.datetime_columns_.append(column)
|
|
43
|
+
continue
|
|
44
|
+
|
|
45
|
+
if (
|
|
46
|
+
pd.api.types.is_object_dtype(series)
|
|
47
|
+
or pd.api.types.is_string_dtype(series)
|
|
48
|
+
):
|
|
49
|
+
if self._looks_like_datetime(series):
|
|
50
|
+
self.datetime_columns_.append(column)
|
|
51
|
+
else:
|
|
52
|
+
missing = [
|
|
53
|
+
column
|
|
54
|
+
for column in self.datetime_columns
|
|
55
|
+
if column not in X.columns
|
|
56
|
+
]
|
|
57
|
+
|
|
58
|
+
if missing:
|
|
59
|
+
raise ValueError(
|
|
60
|
+
"The following datetime columns were not found: "
|
|
61
|
+
f"{missing}"
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
self.datetime_columns_ = list(
|
|
65
|
+
self.datetime_columns
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
return self
|
|
69
|
+
|
|
70
|
+
def transform(self, X):
|
|
71
|
+
"""Extract datetime features."""
|
|
72
|
+
|
|
73
|
+
if not isinstance(X, pd.DataFrame):
|
|
74
|
+
raise TypeError(
|
|
75
|
+
"X must be a pandas DataFrame."
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
if not hasattr(self, "datetime_columns_"):
|
|
79
|
+
raise RuntimeError(
|
|
80
|
+
"DateTimeFeatureExtractor must be fitted before transform."
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
result = X.copy()
|
|
84
|
+
|
|
85
|
+
for column in self.datetime_columns_:
|
|
86
|
+
if column not in result.columns:
|
|
87
|
+
continue
|
|
88
|
+
|
|
89
|
+
datetime_series = pd.to_datetime(
|
|
90
|
+
result[column],
|
|
91
|
+
errors="coerce",
|
|
92
|
+
format="mixed",
|
|
93
|
+
)
|
|
94
|
+
|
|
95
|
+
prefix = str(column)
|
|
96
|
+
|
|
97
|
+
result[f"{prefix}_year"] = (
|
|
98
|
+
datetime_series.dt.year
|
|
99
|
+
)
|
|
100
|
+
|
|
101
|
+
result[f"{prefix}_month"] = (
|
|
102
|
+
datetime_series.dt.month
|
|
103
|
+
)
|
|
104
|
+
|
|
105
|
+
result[f"{prefix}_day"] = (
|
|
106
|
+
datetime_series.dt.day
|
|
107
|
+
)
|
|
108
|
+
|
|
109
|
+
result[f"{prefix}_weekday"] = (
|
|
110
|
+
datetime_series.dt.weekday
|
|
111
|
+
)
|
|
112
|
+
|
|
113
|
+
result[f"{prefix}_quarter"] = (
|
|
114
|
+
datetime_series.dt.quarter
|
|
115
|
+
)
|
|
116
|
+
|
|
117
|
+
result[f"{prefix}_hour"] = (
|
|
118
|
+
datetime_series.dt.hour
|
|
119
|
+
)
|
|
120
|
+
|
|
121
|
+
result[f"{prefix}_is_weekend"] = (
|
|
122
|
+
datetime_series.dt.weekday >= 5
|
|
123
|
+
)
|
|
124
|
+
|
|
125
|
+
result = result.drop(
|
|
126
|
+
columns=[column]
|
|
127
|
+
)
|
|
128
|
+
|
|
129
|
+
return result
|
|
130
|
+
|
|
131
|
+
def _looks_like_datetime(
|
|
132
|
+
self,
|
|
133
|
+
series: pd.Series,
|
|
134
|
+
) -> bool:
|
|
135
|
+
"""Determine whether an object/string column is datetime-like."""
|
|
136
|
+
|
|
137
|
+
non_null = series.dropna()
|
|
138
|
+
|
|
139
|
+
if non_null.empty:
|
|
140
|
+
return False
|
|
141
|
+
|
|
142
|
+
sample = non_null.astype(str).head(100)
|
|
143
|
+
|
|
144
|
+
if sample.empty:
|
|
145
|
+
return False
|
|
146
|
+
|
|
147
|
+
try:
|
|
148
|
+
parsed = pd.to_datetime(
|
|
149
|
+
sample,
|
|
150
|
+
errors="coerce",
|
|
151
|
+
format="mixed",
|
|
152
|
+
)
|
|
153
|
+
except (TypeError, ValueError):
|
|
154
|
+
return False
|
|
155
|
+
|
|
156
|
+
return bool(
|
|
157
|
+
parsed.notna().mean() >= 0.90
|
|
158
|
+
)
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
class NumericalFeatureTransformer(
|
|
162
|
+
BaseEstimator,
|
|
163
|
+
TransformerMixin,
|
|
164
|
+
):
|
|
165
|
+
"""
|
|
166
|
+
Generate optional mathematical features for numerical columns.
|
|
167
|
+
|
|
168
|
+
Supported transformations:
|
|
169
|
+
|
|
170
|
+
- log1p
|
|
171
|
+
- square
|
|
172
|
+
- square root
|
|
173
|
+
|
|
174
|
+
Invalid mathematical operations are represented as NaN rather than
|
|
175
|
+
infinity or complex values.
|
|
176
|
+
"""
|
|
177
|
+
|
|
178
|
+
def __init__(
|
|
179
|
+
self,
|
|
180
|
+
numerical_columns=None,
|
|
181
|
+
add_log=False,
|
|
182
|
+
add_square=False,
|
|
183
|
+
add_sqrt=False,
|
|
184
|
+
):
|
|
185
|
+
self.numerical_columns = numerical_columns
|
|
186
|
+
self.add_log = add_log
|
|
187
|
+
self.add_square = add_square
|
|
188
|
+
self.add_sqrt = add_sqrt
|
|
189
|
+
|
|
190
|
+
def fit(self, X, y=None):
|
|
191
|
+
"""Fit the numerical feature transformer."""
|
|
192
|
+
|
|
193
|
+
if not isinstance(X, pd.DataFrame):
|
|
194
|
+
raise TypeError(
|
|
195
|
+
"X must be a pandas DataFrame."
|
|
196
|
+
)
|
|
197
|
+
|
|
198
|
+
if self.numerical_columns is None:
|
|
199
|
+
self.numerical_columns_ = (
|
|
200
|
+
X.select_dtypes(
|
|
201
|
+
include="number"
|
|
202
|
+
).columns.tolist()
|
|
203
|
+
)
|
|
204
|
+
else:
|
|
205
|
+
missing = [
|
|
206
|
+
column
|
|
207
|
+
for column in self.numerical_columns
|
|
208
|
+
if column not in X.columns
|
|
209
|
+
]
|
|
210
|
+
|
|
211
|
+
if missing:
|
|
212
|
+
raise ValueError(
|
|
213
|
+
"The following numerical columns were not found: "
|
|
214
|
+
f"{missing}"
|
|
215
|
+
)
|
|
216
|
+
|
|
217
|
+
self.numerical_columns_ = list(
|
|
218
|
+
self.numerical_columns
|
|
219
|
+
)
|
|
220
|
+
|
|
221
|
+
return self
|
|
222
|
+
|
|
223
|
+
def transform(self, X):
|
|
224
|
+
"""Generate numerical features."""
|
|
225
|
+
|
|
226
|
+
if not isinstance(X, pd.DataFrame):
|
|
227
|
+
raise TypeError(
|
|
228
|
+
"X must be a pandas DataFrame."
|
|
229
|
+
)
|
|
230
|
+
|
|
231
|
+
if not hasattr(self, "numerical_columns_"):
|
|
232
|
+
raise RuntimeError(
|
|
233
|
+
"NumericalFeatureTransformer must be fitted before transform."
|
|
234
|
+
)
|
|
235
|
+
|
|
236
|
+
result = X.copy()
|
|
237
|
+
|
|
238
|
+
for column in self.numerical_columns_:
|
|
239
|
+
if column not in result.columns:
|
|
240
|
+
continue
|
|
241
|
+
|
|
242
|
+
values = pd.to_numeric(
|
|
243
|
+
result[column],
|
|
244
|
+
errors="coerce",
|
|
245
|
+
)
|
|
246
|
+
|
|
247
|
+
safe_name = str(column)
|
|
248
|
+
|
|
249
|
+
if self.add_log:
|
|
250
|
+
log_values = pd.Series(
|
|
251
|
+
np.nan,
|
|
252
|
+
index=result.index,
|
|
253
|
+
dtype=float,
|
|
254
|
+
)
|
|
255
|
+
|
|
256
|
+
valid_log = values > -1
|
|
257
|
+
|
|
258
|
+
log_values.loc[valid_log] = np.log1p(
|
|
259
|
+
values.loc[valid_log]
|
|
260
|
+
)
|
|
261
|
+
|
|
262
|
+
result[
|
|
263
|
+
f"{safe_name}_log1p"
|
|
264
|
+
] = log_values
|
|
265
|
+
|
|
266
|
+
if self.add_square:
|
|
267
|
+
result[
|
|
268
|
+
f"{safe_name}_squared"
|
|
269
|
+
] = values ** 2
|
|
270
|
+
|
|
271
|
+
if self.add_sqrt:
|
|
272
|
+
sqrt_values = pd.Series(
|
|
273
|
+
np.nan,
|
|
274
|
+
index=result.index,
|
|
275
|
+
dtype=float,
|
|
276
|
+
)
|
|
277
|
+
|
|
278
|
+
valid_sqrt = values >= 0
|
|
279
|
+
|
|
280
|
+
sqrt_values.loc[valid_sqrt] = np.sqrt(
|
|
281
|
+
values.loc[valid_sqrt]
|
|
282
|
+
)
|
|
283
|
+
|
|
284
|
+
result[
|
|
285
|
+
f"{safe_name}_sqrt"
|
|
286
|
+
] = sqrt_values
|
|
287
|
+
|
|
288
|
+
return result
|
|
289
|
+
|
|
290
|
+
|
|
291
|
+
class FeatureEngineeringEngine:
|
|
292
|
+
"""
|
|
293
|
+
Coordinate ModelForge feature-engineering transformations.
|
|
294
|
+
|
|
295
|
+
The engine can independently transform datetime and numerical features,
|
|
296
|
+
or apply both transformations in sequence.
|
|
297
|
+
"""
|
|
298
|
+
|
|
299
|
+
def fit_transform(
|
|
300
|
+
self,
|
|
301
|
+
data: pd.DataFrame,
|
|
302
|
+
datetime_columns=None,
|
|
303
|
+
numerical_columns=None,
|
|
304
|
+
add_log=False,
|
|
305
|
+
add_square=False,
|
|
306
|
+
add_sqrt=False,
|
|
307
|
+
) -> pd.DataFrame:
|
|
308
|
+
"""Fit and apply the complete feature-engineering pipeline."""
|
|
309
|
+
|
|
310
|
+
return self.transform(
|
|
311
|
+
data=data,
|
|
312
|
+
datetime_columns=datetime_columns,
|
|
313
|
+
numerical_columns=numerical_columns,
|
|
314
|
+
add_log=add_log,
|
|
315
|
+
add_square=add_square,
|
|
316
|
+
add_sqrt=add_sqrt,
|
|
317
|
+
)
|
|
318
|
+
|
|
319
|
+
def transform_datetime(
|
|
320
|
+
self,
|
|
321
|
+
data: pd.DataFrame,
|
|
322
|
+
datetime_columns=None,
|
|
323
|
+
) -> pd.DataFrame:
|
|
324
|
+
"""Generate datetime features."""
|
|
325
|
+
|
|
326
|
+
extractor = DateTimeFeatureExtractor(
|
|
327
|
+
datetime_columns=datetime_columns
|
|
328
|
+
)
|
|
329
|
+
|
|
330
|
+
return extractor.fit_transform(data)
|
|
331
|
+
|
|
332
|
+
def transform_numeric(
|
|
333
|
+
self,
|
|
334
|
+
data: pd.DataFrame,
|
|
335
|
+
numerical_columns=None,
|
|
336
|
+
add_log=False,
|
|
337
|
+
add_square=False,
|
|
338
|
+
add_sqrt=False,
|
|
339
|
+
) -> pd.DataFrame:
|
|
340
|
+
"""Generate numerical features."""
|
|
341
|
+
|
|
342
|
+
transformer = NumericalFeatureTransformer(
|
|
343
|
+
numerical_columns=numerical_columns,
|
|
344
|
+
add_log=add_log,
|
|
345
|
+
add_square=add_square,
|
|
346
|
+
add_sqrt=add_sqrt,
|
|
347
|
+
)
|
|
348
|
+
|
|
349
|
+
return transformer.fit_transform(data)
|
|
350
|
+
|
|
351
|
+
def transform(
|
|
352
|
+
self,
|
|
353
|
+
data: pd.DataFrame,
|
|
354
|
+
datetime_columns=None,
|
|
355
|
+
numerical_columns=None,
|
|
356
|
+
add_log=False,
|
|
357
|
+
add_square=False,
|
|
358
|
+
add_sqrt=False,
|
|
359
|
+
) -> pd.DataFrame:
|
|
360
|
+
"""
|
|
361
|
+
Apply the complete feature-engineering pipeline.
|
|
362
|
+
|
|
363
|
+
Datetime transformations are applied first, followed by numerical
|
|
364
|
+
transformations.
|
|
365
|
+
"""
|
|
366
|
+
|
|
367
|
+
if not isinstance(data, pd.DataFrame):
|
|
368
|
+
raise TypeError(
|
|
369
|
+
"data must be a pandas DataFrame."
|
|
370
|
+
)
|
|
371
|
+
|
|
372
|
+
result = data.copy()
|
|
373
|
+
|
|
374
|
+
datetime_extractor = DateTimeFeatureExtractor(
|
|
375
|
+
datetime_columns=datetime_columns
|
|
376
|
+
)
|
|
377
|
+
|
|
378
|
+
result = datetime_extractor.fit_transform(
|
|
379
|
+
result
|
|
380
|
+
)
|
|
381
|
+
|
|
382
|
+
numerical_transformer = NumericalFeatureTransformer(
|
|
383
|
+
numerical_columns=numerical_columns,
|
|
384
|
+
add_log=add_log,
|
|
385
|
+
add_square=add_square,
|
|
386
|
+
add_sqrt=add_sqrt,
|
|
387
|
+
)
|
|
388
|
+
|
|
389
|
+
result = numerical_transformer.fit_transform(
|
|
390
|
+
result
|
|
391
|
+
)
|
|
392
|
+
|
|
393
|
+
return result
|