mlpipe-cli 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.
Files changed (46) hide show
  1. mlpipe/__init__.py +43 -0
  2. mlpipe/__main__.py +6 -0
  3. mlpipe/artifacts/__init__.py +23 -0
  4. mlpipe/artifacts/manager.py +246 -0
  5. mlpipe/artifacts/serialization.py +67 -0
  6. mlpipe/cli/__init__.py +5 -0
  7. mlpipe/cli/main.py +667 -0
  8. mlpipe/core/__init__.py +35 -0
  9. mlpipe/core/config.py +76 -0
  10. mlpipe/core/exceptions.py +65 -0
  11. mlpipe/core/pipeline.py +435 -0
  12. mlpipe/core/result.py +50 -0
  13. mlpipe/data/__init__.py +20 -0
  14. mlpipe/data/ingestion.py +138 -0
  15. mlpipe/data/profiling.py +227 -0
  16. mlpipe/data/splitting.py +130 -0
  17. mlpipe/data/validation.py +248 -0
  18. mlpipe/evaluation/__init__.py +11 -0
  19. mlpipe/evaluation/evaluator.py +146 -0
  20. mlpipe/evaluation/metrics.py +53 -0
  21. mlpipe/explainability/__init__.py +5 -0
  22. mlpipe/explainability/importance.py +65 -0
  23. mlpipe/models/__init__.py +13 -0
  24. mlpipe/models/classification.py +156 -0
  25. mlpipe/models/registry.py +32 -0
  26. mlpipe/models/regression.py +126 -0
  27. mlpipe/models/selection.py +24 -0
  28. mlpipe/preprocessing/__init__.py +21 -0
  29. mlpipe/preprocessing/builder.py +163 -0
  30. mlpipe/preprocessing/categorical.py +17 -0
  31. mlpipe/preprocessing/datetime.py +55 -0
  32. mlpipe/preprocessing/numeric.py +17 -0
  33. mlpipe/tuning/__init__.py +10 -0
  34. mlpipe/tuning/search.py +140 -0
  35. mlpipe/tuning/spaces.py +11 -0
  36. mlpipe/utils/__init__.py +13 -0
  37. mlpipe/utils/hashing.py +15 -0
  38. mlpipe/utils/logging.py +37 -0
  39. mlpipe/utils/timing.py +33 -0
  40. mlpipe/version.py +3 -0
  41. mlpipe_cli-0.1.0.dist-info/METADATA +264 -0
  42. mlpipe_cli-0.1.0.dist-info/RECORD +46 -0
  43. mlpipe_cli-0.1.0.dist-info/WHEEL +5 -0
  44. mlpipe_cli-0.1.0.dist-info/entry_points.txt +2 -0
  45. mlpipe_cli-0.1.0.dist-info/licenses/LICENSE +21 -0
  46. mlpipe_cli-0.1.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,156 @@
