evalsuite-python 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.
evalsuite/__init__.py ADDED
@@ -0,0 +1,159 @@
1
+ """EvalSuite: unified, reproducible evaluation for machine learning and research.
2
+
3
+ >>> import evalsuite as es
4
+ >>> result = es.evaluate([0, 1, 1, 0], [0, 1, 0, 0])
5
+ >>> print(result.summary()) # doctest: +SKIP
6
+ """
7
+
8
+ from . import classification, plot, regression, stats
9
+ from .api import evaluate
10
+ from .classification import (
11
+ accuracy,
12
+ average_precision,
13
+ balanced_accuracy,
14
+ brier_score,
15
+ calibration_curve,
16
+ cohen_kappa,
17
+ confusion_matrix,
18
+ expected_calibration_error,
19
+ f1,
20
+ fbeta,
21
+ hamming_loss,
22
+ jaccard,
23
+ log_loss,
24
+ mcc,
25
+ npv,
26
+ pr_curve,
27
+ precision,
28
+ recall,
29
+ roc_auc,
30
+ roc_curve,
31
+ specificity,
32
+ top_k_accuracy,
33
+ )
34
+ from .core.exceptions import (
35
+ EvalSuiteError,
36
+ InputValidationError,
37
+ MetricInputError,
38
+ OptionalDependencyError,
39
+ StatisticalTestError,
40
+ UndefinedMetricWarning,
41
+ UnsupportedTaskError,
42
+ )
43
+ from .core.registry import MetricInfo, list_metrics, metric_info
44
+ from .core.result import EvaluationResult, MetricResult
45
+ from .regression import (
46
+ adjusted_r2,
47
+ explained_variance,
48
+ huber_loss,
49
+ mae,
50
+ mape,
51
+ max_error,
52
+ mean_bias_error,
53
+ median_absolute_error,
54
+ mse,
55
+ msle,
56
+ quantile_loss,
57
+ r2,
58
+ rae,
59
+ rmse,
60
+ rmsle,
61
+ rse,
62
+ smape,
63
+ )
64
+ from .reporting import ClassificationReport, classification_report
65
+ from .stats import (
66
+ ComparisonResult,
67
+ ConfidenceInterval,
68
+ TestResult,
69
+ accuracy_ci,
70
+ adjust_pvalues,
71
+ bootstrap_ci,
72
+ cliffs_delta,
73
+ cohens_d,
74
+ compare,
75
+ delong_test,
76
+ hedges_g,
77
+ mcnemar_test,
78
+ paired_bootstrap_test,
79
+ proportion_ci,
80
+ roc_auc_ci,
81
+ )
82
+ from .version import __version__
83
+
84
+ __all__ = [
85
+ "plot",
86
+ "expected_calibration_error",
87
+ "calibration_curve",
88
+ "classification_report",
89
+ "ClassificationReport",
90
+ "stats",
91
+ "roc_auc_ci",
92
+ "proportion_ci",
93
+ "paired_bootstrap_test",
94
+ "mcnemar_test",
95
+ "hedges_g",
96
+ "delong_test",
97
+ "compare",
98
+ "cohens_d",
99
+ "cliffs_delta",
100
+ "bootstrap_ci",
101
+ "adjust_pvalues",
102
+ "accuracy_ci",
103
+ "TestResult",
104
+ "ConfidenceInterval",
105
+ "ComparisonResult",
106
+ "EvalSuiteError",
107
+ "EvaluationResult",
108
+ "InputValidationError",
109
+ "MetricInfo",
110
+ "MetricInputError",
111
+ "MetricResult",
112
+ "OptionalDependencyError",
113
+ "StatisticalTestError",
114
+ "UndefinedMetricWarning",
115
+ "UnsupportedTaskError",
116
+ "__version__",
117
+ "accuracy",
118
+ "adjusted_r2",
119
+ "average_precision",
120
+ "balanced_accuracy",
121
+ "brier_score",
122
+ "classification",
123
+ "cohen_kappa",
124
+ "confusion_matrix",
125
+ "evaluate",
126
+ "explained_variance",
127
+ "f1",
128
+ "fbeta",
129
+ "hamming_loss",
130
+ "huber_loss",
131
+ "jaccard",
132
+ "list_metrics",
133
+ "log_loss",
134
+ "mae",
135
+ "mape",
136
+ "max_error",
137
+ "mcc",
138
+ "mean_bias_error",
139
+ "median_absolute_error",
140
+ "metric_info",
141
+ "mse",
142
+ "msle",
143
+ "npv",
144
+ "pr_curve",
145
+ "precision",
146
+ "quantile_loss",
147
+ "r2",
148
+ "rae",
149
+ "recall",
150
+ "regression",
151
+ "rmse",
152
+ "rmsle",
153
+ "roc_auc",
154
+ "roc_curve",
155
+ "rse",
156
+ "smape",
157
+ "specificity",
158
+ "top_k_accuracy",
159
+ ]
evalsuite/__main__.py ADDED
@@ -0,0 +1,5 @@
1
+ """``python -m evalsuite`` runs the command-line interface."""
2
+
3
+ from .cli.main import main
4
+
5
+ raise SystemExit(main())
evalsuite/api.py ADDED
@@ -0,0 +1,264 @@
1
+ """High-level API: ``evaluate()``."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import platform
6
+ from collections.abc import Sequence
7
+ from typing import Any, Callable, Optional
8
+
9
+ import numpy as np
10
+
11
+ from .classification import metrics as clf
12
+ from .classification._common import averaged
13
+ from .core.context import ClassificationContext
14
+ from .core.exceptions import InputValidationError, UnsupportedTaskError
15
+ from .core.result import EvaluationResult, MetricResult
16
+ from .core.types import ArrayLike, ZeroDivision
17
+ from .core.validation import target_type, to_numpy
18
+ from .regression import metrics as reg
19
+ from .version import __version__
20
+
21
+ __all__ = ["evaluate"]
22
+
23
+ CtxMetric = Callable[[ClassificationContext], MetricResult]
24
+
25
+
26
+ def _classification_registry(
27
+ average: Optional[str], pos_label: Any, zero_division: ZeroDivision
28
+ ) -> dict[str, tuple[CtxMetric, bool]]:
29
+ """metric id -> (function of a shared context, needs probabilities)."""
30
+
31
+ def avg(fn: Any, metric: str, name: str) -> CtxMetric:
32
+ return lambda ctx: averaged(
33
+ ctx,
34
+ fn,
35
+ metric=metric,
36
+ name=name,
37
+ average=average,
38
+ pos_label=pos_label,
39
+ zero_division=zero_division,
40
+ )
41
+
42
+ return {
43
+ "accuracy": (clf._accuracy, False),
44
+ "balanced_accuracy": (clf._balanced_accuracy, False),
45
+ "precision": (avg(clf._precision, "precision", "Precision"), False),
46
+ "recall": (avg(clf._recall, "recall", "Recall"), False),
47
+ "f1": (avg(clf._fbeta_fn(1.0), "f1", "F1"), False),
48
+ "specificity": (avg(clf._specificity, "specificity", "Specificity"), False),
49
+ "npv": (avg(clf._npv, "npv", "NPV"), False),
50
+ "jaccard": (avg(clf._jaccard, "jaccard", "Jaccard"), False),
51
+ "mcc": (lambda ctx: clf._mcc(ctx, zero_division), False),
52
+ "cohen_kappa": (lambda ctx: clf._cohen_kappa(ctx, None, zero_division), False),
53
+ "hamming_loss": (_hamming, False),
54
+ "roc_auc": (lambda ctx: clf._roc_auc(ctx, average=average, pos_label=pos_label), True),
55
+ "average_precision": (
56
+ lambda ctx: clf._average_precision(ctx, average=average, pos_label=pos_label),
57
+ True,
58
+ ),
59
+ "log_loss": (lambda ctx: clf._log_loss(ctx, pos_label), True),
60
+ "brier_score": (lambda ctx: clf._brier(ctx, pos_label), True),
61
+ }
62
+
63
+
64
+ def _hamming(ctx: ClassificationContext) -> MetricResult:
65
+ if ctx.target_type == "multilabel":
66
+ assert ctx.y_pred is not None # noqa: S101
67
+ wrong = (ctx.y_true != ctx.y_pred).mean(axis=1)
68
+ else:
69
+ wrong = (ctx.true_idx != ctx.pred_idx).astype(float)
70
+ return MetricResult("hamming_loss", "Hamming loss", float(np.average(wrong, weights=ctx.weights)))
71
+
72
+
73
+ _DEFAULT_SINGLE = [
74
+ "accuracy",
75
+ "balanced_accuracy",
76
+ "precision",
77
+ "recall",
78
+ "f1",
79
+ "specificity",
80
+ "mcc",
81
+ "cohen_kappa",
82
+ ]
83
+ _DEFAULT_MULTILABEL = ["accuracy", "hamming_loss", "precision", "recall", "f1", "jaccard"]
84
+ _DEFAULT_PROB = ["roc_auc", "average_precision", "log_loss", "brier_score"]
85
+ _DEFAULT_PROB_MULTILABEL = ["roc_auc", "average_precision"]
86
+
87
+ _REGRESSION: dict[str, Callable[..., MetricResult]] = {
88
+ "mae": reg.mae,
89
+ "mse": reg.mse,
90
+ "rmse": reg.rmse,
91
+ "r2": reg.r2,
92
+ "explained_variance": reg.explained_variance,
93
+ "median_absolute_error": reg.median_absolute_error,
94
+ "max_error": reg.max_error,
95
+ "mean_bias_error": reg.mean_bias_error,
96
+ "mape": reg.mape,
97
+ "smape": reg.smape,
98
+ "msle": reg.msle,
99
+ "rmsle": reg.rmsle,
100
+ "rae": reg.rae,
101
+ "rse": reg.rse,
102
+ "huber_loss": reg.huber_loss,
103
+ "quantile_loss": reg.quantile_loss,
104
+ }
105
+ _DEFAULT_REGRESSION = [
106
+ "mae",
107
+ "mse",
108
+ "rmse",
109
+ "r2",
110
+ "explained_variance",
111
+ "median_absolute_error",
112
+ "max_error",
113
+ "mean_bias_error",
114
+ ]
115
+ _NO_WEIGHTS = {"max_error"}
116
+
117
+
118
+ def _infer_task(y_true: ArrayLike, y_prob: Optional[ArrayLike]) -> str:
119
+ if y_prob is not None:
120
+ return "classification"
121
+ return (
122
+ "regression"
123
+ if target_type(to_numpy(y_true, "y_true", allow_2d=True)) == "continuous"
124
+ else "classification"
125
+ )
126
+
127
+
128
+ def evaluate(
129
+ y_true: ArrayLike,
130
+ y_pred: Optional[ArrayLike] = None,
131
+ *,
132
+ y_prob: Optional[ArrayLike] = None,
133
+ task: Optional[str] = None,
134
+ metrics: Optional[Sequence[str]] = None,
135
+ average: Optional[str] = "auto",
136
+ labels: Optional[ArrayLike] = None,
137
+ pos_label: Any = None,
138
+ sample_weight: Optional[ArrayLike] = None,
139
+ zero_division: ZeroDivision = "warn",
140
+ ) -> EvaluationResult:
141
+ """Evaluate predictions with a standard set of metrics (or the ``metrics`` you name).
142
+
143
+ The task is inferred when omitted: probabilities or integer/string labels mean classification, non-integer
144
+ numbers mean regression. Inputs are validated once and shared intermediate results (the confusion matrix)
145
+ are computed once for all metrics.
146
+
147
+ >>> import evalsuite as es
148
+ >>> r = es.evaluate([0, 1, 1, 0], [0, 1, 0, 0])
149
+ >>> round(r["accuracy"], 2)
150
+ 0.75
151
+ """
152
+ task = task or _infer_task(y_true, y_prob)
153
+ if task == "classification":
154
+ return _evaluate_classification(
155
+ y_true, y_pred, y_prob, metrics, average, labels, pos_label, sample_weight, zero_division
156
+ )
157
+ if task == "regression":
158
+ if y_pred is None:
159
+ raise InputValidationError("Regression evaluation needs y_pred.")
160
+ return _evaluate_regression(y_true, y_pred, metrics, sample_weight)
161
+ raise UnsupportedTaskError("task must be 'classification' or 'regression' in EvalSuite 0.1.")
162
+
163
+
164
+ def _metadata(**extra: Any) -> dict[str, Any]:
165
+ return {
166
+ "evalsuite_version": __version__,
167
+ "numpy_version": np.__version__,
168
+ "python_version": platform.python_version(),
169
+ **extra,
170
+ }
171
+
172
+
173
+ def _evaluate_classification(
174
+ y_true: ArrayLike,
175
+ y_pred: Optional[ArrayLike],
176
+ y_prob: Optional[ArrayLike],
177
+ metrics: Optional[Sequence[str]],
178
+ average: Optional[str],
179
+ labels: Optional[ArrayLike],
180
+ pos_label: Any,
181
+ sample_weight: Optional[ArrayLike],
182
+ zero_division: ZeroDivision,
183
+ ) -> EvaluationResult:
184
+ if y_pred is None and y_prob is None:
185
+ raise InputValidationError("Classification evaluation needs y_pred, y_prob, or both.")
186
+ ctx = ClassificationContext(y_true, y_pred, y_prob=y_prob, labels=labels, sample_weight=sample_weight)
187
+ registry = _classification_registry(average, pos_label, zero_division)
188
+ if metrics is None:
189
+ multilabel = ctx.target_type == "multilabel"
190
+ names = [] if y_pred is None else list(_DEFAULT_MULTILABEL if multilabel else _DEFAULT_SINGLE)
191
+ if y_prob is not None:
192
+ names += _DEFAULT_PROB_MULTILABEL if multilabel else _DEFAULT_PROB
193
+ else:
194
+ names = list(metrics)
195
+ unknown = [m for m in names if m not in registry]
196
+ if unknown:
197
+ raise InputValidationError(
198
+ f"Unknown classification metric(s): {', '.join(unknown)}. Available: {', '.join(registry)}."
199
+ )
200
+ out: dict[str, MetricResult] = {}
201
+ for name in names:
202
+ fn, needs_prob = registry[name]
203
+ if needs_prob and y_prob is None:
204
+ raise InputValidationError(f"Metric '{name}' needs y_prob (predicted probabilities).")
205
+ if not needs_prob and y_pred is None:
206
+ raise InputValidationError(f"Metric '{name}' needs y_pred (predicted labels).")
207
+ out[name] = fn(ctx)
208
+ cm = None if ctx.target_type == "multilabel" or y_pred is None else ctx.confusion_matrix
209
+ return EvaluationResult(
210
+ task="classification",
211
+ metrics=out,
212
+ n_samples=ctx.n,
213
+ target_type=ctx.target_type,
214
+ labels=tuple(ctx.labels.tolist()),
215
+ confusion_matrix=cm,
216
+ metadata=_metadata(average=average, zero_division=zero_division, weighted=sample_weight is not None),
217
+ )
218
+
219
+
220
+ def _evaluate_regression(
221
+ y_true: ArrayLike, y_pred: ArrayLike, metrics: Optional[Sequence[str]], sample_weight: Optional[ArrayLike]
222
+ ) -> EvaluationResult:
223
+ yt = to_numpy(y_true, "y_true", allow_2d=True)
224
+ multi = yt.ndim == 2
225
+ if metrics is None:
226
+ names = [m for m in _DEFAULT_REGRESSION if not (multi and m == "max_error")]
227
+ else:
228
+ names = list(metrics)
229
+ unknown = [m for m in names if m not in _REGRESSION]
230
+ if unknown:
231
+ raise InputValidationError(
232
+ f"Unknown regression metric(s): {', '.join(unknown)}. Available: {', '.join(_REGRESSION)}."
233
+ )
234
+ out: dict[str, MetricResult] = {}
235
+ shared = reg._Inputs(y_true, y_pred, sample_weight) # validate once for every metric
236
+ token = reg._SHARED.set((id(y_true), id(y_pred), id(sample_weight), shared))
237
+ try:
238
+ _regression_loop(names, out, y_true, y_pred, sample_weight)
239
+ finally:
240
+ reg._SHARED.reset(token)
241
+ return EvaluationResult(
242
+ task="regression",
243
+ metrics=out,
244
+ n_samples=yt.shape[0],
245
+ target_type="continuous",
246
+ metadata=_metadata(weighted=sample_weight is not None, outputs=yt.shape[1] if multi else 1),
247
+ )
248
+
249
+
250
+ def _regression_loop(
251
+ names: list[str],
252
+ out: dict[str, MetricResult],
253
+ y_true: ArrayLike,
254
+ y_pred: ArrayLike,
255
+ sample_weight: Optional[ArrayLike],
256
+ ) -> None:
257
+ for name in names:
258
+ fn = _REGRESSION[name]
259
+ if name in _NO_WEIGHTS:
260
+ if sample_weight is not None:
261
+ raise InputValidationError(f"Metric '{name}' does not support sample_weight.")
262
+ out[name] = fn(y_true, y_pred)
263
+ else:
264
+ out[name] = fn(y_true, y_pred, sample_weight=sample_weight)
@@ -0,0 +1,272 @@
1
+ """Speed and memory benchmarks, against scikit-learn when it is installed.
2
+
3
+ Each case times the fastest of ``repeat`` runs (after one warm-up) and measures peak traced memory with
4
+ ``tracemalloc`` (NumPy reports its allocations to it). Both libraries compute the same metrics on the same
5
+ data, and the largest absolute difference between their results is reported, so speed is never shown for
6
+ numbers that disagree.
7
+
8
+ >>> from evalsuite.benchmarks import run_benchmarks
9
+ >>> print(run_benchmarks(sizes=(10_000,), repeat=3)) # doctest: +SKIP
10
+ """
11
+
12
+ from __future__ import annotations
13
+
14
+ import json
15
+ import platform
16
+ import time
17
+ import tracemalloc
18
+ import warnings
19
+ from collections.abc import Callable, Sequence
20
+ from dataclasses import dataclass, field
21
+ from types import MappingProxyType
22
+ from typing import TYPE_CHECKING, Any, Optional, cast
23
+
24
+ import numpy as np
25
+
26
+ from .core.result import _json_safe, _latex_escape, _latex_table
27
+
28
+ if TYPE_CHECKING:
29
+ import pandas as pd
30
+
31
+ __all__ = ["BenchmarkResult", "run_benchmarks"]
32
+
33
+ _HEADER = (
34
+ "case",
35
+ "n",
36
+ "evalsuite_ms",
37
+ "sklearn_ms",
38
+ "speedup",
39
+ "evalsuite_peak_mb",
40
+ "sklearn_peak_mb",
41
+ "max_abs_diff",
42
+ )
43
+
44
+
45
+ def _measure(fn: Callable[[], Any], repeat: int) -> tuple[float, float, Any]:
46
+ """(fastest seconds, peak MiB, result)."""
47
+ with warnings.catch_warnings():
48
+ warnings.simplefilter("ignore")
49
+ result = fn() # warm-up
50
+ best = float("inf")
51
+ for _ in range(repeat):
52
+ t0 = time.perf_counter()
53
+ fn()
54
+ best = min(best, time.perf_counter() - t0)
55
+ tracemalloc.start()
56
+ try:
57
+ fn()
58
+ _, peak = tracemalloc.get_traced_memory()
59
+ finally:
60
+ tracemalloc.stop()
61
+ return best, peak / 2**20, result
62
+
63
+
64
+ def _cases(n: int, rng: np.random.Generator) -> list[tuple[str, Callable[[], Any], Optional[Callable[[], Any]]]]:
65
+ import evalsuite as es
66
+
67
+ y = rng.integers(0, 2, n)
68
+ p = np.where(rng.random(n) < 0.8, y, 1 - y)
69
+ prob = 1 / (1 + np.exp(-(2.0 * (y - 0.5) + rng.normal(0, 1, n))))
70
+ yk = rng.integers(0, 10, n)
71
+ pk = np.where(rng.random(n) < 0.7, yk, rng.integers(0, 10, n))
72
+ yr = rng.normal(10, 3, n)
73
+ pr = yr + rng.normal(0, 1, n)
74
+ names = ["accuracy", "balanced_accuracy", "precision", "recall", "f1", "specificity", "mcc", "cohen_kappa"]
75
+
76
+ def es_binary() -> list[float]:
77
+ r = es.evaluate(y, p, metrics=names)
78
+ return [float(r[m]) for m in names]
79
+
80
+ def es_reg() -> list[float]:
81
+ r = es.evaluate(yr, pr, metrics=["mae", "mse", "rmse", "r2"])
82
+ return [float(r[m]) for m in ("mae", "mse", "rmse", "r2")]
83
+
84
+ cases: list[tuple[str, Callable[[], Any], Optional[Callable[[], Any]]]] = [
85
+ ("binary: 8 label metrics via evaluate()", es_binary, None),
86
+ ("10 classes: macro F1", lambda: [float(es.f1(yk, pk, average="macro"))], None),
87
+ ("binary: ROC AUC", lambda: [float(es.roc_auc(y, prob))], None),
88
+ ("regression: MAE, MSE, RMSE, R² via evaluate()", es_reg, None),
89
+ ]
90
+ try:
91
+ import sklearn.metrics as skm # type: ignore[import-untyped]
92
+ except ImportError:
93
+ return cases
94
+
95
+ def sk_binary() -> list[float]:
96
+ return [
97
+ skm.accuracy_score(y, p),
98
+ skm.balanced_accuracy_score(y, p),
99
+ skm.precision_score(y, p),
100
+ skm.recall_score(y, p),
101
+ skm.f1_score(y, p),
102
+ skm.recall_score(y, p, pos_label=0),
103
+ skm.matthews_corrcoef(y, p),
104
+ skm.cohen_kappa_score(y, p),
105
+ ]
106
+
107
+ def sk_reg() -> list[float]:
108
+ mse = skm.mean_squared_error(yr, pr)
109
+ return [skm.mean_absolute_error(yr, pr), mse, float(np.sqrt(mse)), skm.r2_score(yr, pr)]
110
+
111
+ sk = [sk_binary, lambda: [skm.f1_score(yk, pk, average="macro")], lambda: [skm.roc_auc_score(y, prob)], sk_reg]
112
+ return [(name, es_fn, sk_fn) for (name, es_fn, _), sk_fn in zip(cases, sk)]
113
+
114
+
115
+ @dataclass(frozen=True, eq=False)
116
+ class BenchmarkResult:
117
+ """Rows of timings (milliseconds), peak memory (MiB) and agreement, plus the environment they ran in."""
118
+
119
+ rows: tuple[Any, ...]
120
+ environment: Any = field(default_factory=dict)
121
+
122
+ def __post_init__(self) -> None:
123
+ object.__setattr__(self, "rows", tuple(MappingProxyType(dict(r)) for r in self.rows))
124
+ object.__setattr__(self, "environment", MappingProxyType(dict(self.environment)))
125
+
126
+ def _cells(self, digits: int) -> list[list[str]]:
127
+ def f(v: Any, d: int = digits) -> str:
128
+ return "–" if v is None else f"{v:.{d}f}"
129
+
130
+ return [
131
+ [
132
+ r["case"],
133
+ f"{r['n']:,}",
134
+ f(r["evalsuite_ms"]),
135
+ f(r["sklearn_ms"]),
136
+ "–" if r["speedup"] is None else f"{r['speedup']:.2f}×",
137
+ f(r["evalsuite_peak_mb"], 2),
138
+ f(r["sklearn_peak_mb"], 2),
139
+ "–" if r["max_abs_diff"] is None else f"{r['max_abs_diff']:.1e}",
140
+ ]
141
+ for r in self.rows
142
+ ]
143
+
144
+ _TITLES = (
145
+ "Case",
146
+ "n",
147
+ "EvalSuite (ms)",
148
+ "scikit-learn (ms)",
149
+ "Speed-up",
150
+ "EvalSuite peak (MiB)",
151
+ "scikit-learn peak (MiB)",
152
+ "Max |difference|",
153
+ )
154
+
155
+ def summary(self, *, digits: int = 3) -> str:
156
+ cells = self._cells(digits)
157
+ widths = [max(len(t), *(len(c[i]) for c in cells)) for i, t in enumerate(self._TITLES)]
158
+
159
+ def line(c: Sequence[str]) -> str:
160
+ return " ".join(x.ljust(widths[i]) if i == 0 else x.rjust(widths[i]) for i, x in enumerate(c))
161
+
162
+ env = self.environment
163
+ head = (
164
+ f"EvalSuite {env['evalsuite']} benchmarks | Python {env['python']} | NumPy {env['numpy']}"
165
+ + (f" | scikit-learn {env['sklearn']}" if env.get("sklearn") else "")
166
+ + f" | {env['machine']} | fastest of {env['repeat']} runs"
167
+ )
168
+ note = "Speed-up > 1 means EvalSuite is faster. Max |difference| compares the two libraries' results."
169
+ return "\n".join([head, "", line(self._TITLES), *(line(c) for c in cells), "", note])
170
+
171
+ def __repr__(self) -> str:
172
+ return self.summary()
173
+
174
+ def to_dict(self) -> dict[str, Any]:
175
+ return cast(
176
+ "dict[str, Any]",
177
+ _json_safe({"environment": dict(self.environment), "rows": [dict(r) for r in self.rows]}),
178
+ )
179
+
180
+ def to_json(self, *, indent: Optional[int] = 2) -> str:
181
+ return json.dumps(self.to_dict(), indent=indent, allow_nan=False)
182
+
183
+ def to_dataframe(self) -> pd.DataFrame:
184
+ import pandas as pd
185
+
186
+ frame: pd.DataFrame = pd.DataFrame([dict(r) for r in self.rows])
187
+ return frame
188
+
189
+ def to_csv(self, path: Optional[str] = None) -> str:
190
+ from .core.export import csv_text
191
+
192
+ rows = [[r[h] if r[h] is not None else float("nan") for h in _HEADER] for r in self.rows]
193
+ text = csv_text(list(_HEADER), rows)
194
+ if path is not None:
195
+ with open(path, "w", encoding="utf-8", newline="") as fh:
196
+ fh.write(text)
197
+ return text
198
+
199
+ def to_markdown(self, *, digits: int = 3) -> str:
200
+ lines = ["| " + " | ".join(self._TITLES) + " |", "| --- |" + " ---: |" * (len(self._TITLES) - 1)]
201
+ return "\n".join(lines + ["| " + " | ".join(c) + " |" for c in self._cells(digits)])
202
+
203
+ def to_latex(self, *, digits: int = 3, caption: Optional[str] = None, label: Optional[str] = None) -> str:
204
+ return _latex_table(
205
+ [_latex_escape(t) for t in self._TITLES],
206
+ [[_latex_escape(x) for x in c] for c in self._cells(digits)],
207
+ caption=caption or "EvalSuite benchmarks.",
208
+ label=label,
209
+ )
210
+
211
+ def to_html(self, *, digits: int = 3, full: bool = False) -> str:
212
+ from .core.export import html_document, html_table
213
+
214
+ table = html_table(list(self._TITLES), self._cells(digits), caption="Benchmarks")
215
+ env = self.environment
216
+ meta = f"Python {env['python']}, NumPy {env['numpy']}, {env['machine']}"
217
+ return html_document("EvalSuite benchmarks", table, meta) if full else table
218
+
219
+
220
+ def run_benchmarks(
221
+ sizes: Sequence[int] = (1_000, 100_000, 1_000_000),
222
+ *,
223
+ repeat: int = 5,
224
+ compare_sklearn: bool = True,
225
+ random_state: Optional[int] = 0,
226
+ ) -> BenchmarkResult:
227
+ """Time and memory for common evaluation workloads at each size, against scikit-learn if installed."""
228
+ import evalsuite as es
229
+
230
+ if repeat < 1:
231
+ raise ValueError("repeat must be at least 1.")
232
+ rng = np.random.default_rng(random_state)
233
+ rows: list[dict[str, Any]] = []
234
+ sk_version: Optional[str] = None
235
+ if compare_sklearn:
236
+ try:
237
+ import sklearn
238
+
239
+ sk_version = sklearn.__version__
240
+ except ImportError:
241
+ compare_sklearn = False
242
+ for n in sizes:
243
+ for name, es_fn, sk_fn in _cases(int(n), rng):
244
+ es_t, es_mem, es_val = _measure(es_fn, repeat)
245
+ row: dict[str, Any] = {
246
+ "case": name,
247
+ "n": int(n),
248
+ "evalsuite_ms": es_t * 1000,
249
+ "evalsuite_peak_mb": es_mem,
250
+ "sklearn_ms": None,
251
+ "sklearn_peak_mb": None,
252
+ "speedup": None,
253
+ "max_abs_diff": None,
254
+ }
255
+ if compare_sklearn and sk_fn is not None:
256
+ sk_t, sk_mem, sk_val = _measure(sk_fn, repeat)
257
+ row.update(
258
+ sklearn_ms=sk_t * 1000,
259
+ sklearn_peak_mb=sk_mem,
260
+ speedup=sk_t / es_t if es_t else None,
261
+ max_abs_diff=float(np.max(np.abs(np.asarray(es_val, float) - np.asarray(sk_val, float)))),
262
+ )
263
+ rows.append(row)
264
+ env = {
265
+ "evalsuite": es.__version__,
266
+ "python": platform.python_version(),
267
+ "numpy": np.__version__,
268
+ "sklearn": sk_version,
269
+ "machine": f"{platform.system()} {platform.machine()}",
270
+ "repeat": repeat,
271
+ }
272
+ return BenchmarkResult(tuple(rows), env)