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.
- mlpipe/__init__.py +43 -0
- mlpipe/__main__.py +6 -0
- mlpipe/artifacts/__init__.py +23 -0
- mlpipe/artifacts/manager.py +246 -0
- mlpipe/artifacts/serialization.py +67 -0
- mlpipe/cli/__init__.py +5 -0
- mlpipe/cli/main.py +667 -0
- mlpipe/core/__init__.py +35 -0
- mlpipe/core/config.py +76 -0
- mlpipe/core/exceptions.py +65 -0
- mlpipe/core/pipeline.py +435 -0
- mlpipe/core/result.py +50 -0
- mlpipe/data/__init__.py +20 -0
- mlpipe/data/ingestion.py +138 -0
- mlpipe/data/profiling.py +227 -0
- mlpipe/data/splitting.py +130 -0
- mlpipe/data/validation.py +248 -0
- mlpipe/evaluation/__init__.py +11 -0
- mlpipe/evaluation/evaluator.py +146 -0
- mlpipe/evaluation/metrics.py +53 -0
- mlpipe/explainability/__init__.py +5 -0
- mlpipe/explainability/importance.py +65 -0
- mlpipe/models/__init__.py +13 -0
- mlpipe/models/classification.py +156 -0
- mlpipe/models/registry.py +32 -0
- mlpipe/models/regression.py +126 -0
- mlpipe/models/selection.py +24 -0
- mlpipe/preprocessing/__init__.py +21 -0
- mlpipe/preprocessing/builder.py +163 -0
- mlpipe/preprocessing/categorical.py +17 -0
- mlpipe/preprocessing/datetime.py +55 -0
- mlpipe/preprocessing/numeric.py +17 -0
- mlpipe/tuning/__init__.py +10 -0
- mlpipe/tuning/search.py +140 -0
- mlpipe/tuning/spaces.py +11 -0
- mlpipe/utils/__init__.py +13 -0
- mlpipe/utils/hashing.py +15 -0
- mlpipe/utils/logging.py +37 -0
- mlpipe/utils/timing.py +33 -0
- mlpipe/version.py +3 -0
- mlpipe_cli-0.1.0.dist-info/METADATA +264 -0
- mlpipe_cli-0.1.0.dist-info/RECORD +46 -0
- mlpipe_cli-0.1.0.dist-info/WHEEL +5 -0
- mlpipe_cli-0.1.0.dist-info/entry_points.txt +2 -0
- mlpipe_cli-0.1.0.dist-info/licenses/LICENSE +21 -0
- 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
|
+
]
|