1
+ """Classification candidate models and parameter spaces."""
2
+
3
+ from typing import List
4
+
5
+ from scipy.stats import randint, uniform
6
+ from sklearn.ensemble import (
7
+ HistGradientBoostingClassifier,
8
+ RandomForestClassifier,
9
+ )
10
+ from sklearn.linear_model import LogisticRegression
11
+ from sklearn.neighbors import KNeighborsClassifier
12
+ from sklearn.tree import DecisionTreeClassifier
13
+
14
+ from mlpipe.models.registry import ModelCandidate
15
+
16
+
17
+ def get_classification_candidates() -> List[ModelCandidate]:
18
+ """Return all registered classification candidates."""
19
+
20
+ return [
21
+ # 1. Logistic Regression
22
+ ModelCandidate(
23
+ name="Logistic Regression",
24
+ estimator_factory=lambda random_seed=42, **kwargs: LogisticRegression(
25
+ random_state=random_seed, max_iter=1000, **kwargs
26
+ ),
27
+ task="classification",
28
+ supports_feature_importance=True,
29
+ importance_type="linear",
30
+ requires_scaling=True,
31
+ param_grid_fast={
32
+ "C": [0.1, 1.0, 10.0],
33
+ },
34
+ param_grid_balanced={
35
+ "C": uniform(0.01, 10.0),
36
+ "penalty": ["l2"],
37
+ "solver": ["lbfgs"],
38
+ },
39
+ param_grid_thorough={
40
+ "C": uniform(0.001, 50.0),
41
+ "penalty": ["l2"],
42
+ "solver": ["lbfgs", "saga"],
43
+ },
44
+ ),
45
+
46
+ # 2. Random Forest
47
+ ModelCandidate(
48
+ name="Random Forest",
49
+ estimator_factory=lambda random_seed=42, **kwargs: RandomForestClassifier(
50
+ random_state=random_seed, n_jobs=-1, **kwargs
51
+ ),
52
+ task="classification",
53
+ supports_feature_importance=True,
54
+ importance_type="tree",
55
+ requires_scaling=False,
56
+ param_grid_fast={
57
+ "n_estimators": [50, 100],
58
+ "max_depth": [5, 10, None],
59
+ },
60
+ param_grid_balanced={
61
+ "n_estimators": randint(50, 200),
62
+ "max_depth": [5, 10, 20, None],
63
+ "min_samples_split": randint(2, 10),
64
+ "min_samples_leaf": randint(1, 6),
65
+ },
66
+ param_grid_thorough={
67
+ "n_estimators": randint(50, 300),
68
+ "max_depth": [5, 10, 20, 30, None],
69
+ "min_samples_split": randint(2, 20),
70
+ "min_samples_leaf": randint(1, 10),
71
+ "max_features": ["sqrt", "log2", None],
72
+ },
73
+ ),
74
+
75
+ # 3. HistGradientBoosting
76
+ ModelCandidate(
77
+ name="HistGradientBoosting",
78
+ estimator_factory=lambda random_seed=42, **kwargs: HistGradientBoostingClassifier(
79
+ random_state=random_seed, **kwargs
80
+ ),
81
+ task="classification",
82
+ supports_feature_importance=False, # sklearn's HistGradientBoosting doesn't expose feature_importances_ natively
83
+ importance_type="none",
84
+ requires_scaling=False,
85
+ param_grid_fast={
86
+ "max_iter": [50, 100],
87
+ "learning_rate": [0.05, 0.1],
88
+ },
89
+ param_grid_balanced={
90
+ "max_iter": randint(50, 200),
91
+ "learning_rate": uniform(0.01, 0.25),
92
+ "max_depth": [3, 5, 10, None],
93
+ "min_samples_leaf": randint(10, 40),
94
+ },
95
+ param_grid_thorough={
96
+ "max_iter": randint(50, 300),
97
+ "learning_rate": uniform(0.005, 0.3),
98
+ "max_depth": [3, 5, 10, 20, None],
99
+ "min_samples_leaf": randint(5, 50),
100
+ "l2_regularization": uniform(0.0, 2.0),
101
+ },
102
+ ),
103
+
104
+ # 4. Decision Tree
105
+ ModelCandidate(
106
+ name="Decision Tree",
107
+ estimator_factory=lambda random_seed=42, **kwargs: DecisionTreeClassifier(
108
+ random_state=random_seed, **kwargs
109
+ ),
110
+ task="classification",
111
+ supports_feature_importance=True,
112
+ importance_type="tree",
113
+ requires_scaling=False,
114
+ param_grid_fast={
115
+ "max_depth": [3, 5, 10, None],
116
+ },
117
+ param_grid_balanced={
118
+ "max_depth": [3, 5, 10, 20, None],
119
+ "min_samples_split": randint(2, 15),
120
+ "min_samples_leaf": randint(1, 8),
121
+ },
122
+ param_grid_thorough={
123
+ "max_depth": [3, 5, 10, 20, 30, None],
124
+ "min_samples_split": randint(2, 30),
125
+ "min_samples_leaf": randint(1, 15),
126
+ "criterion": ["gini", "entropy", "log_loss"],
127
+ },
128
+ ),
129
+
130
+ # 5. K-Nearest Neighbors
131
+ ModelCandidate(
132
+ name="K-Nearest Neighbors",
133
+ estimator_factory=lambda random_seed=42, **kwargs: KNeighborsClassifier(
134
+ **kwargs
135
+ ),
136
+ task="classification",
137
+ supports_feature_importance=False,
138
+ importance_type="none",
139
+ requires_scaling=True,
140
+ param_grid_fast={
141
+ "n_neighbors": [3, 5, 7],
142
+ },
143
+ param_grid_balanced={
144
+ "n_neighbors": randint(3, 15),
145
+ "weights": ["uniform"],
146
+ "metric": ["euclidean", "manhattan"],
147
+ },
148
+ param_grid_thorough={
149
+ "n_neighbors": randint(2, 25),
150
+ "weights": ["uniform"],
151
+ "metric": ["euclidean", "manhattan", "minkowski"],
152
+ "p": [1, 2],
153
+ },
154
+ ),
155
+
156
+ ]
@@ -0,0 +1,32 @@
1
+ """Model candidate representations and specifications."""
2
+
3
+ from dataclasses import dataclass, field
4
+ from typing import Any, Callable, Dict, Optional, Type
5
+
6
+
7
+ @dataclass
8
+ class ModelCandidate:
9
+ """Specification of an ML estimator candidate."""
10
+
11
+ name: str
12
+ estimator_factory: Callable[..., Any]
13
+ task: str # "classification" or "regression"
14
+ supports_feature_importance: bool = True
15
+ importance_type: str = "none" # "tree", "linear", or "none"
16
+ requires_scaling: bool = False
17
+ param_grid_fast: Dict[str, Any] = field(default_factory=dict)
18
+ param_grid_balanced: Dict[str, Any] = field(default_factory=dict)
19
+ param_grid_thorough: Dict[str, Any] = field(default_factory=dict)
20
+
21
+ def create_estimator(self, random_seed: int = 42, **kwargs) -> Any:
22
+ """Instantiate estimator with seed if supported."""
23
+ return self.estimator_factory(random_seed=random_seed, **kwargs)
24
+
25
+ def get_search_space(self, mode: str) -> Dict[str, Any]:
26
+ """Return the hyperparameter search space for the given training mode."""
27
+ mode = mode.lower()
28
+ if mode == "fast":
29
+ return self.param_grid_fast
30
+ elif mode == "thorough":
31
+ return self.param_grid_thorough
32
+ return self.param_grid_balanced
@@ -0,0 +1,126 @@
1
+ """Regression candidate models and parameter spaces."""
2
+
3
+ from typing import List
4
+
5
+ from scipy.stats import randint, uniform
6
+ from sklearn.ensemble import (
7
+ HistGradientBoostingRegressor,
8
+ RandomForestRegressor,
9
+ )
10
+ from sklearn.linear_model import Ridge
11
+ from sklearn.tree import DecisionTreeRegressor
12
+
13
+ from mlpipe.models.registry import ModelCandidate
14
+
15
+
16
+ def get_regression_candidates() -> List[ModelCandidate]:
17
+ """Return all registered regression candidates."""
18
+
19
+ return [
20
+ # 1. Ridge Regression
21
+ ModelCandidate(
22
+ name="Ridge",
23
+ estimator_factory=lambda random_seed=42, **kwargs: Ridge(
24
+ random_state=random_seed, **kwargs
25
+ ),
26
+ task="regression",
27
+ supports_feature_importance=True,
28
+ importance_type="linear",
29
+ requires_scaling=True,
30
+ param_grid_fast={
31
+ "alpha": [0.1, 1.0, 10.0],
32
+ },
33
+ param_grid_balanced={
34
+ "alpha": uniform(0.01, 50.0),
35
+ "solver": ["auto", "svd", "cholesky", "lsqr"],
36
+ },
37
+ param_grid_thorough={
38
+ "alpha": uniform(0.001, 200.0),
39
+ "solver": ["auto", "svd", "cholesky", "lsqr", "sag"],
40
+ },
41
+ ),
42
+
43
+ # 2. Random Forest Regressor
44
+ ModelCandidate(
45
+ name="Random Forest Regressor",
46
+ estimator_factory=lambda random_seed=42, **kwargs: RandomForestRegressor(
47
+ random_state=random_seed, n_jobs=-1, **kwargs
48
+ ),
49
+ task="regression",
50
+ supports_feature_importance=True,
51
+ importance_type="tree",
52
+ requires_scaling=False,
53
+ param_grid_fast={
54
+ "n_estimators": [50, 100],
55
+ "max_depth": [5, 10, None],
56
+ },
57
+ param_grid_balanced={
58
+ "n_estimators": randint(50, 200),
59
+ "max_depth": [5, 10, 20, None],
60
+ "min_samples_split": randint(2, 10),
61
+ "min_samples_leaf": randint(1, 6),
62
+ },
63
+ param_grid_thorough={
64
+ "n_estimators": randint(50, 300),
65
+ "max_depth": [5, 10, 20, 30, None],
66
+ "min_samples_split": randint(2, 20),
67
+ "min_samples_leaf": randint(1, 10),
68
+ "max_features": ["sqrt", "log2", 1.0],
69
+ },
70
+ ),
71
+
72
+ # 3. HistGradientBoosting Regressor
73
+ ModelCandidate(
74
+ name="HistGradientBoosting Regressor",
75
+ estimator_factory=lambda random_seed=42, **kwargs: HistGradientBoostingRegressor(
76
+ random_state=random_seed, **kwargs
77
+ ),
78
+ task="regression",
79
+ supports_feature_importance=False,
80
+ importance_type="none",
81
+ requires_scaling=False,
82
+ param_grid_fast={
83
+ "max_iter": [50, 100],
84
+ "learning_rate": [0.05, 0.1],
85
+ },
86
+ param_grid_balanced={
87
+ "max_iter": randint(50, 200),
88
+ "learning_rate": uniform(0.01, 0.25),
89
+ "max_depth": [3, 5, 10, None],
90
+ "min_samples_leaf": randint(10, 40),
91
+ },
92
+ param_grid_thorough={
93
+ "max_iter": randint(50, 300),
94
+ "learning_rate": uniform(0.005, 0.3),
95
+ "max_depth": [3, 5, 10, 20, None],
96
+ "min_samples_leaf": randint(5, 50),
97
+ "l2_regularization": uniform(0.0, 2.0),
98
+ },
99
+ ),
100
+
101
+ # 4. Decision Tree Regressor
102
+ ModelCandidate(
103
+ name="Decision Tree Regressor",
104
+ estimator_factory=lambda random_seed=42, **kwargs: DecisionTreeRegressor(
105
+ random_state=random_seed, **kwargs
106
+ ),
107
+ task="regression",
108
+ supports_feature_importance=True,
109
+ importance_type="tree",
110
+ requires_scaling=False,
111
+ param_grid_fast={
112
+ "max_depth": [3, 5, 10, None],
113
+ },
114
+ param_grid_balanced={
115
+ "max_depth": [3, 5, 10, 20, None],
116
+ "min_samples_split": randint(2, 15),
117
+ "min_samples_leaf": randint(1, 8),
118
+ },
119
+ param_grid_thorough={
120
+ "max_depth": [3, 5, 10, 20, 30, None],
121
+ "min_samples_split": randint(2, 30),
122
+ "min_samples_leaf": randint(1, 15),
123
+ "criterion": ["squared_error", "friedman_mse", "absolute_error"],
124
+ },
125
+ ),
126
+ ]
@@ -0,0 +1,24 @@
1
+ """Model selection and retrieval helpers."""
2
+
3
+ from typing import List
4
+
5
+ from mlpipe.models.classification import get_classification_candidates
6
+ from mlpipe.models.regression import get_regression_candidates
7
+ from mlpipe.models.registry import ModelCandidate
8
+
9
+
10
+ def get_candidates_for_task(task_type: str, mode: str = "balanced") -> List[ModelCandidate]:
11
+ """
12
+ Retrieve appropriate candidate models for a given task and training mode.
13
+ """
14
+ if task_type == "classification":
15
+ candidates = get_classification_candidates()
16
+ elif task_type == "regression":
17
+ candidates = get_regression_candidates()
18
+ else:
19
+ raise ValueError(f"Unknown task type '{task_type}'")
20
+
21
+ if mode == "fast":
22
+ # For fast mode, we still evaluate all models or the fastest subset
23
+ return candidates
24
+ return candidates
@@ -0,0 +1,21 @@
1
+ """Feature preprocessing pipelines and builders for MLPipe."""
2
+
3
+ from mlpipe.preprocessing.builder import (
4
+ ColumnAssignments,
5
+ build_preprocessor,
6
+ classify_columns,
7
+ get_transformed_feature_names,
8
+ )
9
+ from mlpipe.preprocessing.categorical import build_categorical_transformer
10
+ from mlpipe.preprocessing.datetime import DatetimeFeatureExtractor
11
+ from mlpipe.preprocessing.numeric import build_numeric_transformer
12
+
13
+ __all__ = [
14
+ "ColumnAssignments",
15
+ "build_preprocessor",
16
+ "classify_columns",
17
+ "get_transformed_feature_names",
18
+ "build_categorical_transformer",
19
+ "DatetimeFeatureExtractor",
20
+ "build_numeric_transformer",
21
+ ]
@@ -0,0 +1,163 @@
1
+ """
2
+ Preprocessing pipeline builder and column classifier for MLPipe.
3
+
4
+ Constructs unified sklearn ColumnTransformer structures with leakage-free feature processing.
5
+ """
6
+
7
+ from dataclasses import dataclass
8
+ import re
9
+ from typing import Any, Dict, List, Optional, Tuple
10
+
11
+ import numpy as np
12
+ import pandas as pd
13
+ from sklearn.compose import ColumnTransformer
14
+ from sklearn.pipeline import Pipeline
15
+
16
+ from mlpipe.core.exceptions import PreprocessingError
17
+ from mlpipe.data.profiling import detect_column_type
18
+ from mlpipe.preprocessing.categorical import build_categorical_transformer
19
+ from mlpipe.preprocessing.datetime import DatetimeFeatureExtractor
20
+ from mlpipe.preprocessing.numeric import build_numeric_transformer
21
+ from mlpipe.utils.logging import get_logger
22
+
23
+ logger = get_logger("preprocessing")
24
+
25
+
26
+ @dataclass
27
+ class ColumnAssignments:
28
+ """Summary of column assignments across transformation types."""
29
+
30
+ numeric: List[str]
31
+ categorical: List[str]
32
+ datetime: List[str]
33
+ dropped: List[str]
34
+
35
+ def to_dict(self) -> Dict[str, List[str]]:
36
+ return {
37
+ "numeric": self.numeric,
38
+ "categorical": self.categorical,
39
+ "datetime": self.datetime,
40
+ "dropped": self.dropped,
41
+ }
42
+
43
+
44
+ def classify_columns(df: pd.DataFrame) -> ColumnAssignments:
45
+ """
46
+ Categorize columns into numeric, categorical, datetime, or dropped.
47
+
48
+ Drops:
49
+ - Constant columns (<= 1 unique value)
50
+ - Columns with > 90% missing values
51
+ - Obvious unique ID / index columns (all unique string/integer identifiers)
52
+ """
53
+ n_rows = len(df)
54
+ numeric_cols: List[str] = []
55
+ categorical_cols: List[str] = []
56
+ datetime_cols: List[str] = []
57
+ dropped_cols: List[str] = []
58
+
59
+ id_pattern = re.compile(r"(^id$|_id$|^id_|^index$|^guid$|^uuid$)", re.IGNORECASE)
60
+
61
+ for col in df.columns:
62
+ series = df[col]
63
+ non_null = series.dropna()
64
+ n_unique = non_null.nunique()
65
+
66
+ # 1. Constant column check
67
+ if n_unique <= 1:
68
+ dropped_cols.append(col)
69
+ logger.info("Dropping constant column '%s' (unique values: %d)", col, n_unique)
70
+ continue
71
+
72
+ # 2. Extreme missingness check (> 90%)
73
+ if series.isna().mean() > 0.90:
74
+ dropped_cols.append(col)
75
+ logger.info("Dropping column '%s' with >90%% missing values", col)
76
+ continue
77
+
78
+ # 3. ID / index column heuristic
79
+ if n_unique == n_rows and (id_pattern.search(str(col)) or series.dtype == object):
80
+ dropped_cols.append(col)
81
+ logger.info("Dropping identifier column '%s' (100%% unique values)", col)
82
+ continue
83
+
84
+ detected = detect_column_type(series)
85
+ if detected == "numeric":
86
+ numeric_cols.append(col)
87
+ elif detected == "datetime":
88
+ datetime_cols.append(col)
89
+ else: # categorical or boolean
90
+ categorical_cols.append(col)
91
+
92
+ if not numeric_cols and not categorical_cols and not datetime_cols:
93
+ raise PreprocessingError(
94
+ "No usable feature columns remain after filtering constant/ID/missing columns.",
95
+ "Verify that your dataset contains informative feature columns with variation."
96
+ )
97
+
98
+ return ColumnAssignments(
99
+ numeric=numeric_cols,
100
+ categorical=categorical_cols,
101
+ datetime=datetime_cols,
102
+ dropped=dropped_cols,
103
+ )
104
+
105
+
106
+ def build_preprocessor(
107
+ assignments: ColumnAssignments,
108
+ with_scaling: bool = True,
109
+ ) -> ColumnTransformer:
110
+ """
111
+ Construct an unfitted ColumnTransformer based on column assignments.
112
+
113
+ Args:
114
+ assignments: ColumnAssignments partitioning features.
115
+ with_scaling: Whether to apply StandardScaler to numeric features.
116
+
117
+ Returns:
118
+ Unfitted ColumnTransformer instance.
119
+ """
120
+ transformers = []
121
+
122
+ if assignments.numeric:
123
+ num_pipeline = build_numeric_transformer(with_scaling=with_scaling)
124
+ transformers.append(("numeric", num_pipeline, assignments.numeric))
125
+
126
+ if assignments.categorical:
127
+ cat_pipeline = build_categorical_transformer()
128
+ transformers.append(("categorical", cat_pipeline, assignments.categorical))
129
+
130
+ if assignments.datetime:
131
+ dt_pipeline = Pipeline([
132
+ ("extractor", DatetimeFeatureExtractor())
133
+ ])
134
+ transformers.append(("datetime", dt_pipeline, assignments.datetime))
135
+
136
+ return ColumnTransformer(
137
+ transformers=transformers,
138
+ remainder="drop",
139
+ verbose_feature_names_out=False,
140
+ )
141
+
142
+
143
+ def get_transformed_feature_names(fitted_preprocessor: ColumnTransformer) -> List[str]:
144
+ """
145
+ Extract human-readable feature names from a fitted ColumnTransformer.
146
+ """
147
+ try:
148
+ return list(fitted_preprocessor.get_feature_names_out())
149
+ except Exception as e:
150
+ logger.debug("Failed to get feature names via get_feature_names_out: %s", e)
151
+ # Fallback: inspect transformers
152
+ names: List[str] = []
153
+ for name, trans, cols in fitted_preprocessor.transformers_:
154
+ if name == "remainder" or trans == "drop":
155
+ continue
156
+ if hasattr(trans, "get_feature_names_out"):
157
+ try:
158
+ names.extend(list(trans.get_feature_names_out(cols)))
159
+ except Exception:
160
+ names.extend(list(cols))
161
+ else:
162
+ names.extend(list(cols))
163
+ return names
@@ -0,0 +1,17 @@
1
+ """Categorical feature preprocessing pipelines."""
2
+
3
+ from sklearn.impute import SimpleImputer
4
+ from sklearn.pipeline import Pipeline
5
+ from sklearn.preprocessing import OneHotEncoder
6
+
7
+
8
+ def build_categorical_transformer() -> Pipeline:
9
+ """
10
+ Build a preprocessing pipeline for categorical features.
11
+
12
+ Applies most-frequent imputation followed by one-hot encoding with unseen category handling.
13
+ """
14
+ return Pipeline([
15
+ ("imputer", SimpleImputer(strategy="most_frequent")),
16
+ ("encoder", OneHotEncoder(handle_unknown="ignore", sparse_output=False)),
17
+ ])
@@ -0,0 +1,55 @@
1
+ """Datetime feature extraction transformer."""
2
+
3
+ from typing import List, Optional
4
+
5
+ import numpy as np
6
+ import pandas as pd
7
+ from sklearn.base import BaseEstimator, TransformerMixin
8
+
9
+
10
+ class DatetimeFeatureExtractor(BaseEstimator, TransformerMixin):
11
+ """
12
+ Extracts tabular calendar features (year, month, day, dayofweek) from datetime columns.
13
+ """
14
+
15
+ def __init__(self):
16
+ self.feature_names_: List[str] = []
17
+
18
+ def fit(self, X, y=None):
19
+ cols = X.columns if hasattr(X, "columns") else [f"dt_{i}" for i in range(X.shape[1])]
20
+ names = []
21
+ for col in cols:
22
+ names.extend([
23
+ f"{col}_year",
24
+ f"{col}_month",
25
+ f"{col}_day",
26
+ f"{col}_dayofweek",
27
+ ])
28
+ self.feature_names_ = names
29
+ return self
30
+
31
+ def transform(self, X):
32
+ df = X if isinstance(X, pd.DataFrame) else pd.DataFrame(X)
33
+ extracted = []
34
+
35
+ for col in df.columns:
36
+ s = pd.to_datetime(df[col], errors="coerce")
37
+ extracted.append(s.dt.year.fillna(-1).astype(float).values)
38
+ extracted.append(s.dt.month.fillna(-1).astype(float).values)
39
+ extracted.append(s.dt.day.fillna(-1).astype(float).values)
40
+ extracted.append(s.dt.dayofweek.fillna(-1).astype(float).values)
41
+
42
+ if not extracted:
43
+ return np.empty((len(df), 0))
44
+
45
+ return np.column_stack(extracted)
46
+
47
+ def get_feature_names_out(self, input_features: Optional[List[str]] = None) -> List[str]:
48
+ if self.feature_names_:
49
+ return self.feature_names_
50
+ if input_features is not None:
51
+ names = []
52
+ for col in input_features:
53
+ names.extend([f"{col}_year", f"{col}_month", f"{col}_day", f"{col}_dayofweek"])
54
+ return names
55
+ return []
@@ -0,0 +1,17 @@
1
+ """Numeric feature preprocessing pipelines."""
2
+
3
+ from sklearn.impute import SimpleImputer
4
+ from sklearn.pipeline import Pipeline
5
+ from sklearn.preprocessing import StandardScaler
6
+
7
+
8
+ def build_numeric_transformer(with_scaling: bool = True) -> Pipeline:
9
+ """
10
+ Build a preprocessing pipeline for numeric features.
11
+
12
+ Applies median imputation followed optionally by standard scaling.
13
+ """
14
+ steps = [("imputer", SimpleImputer(strategy="median"))]
15
+ if with_scaling:
16
+ steps.append(("scaler", StandardScaler()))
17
+ return Pipeline(steps)
@@ -0,0 +1,10 @@
1
+ """Hyperparameter tuning and search engine for MLPipe."""
2
+
3
+ from mlpipe.tuning.search import TuningResult, tune_candidate
4
+ from mlpipe.tuning.spaces import get_search_iterations
5
+
6
+ __all__ = [
7
+ "TuningResult",
8
+ "tune_candidate",
9
+ "get_search_iterations",
10
+ ]