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/cli/main.py
ADDED
|
@@ -0,0 +1,667 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Terminal Command Line Interface for MLPipe.
|
|
3
|
+
|
|
4
|
+
Built with Typer and Rich to deliver a polished, developer-focused terminal experience.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import json
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
import sys
|
|
10
|
+
from typing import Optional
|
|
11
|
+
|
|
12
|
+
import pandas as pd
|
|
13
|
+
from rich.console import Console
|
|
14
|
+
from rich.panel import Panel
|
|
15
|
+
from rich.table import Table
|
|
16
|
+
import typer
|
|
17
|
+
|
|
18
|
+
from mlpipe.artifacts.manager import inspect_run_directory
|
|
19
|
+
from mlpipe.core.config import TaskType, TrainingMode
|
|
20
|
+
from mlpipe.core.exceptions import MLPipeError
|
|
21
|
+
from mlpipe.core.pipeline import Pipeline
|
|
22
|
+
from mlpipe.data.ingestion import load_dataset
|
|
23
|
+
from mlpipe.data.profiling import profile_dataset
|
|
24
|
+
from mlpipe.data.splitting import split_data
|
|
25
|
+
from mlpipe.data.validation import detect_task, validate_dataset
|
|
26
|
+
from mlpipe.version import __version__
|
|
27
|
+
|
|
28
|
+
# Cross-platform encoding-safe glyphs
|
|
29
|
+
def _can_encode(char: str) -> bool:
|
|
30
|
+
try:
|
|
31
|
+
char.encode(sys.stdout.encoding or "utf-8")
|
|
32
|
+
return True
|
|
33
|
+
except Exception:
|
|
34
|
+
return False
|
|
35
|
+
|
|
36
|
+
_USE_UNICODE = _can_encode("✓") and _can_encode("—")
|
|
37
|
+
|
|
38
|
+
CHECK = "✓" if _USE_UNICODE else "[OK]"
|
|
39
|
+
CROSS = "✗" if _USE_UNICODE else "[X]"
|
|
40
|
+
WARN = "⚠" if _USE_UNICODE else "[!]"
|
|
41
|
+
DASH = "—" if _USE_UNICODE else "-"
|
|
42
|
+
ARROW = "→" if _USE_UNICODE else "->"
|
|
43
|
+
BULLET = "•" if _USE_UNICODE else "*"
|
|
44
|
+
RULE = "─" if _USE_UNICODE else "-"
|
|
45
|
+
|
|
46
|
+
app = typer.Typer(
|
|
47
|
+
name="mlpipe",
|
|
48
|
+
help="MLPipe: Production-ready tabular ML automation library and terminal CLI.",
|
|
49
|
+
add_completion=False,
|
|
50
|
+
no_args_is_help=True,
|
|
51
|
+
)
|
|
52
|
+
console = Console()
|
|
53
|
+
err_console = Console(stderr=True)
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def _version_callback(value: bool):
|
|
57
|
+
if value:
|
|
58
|
+
console.print(f"[bold cyan]MLPipe[/bold cyan] version [green]{__version__}[/green]")
|
|
59
|
+
raise typer.Exit()
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
@app.callback()
|
|
63
|
+
def main(
|
|
64
|
+
version: Optional[bool] = typer.Option(
|
|
65
|
+
None,
|
|
66
|
+
"--version",
|
|
67
|
+
"-v",
|
|
68
|
+
help="Show MLPipe version and exit.",
|
|
69
|
+
callback=_version_callback,
|
|
70
|
+
is_eager=True,
|
|
71
|
+
),
|
|
72
|
+
):
|
|
73
|
+
"""Automate the machine learning lifecycle directly from the terminal."""
|
|
74
|
+
pass
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
@app.command("version")
|
|
78
|
+
def version_cmd():
|
|
79
|
+
"""Display the installed MLPipe version."""
|
|
80
|
+
console.print(f"[bold cyan]MLPipe[/bold cyan] v[green]{__version__}[/green]")
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
# ─── 1. Profile Command ───────────────────────────────────────────────────────
|
|
84
|
+
|
|
85
|
+
@app.command("profile")
|
|
86
|
+
def profile_cmd(
|
|
87
|
+
dataset_path: Path = typer.Argument(
|
|
88
|
+
...,
|
|
89
|
+
help="Path to the CSV dataset.",
|
|
90
|
+
exists=True,
|
|
91
|
+
dir_okay=False,
|
|
92
|
+
readable=True,
|
|
93
|
+
),
|
|
94
|
+
format: str = typer.Option(
|
|
95
|
+
"human",
|
|
96
|
+
"--format",
|
|
97
|
+
"-f",
|
|
98
|
+
help="Output format: 'human' or 'json'.",
|
|
99
|
+
),
|
|
100
|
+
):
|
|
101
|
+
"""Profile a dataset and view structure, data types, missingness, and distributions."""
|
|
102
|
+
try:
|
|
103
|
+
ds = load_dataset(dataset_path)
|
|
104
|
+
profile = profile_dataset(ds)
|
|
105
|
+
|
|
106
|
+
if format.lower() == "json":
|
|
107
|
+
print(profile.to_json())
|
|
108
|
+
return
|
|
109
|
+
|
|
110
|
+
# Human-readable output
|
|
111
|
+
console.print(f"\n[bold cyan]MLPipe Dataset Profile[/bold cyan] {DASH} [bold]{ds.filename}[/bold]\n")
|
|
112
|
+
|
|
113
|
+
# Overview Table
|
|
114
|
+
ov_table = Table(title="Dataset Overview", show_header=False, border_style="dim")
|
|
115
|
+
ov_table.add_row("Rows", f"{profile.num_rows:,}")
|
|
116
|
+
ov_table.add_row("Columns", f"{profile.num_cols:,}")
|
|
117
|
+
ov_table.add_row("Memory Usage", f"{profile.memory_mb} MB")
|
|
118
|
+
ov_table.add_row("Duplicate Rows", f"{profile.duplicate_rows:,}")
|
|
119
|
+
ov_table.add_row("Total Missing Values", f"{profile.total_missing_values:,}")
|
|
120
|
+
console.print(ov_table)
|
|
121
|
+
console.print()
|
|
122
|
+
|
|
123
|
+
# Columns Table
|
|
124
|
+
col_table = Table(title="Column Details", header_style="bold magenta")
|
|
125
|
+
col_table.add_column("Column", style="cyan")
|
|
126
|
+
col_table.add_column("Type", style="yellow")
|
|
127
|
+
col_table.add_column("Missing", justify="right")
|
|
128
|
+
col_table.add_column("Missing %", justify="right")
|
|
129
|
+
col_table.add_column("Unique", justify="right")
|
|
130
|
+
col_table.add_column("Stats / Top Values", style="dim")
|
|
131
|
+
col_table.add_column("Flags", style="red")
|
|
132
|
+
|
|
133
|
+
for c in profile.columns:
|
|
134
|
+
stats_str = ""
|
|
135
|
+
if c.detected_type == "numeric":
|
|
136
|
+
stats_str = f"min: {c.min_val}, max: {c.max_val}, mean: {c.mean_val}"
|
|
137
|
+
elif c.top_values:
|
|
138
|
+
stats_str = ", ".join(f"{v['value']} ({v['count']})" for v in c.top_values[:3])
|
|
139
|
+
elif c.min_date:
|
|
140
|
+
stats_str = f"{c.min_date} {ARROW} {c.max_date}"
|
|
141
|
+
|
|
142
|
+
flags_str = "; ".join(c.flags) if c.flags else DASH
|
|
143
|
+
|
|
144
|
+
col_table.add_row(
|
|
145
|
+
c.name,
|
|
146
|
+
c.detected_type,
|
|
147
|
+
f"{c.missing_count:,}",
|
|
148
|
+
f"{c.missing_pct:.1f}%",
|
|
149
|
+
f"{c.unique_count:,}",
|
|
150
|
+
stats_str,
|
|
151
|
+
flags_str,
|
|
152
|
+
)
|
|
153
|
+
|
|
154
|
+
console.print(col_table)
|
|
155
|
+
|
|
156
|
+
if profile.warnings:
|
|
157
|
+
console.print("\n[bold yellow]Dataset Warnings:[/bold yellow]")
|
|
158
|
+
for w in profile.warnings:
|
|
159
|
+
console.print(f" [yellow]{WARN}[/yellow] {w}")
|
|
160
|
+
console.print()
|
|
161
|
+
|
|
162
|
+
except MLPipeError as e:
|
|
163
|
+
err_console.print(f"[bold red]Error:[/bold red] {e}")
|
|
164
|
+
raise typer.Exit(code=1)
|
|
165
|
+
except Exception as e:
|
|
166
|
+
err_console.print(f"[bold red]Unexpected Error:[/bold red] {e}")
|
|
167
|
+
raise typer.Exit(code=1)
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
# ─── 2. Validate Command ──────────────────────────────────────────────────────
|
|
171
|
+
|
|
172
|
+
@app.command("validate")
|
|
173
|
+
def validate_cmd(
|
|
174
|
+
dataset_path: Path = typer.Argument(
|
|
175
|
+
...,
|
|
176
|
+
help="Path to the CSV dataset.",
|
|
177
|
+
exists=True,
|
|
178
|
+
dir_okay=False,
|
|
179
|
+
readable=True,
|
|
180
|
+
),
|
|
181
|
+
target: str = typer.Option(
|
|
182
|
+
...,
|
|
183
|
+
"--target",
|
|
184
|
+
"-t",
|
|
185
|
+
help="Name of the target column to predict.",
|
|
186
|
+
),
|
|
187
|
+
task: str = typer.Option(
|
|
188
|
+
"auto",
|
|
189
|
+
"--task",
|
|
190
|
+
help="Task type override ('auto', 'classification', 'regression').",
|
|
191
|
+
),
|
|
192
|
+
format: str = typer.Option(
|
|
193
|
+
"human",
|
|
194
|
+
"--format",
|
|
195
|
+
"-f",
|
|
196
|
+
help="Output format: 'human' or 'json'.",
|
|
197
|
+
),
|
|
198
|
+
):
|
|
199
|
+
"""Validate dataset suitability and verify pre-training feasibility."""
|
|
200
|
+
try:
|
|
201
|
+
ds = load_dataset(dataset_path)
|
|
202
|
+
report = validate_dataset(ds, target_column=target, task_override=task)
|
|
203
|
+
|
|
204
|
+
if format.lower() == "json":
|
|
205
|
+
print(report.to_json())
|
|
206
|
+
if not report.is_valid:
|
|
207
|
+
raise typer.Exit(code=1)
|
|
208
|
+
return
|
|
209
|
+
|
|
210
|
+
console.print(f"\n[bold cyan]MLPipe Data Validation[/bold cyan] {DASH} [bold]{ds.filename}[/bold]\n")
|
|
211
|
+
|
|
212
|
+
# Status
|
|
213
|
+
status_color = "green" if report.is_valid else "red"
|
|
214
|
+
status_text = "PASSED" if report.is_valid else "FAILED"
|
|
215
|
+
console.print(f"Status: [bold {status_color}]{status_text}[/bold {status_color}]")
|
|
216
|
+
console.print(f"Detected Task: [bold]{report.detected_task.capitalize()}[/bold]")
|
|
217
|
+
console.print(f"Target Column: [bold]{target}[/bold]\n")
|
|
218
|
+
|
|
219
|
+
if report.errors:
|
|
220
|
+
console.print("[bold red]Fatal Errors:[/bold red]")
|
|
221
|
+
for err in report.errors:
|
|
222
|
+
console.print(f" [bold red]{CROSS}[/bold red] {err}")
|
|
223
|
+
console.print()
|
|
224
|
+
|
|
225
|
+
if report.warnings:
|
|
226
|
+
console.print("[bold yellow]Warnings (Non-blocking):[/bold yellow]")
|
|
227
|
+
for warn in report.warnings:
|
|
228
|
+
console.print(f" [bold yellow]{WARN}[/bold yellow] {warn}")
|
|
229
|
+
console.print()
|
|
230
|
+
|
|
231
|
+
if report.is_valid:
|
|
232
|
+
console.print(f"[bold green]{CHECK} Dataset is ready for automated training.[/bold green]\n")
|
|
233
|
+
else:
|
|
234
|
+
raise typer.Exit(code=1)
|
|
235
|
+
|
|
236
|
+
except typer.Exit:
|
|
237
|
+
raise
|
|
238
|
+
except MLPipeError as e:
|
|
239
|
+
err_console.print(f"[bold red]Error:[/bold red] {e}")
|
|
240
|
+
raise typer.Exit(code=1)
|
|
241
|
+
except Exception as e:
|
|
242
|
+
err_console.print(f"[bold red]Unexpected Error:[/bold red] {e}")
|
|
243
|
+
raise typer.Exit(code=1)
|
|
244
|
+
|
|
245
|
+
|
|
246
|
+
# ─── 3. Train Command ─────────────────────────────────────────────────────────
|
|
247
|
+
|
|
248
|
+
@app.command("train")
|
|
249
|
+
def train_cmd(
|
|
250
|
+
dataset_path: Path = typer.Argument(
|
|
251
|
+
...,
|
|
252
|
+
help="Path to the CSV dataset to train on.",
|
|
253
|
+
exists=True,
|
|
254
|
+
dir_okay=False,
|
|
255
|
+
readable=True,
|
|
256
|
+
),
|
|
257
|
+
target: str = typer.Option(
|
|
258
|
+
...,
|
|
259
|
+
"--target",
|
|
260
|
+
"-t",
|
|
261
|
+
help="Name of the target column to predict.",
|
|
262
|
+
),
|
|
263
|
+
task: str = typer.Option(
|
|
264
|
+
"auto",
|
|
265
|
+
"--task",
|
|
266
|
+
help="Task type: 'auto', 'classification', or 'regression'.",
|
|
267
|
+
),
|
|
268
|
+
mode: str = typer.Option(
|
|
269
|
+
"balanced",
|
|
270
|
+
"--mode",
|
|
271
|
+
"-m",
|
|
272
|
+
help="Training mode budget: 'fast', 'balanced', or 'thorough'.",
|
|
273
|
+
),
|
|
274
|
+
output: Path = typer.Option(
|
|
275
|
+
Path("./mlpipe_runs"),
|
|
276
|
+
"--output",
|
|
277
|
+
"-o",
|
|
278
|
+
help="Base directory to save run artifacts.",
|
|
279
|
+
),
|
|
280
|
+
format: str = typer.Option(
|
|
281
|
+
"human",
|
|
282
|
+
"--format",
|
|
283
|
+
"-f",
|
|
284
|
+
help="Output format: 'human' or 'json'.",
|
|
285
|
+
),
|
|
286
|
+
verbose: bool = typer.Option(
|
|
287
|
+
False,
|
|
288
|
+
"--verbose",
|
|
289
|
+
help="Enable detailed logging.",
|
|
290
|
+
),
|
|
291
|
+
export_splits: Optional[Path] = typer.Option(
|
|
292
|
+
None,
|
|
293
|
+
"--export-splits",
|
|
294
|
+
"-s",
|
|
295
|
+
help="Optional destination directory to copy train.csv, test.csv, and test_predictions.csv.",
|
|
296
|
+
),
|
|
297
|
+
):
|
|
298
|
+
"""Train multiple candidate ML models, tune hyperparameters, evaluate, and save artifacts."""
|
|
299
|
+
try:
|
|
300
|
+
pipeline = Pipeline(
|
|
301
|
+
target=target,
|
|
302
|
+
task=task,
|
|
303
|
+
mode=mode,
|
|
304
|
+
output_dir=output,
|
|
305
|
+
verbose=verbose,
|
|
306
|
+
)
|
|
307
|
+
|
|
308
|
+
if format.lower() != "json":
|
|
309
|
+
console.print(f"\n[bold cyan]MLPipe[/bold cyan] v[green]{__version__}[/green]\n")
|
|
310
|
+
|
|
311
|
+
# Load dataset for display header
|
|
312
|
+
ds = load_dataset(dataset_path)
|
|
313
|
+
|
|
314
|
+
console.print("[bold]Dataset[/bold]")
|
|
315
|
+
console.print(RULE * 36)
|
|
316
|
+
console.print(f"File {ds.filename}")
|
|
317
|
+
console.print(f"Rows {ds.num_rows:,}")
|
|
318
|
+
console.print(f"Columns {ds.num_cols:,}")
|
|
319
|
+
console.print(f"Target {target}\n")
|
|
320
|
+
|
|
321
|
+
def progress_hook(stage: str, msg: str):
|
|
322
|
+
if stage == "ingestion":
|
|
323
|
+
console.print(f"[green]{CHECK}[/green] Dataset loaded")
|
|
324
|
+
elif stage == "profiling":
|
|
325
|
+
console.print(f"[green]{CHECK}[/green] Dataset profiled")
|
|
326
|
+
elif stage == "validation":
|
|
327
|
+
console.print(f"[green]{CHECK}[/green] Validation passed")
|
|
328
|
+
elif stage == "task_detection":
|
|
329
|
+
console.print(f"[green]{CHECK}[/green] {msg}")
|
|
330
|
+
elif stage == "splitting":
|
|
331
|
+
console.print(f"[green]{CHECK}[/green] Data split into Training & Testing sets")
|
|
332
|
+
elif stage == "preprocessing":
|
|
333
|
+
console.print("\n[bold]Preprocessing[/bold]")
|
|
334
|
+
console.print(f"[green]{CHECK}[/green] Numeric features")
|
|
335
|
+
console.print(f"[green]{CHECK}[/green] Categorical features")
|
|
336
|
+
console.print(f"[green]{CHECK}[/green] Missing-value handling\n")
|
|
337
|
+
console.print("[bold]Training[/bold]")
|
|
338
|
+
console.print(RULE * 36)
|
|
339
|
+
elif stage == "training":
|
|
340
|
+
model_part = msg.replace("Training & tuning candidate: ", "").replace("...", "")
|
|
341
|
+
console.print(f"[green]{CHECK}[/green] {model_part}")
|
|
342
|
+
|
|
343
|
+
result = pipeline.fit(ds, on_progress=progress_hook)
|
|
344
|
+
|
|
345
|
+
# Leaderboard Table
|
|
346
|
+
console.print("\n[bold]Model Evaluation[/bold]")
|
|
347
|
+
console.print(RULE * 36)
|
|
348
|
+
|
|
349
|
+
lb_table = Table(header_style="bold cyan", border_style="dim")
|
|
350
|
+
lb_table.add_column("Model", style="bold")
|
|
351
|
+
lb_table.add_column(f"CV ({result.primary_metric})", justify="right", style="yellow")
|
|
352
|
+
lb_table.add_column(f"Test ({result.primary_metric})", justify="right", style="green")
|
|
353
|
+
lb_table.add_column("Time", justify="right", style="dim")
|
|
354
|
+
lb_table.add_column("Status")
|
|
355
|
+
|
|
356
|
+
for row in result.leaderboard:
|
|
357
|
+
cv_val = f"{row['cv_score']:.4f}" if row['cv_score'] is not None else DASH
|
|
358
|
+
test_val = f"{row['test_score']:.4f}" if row['test_score'] is not None else DASH
|
|
359
|
+
time_val = f"{row['training_time_s']:.1f}s"
|
|
360
|
+
status_val = f"[green]{CHECK}[/green]" if row['status'] == "success" else f"[red]{CROSS} ({row.get('error', 'fail')})[/red]"
|
|
361
|
+
|
|
362
|
+
lb_table.add_row(row["model"], cv_val, test_val, time_val, status_val)
|
|
363
|
+
|
|
364
|
+
console.print(lb_table)
|
|
365
|
+
console.print(f"\n[green]{CHECK} Best model selected[/green]\n")
|
|
366
|
+
|
|
367
|
+
# Best Model Panel
|
|
368
|
+
panel_content = (
|
|
369
|
+
f"[bold green]{result.best_model_name}[/bold green]\n"
|
|
370
|
+
f"Primary metric: [bold]{result.primary_metric}[/bold]\n"
|
|
371
|
+
f"CV Score: [bold]{result.best_cv_score:.4f}[/bold]\n"
|
|
372
|
+
f"Test Score: [bold]{result.test_score:.4f}[/bold]"
|
|
373
|
+
)
|
|
374
|
+
console.print(Panel(panel_content, title="Best Model", border_style="green"))
|
|
375
|
+
|
|
376
|
+
# Hold-Out Test Set Verification Preview (Results displayed in front of the user)
|
|
377
|
+
if result.test_preview:
|
|
378
|
+
console.print(f"\n[bold]Hold-Out Test Set Verification Preview[/bold] (First {len(result.test_preview)} rows)")
|
|
379
|
+
console.print(RULE * 44)
|
|
380
|
+
|
|
381
|
+
test_table = Table(header_style="bold cyan", border_style="dim")
|
|
382
|
+
test_table.add_column("Row", style="dim", justify="right")
|
|
383
|
+
test_table.add_column("Sample Features", style="cyan")
|
|
384
|
+
test_table.add_column(f"Actual ({target})", justify="right", style="bold yellow")
|
|
385
|
+
test_table.add_column(f"Predicted ({target})", justify="right", style="bold green")
|
|
386
|
+
test_table.add_column("Evaluation", justify="center")
|
|
387
|
+
|
|
388
|
+
for row_data in result.test_preview:
|
|
389
|
+
feat_str = ", ".join(f"{k}={v}" for k, v in row_data.get("features", {}).items())
|
|
390
|
+
act_val = row_data["actual"]
|
|
391
|
+
pred_val = row_data["predicted"]
|
|
392
|
+
|
|
393
|
+
if result.task_type == "classification":
|
|
394
|
+
match = row_data.get("match", False)
|
|
395
|
+
eval_str = f"[bold green]{CHECK} Match[/bold green]" if match else f"[bold red]{CROSS} Mismatch[/bold red]"
|
|
396
|
+
test_table.add_row(f"#{row_data['row_idx']}", feat_str, str(act_val), str(pred_val), eval_str)
|
|
397
|
+
else:
|
|
398
|
+
err = row_data.get("error", 0.0)
|
|
399
|
+
eval_str = f"Diff: {err:,.2f}"
|
|
400
|
+
test_table.add_row(
|
|
401
|
+
f"#{row_data['row_idx']}",
|
|
402
|
+
feat_str,
|
|
403
|
+
f"{act_val:,.2f}" if isinstance(act_val, (int, float)) else str(act_val),
|
|
404
|
+
f"{pred_val:,.2f}" if isinstance(pred_val, (int, float)) else str(pred_val),
|
|
405
|
+
eval_str,
|
|
406
|
+
)
|
|
407
|
+
|
|
408
|
+
console.print(test_table)
|
|
409
|
+
|
|
410
|
+
# Generated Datasets and Artifacts
|
|
411
|
+
console.print("\n[bold]Generated Datasets & Artifacts[/bold]")
|
|
412
|
+
console.print(RULE * 44)
|
|
413
|
+
if result.train_path and result.train_path.exists():
|
|
414
|
+
console.print(f"[green]{CHECK}[/green] Training dataset: [bold cyan]{result.train_path.name}[/bold cyan] ({result.metadata.get('train_rows', 0):,} rows)")
|
|
415
|
+
if result.test_path and result.test_path.exists():
|
|
416
|
+
console.print(f"[green]{CHECK}[/green] Testing dataset: [bold cyan]{result.test_path.name}[/bold cyan] ({result.metadata.get('test_rows', 0):,} rows)")
|
|
417
|
+
if result.test_predictions_path and result.test_predictions_path.exists():
|
|
418
|
+
console.print(f"[green]{CHECK}[/green] Test predictions: [bold cyan]{result.test_predictions_path.name}[/bold cyan] (features + actual + predicted)")
|
|
419
|
+
console.print(f"[green]{CHECK}[/green] Pipeline saved: [bold cyan]pipeline.joblib[/bold cyan]")
|
|
420
|
+
console.print(f"[green]{CHECK}[/green] Model saved: [bold cyan]model.joblib[/bold cyan]")
|
|
421
|
+
console.print(f"[green]{CHECK}[/green] Metrics saved: [bold cyan]metrics.json[/bold cyan]")
|
|
422
|
+
console.print(f"[green]{CHECK}[/green] Run report: [bold cyan]report.txt[/bold cyan]\n")
|
|
423
|
+
|
|
424
|
+
if export_splits:
|
|
425
|
+
import shutil
|
|
426
|
+
export_splits.mkdir(parents=True, exist_ok=True)
|
|
427
|
+
if result.train_path and result.train_path.exists():
|
|
428
|
+
shutil.copy(result.train_path, export_splits / "train.csv")
|
|
429
|
+
if result.test_path and result.test_path.exists():
|
|
430
|
+
shutil.copy(result.test_path, export_splits / "test.csv")
|
|
431
|
+
if result.test_predictions_path and result.test_predictions_path.exists():
|
|
432
|
+
shutil.copy(result.test_predictions_path, export_splits / "test_predictions.csv")
|
|
433
|
+
console.print(f"[green]{CHECK}[/green] Exported splits copied to: [bold]{export_splits}[/bold]\n")
|
|
434
|
+
|
|
435
|
+
console.print(f"[bold]Output Directory:[/bold]\n{result.artifacts_dir}\n")
|
|
436
|
+
|
|
437
|
+
else:
|
|
438
|
+
# JSON format
|
|
439
|
+
result = pipeline.fit(dataset_path)
|
|
440
|
+
print(json.dumps(result.to_dict(), indent=2))
|
|
441
|
+
|
|
442
|
+
except MLPipeError as e:
|
|
443
|
+
err_console.print(f"\n[bold red]MLPipe Error:[/bold red]\n{e}")
|
|
444
|
+
raise typer.Exit(code=1)
|
|
445
|
+
except Exception as e:
|
|
446
|
+
err_console.print(f"\n[bold red]Unexpected Error:[/bold red] {e}")
|
|
447
|
+
if verbose:
|
|
448
|
+
err_console.print_exception()
|
|
449
|
+
raise typer.Exit(code=1)
|
|
450
|
+
|
|
451
|
+
|
|
452
|
+
# ─── 4. Predict Command ───────────────────────────────────────────────────────
|
|
453
|
+
|
|
454
|
+
@app.command("predict")
|
|
455
|
+
def predict_cmd(
|
|
456
|
+
model_path: Path = typer.Argument(
|
|
457
|
+
...,
|
|
458
|
+
help="Path to pipeline.joblib or run directory.",
|
|
459
|
+
exists=True,
|
|
460
|
+
),
|
|
461
|
+
data_path: Path = typer.Argument(
|
|
462
|
+
...,
|
|
463
|
+
help="Path to the new CSV data for predictions.",
|
|
464
|
+
exists=True,
|
|
465
|
+
dir_okay=False,
|
|
466
|
+
),
|
|
467
|
+
output: Optional[Path] = typer.Option(
|
|
468
|
+
None,
|
|
469
|
+
"--output",
|
|
470
|
+
"-o",
|
|
471
|
+
help="Optional path to save predictions as a CSV.",
|
|
472
|
+
),
|
|
473
|
+
):
|
|
474
|
+
"""Generate predictions on new data using a trained MLPipe pipeline."""
|
|
475
|
+
try:
|
|
476
|
+
pipeline = Pipeline.load(model_path)
|
|
477
|
+
predictions = pipeline.predict(data_path)
|
|
478
|
+
|
|
479
|
+
console.print("\n[bold cyan]Predictions[/bold cyan]")
|
|
480
|
+
console.print(RULE * 24)
|
|
481
|
+
|
|
482
|
+
# Show up to first 10 predictions
|
|
483
|
+
sample_size = min(10, len(predictions))
|
|
484
|
+
for i in range(sample_size):
|
|
485
|
+
console.print(f"Row {i+1:<4} [bold]{predictions[i]}[/bold]")
|
|
486
|
+
|
|
487
|
+
if len(predictions) > sample_size:
|
|
488
|
+
console.print(f"[dim]... and {len(predictions) - sample_size:,} more rows[/dim]")
|
|
489
|
+
|
|
490
|
+
console.print(f"\n[green]{CHECK}[/green] [bold]{len(predictions):,}[/bold] predictions generated.\n")
|
|
491
|
+
|
|
492
|
+
if output:
|
|
493
|
+
out_df = pd.DataFrame({"prediction": predictions})
|
|
494
|
+
out_df.to_csv(output, index=False)
|
|
495
|
+
console.print(f"[green]{CHECK}[/green] Saved predictions to [bold]{output}[/bold]\n")
|
|
496
|
+
|
|
497
|
+
except MLPipeError as e:
|
|
498
|
+
err_console.print(f"[bold red]Error:[/bold red] {e}")
|
|
499
|
+
raise typer.Exit(code=1)
|
|
500
|
+
except Exception as e:
|
|
501
|
+
err_console.print(f"[bold red]Unexpected Error:[/bold red] {e}")
|
|
502
|
+
raise typer.Exit(code=1)
|
|
503
|
+
|
|
504
|
+
|
|
505
|
+
# ─── 5. Inspect Command ───────────────────────────────────────────────────────
|
|
506
|
+
|
|
507
|
+
@app.command("inspect")
|
|
508
|
+
def inspect_cmd(
|
|
509
|
+
run_dir: Path = typer.Argument(
|
|
510
|
+
...,
|
|
511
|
+
help="Path to the MLPipe run directory (e.g. ./mlpipe_runs/<run_id>).",
|
|
512
|
+
exists=True,
|
|
513
|
+
file_okay=False,
|
|
514
|
+
),
|
|
515
|
+
format: str = typer.Option(
|
|
516
|
+
"human",
|
|
517
|
+
"--format",
|
|
518
|
+
"-f",
|
|
519
|
+
help="Output format: 'human' or 'json'.",
|
|
520
|
+
),
|
|
521
|
+
):
|
|
522
|
+
"""Inspect previous run metadata, leaderboard, and artifacts."""
|
|
523
|
+
try:
|
|
524
|
+
meta = inspect_run_directory(run_dir)
|
|
525
|
+
|
|
526
|
+
if format.lower() == "json":
|
|
527
|
+
print(json.dumps(meta, indent=2))
|
|
528
|
+
return
|
|
529
|
+
|
|
530
|
+
console.print(f"\n[bold cyan]MLPipe Run Inspection[/bold cyan] {DASH} [bold]{meta.get('run_id')}[/bold]\n")
|
|
531
|
+
|
|
532
|
+
table = Table(title="Run Summary", show_header=False, border_style="dim")
|
|
533
|
+
table.add_row("Run ID", str(meta.get("run_id")))
|
|
534
|
+
table.add_row("Timestamp", str(meta.get("timestamp")))
|
|
535
|
+
table.add_row("Dataset", f"{meta.get('dataset', {}).get('filename')} ({meta.get('dataset', {}).get('rows')} rows)")
|
|
536
|
+
table.add_row("Target", str(meta.get("target_column")))
|
|
537
|
+
table.add_row("Task", str(meta.get("task_type")).capitalize())
|
|
538
|
+
table.add_row("Best Model", f"[bold green]{meta.get('best_model')}[/bold green]")
|
|
539
|
+
table.add_row("Primary Metric", str(meta.get("primary_metric")))
|
|
540
|
+
table.add_row("CV Score", str(meta.get("best_cv_score")))
|
|
541
|
+
table.add_row("Test Score", str(meta.get("test_score")))
|
|
542
|
+
table.add_row("Training Mode", str(meta.get("training_mode")))
|
|
543
|
+
table.add_row("Seed", str(meta.get("random_seed")))
|
|
544
|
+
table.add_row("Elapsed Time", f"{meta.get('elapsed_time_s', 0):.2f}s")
|
|
545
|
+
|
|
546
|
+
if "splits" in meta and meta["splits"].get("train_samples") is not None:
|
|
547
|
+
sp = meta["splits"]
|
|
548
|
+
table.add_row("Training Split", f"{sp['train_samples']:,} rows ({sp.get('train_file')})")
|
|
549
|
+
table.add_row("Testing Split", f"{sp['test_samples']:,} rows ({sp.get('test_file')})")
|
|
550
|
+
table.add_row("Predictions File", str(sp.get("predictions_file")))
|
|
551
|
+
|
|
552
|
+
console.print(table)
|
|
553
|
+
console.print()
|
|
554
|
+
|
|
555
|
+
# Available Artifacts
|
|
556
|
+
artifacts = meta.get("artifacts", [])
|
|
557
|
+
console.print("[bold]Generated Artifacts:[/bold]")
|
|
558
|
+
for art in artifacts:
|
|
559
|
+
console.print(f" [green]{BULLET}[/green] {art}")
|
|
560
|
+
console.print()
|
|
561
|
+
|
|
562
|
+
except MLPipeError as e:
|
|
563
|
+
err_console.print(f"[bold red]Error:[/bold red] {e}")
|
|
564
|
+
raise typer.Exit(code=1)
|
|
565
|
+
except Exception as e:
|
|
566
|
+
err_console.print(f"[bold red]Unexpected Error:[/bold red] {e}")
|
|
567
|
+
raise typer.Exit(code=1)
|
|
568
|
+
|
|
569
|
+
|
|
570
|
+
# ─── 6. Split Command ─────────────────────────────────────────────────────────
|
|
571
|
+
|
|
572
|
+
@app.command("split")
|
|
573
|
+
def split_cmd(
|
|
574
|
+
dataset_path: Path = typer.Argument(
|
|
575
|
+
...,
|
|
576
|
+
help="Path to the CSV dataset to split.",
|
|
577
|
+
exists=True,
|
|
578
|
+
dir_okay=False,
|
|
579
|
+
readable=True,
|
|
580
|
+
),
|
|
581
|
+
target: str = typer.Option(
|
|
582
|
+
...,
|
|
583
|
+
"--target",
|
|
584
|
+
"-t",
|
|
585
|
+
help="Target column to stratify (for classification) and preserve in splits.",
|
|
586
|
+
),
|
|
587
|
+
test_size: float = typer.Option(
|
|
588
|
+
0.20,
|
|
589
|
+
"--test-size",
|
|
590
|
+
"-s",
|
|
591
|
+
help="Fraction of data for the holdout test set (default: 0.20).",
|
|
592
|
+
),
|
|
593
|
+
output_dir: Path = typer.Option(
|
|
594
|
+
Path("./splits"),
|
|
595
|
+
"--output-dir",
|
|
596
|
+
"-o",
|
|
597
|
+
help="Directory to save train.csv and test.csv.",
|
|
598
|
+
),
|
|
599
|
+
seed: int = typer.Option(
|
|
600
|
+
42,
|
|
601
|
+
"--seed",
|
|
602
|
+
help="Random seed for reproducible splitting.",
|
|
603
|
+
),
|
|
604
|
+
):
|
|
605
|
+
"""Explicitly split a dataset into train.csv and test.csv without data leakage."""
|
|
606
|
+
try:
|
|
607
|
+
ds = load_dataset(dataset_path)
|
|
608
|
+
if target not in ds.data.columns:
|
|
609
|
+
raise MLPipeError(f"Target column '{target}' not found in dataset columns: {list(ds.data.columns)}")
|
|
610
|
+
|
|
611
|
+
task_type = detect_task(ds.data[target])
|
|
612
|
+
split_res = split_data(
|
|
613
|
+
df=ds.data,
|
|
614
|
+
target_column=target,
|
|
615
|
+
task_type=task_type,
|
|
616
|
+
test_size=test_size,
|
|
617
|
+
random_seed=seed,
|
|
618
|
+
)
|
|
619
|
+
|
|
620
|
+
train_file, test_file = split_res.export(output_dir)
|
|
621
|
+
|
|
622
|
+
console.print(f"\n[bold cyan]MLPipe Dataset Split[/bold cyan] {DASH} [bold]{ds.filename}[/bold]\n")
|
|
623
|
+
|
|
624
|
+
table = Table(title="Data Splits Summary", header_style="bold cyan", border_style="dim")
|
|
625
|
+
table.add_column("Set", style="bold")
|
|
626
|
+
table.add_column("Rows", justify="right")
|
|
627
|
+
table.add_column("Percentage", justify="right")
|
|
628
|
+
table.add_column("Stratified", justify="center")
|
|
629
|
+
table.add_column("Saved Path", style="dim")
|
|
630
|
+
|
|
631
|
+
table.add_row(
|
|
632
|
+
"Training Set",
|
|
633
|
+
f"{split_res.train_size:,}",
|
|
634
|
+
f"{100*(1-test_size):.1f}%",
|
|
635
|
+
f"[green]{CHECK}[/green]" if split_res.is_stratified else DASH,
|
|
636
|
+
str(train_file.resolve()),
|
|
637
|
+
)
|
|
638
|
+
table.add_row(
|
|
639
|
+
"Testing Set",
|
|
640
|
+
f"{split_res.test_size:,}",
|
|
641
|
+
f"{100*test_size:.1f}%",
|
|
642
|
+
f"[green]{CHECK}[/green]" if split_res.is_stratified else DASH,
|
|
643
|
+
str(test_file.resolve()),
|
|
644
|
+
)
|
|
645
|
+
|
|
646
|
+
console.print(table)
|
|
647
|
+
|
|
648
|
+
# Show preview of test set rows right in front of user
|
|
649
|
+
console.print(f"\n[bold]Testing Set Preview[/bold] (First 5 hold-out rows)")
|
|
650
|
+
console.print(RULE * 36)
|
|
651
|
+
preview_cols = [c for c in split_res.test_df.columns if c != target][:3] + [target]
|
|
652
|
+
prev_table = Table(header_style="bold magenta", border_style="dim")
|
|
653
|
+
for c in preview_cols:
|
|
654
|
+
prev_table.add_column(c, style="bold yellow" if c == target else "white")
|
|
655
|
+
for _, row in split_res.test_df.head(5).iterrows():
|
|
656
|
+
prev_table.add_row(*[str(row[c]) for c in preview_cols])
|
|
657
|
+
console.print(prev_table)
|
|
658
|
+
|
|
659
|
+
console.print(f"\n[green]{CHECK}[/green] Datasets successfully exported to [bold]{output_dir}[/bold]\n")
|
|
660
|
+
|
|
661
|
+
except MLPipeError as e:
|
|
662
|
+
err_console.print(f"[bold red]Error:[/bold red] {e}")
|
|
663
|
+
raise typer.Exit(code=1)
|
|
664
|
+
except Exception as e:
|
|
665
|
+
err_console.print(f"[bold red]Unexpected Error:[/bold red] {e}")
|
|
666
|
+
raise typer.Exit(code=1)
|
|
667
|
+
|