model-eval-toolkit 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.
- evalreport/__init__.py +28 -0
- evalreport/__version__.py +2 -0
- evalreport/classification/__init__.py +4 -0
- evalreport/classification/report.py +319 -0
- evalreport/clustering/__init__.py +4 -0
- evalreport/clustering/report.py +174 -0
- evalreport/core/base_report.py +479 -0
- evalreport/core/entrypoints.py +97 -0
- evalreport/core/task_inference.py +180 -0
- evalreport/nlp/__init__.py +5 -0
- evalreport/nlp/text_classification.py +21 -0
- evalreport/nlp/text_generation.py +202 -0
- evalreport/ranking/__init__.py +3 -0
- evalreport/ranking/report.py +274 -0
- evalreport/regression/__init__.py +4 -0
- evalreport/regression/report.py +173 -0
- evalreport/timeseries/__init__.py +4 -0
- evalreport/timeseries/report.py +211 -0
- evalreport/vision/__init__.py +6 -0
- evalreport/vision/detection.py +359 -0
- evalreport/vision/image_classification.py +25 -0
- evalreport/vision/segmentation.py +140 -0
- model_eval_toolkit-0.1.0.dist-info/METADATA +339 -0
- model_eval_toolkit-0.1.0.dist-info/RECORD +27 -0
- model_eval_toolkit-0.1.0.dist-info/WHEEL +5 -0
- model_eval_toolkit-0.1.0.dist-info/licenses/LICENSE +21 -0
- model_eval_toolkit-0.1.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,479 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import html as html_module
|
|
4
|
+
import json
|
|
5
|
+
from dataclasses import dataclass, field
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from typing import Any, Dict, List, Optional
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
@dataclass
|
|
11
|
+
class BaseReport:
|
|
12
|
+
"""Base class for all task-specific reports.
|
|
13
|
+
|
|
14
|
+
Handles the common API:
|
|
15
|
+
- ``run_all()`` to compute metrics and generate plots
|
|
16
|
+
- ``to_dict()`` to get a JSON-serializable summary
|
|
17
|
+
- ``save()`` to persist HTML/Markdown/JSON/PDF outputs
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
metrics: Dict[str, Any] = field(default_factory=dict)
|
|
21
|
+
plots: Dict[str, Any] = field(default_factory=dict)
|
|
22
|
+
insights: List[str] = field(default_factory=list)
|
|
23
|
+
metric_descriptions: Dict[str, str] = field(default_factory=dict)
|
|
24
|
+
# Base directory where report outputs (HTML/JSON/PDF) and plots live.
|
|
25
|
+
# This is typically set by the entrypoint based on output_path and
|
|
26
|
+
# defaults to "reports/" when not provided.
|
|
27
|
+
output_dir: Optional[Path] = None
|
|
28
|
+
|
|
29
|
+
def run_all(self) -> Dict[str, Any]:
|
|
30
|
+
"""Run metrics, visualization, and insights.
|
|
31
|
+
|
|
32
|
+
Subclasses should override ``_compute_metrics``,
|
|
33
|
+
``_generate_plots``, and ``_generate_insights``.
|
|
34
|
+
"""
|
|
35
|
+
self._compute_metrics()
|
|
36
|
+
self._generate_plots()
|
|
37
|
+
self._generate_insights()
|
|
38
|
+
return self.to_dict()
|
|
39
|
+
|
|
40
|
+
# Hooks for subclasses -------------------------------------------------
|
|
41
|
+
def _compute_metrics(self) -> None: # pragma: no cover - to be overridden
|
|
42
|
+
...
|
|
43
|
+
|
|
44
|
+
def _generate_plots(self) -> None: # pragma: no cover - to be overridden
|
|
45
|
+
...
|
|
46
|
+
|
|
47
|
+
def _generate_insights(self) -> None: # pragma: no cover - to be overridden
|
|
48
|
+
...
|
|
49
|
+
|
|
50
|
+
# Public helpers -------------------------------------------------------
|
|
51
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
52
|
+
return {
|
|
53
|
+
"metrics": self.metrics,
|
|
54
|
+
"insights": self.insights,
|
|
55
|
+
"plots": self.plots,
|
|
56
|
+
"metric_descriptions": self.metric_descriptions,
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
def save(self, path: str, format: Optional[str] = None) -> None:
|
|
60
|
+
"""Save the report to disk.
|
|
61
|
+
|
|
62
|
+
For v0.1 this implements a minimal HTML/JSON/Markdown/PDF writer;
|
|
63
|
+
richer templating can be added incrementally.
|
|
64
|
+
"""
|
|
65
|
+
target = Path(path)
|
|
66
|
+
fmt = (format or target.suffix.lstrip(".") or "html").lower()
|
|
67
|
+
|
|
68
|
+
if fmt == "json":
|
|
69
|
+
target.write_text(json.dumps(self.to_dict(), indent=2), encoding="utf-8")
|
|
70
|
+
elif fmt in {"html", "htm"}:
|
|
71
|
+
html = self._to_simple_html()
|
|
72
|
+
target.write_text(html, encoding="utf-8")
|
|
73
|
+
elif fmt in {"md", "markdown"}:
|
|
74
|
+
md = self._to_simple_markdown()
|
|
75
|
+
target.write_text(md, encoding="utf-8")
|
|
76
|
+
elif fmt == "pdf":
|
|
77
|
+
self._to_simple_pdf(target)
|
|
78
|
+
else:
|
|
79
|
+
raise ValueError(f"Unsupported output format: {fmt!r}")
|
|
80
|
+
|
|
81
|
+
@staticmethod
|
|
82
|
+
def _format_metric_value(v: Any) -> str:
|
|
83
|
+
if isinstance(v, (dict, list)):
|
|
84
|
+
try:
|
|
85
|
+
return json.dumps(v, indent=2, default=str)
|
|
86
|
+
except TypeError:
|
|
87
|
+
return str(v)
|
|
88
|
+
return str(v)
|
|
89
|
+
|
|
90
|
+
def _to_simple_html(self) -> str:
|
|
91
|
+
"""Order: Metrics → Plots → Insights → Metric reference (descriptions)."""
|
|
92
|
+
|
|
93
|
+
def esc(s: Any) -> str:
|
|
94
|
+
return html_module.escape(self._format_metric_value(s), quote=True)
|
|
95
|
+
|
|
96
|
+
# 1) Metrics: name + value only (clean table)
|
|
97
|
+
metric_rows = []
|
|
98
|
+
for k, v in sorted(self.metrics.items()):
|
|
99
|
+
metric_rows.append(
|
|
100
|
+
"<tr>"
|
|
101
|
+
f"<td class='metric-key'><strong>{html_module.escape(str(k))}</strong></td>"
|
|
102
|
+
f"<td class='metric-value'><pre class='metric-pre'>{esc(v)}</pre></td>"
|
|
103
|
+
"</tr>"
|
|
104
|
+
)
|
|
105
|
+
rows_html = "\n".join(metric_rows) or "<tr><td colspan='2'>No metrics.</td></tr>"
|
|
106
|
+
|
|
107
|
+
# 2) Plots
|
|
108
|
+
plots_html_parts: List[str] = []
|
|
109
|
+
for name, path in self.plots.items():
|
|
110
|
+
title = html_module.escape(name.replace("_", " ").title())
|
|
111
|
+
path_esc = html_module.escape(str(path), quote=True)
|
|
112
|
+
plots_html_parts.append(
|
|
113
|
+
f"<div class='plot-card'>"
|
|
114
|
+
f"<h3 class='plot-title'>{title}</h3>"
|
|
115
|
+
f"<img src='{path_esc}' alt='{title}' loading='lazy' />"
|
|
116
|
+
f"<p class='plot-path'><code>{path_esc}</code></p>"
|
|
117
|
+
f"</div>"
|
|
118
|
+
)
|
|
119
|
+
plots_html = "\n".join(plots_html_parts) or "<p class='muted'>No plots generated.</p>"
|
|
120
|
+
|
|
121
|
+
# 3) Insights
|
|
122
|
+
insights_items = "".join(f"<li class='insight-item'>{html_module.escape(ins)}</li>" for ins in self.insights)
|
|
123
|
+
insights_block = (
|
|
124
|
+
f"<ul class='insights-list'>{insights_items}</ul>"
|
|
125
|
+
if insights_items
|
|
126
|
+
else "<p class='muted'>No automated insights.</p>"
|
|
127
|
+
)
|
|
128
|
+
|
|
129
|
+
# 4) Metric reference (descriptions at end)
|
|
130
|
+
desc_rows = []
|
|
131
|
+
for name in sorted(self.metrics.keys()):
|
|
132
|
+
desc = self.metric_descriptions.get(name, "")
|
|
133
|
+
desc_text = desc if desc else "—"
|
|
134
|
+
desc_rows.append(
|
|
135
|
+
"<tr>"
|
|
136
|
+
f"<td class='ref-metric'>{html_module.escape(str(name))}</td>"
|
|
137
|
+
f"<td class='ref-desc'>{html_module.escape(desc_text)}</td>"
|
|
138
|
+
"</tr>"
|
|
139
|
+
)
|
|
140
|
+
ref_html = "\n".join(desc_rows) or "<tr><td colspan='2'>No metrics to describe.</td></tr>"
|
|
141
|
+
|
|
142
|
+
return f"""<!DOCTYPE html>
|
|
143
|
+
<html lang="en">
|
|
144
|
+
<head>
|
|
145
|
+
<meta charset="utf-8" />
|
|
146
|
+
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
147
|
+
<title>Evaluation Report</title>
|
|
148
|
+
<style>
|
|
149
|
+
:root {{
|
|
150
|
+
--bg: #f0f4f8;
|
|
151
|
+
--card: #ffffff;
|
|
152
|
+
--border: #e2e8f0;
|
|
153
|
+
--text: #1e293b;
|
|
154
|
+
--muted: #64748b;
|
|
155
|
+
--accent: #0f766e;
|
|
156
|
+
--accent-soft: #ccfbf1;
|
|
157
|
+
}}
|
|
158
|
+
* {{ box-sizing: border-box; }}
|
|
159
|
+
body {{
|
|
160
|
+
font-family: ui-sans-serif, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
|
|
161
|
+
max-width: 1000px;
|
|
162
|
+
margin: 0 auto;
|
|
163
|
+
padding: 28px 20px 48px;
|
|
164
|
+
background: var(--bg);
|
|
165
|
+
color: var(--text);
|
|
166
|
+
line-height: 1.5;
|
|
167
|
+
}}
|
|
168
|
+
h1 {{
|
|
169
|
+
font-size: 1.75rem;
|
|
170
|
+
font-weight: 700;
|
|
171
|
+
margin: 0 0 8px;
|
|
172
|
+
letter-spacing: -0.02em;
|
|
173
|
+
}}
|
|
174
|
+
.subtitle {{
|
|
175
|
+
color: var(--muted);
|
|
176
|
+
font-size: 0.95rem;
|
|
177
|
+
margin-bottom: 28px;
|
|
178
|
+
}}
|
|
179
|
+
.section {{
|
|
180
|
+
background: var(--card);
|
|
181
|
+
border-radius: 12px;
|
|
182
|
+
padding: 20px 22px;
|
|
183
|
+
margin-bottom: 20px;
|
|
184
|
+
border: 1px solid var(--border);
|
|
185
|
+
box-shadow: 0 1px 2px rgba(15, 23, 42, 0.04);
|
|
186
|
+
}}
|
|
187
|
+
.section h2 {{
|
|
188
|
+
font-size: 1.1rem;
|
|
189
|
+
margin: 0 0 14px;
|
|
190
|
+
padding-bottom: 10px;
|
|
191
|
+
border-bottom: 2px solid var(--accent-soft);
|
|
192
|
+
color: var(--accent);
|
|
193
|
+
font-weight: 600;
|
|
194
|
+
}}
|
|
195
|
+
.section-num {{
|
|
196
|
+
display: inline-block;
|
|
197
|
+
width: 1.5rem;
|
|
198
|
+
height: 1.5rem;
|
|
199
|
+
line-height: 1.5rem;
|
|
200
|
+
text-align: center;
|
|
201
|
+
border-radius: 6px;
|
|
202
|
+
background: var(--accent);
|
|
203
|
+
color: #fff;
|
|
204
|
+
font-size: 0.75rem;
|
|
205
|
+
margin-right: 8px;
|
|
206
|
+
vertical-align: middle;
|
|
207
|
+
}}
|
|
208
|
+
table.data {{
|
|
209
|
+
width: 100%;
|
|
210
|
+
border-collapse: collapse;
|
|
211
|
+
font-size: 0.9rem;
|
|
212
|
+
}}
|
|
213
|
+
table.data th, table.data td {{
|
|
214
|
+
border-bottom: 1px solid var(--border);
|
|
215
|
+
padding: 10px 12px;
|
|
216
|
+
text-align: left;
|
|
217
|
+
vertical-align: top;
|
|
218
|
+
}}
|
|
219
|
+
table.data thead th {{
|
|
220
|
+
background: #f8fafc;
|
|
221
|
+
font-weight: 600;
|
|
222
|
+
color: var(--muted);
|
|
223
|
+
font-size: 0.75rem;
|
|
224
|
+
text-transform: uppercase;
|
|
225
|
+
letter-spacing: 0.04em;
|
|
226
|
+
}}
|
|
227
|
+
.metric-key {{ width: 32%; font-weight: 500; }}
|
|
228
|
+
.metric-value {{ font-family: ui-monospace, Menlo, Monaco, Consolas, monospace; font-size: 0.85rem; }}
|
|
229
|
+
pre.metric-pre {{
|
|
230
|
+
margin: 0;
|
|
231
|
+
white-space: pre-wrap;
|
|
232
|
+
word-break: break-word;
|
|
233
|
+
font-family: inherit;
|
|
234
|
+
font-size: inherit;
|
|
235
|
+
}}
|
|
236
|
+
.ref-metric {{ width: 28%; font-weight: 500; color: #334155; }}
|
|
237
|
+
.ref-desc {{ color: #475569; font-size: 0.9rem; }}
|
|
238
|
+
.plots-grid {{
|
|
239
|
+
display: grid;
|
|
240
|
+
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
|
|
241
|
+
gap: 18px;
|
|
242
|
+
}}
|
|
243
|
+
.plot-card {{
|
|
244
|
+
border: 1px solid var(--border);
|
|
245
|
+
border-radius: 10px;
|
|
246
|
+
padding: 12px;
|
|
247
|
+
background: #fafbfc;
|
|
248
|
+
}}
|
|
249
|
+
.plot-title {{ font-size: 0.95rem; margin: 0 0 10px; color: #334155; }}
|
|
250
|
+
.plot-card img {{
|
|
251
|
+
width: 100%;
|
|
252
|
+
border-radius: 8px;
|
|
253
|
+
border: 1px solid var(--border);
|
|
254
|
+
display: block;
|
|
255
|
+
}}
|
|
256
|
+
.plot-path {{ margin: 8px 0 0; font-size: 0.75rem; color: var(--muted); }}
|
|
257
|
+
.plot-path code {{ background: #f1f5f9; padding: 2px 6px; border-radius: 4px; }}
|
|
258
|
+
.insights-list {{
|
|
259
|
+
margin: 0;
|
|
260
|
+
padding-left: 1.25rem;
|
|
261
|
+
}}
|
|
262
|
+
.insight-item {{ margin-bottom: 8px; padding-left: 4px; }}
|
|
263
|
+
.muted {{ color: var(--muted); font-style: italic; margin: 0; }}
|
|
264
|
+
</style>
|
|
265
|
+
</head>
|
|
266
|
+
<body>
|
|
267
|
+
<h1>Evaluation Report</h1>
|
|
268
|
+
<p class="subtitle">Metrics, visualizations, insights, and a reference for what each metric means.</p>
|
|
269
|
+
|
|
270
|
+
<div class="section">
|
|
271
|
+
<h2><span class="section-num">1</span>Metrics</h2>
|
|
272
|
+
<table class="data">
|
|
273
|
+
<thead><tr><th>Metric</th><th>Value</th></tr></thead>
|
|
274
|
+
<tbody>{rows_html}</tbody>
|
|
275
|
+
</table>
|
|
276
|
+
</div>
|
|
277
|
+
|
|
278
|
+
<div class="section">
|
|
279
|
+
<h2><span class="section-num">2</span>Plots</h2>
|
|
280
|
+
<div class="plots-grid">{plots_html}</div>
|
|
281
|
+
</div>
|
|
282
|
+
|
|
283
|
+
<div class="section">
|
|
284
|
+
<h2><span class="section-num">3</span>Insights</h2>
|
|
285
|
+
{insights_block}
|
|
286
|
+
</div>
|
|
287
|
+
|
|
288
|
+
<div class="section">
|
|
289
|
+
<h2><span class="section-num">4</span>Metric reference</h2>
|
|
290
|
+
<p class="muted" style="margin-top:0;margin-bottom:12px;">What each reported metric indicates (same order as the metrics table).</p>
|
|
291
|
+
<table class="data">
|
|
292
|
+
<thead><tr><th>Metric</th><th>Description</th></tr></thead>
|
|
293
|
+
<tbody>{ref_html}</tbody>
|
|
294
|
+
</table>
|
|
295
|
+
</div>
|
|
296
|
+
</body>
|
|
297
|
+
</html>"""
|
|
298
|
+
|
|
299
|
+
def _to_simple_markdown(self) -> str:
|
|
300
|
+
"""Order: Metrics → Plots → Insights → Metric reference."""
|
|
301
|
+
|
|
302
|
+
lines: List[str] = [
|
|
303
|
+
"# Evaluation Report",
|
|
304
|
+
"",
|
|
305
|
+
"Structured as: **Metrics** → **Plots** → **Insights** → **Metric reference**.",
|
|
306
|
+
"",
|
|
307
|
+
"---",
|
|
308
|
+
"",
|
|
309
|
+
"## 1. Metrics",
|
|
310
|
+
"",
|
|
311
|
+
]
|
|
312
|
+
for k, v in sorted(self.metrics.items()):
|
|
313
|
+
val = self._format_metric_value(v)
|
|
314
|
+
if "\n" in val:
|
|
315
|
+
lines.append(f"### `{k}`")
|
|
316
|
+
lines.append("")
|
|
317
|
+
lines.append("```")
|
|
318
|
+
lines.append(val)
|
|
319
|
+
lines.append("```")
|
|
320
|
+
lines.append("")
|
|
321
|
+
else:
|
|
322
|
+
lines.append(f"- **`{k}`**: {val}")
|
|
323
|
+
if not self.metrics:
|
|
324
|
+
lines.append("_No metrics._")
|
|
325
|
+
lines.append("")
|
|
326
|
+
lines.append("---")
|
|
327
|
+
lines.append("")
|
|
328
|
+
lines.append("## 2. Plots")
|
|
329
|
+
lines.append("")
|
|
330
|
+
if self.plots:
|
|
331
|
+
for name, p in sorted(self.plots.items()):
|
|
332
|
+
title = name.replace("_", " ").title()
|
|
333
|
+
lines.append(f"### {title}")
|
|
334
|
+
lines.append("")
|
|
335
|
+
lines.append(f"")
|
|
336
|
+
lines.append("")
|
|
337
|
+
lines.append(f"*Path:* `{p}`")
|
|
338
|
+
lines.append("")
|
|
339
|
+
else:
|
|
340
|
+
lines.append("_No plots._")
|
|
341
|
+
lines.append("---")
|
|
342
|
+
lines.append("")
|
|
343
|
+
lines.append("## 3. Insights")
|
|
344
|
+
lines.append("")
|
|
345
|
+
if self.insights:
|
|
346
|
+
for ins in self.insights:
|
|
347
|
+
lines.append(f"- {ins}")
|
|
348
|
+
else:
|
|
349
|
+
lines.append("_No automated insights._")
|
|
350
|
+
lines.append("")
|
|
351
|
+
lines.append("---")
|
|
352
|
+
lines.append("")
|
|
353
|
+
lines.append("## 4. Metric reference")
|
|
354
|
+
lines.append("")
|
|
355
|
+
lines.append("What each metric means (aligned with section 1).")
|
|
356
|
+
lines.append("")
|
|
357
|
+
for name in sorted(self.metrics.keys()):
|
|
358
|
+
desc = self.metric_descriptions.get(name, "").strip()
|
|
359
|
+
lines.append(f"- **`{name}`**")
|
|
360
|
+
lines.append(f" - {desc if desc else '_No description._'}")
|
|
361
|
+
if not self.metrics:
|
|
362
|
+
lines.append("_No metrics._")
|
|
363
|
+
lines.append("")
|
|
364
|
+
return "\n".join(lines)
|
|
365
|
+
|
|
366
|
+
def _to_simple_pdf(self, target: Path) -> None:
|
|
367
|
+
"""PDF: Metrics → Plot paths → Insights → Metric reference. Paginates when needed."""
|
|
368
|
+
|
|
369
|
+
try:
|
|
370
|
+
from reportlab.lib.pagesizes import A4
|
|
371
|
+
from reportlab.pdfgen import canvas
|
|
372
|
+
except Exception as exc: # pragma: no cover - dependency issue
|
|
373
|
+
raise RuntimeError(
|
|
374
|
+
"PDF output requires the 'reportlab' package. "
|
|
375
|
+
"Install with: pip install reportlab."
|
|
376
|
+
) from exc
|
|
377
|
+
|
|
378
|
+
width, height = A4
|
|
379
|
+
margin_x = 48
|
|
380
|
+
margin_top = height - 52
|
|
381
|
+
line_h = 12
|
|
382
|
+
section_gap = 18
|
|
383
|
+
bottom_y = 52
|
|
384
|
+
|
|
385
|
+
c = canvas.Canvas(str(target), pagesize=A4)
|
|
386
|
+
|
|
387
|
+
y = margin_top
|
|
388
|
+
|
|
389
|
+
def ensure_space(need: int) -> None:
|
|
390
|
+
nonlocal y, c
|
|
391
|
+
if y - need < bottom_y:
|
|
392
|
+
c.showPage()
|
|
393
|
+
y = margin_top
|
|
394
|
+
|
|
395
|
+
def draw_heading(title: str, size: int = 14) -> None:
|
|
396
|
+
nonlocal y
|
|
397
|
+
ensure_space(section_gap + line_h + 4)
|
|
398
|
+
c.setFont("Helvetica-Bold", size)
|
|
399
|
+
c.drawString(margin_x, y, title)
|
|
400
|
+
y -= size + 6
|
|
401
|
+
|
|
402
|
+
def draw_body_line(text: str, indent: int = 0) -> None:
|
|
403
|
+
nonlocal y
|
|
404
|
+
ensure_space(line_h)
|
|
405
|
+
c.setFont("Helvetica", 9)
|
|
406
|
+
x = margin_x + indent
|
|
407
|
+
# simple wrap for long lines
|
|
408
|
+
max_w = width - margin_x * 2 - indent
|
|
409
|
+
words = text.split()
|
|
410
|
+
line = ""
|
|
411
|
+
for w in words:
|
|
412
|
+
test = (line + " " + w).strip()
|
|
413
|
+
if c.stringWidth(test, "Helvetica", 9) <= max_w:
|
|
414
|
+
line = test
|
|
415
|
+
else:
|
|
416
|
+
if line:
|
|
417
|
+
c.drawString(x, y, line[:500])
|
|
418
|
+
y -= line_h
|
|
419
|
+
ensure_space(line_h)
|
|
420
|
+
line = w
|
|
421
|
+
if line:
|
|
422
|
+
c.drawString(x, y, line[:500])
|
|
423
|
+
y -= line_h
|
|
424
|
+
|
|
425
|
+
# Title
|
|
426
|
+
c.setFont("Helvetica-Bold", 18)
|
|
427
|
+
c.drawString(margin_x, y, "Evaluation Report")
|
|
428
|
+
y -= 22
|
|
429
|
+
c.setFont("Helvetica-Oblique", 9)
|
|
430
|
+
c.setFillColorRGB(0.35, 0.35, 0.35)
|
|
431
|
+
c.drawString(margin_x, y, "Order: Metrics | Plots (paths) | Insights | Metric reference")
|
|
432
|
+
c.setFillColorRGB(0, 0, 0)
|
|
433
|
+
y -= section_gap
|
|
434
|
+
|
|
435
|
+
# 1 Metrics
|
|
436
|
+
draw_heading("1. Metrics", 12)
|
|
437
|
+
for name, value in sorted(self.metrics.items()):
|
|
438
|
+
val_str = self._format_metric_value(value)
|
|
439
|
+
draw_body_line(f"{name}: {val_str[:200]}{'...' if len(val_str) > 200 else ''}", 0)
|
|
440
|
+
if len(val_str) > 200:
|
|
441
|
+
# continuation chunks
|
|
442
|
+
rest = val_str[200:]
|
|
443
|
+
while rest:
|
|
444
|
+
chunk = rest[:120]
|
|
445
|
+
rest = rest[120:]
|
|
446
|
+
draw_body_line(chunk, 12)
|
|
447
|
+
if not self.metrics:
|
|
448
|
+
draw_body_line("(No metrics.)")
|
|
449
|
+
y -= section_gap // 2
|
|
450
|
+
|
|
451
|
+
# 2 Plots (paths only)
|
|
452
|
+
draw_heading("2. Plots (file paths)", 12)
|
|
453
|
+
if self.plots:
|
|
454
|
+
for name, p in sorted(self.plots.items()):
|
|
455
|
+
draw_body_line(f"{name}: {p}", 0)
|
|
456
|
+
else:
|
|
457
|
+
draw_body_line("(No plots.)")
|
|
458
|
+
y -= section_gap // 2
|
|
459
|
+
|
|
460
|
+
# 3 Insights
|
|
461
|
+
draw_heading("3. Insights", 12)
|
|
462
|
+
if self.insights:
|
|
463
|
+
for ins in self.insights:
|
|
464
|
+
draw_body_line(f"- {ins}", 0)
|
|
465
|
+
else:
|
|
466
|
+
draw_body_line("(No insights.)")
|
|
467
|
+
y -= section_gap // 2
|
|
468
|
+
|
|
469
|
+
# 4 Metric reference
|
|
470
|
+
draw_heading("4. Metric reference", 12)
|
|
471
|
+
for name in sorted(self.metrics.keys()):
|
|
472
|
+
desc = self.metric_descriptions.get(name, "").strip() or "(No description.)"
|
|
473
|
+
draw_body_line(f"{name}", 0)
|
|
474
|
+
draw_body_line(desc, 10)
|
|
475
|
+
y -= 4
|
|
476
|
+
if not self.metrics:
|
|
477
|
+
draw_body_line("(No metrics.)")
|
|
478
|
+
|
|
479
|
+
c.save()
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
from typing import Any, Iterable, Optional
|
|
5
|
+
|
|
6
|
+
from ..classification.report import ClassificationReport
|
|
7
|
+
from ..clustering.report import ClusteringReport
|
|
8
|
+
from ..nlp.text_classification import TextClassificationReport
|
|
9
|
+
from ..nlp.text_generation import TextGenerationReport
|
|
10
|
+
from ..regression.report import RegressionReport
|
|
11
|
+
from ..timeseries.report import TimeSeriesReport
|
|
12
|
+
from ..ranking.report import RankingReport
|
|
13
|
+
from ..vision.detection import DetectionReport
|
|
14
|
+
from ..vision.segmentation import SegmentationReport
|
|
15
|
+
from ..vision.image_classification import ImageClassificationReport
|
|
16
|
+
from .task_inference import infer_task
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def generate_report(
|
|
20
|
+
*,
|
|
21
|
+
task: str = "auto",
|
|
22
|
+
y_true: Optional[Iterable[Any]] = None,
|
|
23
|
+
y_pred: Optional[Iterable[Any]] = None,
|
|
24
|
+
y_prob: Optional[Iterable[Any]] = None,
|
|
25
|
+
X: Optional[Iterable[Any]] = None,
|
|
26
|
+
timestamps: Optional[Iterable[Any]] = None,
|
|
27
|
+
embeddings: Optional[Iterable[Any]] = None,
|
|
28
|
+
output_path: Optional[str] = None,
|
|
29
|
+
format: str = "html",
|
|
30
|
+
**kwargs: Any,
|
|
31
|
+
) -> Any:
|
|
32
|
+
"""Unified entry point for generating evaluation reports.
|
|
33
|
+
|
|
34
|
+
This is a thin orchestrator that delegates to the appropriate
|
|
35
|
+
task-specific report class based on the ``task`` argument or
|
|
36
|
+
automatic task inference.
|
|
37
|
+
"""
|
|
38
|
+
if task == "auto":
|
|
39
|
+
task = infer_task(y_true=y_true, y_pred=y_pred, X=X, timestamps=timestamps, embeddings=embeddings)
|
|
40
|
+
|
|
41
|
+
task = task.lower()
|
|
42
|
+
|
|
43
|
+
# Determine base output directory for this report
|
|
44
|
+
if output_path:
|
|
45
|
+
base_dir = Path(output_path).expanduser().resolve().parent
|
|
46
|
+
else:
|
|
47
|
+
base_dir = Path("reports").resolve()
|
|
48
|
+
base_dir.mkdir(parents=True, exist_ok=True)
|
|
49
|
+
|
|
50
|
+
if task in {"classification", "binary_classification", "multiclass", "multilabel"}:
|
|
51
|
+
report = ClassificationReport(y_true=y_true, y_pred=y_pred, y_prob=y_prob, **kwargs)
|
|
52
|
+
elif task == "regression":
|
|
53
|
+
report = RegressionReport(y_true=y_true, y_pred=y_pred, **kwargs)
|
|
54
|
+
elif task in {"clustering", "cluster"}:
|
|
55
|
+
# For clustering, y_pred is treated as cluster assignments/labels.
|
|
56
|
+
report = ClusteringReport(X=X, labels=y_pred, **kwargs)
|
|
57
|
+
elif task in {"timeseries", "forecasting", "time_series"}:
|
|
58
|
+
report = TimeSeriesReport(y_true=y_true, y_pred=y_pred, timestamps=timestamps, **kwargs)
|
|
59
|
+
elif task in {"text_classification", "nlp_text_classification"}:
|
|
60
|
+
report = TextClassificationReport(y_true=y_true, y_pred=y_pred, y_prob=y_prob, **kwargs)
|
|
61
|
+
elif task in {"text_generation", "nlp_text_generation"}:
|
|
62
|
+
report = TextGenerationReport(references=y_true, predictions=y_pred, **kwargs)
|
|
63
|
+
elif task in {"segmentation", "image_segmentation"}:
|
|
64
|
+
report = SegmentationReport(y_true_masks=y_true, y_pred_masks=y_pred, **kwargs)
|
|
65
|
+
elif task in {"detection", "object_detection"}:
|
|
66
|
+
report = DetectionReport(y_true=y_true, y_pred=y_pred, **kwargs)
|
|
67
|
+
elif task in {"image_classification", "vision_classification"}:
|
|
68
|
+
report = ImageClassificationReport(y_true=y_true, y_pred=y_pred, y_prob=y_prob, **kwargs)
|
|
69
|
+
elif task in {"ranking", "recommendation", "recommender"}:
|
|
70
|
+
# y_true = relevant items per user; y_pred = ranked recommendation lists per user
|
|
71
|
+
report = RankingReport(relevant=y_true, ranked=y_pred, **kwargs)
|
|
72
|
+
else:
|
|
73
|
+
raise ValueError(
|
|
74
|
+
f"Unsupported task type for v0.1: {task!r}. "
|
|
75
|
+
"Currently supported: classification, regression, clustering, timeseries, nlp, vision, ranking."
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
# Make sure downstream plots know where they should live
|
|
79
|
+
report.output_dir = base_dir
|
|
80
|
+
|
|
81
|
+
result = report.run_all()
|
|
82
|
+
|
|
83
|
+
# Persist report
|
|
84
|
+
if output_path:
|
|
85
|
+
output_path = str(output_path)
|
|
86
|
+
suffix = Path(output_path).suffix.lower()
|
|
87
|
+
fmt = format or suffix.lstrip(".") or "html"
|
|
88
|
+
report.save(output_path, format=fmt)
|
|
89
|
+
else:
|
|
90
|
+
# Default to reports/<task>_report.html if user did not specify a path
|
|
91
|
+
fmt = (format or "html").lower()
|
|
92
|
+
default_name = f"{task}_report.{fmt}"
|
|
93
|
+
default_path = base_dir / default_name
|
|
94
|
+
report.save(str(default_path), format=fmt)
|
|
95
|
+
|
|
96
|
+
return result
|
|
97
|
+
|