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,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