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,35 @@
1
+ """Core modules for MLPipe."""
2
+
3
+ from mlpipe.core.config import PipelineConfig, TaskType, TrainingMode
4
+ from mlpipe.core.exceptions import (
5
+ ArtifactError,
6
+ ConfigurationError,
7
+ DatasetError,
8
+ EvaluationError,
9
+ MLPipeError,
10
+ PipelineError,
11
+ PredictionError,
12
+ PreprocessingError,
13
+ TrainingError,
14
+ ValidationError,
15
+ )
16
+ from mlpipe.core.pipeline import Pipeline
17
+ from mlpipe.core.result import PipelineResult
18
+
19
+ __all__ = [
20
+ "Pipeline",
21
+ "PipelineResult",
22
+ "PipelineConfig",
23
+ "TaskType",
24
+ "TrainingMode",
25
+ "MLPipeError",
26
+ "DatasetError",
27
+ "ValidationError",
28
+ "PreprocessingError",
29
+ "TrainingError",
30
+ "EvaluationError",
31
+ "ArtifactError",
32
+ "ConfigurationError",
33
+ "PredictionError",
34
+ "PipelineError",
35
+ ]
mlpipe/core/config.py ADDED
@@ -0,0 +1,76 @@
1
+ """
2
+ Configuration models and defaults for MLPipe.
3
+ """
4
+
5
+ from dataclasses import dataclass, field
6
+ from enum import Enum
7
+ from pathlib import Path
8
+ from typing import Optional, Union
9
+
10
+ from mlpipe.core.exceptions import ConfigurationError
11
+
12
+
13
+ class TaskType(str, Enum):
14
+ AUTO = "auto"
15
+ CLASSIFICATION = "classification"
16
+ REGRESSION = "regression"
17
+
18
+
19
+ class TrainingMode(str, Enum):
20
+ FAST = "fast"
21
+ BALANCED = "balanced"
22
+ THOROUGH = "thorough"
23
+
24
+
25
+ @dataclass
26
+ class PipelineConfig:
27
+ """Configuration settings for an MLPipe run."""
28
+
29
+ target: str
30
+ task: Union[TaskType, str] = TaskType.AUTO
31
+ mode: Union[TrainingMode, str] = TrainingMode.BALANCED
32
+ test_size: float = 0.20
33
+ random_seed: int = 42
34
+ cv_folds: int = 5
35
+ output_dir: Path = field(default_factory=lambda: Path("./mlpipe_runs"))
36
+ verbose: bool = False
37
+
38
+ def __post_init__(self):
39
+ # Normalize and validate task
40
+ if isinstance(self.task, str):
41
+ try:
42
+ self.task = TaskType(self.task.lower())
43
+ except ValueError:
44
+ valid = [t.value for t in TaskType]
45
+ raise ConfigurationError(
46
+ f"Invalid task '{self.task}'. Must be one of {valid}.",
47
+ "Use 'auto', 'classification', or 'regression'."
48
+ )
49
+
50
+ # Normalize and validate mode
51
+ if isinstance(self.mode, str):
52
+ try:
53
+ self.mode = TrainingMode(self.mode.lower())
54
+ except ValueError:
55
+ valid = [m.value for m in TrainingMode]
56
+ raise ConfigurationError(
57
+ f"Invalid training mode '{self.mode}'. Must be one of {valid}.",
58
+ "Use 'fast', 'balanced', or 'thorough'."
59
+ )
60
+
61
+ # Validate test_size
62
+ if not (0.05 <= self.test_size <= 0.5):
63
+ raise ConfigurationError(
64
+ f"Invalid test_size '{self.test_size}'. Must be between 0.05 and 0.50.",
65
+ "Choose a reasonable test split ratio, such as 0.20 (20%)."
66
+ )
67
+
68
+ # Validate cv_folds
69
+ if self.cv_folds < 2:
70
+ raise ConfigurationError(
71
+ f"cv_folds must be at least 2, got {self.cv_folds}.",
72
+ "Set cv_folds to at least 2, typically 3 or 5."
73
+ )
74
+
75
+ if not isinstance(self.output_dir, Path):
76
+ self.output_dir = Path(self.output_dir)
@@ -0,0 +1,65 @@
1
+ """
2
+ Custom exceptions for MLPipe.
3
+
4
+ Provides domain-specific errors with user-friendly descriptions and suggested actions.
5
+ """
6
+
7
+ from typing import Optional
8
+
9
+
10
+ class MLPipeError(Exception):
11
+ """Base exception for all MLPipe errors."""
12
+
13
+ def __init__(self, message: str, suggested_action: Optional[str] = None):
14
+ self.message = message
15
+ self.suggested_action = suggested_action
16
+ full_msg = message
17
+ if suggested_action:
18
+ full_msg += f"\n\nSuggested action:\n{suggested_action}"
19
+ super().__init__(full_msg)
20
+
21
+
22
+ class DatasetError(MLPipeError):
23
+ """Raised when there is an issue loading, reading, or parsing a dataset."""
24
+ pass
25
+
26
+
27
+ class ValidationError(MLPipeError):
28
+ """Raised when dataset pre-training validation fails fatal checks."""
29
+ pass
30
+
31
+
32
+ class PreprocessingError(MLPipeError):
33
+ """Raised when feature preprocessing or transformation fails."""
34
+ pass
35
+
36
+
37
+ class TrainingError(MLPipeError):
38
+ """Raised when model training or hyperparameter tuning fails."""
39
+ pass
40
+
41
+
42
+ class EvaluationError(MLPipeError):
43
+ """Raised when model evaluation fails."""
44
+ pass
45
+
46
+
47
+ class ArtifactError(MLPipeError):
48
+ """Raised when saving, loading, or managing artifacts fails."""
49
+ pass
50
+
51
+
52
+ class ConfigurationError(MLPipeError):
53
+ """Raised when invalid options or configurations are provided."""
54
+ pass
55
+
56
+
57
+ class PredictionError(MLPipeError):
58
+ """Raised when generating predictions fails."""
59
+ pass
60
+
61
+
62
+ class PipelineError(MLPipeError):
63
+ """Raised when general pipeline execution fails."""
64
+ pass
65
+
@@ -0,0 +1,435 @@
1
+ """
2
+ Core Pipeline orchestrator for MLPipe.
3
+
4
+ Provides the primary user-facing Python API for automated tabular machine learning:
5
+ data ingestion, validation, splitting, preprocessing, multi-model training,
6
+ cross-validation, hyperparameter tuning, model evaluation, explainability,
7
+ and artifact management.
8
+ """
9
+
10
+ from pathlib import Path
11
+ import time
12
+ from typing import Any, Callable, Dict, List, Optional, Union
13
+
14
+ import numpy as np
15
+ import pandas as pd
16
+ from sklearn.pipeline import Pipeline as SklearnPipeline
17
+
18
+ from mlpipe.artifacts.manager import ArtifactManager, load_pipeline_artifact
19
+ from mlpipe.artifacts.serialization import save_joblib
20
+ from mlpipe.core.config import PipelineConfig, TaskType, TrainingMode
21
+ from mlpipe.core.exceptions import (
22
+ ConfigurationError,
23
+ DatasetError,
24
+ PipelineError,
25
+ PredictionError,
26
+ TrainingError,
27
+ ValidationError,
28
+ )
29
+ from mlpipe.core.result import PipelineResult
30
+ from mlpipe.data.ingestion import Dataset, load_dataset
31
+ from mlpipe.data.profiling import DatasetProfile, profile_dataset
32
+ from mlpipe.data.splitting import SplitData, split_data
33
+ from mlpipe.data.validation import ValidationReport, validate_dataset
34
+ from mlpipe.evaluation.evaluator import build_leaderboard, evaluate_pipeline_on_test
35
+ from mlpipe.evaluation.metrics import select_primary_metric
36
+ from mlpipe.explainability.importance import extract_feature_importance
37
+ from mlpipe.models.selection import get_candidates_for_task
38
+ from mlpipe.preprocessing.builder import classify_columns, build_preprocessor
39
+ from mlpipe.tuning.search import TuningResult, tune_candidate
40
+ from mlpipe.utils.logging import configure_logging, get_logger
41
+
42
+ logger = get_logger("pipeline")
43
+
44
+
45
+ class Pipeline:
46
+ """
47
+ MLPipe Automated Machine Learning Pipeline.
48
+
49
+ Example:
50
+ ```python
51
+ from mlpipe import Pipeline
52
+
53
+ pipe = Pipeline(target="churn", task="auto", mode="balanced")
54
+ result = pipe.fit("customer_churn.csv")
55
+
56
+ print(result.best_model)
57
+ print(result.metrics)
58
+
59
+ predictions = pipe.predict("new_data.csv")
60
+ pipe.save("./model")
61
+ ```
62
+ """
63
+
64
+ def __init__(
65
+ self,
66
+ target: str,
67
+ task: Union[TaskType, str] = TaskType.AUTO,
68
+ mode: Union[TrainingMode, str] = TrainingMode.BALANCED,
69
+ test_size: float = 0.20,
70
+ random_seed: int = 42,
71
+ cv_folds: int = 5,
72
+ output_dir: Union[str, Path] = "./mlpipe_runs",
73
+ verbose: bool = False,
74
+ ):
75
+ self.config = PipelineConfig(
76
+ target=target,
77
+ task=task,
78
+ mode=mode,
79
+ test_size=test_size,
80
+ random_seed=random_seed,
81
+ cv_folds=cv_folds,
82
+ output_dir=Path(output_dir),
83
+ verbose=verbose,
84
+ )
85
+ configure_logging(verbose=verbose)
86
+
87
+ self._fitted_pipeline: Optional[SklearnPipeline] = None
88
+ self._result: Optional[PipelineResult] = None
89
+ self._profile: Optional[DatasetProfile] = None
90
+ self._validation: Optional[ValidationReport] = None
91
+
92
+ @property
93
+ def is_fitted(self) -> bool:
94
+ """Whether the pipeline has been fitted."""
95
+ return self._fitted_pipeline is not None
96
+
97
+ @property
98
+ def result(self) -> Optional[PipelineResult]:
99
+ """Outcome of the latest fit operation."""
100
+ return self._result
101
+
102
+ @property
103
+ def profile(self) -> Optional[DatasetProfile]:
104
+ """Dataset profile calculated during fit."""
105
+ return self._profile
106
+
107
+ @property
108
+ def validation(self) -> Optional[ValidationReport]:
109
+ """Validation report generated during fit."""
110
+ return self._validation
111
+
112
+ def fit(
113
+ self,
114
+ data: Union[str, Path, pd.DataFrame, Dataset],
115
+ on_progress: Optional[Callable[[str, str], None]] = None,
116
+ ) -> PipelineResult:
117
+ """
118
+ Execute the complete automated machine learning lifecycle on the input dataset.
119
+
120
+ Args:
121
+ data: Filepath to CSV, pandas DataFrame, or MLPipe Dataset.
122
+ on_progress: Optional callback invoked with (stage, message).
123
+
124
+ Returns:
125
+ PipelineResult with metrics, leaderboard, and artifact locations.
126
+ """
127
+ def _notify(stage: str, msg: str):
128
+ logger.info("[%s] %s", stage, msg)
129
+ if on_progress:
130
+ on_progress(stage, msg)
131
+
132
+ t_start = time.perf_counter()
133
+
134
+ # ── 1. Data Ingestion ──────────────────────────────────────────────
135
+ _notify("ingestion", "Loading dataset...")
136
+ if isinstance(data, (str, Path)):
137
+ dataset = load_dataset(data)
138
+ elif isinstance(data, Dataset):
139
+ dataset = data
140
+ elif isinstance(data, pd.DataFrame):
141
+ dataset = Dataset(
142
+ filepath=Path("in_memory.csv"),
143
+ df=data.copy(),
144
+ num_rows=len(data),
145
+ num_cols=len(data.columns),
146
+ memory_bytes=int(data.memory_usage(deep=True).sum()),
147
+ sha256_hash="in_memory_dataframe",
148
+ column_names=list(data.columns),
149
+ )
150
+ else:
151
+ raise DatasetError(f"Unsupported data type '{type(data)}'. Provide a CSV path or DataFrame.")
152
+
153
+ # ── 2. Data Profiling ──────────────────────────────────────────────
154
+ _notify("profiling", "Profiling dataset structure and distributions...")
155
+ self._profile = profile_dataset(dataset)
156
+
157
+ # ── 3. Data Validation ─────────────────────────────────────────────
158
+ _notify("validation", "Validating dataset and task compatibility...")
159
+ self._validation = validate_dataset(
160
+ dataset=dataset,
161
+ target_column=self.config.target,
162
+ task_override=self.config.task.value,
163
+ test_size=self.config.test_size,
164
+ cv_folds=self.config.cv_folds,
165
+ )
166
+ self._validation.raise_if_invalid()
167
+
168
+ task_type = self._validation.detected_task
169
+ _notify("task_detection", f"Task detected: {task_type.capitalize()}")
170
+
171
+ # ── 4. Train / Test Splitting ──────────────────────────────────────
172
+ _notify("splitting", f"Splitting data ({int((1-self.config.test_size)*100)}% train / {int(self.config.test_size*100)}% test)...")
173
+ split_res: SplitData = split_data(
174
+ df=dataset.df,
175
+ target_column=self.config.target,
176
+ task_type=task_type,
177
+ test_size=self.config.test_size,
178
+ random_seed=self.config.random_seed,
179
+ )
180
+
181
+ # ── 5. Preprocessing Pipeline Construction ─────────────────────────
182
+ _notify("preprocessing", "Building feature preprocessing pipeline...")
183
+ # Classify columns strictly from training features to avoid leakage
184
+ column_assignments = classify_columns(split_res.X_train)
185
+ preprocessor = build_preprocessor(column_assignments, with_scaling=True)
186
+
187
+ # ── 6. Metric Selection ────────────────────────────────────────────
188
+ primary_metric = select_primary_metric(task_type, split_res.y_train)
189
+ _notify("metric_selection", f"Primary evaluation metric: {primary_metric}")
190
+
191
+ # ── 7. Model Selection & Tuning ────────────────────────────────────
192
+ candidates = get_candidates_for_task(task_type, mode=self.config.mode.value)
193
+ tuning_results: List[TuningResult] = []
194
+
195
+ for candidate in candidates:
196
+ _notify("training", f"Training & tuning candidate: {candidate.name}...")
197
+ res = tune_candidate(
198
+ candidate=candidate,
199
+ preprocessor=preprocessor,
200
+ X_train=split_res.X_train,
201
+ y_train=split_res.y_train,
202
+ primary_metric=primary_metric,
203
+ cv_folds=self.config.cv_folds,
204
+ mode=self.config.mode.value,
205
+ random_seed=self.config.random_seed,
206
+ )
207
+ tuning_results.append(res)
208
+
209
+ # Check if all models failed
210
+ successful_runs = [r for r in tuning_results if r.best_pipeline is not None and not r.error]
211
+ if not successful_runs:
212
+ error_details = "\n".join(f"- {r.candidate_name}: {r.error}" for r in tuning_results)
213
+ raise TrainingError(
214
+ f"All candidate models failed during training:\n{error_details}",
215
+ "Review the errors above and ensure features are compatible."
216
+ )
217
+
218
+ # ── 8. Leaderboard & Model Selection ───────────────────────────────
219
+ _notify("evaluation", "Compiling leaderboard and selecting winning model...")
220
+ leaderboard = build_leaderboard(
221
+ tuning_results=tuning_results,
222
+ X_test=split_res.X_test,
223
+ y_test=split_res.y_test,
224
+ task_type=task_type,
225
+ primary_metric=primary_metric,
226
+ )
227
+
228
+ winning_entry = leaderboard[0]
229
+ best_model_name = winning_entry["model"]
230
+ best_cv_score = winning_entry["cv_score"]
231
+ test_score = winning_entry["test_score"]
232
+ test_metrics = winning_entry.get("test_metrics", {})
233
+
234
+ # Find winning pipeline
235
+ winning_result = next(r for r in tuning_results if r.candidate_name == best_model_name)
236
+ best_pipeline = winning_result.best_pipeline
237
+ self._fitted_pipeline = best_pipeline
238
+
239
+ # ── 9. Explainability ──────────────────────────────────────────────
240
+ _notify("explainability", "Extracting feature importances...")
241
+ feature_importance = extract_feature_importance(best_pipeline)
242
+
243
+ # ── 10. Test Set Predictions & Preview ─────────────────────────────
244
+ _notify("evaluation", "Generating hold-out test set predictions...")
245
+ y_test_pred = best_pipeline.predict(split_res.X_test)
246
+
247
+ test_predictions_df = split_res.X_test.copy()
248
+ test_predictions_df[f"actual_{self.config.target}"] = split_res.y_test.values
249
+ test_predictions_df[f"predicted_{self.config.target}"] = y_test_pred
250
+
251
+ test_preview = []
252
+ preview_n = min(10, len(split_res.X_test))
253
+ candidate_cols = [c for c in split_res.X_test.columns if "id" not in c.lower() and "uuid" not in c.lower()]
254
+ if not candidate_cols:
255
+ candidate_cols = list(split_res.X_test.columns)
256
+ preview_cols = candidate_cols[:3]
257
+
258
+ if task_type == "classification":
259
+ matches = (split_res.y_test.values == y_test_pred)
260
+ test_predictions_df["match"] = matches
261
+ for i in range(preview_n):
262
+ sample_feats = {
263
+ col: (round(float(split_res.X_test.iloc[i][col]), 2) if isinstance(split_res.X_test.iloc[i][col], (float, np.floating)) else split_res.X_test.iloc[i][col])
264
+ for col in preview_cols
265
+ }
266
+ test_preview.append({
267
+ "row_idx": int(i + 1),
268
+ "actual": split_res.y_test.values[i],
269
+ "predicted": y_test_pred[i],
270
+ "match": bool(matches[i]),
271
+ "features": sample_feats,
272
+ })
273
+ else:
274
+ abs_err = np.abs(split_res.y_test.values - y_test_pred)
275
+ test_predictions_df["absolute_error"] = np.round(abs_err, 4)
276
+ for i in range(preview_n):
277
+ sample_feats = {
278
+ col: (round(float(split_res.X_test.iloc[i][col]), 2) if isinstance(split_res.X_test.iloc[i][col], (float, np.floating)) else split_res.X_test.iloc[i][col])
279
+ for col in preview_cols
280
+ }
281
+ test_preview.append({
282
+ "row_idx": int(i + 1),
283
+ "actual": float(np.round(split_res.y_test.values[i], 4)),
284
+ "predicted": float(np.round(y_test_pred[i], 4)),
285
+ "error": float(np.round(abs_err[i], 4)),
286
+ "features": sample_feats,
287
+ })
288
+
289
+ # ── 11. Artifact Generation ────────────────────────────────────────
290
+ _notify("artifacts", "Saving run artifacts and pipeline...")
291
+ artifact_mgr = ArtifactManager(base_output_dir=self.config.output_dir)
292
+ run_id = artifact_mgr.generate_run_id()
293
+ elapsed_s = round(time.perf_counter() - t_start, 2)
294
+
295
+ saved_dir = artifact_mgr.save_run_artifacts(
296
+ run_id=run_id,
297
+ pipeline=best_pipeline,
298
+ dataset_summary=dataset.summary(),
299
+ target_column=self.config.target,
300
+ task_type=task_type,
301
+ training_mode=self.config.mode.value,
302
+ primary_metric=primary_metric,
303
+ best_model_name=best_model_name,
304
+ best_cv_score=best_cv_score,
305
+ test_score=test_score,
306
+ test_metrics=test_metrics,
307
+ leaderboard=leaderboard,
308
+ feature_importance=feature_importance,
309
+ elapsed_time_s=elapsed_s,
310
+ random_seed=self.config.random_seed,
311
+ train_df=split_res.train_df,
312
+ test_df=split_res.test_df,
313
+ test_predictions_df=test_predictions_df,
314
+ )
315
+
316
+ self._result = PipelineResult(
317
+ run_id=run_id,
318
+ target_column=self.config.target,
319
+ task_type=task_type,
320
+ best_model_name=best_model_name,
321
+ primary_metric=primary_metric,
322
+ best_cv_score=best_cv_score,
323
+ test_score=test_score,
324
+ leaderboard=leaderboard,
325
+ test_metrics=test_metrics,
326
+ feature_importance=feature_importance,
327
+ artifacts_dir=saved_dir,
328
+ elapsed_time_s=elapsed_s,
329
+ metadata={
330
+ "training_mode": self.config.mode.value,
331
+ "random_seed": self.config.random_seed,
332
+ "train_rows": split_res.train_size,
333
+ "test_rows": split_res.test_size,
334
+ },
335
+ test_preview=test_preview,
336
+ train_path=saved_dir / "train.csv",
337
+ test_path=saved_dir / "test.csv",
338
+ test_predictions_path=saved_dir / "test_predictions.csv",
339
+ )
340
+
341
+ _notify("complete", f"Pipeline complete in {elapsed_s}s. Best model: {best_model_name} ({best_cv_score})")
342
+ return self._result
343
+
344
+ def predict(self, data: Union[str, Path, pd.DataFrame]) -> np.ndarray:
345
+ """
346
+ Generate predictions using the fitted pipeline.
347
+
348
+ Args:
349
+ data: Path to CSV or pandas DataFrame.
350
+
351
+ Returns:
352
+ Numpy array of predictions.
353
+ """
354
+ if not self.is_fitted or self._fitted_pipeline is None:
355
+ raise PredictionError(
356
+ "Pipeline has not been fitted or loaded.",
357
+ "Call pipeline.fit(data) or Pipeline.load(path) before calling predict."
358
+ )
359
+
360
+ if isinstance(data, (str, Path)):
361
+ dataset = load_dataset(data)
362
+ df = dataset.df
363
+ elif isinstance(data, pd.DataFrame):
364
+ df = data.copy()
365
+ else:
366
+ raise PredictionError(f"Unsupported data type '{type(data)}' for prediction.")
367
+
368
+ # Drop target column if present in prediction data (e.g. test set)
369
+ if self.config.target in df.columns:
370
+ df = df.drop(columns=[self.config.target])
371
+
372
+ try:
373
+ return self._fitted_pipeline.predict(df)
374
+ except Exception as e:
375
+ raise PredictionError(
376
+ f"Prediction failed: {e}",
377
+ "Ensure that the input data contains all required feature columns with compatible formats."
378
+ )
379
+
380
+ def predict_proba(self, data: Union[str, Path, pd.DataFrame]) -> np.ndarray:
381
+ """
382
+ Generate prediction probabilities for classification tasks.
383
+ """
384
+ if not self.is_fitted or self._fitted_pipeline is None:
385
+ raise PredictionError("Pipeline has not been fitted or loaded.")
386
+
387
+ if not hasattr(self._fitted_pipeline, "predict_proba"):
388
+ raise PredictionError("The underlying best model does not support predict_proba.")
389
+
390
+ if isinstance(data, (str, Path)):
391
+ dataset = load_dataset(data)
392
+ df = dataset.df
393
+ elif isinstance(data, pd.DataFrame):
394
+ df = data.copy()
395
+ else:
396
+ raise PredictionError(f"Unsupported data type '{type(data)}' for prediction.")
397
+
398
+ if self.config.target in df.columns:
399
+ df = df.drop(columns=[self.config.target])
400
+
401
+ return self._fitted_pipeline.predict_proba(df)
402
+
403
+ def save(self, path: Union[str, Path]) -> Path:
404
+ """
405
+ Save the fitted pipeline to a file or directory.
406
+
407
+ Args:
408
+ path: Destination file or directory.
409
+ """
410
+ if not self.is_fitted or self._fitted_pipeline is None:
411
+ raise PipelineError("Cannot save an unfitted pipeline.")
412
+
413
+ dest = Path(path).resolve()
414
+ if dest.suffix == ".joblib":
415
+ return save_joblib(self._fitted_pipeline, dest)
416
+ else:
417
+ dest.mkdir(parents=True, exist_ok=True)
418
+ return save_joblib(self._fitted_pipeline, dest / "pipeline.joblib")
419
+
420
+ @classmethod
421
+ def load(cls, path: Union[str, Path], target: str = "unknown") -> "Pipeline":
422
+ """
423
+ Load a previously trained pipeline from disk.
424
+
425
+ Args:
426
+ path: Directory containing 'pipeline.joblib' or path to a .joblib file.
427
+ target: Optional target name for consistency.
428
+
429
+ Returns:
430
+ Configured Pipeline instance ready for prediction.
431
+ """
432
+ fitted_pipe = load_pipeline_artifact(path)
433
+ instance = cls(target=target)
434
+ instance._fitted_pipeline = fitted_pipe
435
+ return instance
mlpipe/core/result.py ADDED
@@ -0,0 +1,50 @@
1
+ """
2
+ Pipeline results and reporting structures.
3
+ """
4
+
5
+ from dataclasses import asdict, dataclass, field
6
+ from pathlib import Path
7
+ from typing import Any, Dict, List, Optional
8
+
9
+
10
+ @dataclass
11
+ class PipelineResult:
12
+ """Encapsulates the complete outcome of an MLPipe pipeline run."""
13
+
14
+ run_id: str
15
+ target_column: str
16
+ task_type: str
17
+ best_model_name: str
18
+ primary_metric: str
19
+ best_cv_score: float
20
+ test_score: float
21
+ leaderboard: List[Dict[str, Any]]
22
+ test_metrics: Dict[str, Any]
23
+ feature_importance: List[Dict[str, Any]]
24
+ artifacts_dir: Path
25
+ elapsed_time_s: float
26
+ metadata: Dict[str, Any] = field(default_factory=dict)
27
+ test_preview: List[Dict[str, Any]] = field(default_factory=list)
28
+ train_path: Optional[Path] = None
29
+ test_path: Optional[Path] = None
30
+ test_predictions_path: Optional[Path] = None
31
+
32
+ def to_dict(self) -> Dict[str, Any]:
33
+ """Convert result to a dictionary representation."""
34
+ data = asdict(self)
35
+ data["artifacts_dir"] = str(self.artifacts_dir) if self.artifacts_dir else None
36
+ data["train_path"] = str(self.train_path) if self.train_path else None
37
+ data["test_path"] = str(self.test_path) if self.test_path else None
38
+ data["test_predictions_path"] = str(self.test_predictions_path) if self.test_predictions_path else None
39
+ return data
40
+
41
+ @property
42
+ def best_model(self) -> str:
43
+ """Convenience alias for best model name."""
44
+ return self.best_model_name
45
+
46
+ @property
47
+ def metrics(self) -> Dict[str, Any]:
48
+ """Convenience alias for test metrics."""
49
+ return self.test_metrics
50
+
@@ -0,0 +1,20 @@
1
+ """Data ingestion, profiling, validation, and splitting for MLPipe."""
2
+
3
+ from mlpipe.data.ingestion import Dataset, load_dataset
4
+ from mlpipe.data.profiling import ColumnProfile, DatasetProfile, detect_column_type, profile_dataset
5
+ from mlpipe.data.splitting import SplitData, split_data
6
+ from mlpipe.data.validation import ValidationReport, detect_task, validate_dataset
7
+
8
+ __all__ = [
9
+ "Dataset",
10
+ "load_dataset",
11
+ "ColumnProfile",
12
+ "DatasetProfile",
13
+ "detect_column_type",
14
+ "profile_dataset",
15
+ "SplitData",
16
+ "split_data",
17
+ "ValidationReport",
18
+ "detect_task",
19
+ "validate_dataset",
20
+ ]