mentis 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.
- mentis/__init__.py +4 -0
- mentis/cli/__init__.py +8 -0
- mentis/cli/main.py +177 -0
- mentis/comparison/__init__.py +21 -0
- mentis/comparison/leaderboard.py +155 -0
- mentis/comparison/metrics.py +183 -0
- mentis/comparison/model_zoo.py +179 -0
- mentis/comparison/trainer.py +201 -0
- mentis/config.py +132 -0
- mentis/constants.py +39 -0
- mentis/deployment/__init__.py +9 -0
- mentis/deployment/checker.py +204 -0
- mentis/exceptions.py +38 -0
- mentis/explainability/__init__.py +28 -0
- mentis/explainability/curves.py +242 -0
- mentis/explainability/permutation.py +87 -0
- mentis/explainability/shap_explainer.py +141 -0
- mentis/guardian.py +495 -0
- mentis/monitoring/__init__.py +11 -0
- mentis/monitoring/dashboard.py +172 -0
- mentis/monitoring/drift.py +130 -0
- mentis/reporting/__init__.py +10 -0
- mentis/reporting/html_report.py +81 -0
- mentis/reporting/pdf_report.py +68 -0
- mentis/reporting/report_builder.py +176 -0
- mentis/scanner/__init__.py +18 -0
- mentis/scanner/base.py +52 -0
- mentis/scanner/checks.py +450 -0
- mentis/scanner/dataset_scanner.py +201 -0
- mentis/scanner/result.py +121 -0
- mentis/templates/report.html.j2 +383 -0
- mentis/templates/styles.css +56 -0
- mentis/utils/__init__.py +39 -0
- mentis/utils/helpers.py +198 -0
- mentis/utils/logger.py +80 -0
- mentis/utils/validators.py +103 -0
- mentis/validation/__init__.py +16 -0
- mentis/validation/auditor.py +138 -0
- mentis/validation/fairness.py +177 -0
- mentis/visualization/__init__.py +8 -0
- mentis/visualization/charts.py +376 -0
- mentis-0.1.0.dist-info/METADATA +79 -0
- mentis-0.1.0.dist-info/RECORD +46 -0
- mentis-0.1.0.dist-info/WHEEL +4 -0
- mentis-0.1.0.dist-info/entry_points.txt +2 -0
- mentis-0.1.0.dist-info/licenses/LICENSE +21 -0
mentis/__init__.py
ADDED
mentis/cli/__init__.py
ADDED
mentis/cli/main.py
ADDED
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Command-line interface for Mentis, built with Typer.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from __future__ import annotations
|
|
6
|
+
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
|
|
9
|
+
import pandas as pd
|
|
10
|
+
import typer
|
|
11
|
+
from rich.console import Console
|
|
12
|
+
from rich.table import Table
|
|
13
|
+
|
|
14
|
+
from mentis import Guardian, MentisConfig
|
|
15
|
+
from mentis.exceptions import MentisError
|
|
16
|
+
|
|
17
|
+
app = typer.Typer(name="mentis", help="Mentis: an AI engineer's toolkit.")
|
|
18
|
+
console = Console()
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def _load_guardian(config: str | None) -> Guardian:
|
|
22
|
+
return Guardian.from_yaml(config) if config else Guardian()
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def _load_dataframe(data_path: str) -> pd.DataFrame:
|
|
26
|
+
path = Path(data_path)
|
|
27
|
+
if not path.exists():
|
|
28
|
+
console.print(f"[red]File not found: {data_path}[/red]")
|
|
29
|
+
raise typer.Exit(code=1)
|
|
30
|
+
if path.suffix == ".parquet":
|
|
31
|
+
return pd.read_parquet(path)
|
|
32
|
+
return pd.read_csv(path)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
@app.command()
|
|
36
|
+
def scan(
|
|
37
|
+
data: str = typer.Argument(..., help="Path to CSV/Parquet dataset."),
|
|
38
|
+
target: str | None = typer.Option(None, help="Target column name."),
|
|
39
|
+
config: str | None = typer.Option(None, help="Path to a Mentis YAML config."),
|
|
40
|
+
) -> None:
|
|
41
|
+
"""Run the Dataset Scanner on a CSV/Parquet file."""
|
|
42
|
+
guardian = _load_guardian(config)
|
|
43
|
+
df = _load_dataframe(data)
|
|
44
|
+
|
|
45
|
+
with console.status("Scanning dataset..."):
|
|
46
|
+
try:
|
|
47
|
+
result = guardian.scan(df, target=target)
|
|
48
|
+
except MentisError as exc:
|
|
49
|
+
console.print(f"[red]Scan failed: {exc}[/red]")
|
|
50
|
+
raise typer.Exit(code=1)
|
|
51
|
+
|
|
52
|
+
console.print(result)
|
|
53
|
+
for finding in result.warnings():
|
|
54
|
+
console.print(f"[yellow]⚠ {finding.message}[/yellow] -> {finding.suggestion}")
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
@app.command()
|
|
58
|
+
def compare(
|
|
59
|
+
config: str = typer.Argument(..., help="Path to a Mentis YAML config."),
|
|
60
|
+
data: str = typer.Option(..., help="Path to CSV/Parquet dataset."),
|
|
61
|
+
) -> None:
|
|
62
|
+
"""Train and compare models using settings from a YAML config."""
|
|
63
|
+
guardian = Guardian.from_yaml(config)
|
|
64
|
+
df = _load_dataframe(data)
|
|
65
|
+
|
|
66
|
+
target = guardian.config.project.target
|
|
67
|
+
if not target:
|
|
68
|
+
console.print("[red]Config must specify project.target for comparison.[/red]")
|
|
69
|
+
raise typer.Exit(code=1)
|
|
70
|
+
|
|
71
|
+
from sklearn.model_selection import train_test_split
|
|
72
|
+
|
|
73
|
+
X = df.drop(columns=[target])
|
|
74
|
+
y = df[target]
|
|
75
|
+
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2, random_state=42)
|
|
76
|
+
|
|
77
|
+
with console.status("Training and comparing models..."):
|
|
78
|
+
try:
|
|
79
|
+
leaderboard = guardian.compare_models(X_train, X_test, y_train, y_test)
|
|
80
|
+
except MentisError as exc:
|
|
81
|
+
console.print(f"[red]Comparison failed: {exc}[/red]")
|
|
82
|
+
raise typer.Exit(code=1)
|
|
83
|
+
|
|
84
|
+
table = Table(title="Model Leaderboard")
|
|
85
|
+
table.add_column("Model")
|
|
86
|
+
table.add_column(leaderboard.primary_metric)
|
|
87
|
+
for r in leaderboard.results:
|
|
88
|
+
score = r.metrics.get(leaderboard.primary_metric, "N/A")
|
|
89
|
+
table.add_row(r.model_name, f"{score:.4f}" if isinstance(score, float) else str(score))
|
|
90
|
+
console.print(table)
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
@app.command()
|
|
94
|
+
def audit(
|
|
95
|
+
project_path: str = typer.Argument(".", help="Path to the project to audit.")
|
|
96
|
+
) -> None:
|
|
97
|
+
"""Audit an ML project's structure and production readiness."""
|
|
98
|
+
guardian = Guardian()
|
|
99
|
+
with console.status("Auditing project..."):
|
|
100
|
+
result = guardian.audit_pipeline(project_path)
|
|
101
|
+
|
|
102
|
+
console.print(f"[bold]Production Readiness Score: {result.score}/100[/bold]")
|
|
103
|
+
for finding in result.failed():
|
|
104
|
+
console.print(f"[yellow]✗ {finding.name}[/yellow] ({finding.severity}) -> {finding.suggestion}")
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
@app.command(name="deploy-check")
|
|
108
|
+
def deploy_check(
|
|
109
|
+
project_path: str = typer.Argument(".", help="Path to the project to check.")
|
|
110
|
+
) -> None:
|
|
111
|
+
"""Check a project's deployment readiness."""
|
|
112
|
+
guardian = Guardian()
|
|
113
|
+
with console.status("Checking deployment readiness..."):
|
|
114
|
+
result = guardian.deploy_check(project_path)
|
|
115
|
+
|
|
116
|
+
console.print(f"[bold]Deployment Score: {result.score}/100[/bold]")
|
|
117
|
+
if result.detected_framework:
|
|
118
|
+
console.print(f"Framework detected: {result.detected_framework}")
|
|
119
|
+
for finding in result.failed():
|
|
120
|
+
console.print(f"[yellow]✗ {finding.name}[/yellow] ({finding.severity}) -> {finding.suggestion}")
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
@app.command()
|
|
124
|
+
def report(
|
|
125
|
+
data: str | None = typer.Option(None, help="Path to CSV/Parquet dataset (optional, enables scan)."),
|
|
126
|
+
target: str | None = typer.Option(None, help="Target column name."),
|
|
127
|
+
project_path: str = typer.Option(".", help="Project path for audit + deploy checks."),
|
|
128
|
+
output_dir: str = typer.Option("mentis_reports", help="Output directory for the report."),
|
|
129
|
+
fmt: str = typer.Option("html", help="Report format: html, markdown, or pdf."),
|
|
130
|
+
config: str | None = typer.Option(None, help="Path to a Mentis YAML config."),
|
|
131
|
+
) -> None:
|
|
132
|
+
"""
|
|
133
|
+
Generate a Mentis report: optionally scans a dataset, then always
|
|
134
|
+
runs pipeline audit and deployment checks, and writes the report.
|
|
135
|
+
"""
|
|
136
|
+
guardian = _load_guardian(config)
|
|
137
|
+
|
|
138
|
+
if data:
|
|
139
|
+
df = _load_dataframe(data)
|
|
140
|
+
with console.status("Scanning dataset..."):
|
|
141
|
+
try:
|
|
142
|
+
guardian.scan(df, target=target)
|
|
143
|
+
except MentisError as exc:
|
|
144
|
+
console.print(f"[yellow]⚠ Scan skipped: {exc}[/yellow]")
|
|
145
|
+
|
|
146
|
+
with console.status("Auditing project structure..."):
|
|
147
|
+
try:
|
|
148
|
+
guardian.audit_pipeline(project_path)
|
|
149
|
+
except Exception as exc:
|
|
150
|
+
console.print(f"[yellow]⚠ Audit skipped: {exc}[/yellow]")
|
|
151
|
+
|
|
152
|
+
with console.status("Checking deployment readiness..."):
|
|
153
|
+
try:
|
|
154
|
+
guardian.deploy_check(project_path)
|
|
155
|
+
except Exception as exc:
|
|
156
|
+
console.print(f"[yellow]⚠ Deploy check skipped: {exc}[/yellow]")
|
|
157
|
+
|
|
158
|
+
with console.status(f"Generating {fmt.upper()} report..."):
|
|
159
|
+
try:
|
|
160
|
+
path = guardian.generate_report(output_path=output_dir, fmt=fmt)
|
|
161
|
+
except MentisError as exc:
|
|
162
|
+
console.print(f"[red]Report generation failed: {exc}[/red]")
|
|
163
|
+
raise typer.Exit(code=1)
|
|
164
|
+
|
|
165
|
+
console.print(f"[green]✓ Report written to:[/green] {path}")
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
def main() -> None:
|
|
169
|
+
app()
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
if __name__ == "__main__":
|
|
173
|
+
main()
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Comparison subpackage: automated multi-model training, evaluation,
|
|
3
|
+
and leaderboard ranking for Mentis.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from mentis.comparison.leaderboard import Leaderboard, ModelResult, build_leaderboard
|
|
7
|
+
from mentis.comparison.metrics import compute_metrics, primary_metric_for_task
|
|
8
|
+
from mentis.comparison.model_zoo import get_model_zoo
|
|
9
|
+
from mentis.comparison.trainer import ModelTrainer
|
|
10
|
+
|
|
11
|
+
__all__ = [
|
|
12
|
+
"ModelTrainer",
|
|
13
|
+
"Leaderboard",
|
|
14
|
+
"ModelResult",
|
|
15
|
+
"build_leaderboard",
|
|
16
|
+
"get_model_zoo",
|
|
17
|
+
"compute_metrics",
|
|
18
|
+
"primary_metric_for_task",
|
|
19
|
+
]
|
|
20
|
+
|
|
21
|
+
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Leaderboard construction for Mentis model comparison results.
|
|
3
|
+
|
|
4
|
+
Separated from the trainer so ranking/formatting logic can evolve
|
|
5
|
+
(e.g. weighted scoring, custom tie-breakers) without touching the
|
|
6
|
+
training loop itself.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
from dataclasses import dataclass, field, asdict
|
|
12
|
+
from typing import Any
|
|
13
|
+
|
|
14
|
+
from mentis.comparison.metrics import is_higher_better
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
@dataclass
|
|
18
|
+
class ModelResult:
|
|
19
|
+
"""
|
|
20
|
+
Result of training and evaluating a single model.
|
|
21
|
+
|
|
22
|
+
Attributes:
|
|
23
|
+
model_name: Name of the model (e.g. "Random Forest").
|
|
24
|
+
metrics: Dict of metric name -> value on the test set.
|
|
25
|
+
cv_scores: Cross-validation scores (primary metric) per fold.
|
|
26
|
+
cv_mean: Mean cross-validation score.
|
|
27
|
+
cv_std: Standard deviation of cross-validation scores.
|
|
28
|
+
training_time_seconds: Wall-clock time spent fitting the model.
|
|
29
|
+
inference_time_seconds: Wall-clock time spent predicting on the
|
|
30
|
+
test set.
|
|
31
|
+
memory_usage_mb: Approximate memory footprint of the fitted
|
|
32
|
+
model.
|
|
33
|
+
feature_importances: Optional dict of feature name -> importance
|
|
34
|
+
score, if the model exposes one.
|
|
35
|
+
error: Populated if training/evaluation failed for this model,
|
|
36
|
+
instead of raising and aborting the whole comparison.
|
|
37
|
+
"""
|
|
38
|
+
|
|
39
|
+
model_name: str
|
|
40
|
+
metrics: dict[str, float] = field(default_factory=dict)
|
|
41
|
+
cv_scores: list[float] = field(default_factory=list)
|
|
42
|
+
cv_mean: float = 0.0
|
|
43
|
+
cv_std: float = 0.0
|
|
44
|
+
training_time_seconds: float = 0.0
|
|
45
|
+
inference_time_seconds: float = 0.0
|
|
46
|
+
memory_usage_mb: float = 0.0
|
|
47
|
+
feature_importances: dict[str, float] | None = None
|
|
48
|
+
error: str | None = None
|
|
49
|
+
|
|
50
|
+
def to_dict(self) -> dict[str, Any]:
|
|
51
|
+
return asdict(self)
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
@dataclass
|
|
55
|
+
class Leaderboard:
|
|
56
|
+
"""
|
|
57
|
+
Ranked collection of `ModelResult`s produced by a model comparison
|
|
58
|
+
run.
|
|
59
|
+
|
|
60
|
+
Attributes:
|
|
61
|
+
task: "classification" or "regression".
|
|
62
|
+
primary_metric: The metric used to rank models.
|
|
63
|
+
results: All `ModelResult`s, sorted best-to-worst by
|
|
64
|
+
`primary_metric`.
|
|
65
|
+
"""
|
|
66
|
+
|
|
67
|
+
task: str
|
|
68
|
+
primary_metric: str
|
|
69
|
+
results: list[ModelResult] = field(default_factory=list)
|
|
70
|
+
|
|
71
|
+
def best_model(self) -> ModelResult | None:
|
|
72
|
+
"""Return the top-ranked `ModelResult`, or None if empty."""
|
|
73
|
+
successful = [r for r in self.results if r.error is None]
|
|
74
|
+
return successful[0] if successful else None
|
|
75
|
+
|
|
76
|
+
def to_dataframe(self):
|
|
77
|
+
"""
|
|
78
|
+
Return the leaderboard as a pandas DataFrame, sorted by rank.
|
|
79
|
+
|
|
80
|
+
Returns:
|
|
81
|
+
A `pandas.DataFrame` with one row per model.
|
|
82
|
+
|
|
83
|
+
Examples:
|
|
84
|
+
>>> lb = Leaderboard(task="classification", primary_metric="f1")
|
|
85
|
+
>>> lb.to_dataframe().empty
|
|
86
|
+
True
|
|
87
|
+
"""
|
|
88
|
+
import pandas as pd
|
|
89
|
+
|
|
90
|
+
rows = []
|
|
91
|
+
for r in self.results:
|
|
92
|
+
row = {"model": r.model_name, "error": r.error}
|
|
93
|
+
row.update(r.metrics)
|
|
94
|
+
row["cv_mean"] = r.cv_mean
|
|
95
|
+
row["cv_std"] = r.cv_std
|
|
96
|
+
row["training_time_s"] = r.training_time_seconds
|
|
97
|
+
row["inference_time_s"] = r.inference_time_seconds
|
|
98
|
+
row["memory_mb"] = r.memory_usage_mb
|
|
99
|
+
rows.append(row)
|
|
100
|
+
|
|
101
|
+
return pd.DataFrame(rows)
|
|
102
|
+
|
|
103
|
+
def to_dict(self) -> dict[str, Any]:
|
|
104
|
+
return {
|
|
105
|
+
"task": self.task,
|
|
106
|
+
"primary_metric": self.primary_metric,
|
|
107
|
+
"results": [r.to_dict() for r in self.results],
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
def __repr__(self) -> str:
|
|
111
|
+
best = self.best_model()
|
|
112
|
+
best_name = best.model_name if best else "N/A"
|
|
113
|
+
return f"<Leaderboard task={self.task!r} best_model={best_name!r} n_models={len(self.results)}>"
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def build_leaderboard(
|
|
117
|
+
task: str,
|
|
118
|
+
model_results: list[ModelResult],
|
|
119
|
+
primary_metric: str,
|
|
120
|
+
) -> Leaderboard:
|
|
121
|
+
"""
|
|
122
|
+
Sort model results into a ranked `Leaderboard`.
|
|
123
|
+
|
|
124
|
+
Args:
|
|
125
|
+
task: "classification" or "regression".
|
|
126
|
+
model_results: Unsorted list of `ModelResult`s.
|
|
127
|
+
primary_metric: Metric name used for ranking.
|
|
128
|
+
|
|
129
|
+
Returns:
|
|
130
|
+
A `Leaderboard` with `results` sorted best-to-worst. Models
|
|
131
|
+
that errored are placed last, in original order.
|
|
132
|
+
|
|
133
|
+
Examples:
|
|
134
|
+
>>> results = [
|
|
135
|
+
... ModelResult(model_name="A", metrics={"f1": 0.8}),
|
|
136
|
+
... ModelResult(model_name="B", metrics={"f1": 0.9}),
|
|
137
|
+
... ]
|
|
138
|
+
>>> lb = build_leaderboard("classification", results, "f1")
|
|
139
|
+
>>> lb.results[0].model_name
|
|
140
|
+
'B'
|
|
141
|
+
"""
|
|
142
|
+
higher_better = is_higher_better(primary_metric)
|
|
143
|
+
|
|
144
|
+
successful = [r for r in model_results if r.error is None]
|
|
145
|
+
failed = [r for r in model_results if r.error is not None]
|
|
146
|
+
|
|
147
|
+
successful.sort(
|
|
148
|
+
key=lambda r: r.metrics.get(primary_metric, float("-inf") if higher_better else float("inf")),
|
|
149
|
+
reverse=higher_better,
|
|
150
|
+
)
|
|
151
|
+
|
|
152
|
+
return Leaderboard(task=task, primary_metric=primary_metric, results=successful + failed)
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Metric computation for classification and regression model comparison.
|
|
3
|
+
|
|
4
|
+
Centralizing metric logic here means the trainer and leaderboard never
|
|
5
|
+
duplicate scikit-learn calls, and adding a new metric only requires a
|
|
6
|
+
change in one place.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
from typing import Any
|
|
12
|
+
|
|
13
|
+
import numpy as np
|
|
14
|
+
from sklearn.metrics import (
|
|
15
|
+
accuracy_score,
|
|
16
|
+
f1_score,
|
|
17
|
+
mean_absolute_error,
|
|
18
|
+
mean_absolute_percentage_error,
|
|
19
|
+
mean_squared_error,
|
|
20
|
+
precision_score,
|
|
21
|
+
r2_score,
|
|
22
|
+
recall_score,
|
|
23
|
+
roc_auc_score,
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
from mentis.utils.logger import get_logger
|
|
27
|
+
|
|
28
|
+
logger = get_logger(__name__)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def compute_classification_metrics(
|
|
32
|
+
y_true: np.ndarray,
|
|
33
|
+
y_pred: np.ndarray,
|
|
34
|
+
y_proba: np.ndarray | None = None,
|
|
35
|
+
) -> dict[str, float]:
|
|
36
|
+
"""
|
|
37
|
+
Compute standard classification metrics.
|
|
38
|
+
|
|
39
|
+
Args:
|
|
40
|
+
y_true: Ground-truth labels.
|
|
41
|
+
y_pred: Predicted labels.
|
|
42
|
+
y_proba: Predicted probabilities for the positive class
|
|
43
|
+
(binary) or full probability matrix (multiclass), used for
|
|
44
|
+
ROC AUC. If None, ROC AUC is omitted.
|
|
45
|
+
|
|
46
|
+
Returns:
|
|
47
|
+
Dict mapping metric name -> value. Keys: "accuracy",
|
|
48
|
+
"precision", "recall", "f1", and "roc_auc" if computable.
|
|
49
|
+
|
|
50
|
+
Examples:
|
|
51
|
+
>>> import numpy as np
|
|
52
|
+
>>> y_true = np.array([0, 1, 1, 0])
|
|
53
|
+
>>> y_pred = np.array([0, 1, 0, 0])
|
|
54
|
+
>>> metrics = compute_classification_metrics(y_true, y_pred)
|
|
55
|
+
>>> round(metrics["accuracy"], 2)
|
|
56
|
+
0.75
|
|
57
|
+
"""
|
|
58
|
+
average = "binary" if len(np.unique(y_true)) == 2 else "macro"
|
|
59
|
+
|
|
60
|
+
metrics: dict[str, float] = {
|
|
61
|
+
"accuracy": float(accuracy_score(y_true, y_pred)),
|
|
62
|
+
"precision": float(precision_score(y_true, y_pred, average=average, zero_division=0)),
|
|
63
|
+
"recall": float(recall_score(y_true, y_pred, average=average, zero_division=0)),
|
|
64
|
+
"f1": float(f1_score(y_true, y_pred, average=average, zero_division=0)),
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
if y_proba is not None:
|
|
68
|
+
try:
|
|
69
|
+
if y_proba.ndim == 2 and y_proba.shape[1] == 2:
|
|
70
|
+
auc = roc_auc_score(y_true, y_proba[:, 1])
|
|
71
|
+
elif y_proba.ndim == 1:
|
|
72
|
+
auc = roc_auc_score(y_true, y_proba)
|
|
73
|
+
else:
|
|
74
|
+
auc = roc_auc_score(y_true, y_proba, multi_class="ovr", average="macro")
|
|
75
|
+
metrics["roc_auc"] = float(auc)
|
|
76
|
+
except ValueError as exc:
|
|
77
|
+
logger.warning(f"Could not compute ROC AUC: {exc}")
|
|
78
|
+
|
|
79
|
+
return metrics
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def compute_regression_metrics(y_true: np.ndarray, y_pred: np.ndarray) -> dict[str, float]:
|
|
83
|
+
"""
|
|
84
|
+
Compute standard regression metrics.
|
|
85
|
+
|
|
86
|
+
Args:
|
|
87
|
+
y_true: Ground-truth target values.
|
|
88
|
+
y_pred: Predicted target values.
|
|
89
|
+
|
|
90
|
+
Returns:
|
|
91
|
+
Dict with keys "rmse", "mae", "mape", "r2".
|
|
92
|
+
|
|
93
|
+
Examples:
|
|
94
|
+
>>> import numpy as np
|
|
95
|
+
>>> y_true = np.array([3.0, 5.0, 2.5])
|
|
96
|
+
>>> y_pred = np.array([2.8, 5.1, 2.4])
|
|
97
|
+
>>> metrics = compute_regression_metrics(y_true, y_pred)
|
|
98
|
+
>>> metrics["r2"] > 0.9
|
|
99
|
+
True
|
|
100
|
+
"""
|
|
101
|
+
rmse = float(np.sqrt(mean_squared_error(y_true, y_pred)))
|
|
102
|
+
mae = float(mean_absolute_error(y_true, y_pred))
|
|
103
|
+
r2 = float(r2_score(y_true, y_pred))
|
|
104
|
+
|
|
105
|
+
try:
|
|
106
|
+
mape = float(mean_absolute_percentage_error(y_true, y_pred))
|
|
107
|
+
except ValueError:
|
|
108
|
+
mape = float("nan")
|
|
109
|
+
|
|
110
|
+
return {"rmse": rmse, "mae": mae, "mape": mape, "r2": r2}
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def compute_metrics(
|
|
114
|
+
task: str,
|
|
115
|
+
y_true: np.ndarray,
|
|
116
|
+
y_pred: np.ndarray,
|
|
117
|
+
y_proba: np.ndarray | None = None,
|
|
118
|
+
) -> dict[str, float]:
|
|
119
|
+
"""
|
|
120
|
+
Dispatch to the correct metric set based on task type.
|
|
121
|
+
|
|
122
|
+
Args:
|
|
123
|
+
task: "classification" or "regression".
|
|
124
|
+
y_true: Ground-truth values.
|
|
125
|
+
y_pred: Predicted values.
|
|
126
|
+
y_proba: Predicted probabilities (classification only).
|
|
127
|
+
|
|
128
|
+
Returns:
|
|
129
|
+
Dict of computed metrics for the given task.
|
|
130
|
+
|
|
131
|
+
Raises:
|
|
132
|
+
ValueError: If `task` is not "classification" or "regression".
|
|
133
|
+
|
|
134
|
+
Examples:
|
|
135
|
+
>>> import numpy as np
|
|
136
|
+
>>> compute_metrics("regression", np.array([1.0, 2.0]), np.array([1.1, 1.9]))["mae"] < 0.2
|
|
137
|
+
True
|
|
138
|
+
"""
|
|
139
|
+
if task == "classification":
|
|
140
|
+
return compute_classification_metrics(y_true, y_pred, y_proba)
|
|
141
|
+
if task == "regression":
|
|
142
|
+
return compute_regression_metrics(y_true, y_pred)
|
|
143
|
+
raise ValueError(f"Unsupported task type: {task!r}")
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
def primary_metric_for_task(task: str) -> str:
|
|
147
|
+
"""
|
|
148
|
+
Return the default metric used to rank models on the leaderboard.
|
|
149
|
+
|
|
150
|
+
Args:
|
|
151
|
+
task: "classification" or "regression".
|
|
152
|
+
|
|
153
|
+
Returns:
|
|
154
|
+
"f1" for classification, "r2" for regression.
|
|
155
|
+
|
|
156
|
+
Examples:
|
|
157
|
+
>>> primary_metric_for_task("classification")
|
|
158
|
+
'f1'
|
|
159
|
+
"""
|
|
160
|
+
return "f1" if task == "classification" else "r2"
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
def is_higher_better(metric_name: str) -> bool:
|
|
164
|
+
"""
|
|
165
|
+
Whether a higher value of the given metric indicates better
|
|
166
|
+
performance (used to sort the leaderboard correctly).
|
|
167
|
+
|
|
168
|
+
Args:
|
|
169
|
+
metric_name: Name of the metric, e.g. "rmse", "f1", "r2".
|
|
170
|
+
|
|
171
|
+
Returns:
|
|
172
|
+
False for error-style metrics (rmse, mae, mape); True otherwise.
|
|
173
|
+
|
|
174
|
+
Examples:
|
|
175
|
+
>>> is_higher_better("rmse")
|
|
176
|
+
False
|
|
177
|
+
>>> is_higher_better("f1")
|
|
178
|
+
True
|
|
179
|
+
"""
|
|
180
|
+
lower_is_better = {"rmse", "mae", "mape"}
|
|
181
|
+
return metric_name not in lower_is_better
|
|
182
|
+
|
|
183
|
+
|