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
mlpipe/__init__.py
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
"""
|
|
2
|
+
MLPipe: Automated Machine Learning Library and Terminal CLI.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from mlpipe.core.config import PipelineConfig, TaskType, TrainingMode
|
|
6
|
+
from mlpipe.core.exceptions import (
|
|
7
|
+
ArtifactError,
|
|
8
|
+
ConfigurationError,
|
|
9
|
+
DatasetError,
|
|
10
|
+
EvaluationError,
|
|
11
|
+
MLPipeError,
|
|
12
|
+
PipelineError,
|
|
13
|
+
PredictionError,
|
|
14
|
+
PreprocessingError,
|
|
15
|
+
TrainingError,
|
|
16
|
+
ValidationError,
|
|
17
|
+
)
|
|
18
|
+
from mlpipe.core.pipeline import Pipeline
|
|
19
|
+
from mlpipe.core.result import PipelineResult
|
|
20
|
+
from mlpipe.data.profiling import DatasetProfile
|
|
21
|
+
from mlpipe.data.validation import ValidationReport
|
|
22
|
+
from mlpipe.version import __version__
|
|
23
|
+
|
|
24
|
+
__all__ = [
|
|
25
|
+
"Pipeline",
|
|
26
|
+
"PipelineResult",
|
|
27
|
+
"DatasetProfile",
|
|
28
|
+
"ValidationReport",
|
|
29
|
+
"PipelineConfig",
|
|
30
|
+
"TaskType",
|
|
31
|
+
"TrainingMode",
|
|
32
|
+
"MLPipeError",
|
|
33
|
+
"DatasetError",
|
|
34
|
+
"ValidationError",
|
|
35
|
+
"PreprocessingError",
|
|
36
|
+
"TrainingError",
|
|
37
|
+
"EvaluationError",
|
|
38
|
+
"ArtifactError",
|
|
39
|
+
"ConfigurationError",
|
|
40
|
+
"PredictionError",
|
|
41
|
+
"PipelineError",
|
|
42
|
+
"__version__",
|
|
43
|
+
]
|
mlpipe/__main__.py
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
"""Artifact management and serialization for MLPipe."""
|
|
2
|
+
|
|
3
|
+
from mlpipe.artifacts.manager import (
|
|
4
|
+
ArtifactManager,
|
|
5
|
+
inspect_run_directory,
|
|
6
|
+
load_pipeline_artifact,
|
|
7
|
+
)
|
|
8
|
+
from mlpipe.artifacts.serialization import (
|
|
9
|
+
load_joblib,
|
|
10
|
+
load_json,
|
|
11
|
+
save_joblib,
|
|
12
|
+
save_json,
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
__all__ = [
|
|
16
|
+
"ArtifactManager",
|
|
17
|
+
"inspect_run_directory",
|
|
18
|
+
"load_pipeline_artifact",
|
|
19
|
+
"load_joblib",
|
|
20
|
+
"load_json",
|
|
21
|
+
"save_joblib",
|
|
22
|
+
"save_json",
|
|
23
|
+
]
|
|
@@ -0,0 +1,246 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Artifact management and inspection for MLPipe runs.
|
|
3
|
+
|
|
4
|
+
Creates reproducible artifact bundles containing model, pipeline, metrics,
|
|
5
|
+
metadata, and textual summaries.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from datetime import datetime, timezone
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
import platform
|
|
11
|
+
import sys
|
|
12
|
+
from typing import Any, Dict, List, Optional, Union
|
|
13
|
+
import uuid
|
|
14
|
+
|
|
15
|
+
import numpy as np
|
|
16
|
+
import pandas as pd
|
|
17
|
+
from sklearn.pipeline import Pipeline
|
|
18
|
+
|
|
19
|
+
from mlpipe.artifacts.serialization import load_joblib, load_json, save_joblib, save_json
|
|
20
|
+
from mlpipe.core.exceptions import ArtifactError
|
|
21
|
+
from mlpipe.version import __version__
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class ArtifactManager:
|
|
25
|
+
"""Manages the creation, saving, and inspection of MLPipe run artifacts."""
|
|
26
|
+
|
|
27
|
+
def __init__(self, base_output_dir: Union[str, Path] = "./mlpipe_runs"):
|
|
28
|
+
self.base_output_dir = Path(base_output_dir)
|
|
29
|
+
|
|
30
|
+
def generate_run_id(self) -> str:
|
|
31
|
+
"""Create a deterministic-friendly, timestamped unique run ID."""
|
|
32
|
+
ts = datetime.now(timezone.utc).strftime("%Y%m%d_%H%M%S")
|
|
33
|
+
uid = uuid.uuid4().hex[:6]
|
|
34
|
+
return f"run_{ts}_{uid}"
|
|
35
|
+
|
|
36
|
+
def save_run_artifacts(
|
|
37
|
+
self,
|
|
38
|
+
run_id: str,
|
|
39
|
+
pipeline: Pipeline,
|
|
40
|
+
dataset_summary: Dict[str, Any],
|
|
41
|
+
target_column: str,
|
|
42
|
+
task_type: str,
|
|
43
|
+
training_mode: str,
|
|
44
|
+
primary_metric: str,
|
|
45
|
+
best_model_name: str,
|
|
46
|
+
best_cv_score: float,
|
|
47
|
+
test_score: float,
|
|
48
|
+
test_metrics: Dict[str, Any],
|
|
49
|
+
leaderboard: List[Dict[str, Any]],
|
|
50
|
+
feature_importance: List[Dict[str, Any]],
|
|
51
|
+
elapsed_time_s: float,
|
|
52
|
+
random_seed: int = 42,
|
|
53
|
+
train_df: Optional[pd.DataFrame] = None,
|
|
54
|
+
test_df: Optional[pd.DataFrame] = None,
|
|
55
|
+
test_predictions_df: Optional[pd.DataFrame] = None,
|
|
56
|
+
) -> Path:
|
|
57
|
+
"""Save all artifacts for a completed pipeline run."""
|
|
58
|
+
run_dir = self.base_output_dir / run_id
|
|
59
|
+
run_dir.mkdir(parents=True, exist_ok=True)
|
|
60
|
+
|
|
61
|
+
# 0. Save data splits if provided
|
|
62
|
+
if train_df is not None:
|
|
63
|
+
train_df.to_csv(run_dir / "train.csv", index=False)
|
|
64
|
+
if test_df is not None:
|
|
65
|
+
test_df.to_csv(run_dir / "test.csv", index=False)
|
|
66
|
+
if test_predictions_df is not None:
|
|
67
|
+
test_predictions_df.to_csv(run_dir / "test_predictions.csv", index=False)
|
|
68
|
+
|
|
69
|
+
# 1. Save complete pipeline (reusable end-to-end)
|
|
70
|
+
save_joblib(pipeline, run_dir / "pipeline.joblib")
|
|
71
|
+
|
|
72
|
+
# 2. Save fitted estimator alone
|
|
73
|
+
if "estimator" in pipeline.named_steps:
|
|
74
|
+
save_joblib(pipeline.named_steps["estimator"], run_dir / "model.joblib")
|
|
75
|
+
|
|
76
|
+
# 3. Save metrics
|
|
77
|
+
all_metrics = {
|
|
78
|
+
"primary_metric": primary_metric,
|
|
79
|
+
"best_cv_score": best_cv_score,
|
|
80
|
+
"test_score": test_score,
|
|
81
|
+
"test_metrics": test_metrics,
|
|
82
|
+
}
|
|
83
|
+
save_json(all_metrics, run_dir / "metrics.json")
|
|
84
|
+
|
|
85
|
+
# 4. Save leaderboard
|
|
86
|
+
# Strip fitted objects if any before serializing
|
|
87
|
+
clean_leaderboard = []
|
|
88
|
+
for row in leaderboard:
|
|
89
|
+
r = dict(row)
|
|
90
|
+
if "pipeline" in r:
|
|
91
|
+
del r["pipeline"]
|
|
92
|
+
clean_leaderboard.append(r)
|
|
93
|
+
save_json(clean_leaderboard, run_dir / "leaderboard.json")
|
|
94
|
+
|
|
95
|
+
# 5. Save feature importance
|
|
96
|
+
save_json(feature_importance, run_dir / "feature_importance.json")
|
|
97
|
+
|
|
98
|
+
# 6. Save metadata
|
|
99
|
+
metadata = {
|
|
100
|
+
"run_id": run_id,
|
|
101
|
+
"timestamp": datetime.now(timezone.utc).isoformat(),
|
|
102
|
+
"mlpipe_version": __version__,
|
|
103
|
+
"python_version": sys.version,
|
|
104
|
+
"platform": platform.platform(),
|
|
105
|
+
"dataset": dataset_summary,
|
|
106
|
+
"target_column": target_column,
|
|
107
|
+
"task_type": task_type,
|
|
108
|
+
"training_mode": training_mode,
|
|
109
|
+
"primary_metric": primary_metric,
|
|
110
|
+
"best_model": best_model_name,
|
|
111
|
+
"best_cv_score": best_cv_score,
|
|
112
|
+
"test_score": test_score,
|
|
113
|
+
"random_seed": random_seed,
|
|
114
|
+
"elapsed_time_s": elapsed_time_s,
|
|
115
|
+
"splits": {
|
|
116
|
+
"train_samples": len(train_df) if train_df is not None else None,
|
|
117
|
+
"test_samples": len(test_df) if test_df is not None else None,
|
|
118
|
+
"train_file": "train.csv" if train_df is not None else None,
|
|
119
|
+
"test_file": "test.csv" if test_df is not None else None,
|
|
120
|
+
"predictions_file": "test_predictions.csv" if test_predictions_df is not None else None,
|
|
121
|
+
},
|
|
122
|
+
}
|
|
123
|
+
save_json(metadata, run_dir / "metadata.json")
|
|
124
|
+
|
|
125
|
+
# 7. Generate text report
|
|
126
|
+
report_text = self._build_text_report(metadata, clean_leaderboard, test_metrics, feature_importance)
|
|
127
|
+
with open(run_dir / "report.txt", "w", encoding="utf-8") as f:
|
|
128
|
+
f.write(report_text)
|
|
129
|
+
|
|
130
|
+
return run_dir
|
|
131
|
+
|
|
132
|
+
def _build_text_report(
|
|
133
|
+
self,
|
|
134
|
+
meta: Dict[str, Any],
|
|
135
|
+
leaderboard: List[Dict[str, Any]],
|
|
136
|
+
test_metrics: Dict[str, Any],
|
|
137
|
+
feature_importance: List[Dict[str, Any]],
|
|
138
|
+
) -> str:
|
|
139
|
+
lines = [
|
|
140
|
+
f"==================================================",
|
|
141
|
+
f"MLPipe Run Report - {meta['run_id']}",
|
|
142
|
+
f"==================================================",
|
|
143
|
+
f"Timestamp: {meta['timestamp']}",
|
|
144
|
+
f"MLPipe Version: {meta['mlpipe_version']}",
|
|
145
|
+
f"Dataset: {meta['dataset'].get('filename', 'unknown')} ({meta['dataset'].get('rows')} rows, {meta['dataset'].get('columns')} cols)",
|
|
146
|
+
f"Dataset SHA256: {meta['dataset'].get('sha256', 'unknown')}",
|
|
147
|
+
f"Target Column: {meta['target_column']}",
|
|
148
|
+
f"Task Type: {meta['task_type'].capitalize()}",
|
|
149
|
+
f"Training Mode: {meta['training_mode']}",
|
|
150
|
+
f"Seed: {meta['random_seed']}",
|
|
151
|
+
f"Elapsed Time: {meta['elapsed_time_s']:.2f}s",
|
|
152
|
+
f"",
|
|
153
|
+
f"BEST MODEL",
|
|
154
|
+
f"--------------------------------------------------",
|
|
155
|
+
f"Model: {meta['best_model']}",
|
|
156
|
+
f"Primary Metric: {meta['primary_metric']}",
|
|
157
|
+
f"Best CV Score: {meta['best_cv_score']}",
|
|
158
|
+
f"Test Score: {meta['test_score']}",
|
|
159
|
+
f"",
|
|
160
|
+
f"MODEL LEADERBOARD",
|
|
161
|
+
f"--------------------------------------------------",
|
|
162
|
+
]
|
|
163
|
+
for row in leaderboard:
|
|
164
|
+
status = row.get("status", "unknown")
|
|
165
|
+
cv = row.get("cv_score", "N/A")
|
|
166
|
+
test = row.get("test_score", "N/A")
|
|
167
|
+
time_s = row.get("training_time_s", "N/A")
|
|
168
|
+
lines.append(f"{row['model']:<25} | CV: {str(cv):<8} | Test: {str(test):<8} | {time_s}s | {status}")
|
|
169
|
+
|
|
170
|
+
lines.extend([
|
|
171
|
+
f"",
|
|
172
|
+
f"TEST EVALUATION METRICS",
|
|
173
|
+
f"--------------------------------------------------",
|
|
174
|
+
])
|
|
175
|
+
for k, v in test_metrics.items():
|
|
176
|
+
if k != "confusion_matrix":
|
|
177
|
+
lines.append(f"{k:<20}: {v}")
|
|
178
|
+
|
|
179
|
+
splits = meta.get("splits", {})
|
|
180
|
+
if splits and splits.get("train_samples") is not None:
|
|
181
|
+
lines.extend([
|
|
182
|
+
f"",
|
|
183
|
+
f"DATA SPLITS & HOLD-OUT TEST SET",
|
|
184
|
+
f"--------------------------------------------------",
|
|
185
|
+
f"Training Samples: {splits.get('train_samples')} ({splits.get('train_file')})",
|
|
186
|
+
f"Testing Samples: {splits.get('test_samples')} ({splits.get('test_file')})",
|
|
187
|
+
f"Test Predictions: {splits.get('predictions_file')}",
|
|
188
|
+
])
|
|
189
|
+
|
|
190
|
+
if feature_importance:
|
|
191
|
+
lines.extend([
|
|
192
|
+
f"",
|
|
193
|
+
f"TOP FEATURE IMPORTANCES",
|
|
194
|
+
f"--------------------------------------------------",
|
|
195
|
+
])
|
|
196
|
+
for item in feature_importance[:10]:
|
|
197
|
+
lines.append(f"{item['feature']:<30}: {item['importance']}")
|
|
198
|
+
|
|
199
|
+
return "\n".join(lines)
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
def load_pipeline_artifact(path: Union[str, Path]) -> Pipeline:
|
|
203
|
+
"""
|
|
204
|
+
Load a fitted pipeline from either a run directory or direct joblib file path.
|
|
205
|
+
"""
|
|
206
|
+
p = Path(path).resolve()
|
|
207
|
+
if p.is_dir():
|
|
208
|
+
candidate_file = p / "pipeline.joblib"
|
|
209
|
+
if candidate_file.exists():
|
|
210
|
+
return load_joblib(candidate_file)
|
|
211
|
+
raise ArtifactError(
|
|
212
|
+
f"No 'pipeline.joblib' found in directory '{p}'.",
|
|
213
|
+
"Provide the path to the pipeline.joblib file or a valid MLPipe run directory."
|
|
214
|
+
)
|
|
215
|
+
|
|
216
|
+
if not p.exists():
|
|
217
|
+
raise ArtifactError(f"Pipeline file '{p}' does not exist.")
|
|
218
|
+
|
|
219
|
+
return load_joblib(p)
|
|
220
|
+
|
|
221
|
+
|
|
222
|
+
def inspect_run_directory(run_dir: Union[str, Path]) -> Dict[str, Any]:
|
|
223
|
+
"""Inspect and return metadata and artifacts summary from a run directory."""
|
|
224
|
+
dir_path = Path(run_dir).resolve()
|
|
225
|
+
if not dir_path.is_dir():
|
|
226
|
+
raise ArtifactError(f"Path '{dir_path}' is not a directory.")
|
|
227
|
+
|
|
228
|
+
meta_file = dir_path / "metadata.json"
|
|
229
|
+
if not meta_file.exists():
|
|
230
|
+
raise ArtifactError(
|
|
231
|
+
f"Directory '{dir_path.name}' does not contain metadata.json.",
|
|
232
|
+
"Verify that this is a completed MLPipe run directory."
|
|
233
|
+
)
|
|
234
|
+
|
|
235
|
+
metadata = load_json(meta_file)
|
|
236
|
+
|
|
237
|
+
# Check available artifacts
|
|
238
|
+
available_artifacts = [f.name for f in dir_path.iterdir() if f.is_file()]
|
|
239
|
+
metadata["artifacts"] = available_artifacts
|
|
240
|
+
|
|
241
|
+
# Load metrics if available
|
|
242
|
+
metrics_file = dir_path / "metrics.json"
|
|
243
|
+
if metrics_file.exists():
|
|
244
|
+
metadata["detailed_metrics"] = load_json(metrics_file)
|
|
245
|
+
|
|
246
|
+
return metadata
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
"""Serialization utilities using joblib and JSON."""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from typing import Any, Union
|
|
6
|
+
|
|
7
|
+
import joblib
|
|
8
|
+
|
|
9
|
+
from mlpipe.core.exceptions import ArtifactError
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def save_joblib(obj: Any, path: Union[str, Path]) -> Path:
|
|
13
|
+
"""Save a Python object using joblib."""
|
|
14
|
+
path = Path(path)
|
|
15
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
16
|
+
try:
|
|
17
|
+
joblib.dump(obj, path)
|
|
18
|
+
return path
|
|
19
|
+
except Exception as e:
|
|
20
|
+
raise ArtifactError(
|
|
21
|
+
f"Failed to serialize object to '{path}': {e}",
|
|
22
|
+
"Check disk permissions and available space."
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def load_joblib(path: Union[str, Path]) -> Any:
|
|
27
|
+
"""Load a Python object from a joblib file."""
|
|
28
|
+
path = Path(path)
|
|
29
|
+
if not path.exists():
|
|
30
|
+
raise ArtifactError(
|
|
31
|
+
f"Joblib file not found at '{path}'.",
|
|
32
|
+
"Verify that the file path is correct."
|
|
33
|
+
)
|
|
34
|
+
try:
|
|
35
|
+
return joblib.load(path)
|
|
36
|
+
except Exception as e:
|
|
37
|
+
raise ArtifactError(
|
|
38
|
+
f"Failed to load object from '{path}': {e}",
|
|
39
|
+
"Ensure the file was generated by a compatible version of joblib and scikit-learn."
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def save_json(data: Any, path: Union[str, Path], indent: int = 2) -> Path:
|
|
44
|
+
"""Save data structure to formatted JSON."""
|
|
45
|
+
path = Path(path)
|
|
46
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
47
|
+
try:
|
|
48
|
+
with open(path, "w", encoding="utf-8") as f:
|
|
49
|
+
json.dump(data, f, indent=indent, default=str)
|
|
50
|
+
return path
|
|
51
|
+
except Exception as e:
|
|
52
|
+
raise ArtifactError(
|
|
53
|
+
f"Failed to write JSON artifact to '{path}': {e}",
|
|
54
|
+
"Verify write permissions."
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def load_json(path: Union[str, Path]) -> Any:
|
|
59
|
+
"""Load data from a JSON file."""
|
|
60
|
+
path = Path(path)
|
|
61
|
+
if not path.exists():
|
|
62
|
+
raise ArtifactError(f"JSON artifact not found at '{path}'.")
|
|
63
|
+
try:
|
|
64
|
+
with open(path, "r", encoding="utf-8") as f:
|
|
65
|
+
return json.load(f)
|
|
66
|
+
except Exception as e:
|
|
67
|
+
raise ArtifactError(f"Failed to parse JSON artifact from '{path}': {e}")
|