evalstats 0.1.9__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.
evalstats/__init__.py ADDED
@@ -0,0 +1,82 @@
1
+ """evalstats: Statistical analysis and visualization for prompt benchmarking."""
2
+
3
+ from evalstats.core.types import BenchmarkResult, MultiModelBenchmark
4
+ from evalstats.core.paired import pairwise_differences, all_pairwise, vs_baseline, friedman_nemenyi, FriedmanResult
5
+ from evalstats.core.ranking import bootstrap_ranks
6
+ from evalstats.core.variance import (
7
+ robustness_metrics,
8
+ seed_variance_decomposition,
9
+ SeedVarianceResult,
10
+ )
11
+ from evalstats.core.router import (
12
+ analyze,
13
+ analyze_factorial,
14
+ AnalysisBundle,
15
+ AnalysisResult,
16
+ BenchmarkShape,
17
+ MultiModelBundle,
18
+ )
19
+ from evalstats.core.summary import print_analysis_summary, print_brief_summary
20
+ from evalstats.vis.point_estimates import plot_point_estimates
21
+ from evalstats.vis.critical_difference import plot_critical_difference
22
+ from evalstats.vis.forest import plot_ci_forest
23
+ from evalstats.vis.scoreboard import plot_accuracy_bar
24
+ from evalstats.io import from_dataframe, DataLoadReport
25
+ from evalstats.core.resampling import bayes_binary_ci_1d, bayes_paired_diff_ci
26
+ from evalstats.core import bayes_evals
27
+ from evalstats.compare import (
28
+ compare_prompts,
29
+ compare_models,
30
+ CompareReport,
31
+ EntityStats,
32
+ )
33
+ from evalstats.config import set_alpha_ci, get_alpha_ci
34
+
35
+ __version__ = "0.1.9"
36
+
37
+ __all__ = [
38
+ "BenchmarkResult",
39
+ "MultiModelBenchmark",
40
+ "pairwise_differences",
41
+ "all_pairwise",
42
+ "vs_baseline",
43
+ "friedman_nemenyi",
44
+ "FriedmanResult",
45
+ "bootstrap_ranks",
46
+ "robustness_metrics",
47
+ "seed_variance_decomposition",
48
+ "SeedVarianceResult",
49
+ "analyze",
50
+ "AnalysisBundle",
51
+ "AnalysisResult",
52
+ "BenchmarkShape",
53
+ "MultiModelBundle",
54
+ "print_analysis_summary",
55
+ "print_brief_summary",
56
+ "plot_point_estimates",
57
+ "plot_critical_difference",
58
+ "plot_ci_forest",
59
+ "plot_accuracy_bar",
60
+ "from_dataframe",
61
+ "DataLoadReport",
62
+ "bayes_binary_ci_1d",
63
+ "bayes_paired_diff_ci",
64
+ "bayes_evals",
65
+ "compare_prompts",
66
+ "compare_models",
67
+ "CompareReport",
68
+ "EntityStats",
69
+ "analyze_factorial",
70
+ "set_alpha_ci",
71
+ "get_alpha_ci",
72
+ ]
73
+
74
+ # LMMInfo and FactorialLMMInfo are exported lazily so that statsmodels/pymer4
75
+ # are not hard dependencies. Access via:
76
+ # from evalstats.core.mixed_effects import LMMInfo, FactorialLMMInfo
77
+ # or inspect bundle.lmm_info / bundle.factorial_lmm_info at runtime.
78
+ try:
79
+ from evalstats.core.mixed_effects import LMMInfo, FactorialLMMInfo
80
+ __all__ = __all__ + ["LMMInfo", "FactorialLMMInfo"]
81
+ except ImportError:
82
+ pass
evalstats/cli.py ADDED
@@ -0,0 +1,596 @@
1
+ """Command-line interface for evalstats.
2
+
3
+ Entry point declared in pyproject.toml::
4
+
5
+ [project.scripts]
6
+ evalstats = "evalstats.cli:main"
7
+
8
+ Usage::
9
+
10
+ evalstats analyze data.csv
11
+ evalstats analyze data.xlsx --sheet "Results"
12
+ evalstats analyze data.csv --ci 0.90 --n-bootstrap 5000
13
+ evalstats analyze data.csv --evaluator-mode per_evaluator
14
+ """
15
+
16
+ from __future__ import annotations
17
+
18
+ import argparse
19
+ import io
20
+ import json
21
+ import sys
22
+ from contextlib import redirect_stdout
23
+ from dataclasses import asdict, is_dataclass
24
+ from pathlib import Path
25
+ from typing import Union
26
+
27
+ import numpy as np
28
+ import pandas as pd
29
+
30
+ from evalstats.config import set_alpha_ci
31
+
32
+
33
+ # ---------------------------------------------------------------------------
34
+ # Entry point
35
+ # ---------------------------------------------------------------------------
36
+
37
+ def main() -> None:
38
+ parser = _build_parser()
39
+ args = parser.parse_args()
40
+ if args.command == "analyze":
41
+ _cmd_analyze(args)
42
+ else:
43
+ parser.print_help()
44
+ sys.exit(1)
45
+
46
+
47
+ # ---------------------------------------------------------------------------
48
+ # Argument parser
49
+ # ---------------------------------------------------------------------------
50
+
51
+ _ANALYZE_EPILOG = """\
52
+ FILE FORMATS
53
+ ------------
54
+
55
+ Wide format (rows = inputs, columns = prompt templates):
56
+
57
+ input, Template A, Template B, Template C
58
+ example_1, 0.85, 0.72, 0.91
59
+ example_2, 0.63, 0.88, 0.77
60
+
61
+ The first column contains input identifiers. Each subsequent column is a
62
+ prompt template. All score values must be numeric.
63
+ Multiple evaluators are not supported in wide format.
64
+
65
+ Long / tidy format (one observation per row):
66
+
67
+ Required columns (case-insensitive):
68
+ prompt — prompt template name
69
+ input — input identifier
70
+ score — numeric score
71
+
72
+ Optional columns:
73
+ evaluator — evaluator name (enables multi-evaluator analysis; use
74
+ --evaluator-mode to control how evaluators are combined)
75
+ model — model name (enables multi-model analysis)
76
+ run — run index (adds run dimension; ≥3 runs per cell
77
+ enables seed-variance / instability metrics)
78
+
79
+ Example – single model, one implicit evaluator, a single run:
80
+
81
+ prompt, input, score
82
+ Template A, ex_1, 0.85
83
+ Template A, ex_1, 0.91
84
+ Template B, ex_1, 0.72
85
+ Template B, ex_1, 0.88
86
+ ...
87
+
88
+ Example – single model, multiple evaluators, with multiple runs:
89
+
90
+ prompt, input, run, evaluator, score
91
+ Template A, ex_1, 0, accuracy, 0.85
92
+ Template A, ex_1, 0, fluency, 0.91
93
+ Template A, ex_1, 1, accuracy, 0.83
94
+ ...
95
+
96
+ Example – multi-model:
97
+
98
+ model, prompt, input, score
99
+ GPT-4, Template A, ex_1, 0.85
100
+ Claude, Template A, ex_1, 0.90
101
+ ...
102
+
103
+ Column name aliases (all case-insensitive):
104
+ prompt → template, prompt_template
105
+ input → example, item, id, input_label
106
+ score → value, result, metric
107
+ evaluator → eval, judge, criterion, metric_name
108
+ model → model_label, model_name
109
+ run → seed, repeat, run_id, trial
110
+ """
111
+
112
+
113
+ def _build_parser() -> argparse.ArgumentParser:
114
+ parser = argparse.ArgumentParser(
115
+ prog="evalstats",
116
+ description="Statistical analysis for comparing prompt and model performance on benchmarks.",
117
+ formatter_class=argparse.RawDescriptionHelpFormatter,
118
+ )
119
+ sub = parser.add_subparsers(dest="command", metavar="command")
120
+ sub.required = True
121
+
122
+ analyze = sub.add_parser(
123
+ "analyze",
124
+ help="Load a dataset and run statistical analysis.",
125
+ description="Run statistical analysis on a benchmark dataset.",
126
+ epilog=_ANALYZE_EPILOG,
127
+ formatter_class=argparse.RawDescriptionHelpFormatter,
128
+ )
129
+ analyze.add_argument(
130
+ "file",
131
+ type=Path,
132
+ help="Path to a CSV or XLSX benchmark file.",
133
+ )
134
+ analyze.add_argument(
135
+ "--format",
136
+ choices=["auto", "wide", "long"],
137
+ default="auto",
138
+ metavar="FORMAT",
139
+ help=(
140
+ "Data format: 'wide' (rows=inputs, cols=prompt templates), 'long' (tidy "
141
+ "format with prompt/input/score columns), or 'auto' (default)."
142
+ ),
143
+ )
144
+ analyze.add_argument(
145
+ "--sheet",
146
+ default="0",
147
+ metavar="SHEET",
148
+ help="Sheet name or 0-based index for XLSX files (default: 0).",
149
+ )
150
+ analyze.add_argument(
151
+ "--evaluator-mode",
152
+ choices=["aggregate", "per_evaluator"],
153
+ default="aggregate",
154
+ metavar="MODE",
155
+ help=(
156
+ "How to handle multiple evaluators: 'aggregate' (default) averages scores "
157
+ "across evaluators before analysis; 'per_evaluator' runs a separate full "
158
+ "analysis for each evaluator and prints each in turn. "
159
+ "Only applies when an 'evaluator' column is present in the data."
160
+ ),
161
+ )
162
+ analyze.add_argument(
163
+ "--ci",
164
+ type=float,
165
+ default=None,
166
+ metavar="FLOAT",
167
+ help=(
168
+ "Confidence level for intervals. If omitted, uses the project-wide "
169
+ "default from evalstats.config.get_alpha_ci() (0.99)."
170
+ ),
171
+ )
172
+ analyze.add_argument(
173
+ "--method",
174
+ choices=[
175
+ "auto",
176
+ "bootstrap",
177
+ "bca",
178
+ "bayes_bootstrap",
179
+ "smooth_bootstrap",
180
+ "permutation",
181
+ "sign_test",
182
+ "lmm",
183
+ "bayes_binary",
184
+ "wilson",
185
+ "newcombe",
186
+ "fisher_exact",
187
+ ],
188
+ default="auto",
189
+ metavar="METHOD",
190
+ help=(
191
+ "Inference method (default: auto). Use 'lmm' for mixed-effects modeling; "
192
+ "binary-only modes include 'bayes_binary', 'wilson', 'newcombe', and "
193
+ "'fisher_exact'."
194
+ ),
195
+ )
196
+ analyze.add_argument(
197
+ "--backend",
198
+ choices=["statsmodels", "pymer4"],
199
+ default="statsmodels",
200
+ metavar="BACKEND",
201
+ help=(
202
+ "LMM backend when --method lmm (default: statsmodels). "
203
+ "Ignored for non-LMM methods."
204
+ ),
205
+ )
206
+ analyze.add_argument(
207
+ "--n-bootstrap",
208
+ type=int,
209
+ default=10_000,
210
+ metavar="INT",
211
+ help="Number of bootstrap resamples (default: 10000).",
212
+ )
213
+ analyze.add_argument(
214
+ "--correction",
215
+ choices=["holm", "bonferroni", "fdr_bh", "none"],
216
+ default="fdr_bh",
217
+ help="Multiple-comparisons p-value correction (default: fdr_bh).",
218
+ )
219
+ analyze.add_argument(
220
+ "--reference",
221
+ default="grand_mean",
222
+ metavar="LABEL",
223
+ help=(
224
+ "Reference label retained for compatibility. In robustness-first mode, "
225
+ "absolute means and CIs are reported directly."
226
+ ),
227
+ )
228
+ analyze.add_argument(
229
+ "--failure-threshold",
230
+ type=float,
231
+ default=None,
232
+ metavar="FLOAT",
233
+ help="Report fraction of inputs scoring below this value (robustness table).",
234
+ )
235
+ analyze.add_argument(
236
+ "--spread-percentiles",
237
+ nargs=2,
238
+ type=float,
239
+ default=(10.0, 90.0),
240
+ metavar=("LOW", "HIGH"),
241
+ help=(
242
+ "Retained for compatibility (default: 10 90)."
243
+ ),
244
+ )
245
+ analyze.add_argument(
246
+ "--statistic",
247
+ choices=["mean", "median"],
248
+ default="mean",
249
+ metavar="STAT",
250
+ help="Central tendency for estimates and resampling (default: mean).",
251
+ )
252
+ analyze.add_argument(
253
+ "--template-model-collapse",
254
+ choices=["mean", "as_runs"],
255
+ default="as_runs",
256
+ metavar="MODE",
257
+ help=(
258
+ "Multi-model template collapse mode: 'mean' or 'as_runs' (default: as_runs)."
259
+ ),
260
+ )
261
+ analyze.add_argument(
262
+ "--simultaneous-ci",
263
+ action=argparse.BooleanOptionalAction,
264
+ default=True,
265
+ help=(
266
+ "Use simultaneous (family-wise) pairwise CIs (default: enabled). "
267
+ "Use --no-simultaneous-ci for marginal CIs."
268
+ ),
269
+ )
270
+ analyze.add_argument(
271
+ "--omnibus",
272
+ action="store_true",
273
+ help="Run an omnibus test in addition to pairwise comparisons.",
274
+ )
275
+ analyze.add_argument(
276
+ "--p-values",
277
+ action="store_true",
278
+ default=False,
279
+ help=(
280
+ "Show p-values in pairwise comparison tables. The test used is "
281
+ "determined by --pairwise-test (default: auto). When --omnibus is "
282
+ "also set, 'auto' selects Wilcoxon signed-rank as the Friedman "
283
+ "post-hoc; otherwise bootstrap p-values are shown for bootstrap "
284
+ "methods and Wilcoxon for LMM/other methods."
285
+ ),
286
+ )
287
+ analyze.add_argument(
288
+ "--pairwise-test",
289
+ choices=["auto", "bootstrap", "wilcoxon", "nemenyi"],
290
+ default="auto",
291
+ metavar="TEST",
292
+ help=(
293
+ "Pairwise p-value test to use when --p-values is enabled (or when "
294
+ "this flag is set explicitly, which also enables p-values). "
295
+ "Choices: 'auto' (default), 'bootstrap', 'wilcoxon', 'nemenyi'."
296
+ ),
297
+ )
298
+ analyze.add_argument(
299
+ "--top-pairwise",
300
+ type=int,
301
+ default=5,
302
+ metavar="INT",
303
+ help="Number of pairwise comparisons to show in summary (default: 5).",
304
+ )
305
+ analyze.add_argument(
306
+ "--brief",
307
+ action="store_true",
308
+ help=(
309
+ "Print only the executive leaderboard (entity names, significance groups, "
310
+ "means, CIs, verdicts). Omits the full statistical breakdown — interval "
311
+ "plots, pairwise tables, and robustness section. Useful for a quick result "
312
+ "at a glance. Use --out to save the full analysis alongside."
313
+ ),
314
+ )
315
+ analyze.add_argument(
316
+ "--out",
317
+ nargs="+",
318
+ default=None,
319
+ metavar="PATH",
320
+ help=(
321
+ "Optional output artifact paths. Supported suffixes: .md/.txt (summary), "
322
+ ".json (structured analysis), and .png (robustness interval plot)."
323
+ ),
324
+ )
325
+ return parser
326
+
327
+
328
+ # ---------------------------------------------------------------------------
329
+ # analyze command
330
+ # ---------------------------------------------------------------------------
331
+
332
+ def _cmd_analyze(args: argparse.Namespace) -> None:
333
+ ci = getattr(args, "ci", None)
334
+ if ci is not None:
335
+ set_alpha_ci(1.0 - ci)
336
+
337
+ path = args.file.expanduser().resolve()
338
+ if not path.exists():
339
+ _die(f"file not found: {path}")
340
+
341
+ # --- Load file ---
342
+ print(f"Loading {path.name} ...", flush=True)
343
+ sheet = _parse_sheet(args.sheet)
344
+ try:
345
+ df = _load_file(path, sheet=sheet)
346
+ except ImportError as exc:
347
+ _die(
348
+ f"{exc}\n"
349
+ "Install openpyxl for XLSX support: pip install openpyxl\n"
350
+ "Or install with the xlsx extra: pip install evalstats[xlsx]"
351
+ )
352
+ except Exception as exc:
353
+ _die(f"could not read file: {exc}")
354
+
355
+ print(f" {len(df)} rows × {len(df.columns)} columns: {list(df.columns)}")
356
+
357
+ # --- Detect / parse format ---
358
+ from evalstats.io import from_dataframe
359
+
360
+ try:
361
+ result, report = from_dataframe(
362
+ df,
363
+ format=args.format,
364
+ return_report=True,
365
+ )
366
+ except Exception as exc:
367
+ _die(f"could not parse data: {exc}")
368
+
369
+ if args.format == "auto":
370
+ print(f" Detected format: {report.format_detected}")
371
+
372
+ # --- Show what was loaded ---
373
+ from evalstats.core.types import BenchmarkResult, MultiModelBenchmark
374
+
375
+ if isinstance(result, MultiModelBenchmark):
376
+ runs_str = f" × {result.n_runs} runs" if result.n_runs > 1 else ""
377
+ evals_str = f" × {result.n_evaluators} evaluators" if result.n_evaluators > 1 else ""
378
+ print(
379
+ f" MultiModelBenchmark: {result.n_models} models × "
380
+ f"{result.n_templates} prompts × {result.n_inputs} inputs{runs_str}{evals_str}"
381
+ )
382
+ print(f" Models: {result.model_labels}")
383
+ print(f" Prompts: {result.template_labels}")
384
+ if result.n_evaluators > 1:
385
+ print(f" Evaluators: {result.evaluator_names}")
386
+ else:
387
+ runs_str = f" × {result.n_runs} runs" if result.n_runs > 1 else ""
388
+ evals_str = f" × {result.n_evaluators} evaluators" if result.n_evaluators > 1 else ""
389
+ print(
390
+ f" BenchmarkResult: {result.n_templates} prompts × "
391
+ f"{result.n_inputs} inputs{runs_str}{evals_str}"
392
+ )
393
+ print(f" Prompts: {result.template_labels}")
394
+ if result.n_evaluators > 1:
395
+ print(f" Evaluators: {result.evaluator_names}")
396
+
397
+ # --- Validate --evaluator-mode ---
398
+ evaluator_mode = args.evaluator_mode
399
+
400
+ # --- Validate --reference ---
401
+ if args.reference != "grand_mean":
402
+ if args.reference not in result.template_labels:
403
+ _die(
404
+ f"--reference '{args.reference}' not found in prompt template labels.\n"
405
+ f" Available: {result.template_labels}"
406
+ )
407
+
408
+ print()
409
+
410
+ # --- Run analysis ---
411
+ from evalstats.core.router import analyze
412
+ from evalstats.core.summary import print_analysis_summary
413
+
414
+ print("Running analysis ...", flush=True)
415
+ try:
416
+ analysis = analyze(
417
+ result,
418
+ evaluator_mode=evaluator_mode,
419
+ reference=args.reference,
420
+ method=getattr(args, "method", "auto"),
421
+ backend=getattr(args, "backend", "statsmodels"),
422
+ ci=ci,
423
+ n_bootstrap=getattr(args, "n_bootstrap", 10_000),
424
+ correction=getattr(args, "correction", "fdr_bh"),
425
+ spread_percentiles=tuple(getattr(args, "spread_percentiles", (10, 90))),
426
+ failure_threshold=getattr(args, "failure_threshold", None),
427
+ statistic=getattr(args, "statistic", "mean"),
428
+ template_model_collapse=getattr(args, "template_model_collapse", "as_runs"),
429
+ simultaneous_ci=getattr(args, "simultaneous_ci", True),
430
+ omnibus=getattr(args, "omnibus", False),
431
+ p_values=getattr(args, "p_values", False),
432
+ pairwise_test=getattr(args, "pairwise_test", "auto"),
433
+ )
434
+ except (ValueError, NotImplementedError) as exc:
435
+ _die(str(exc))
436
+
437
+ print()
438
+ summary_buffer = io.StringIO()
439
+ with redirect_stdout(summary_buffer):
440
+ if getattr(args, "brief", False):
441
+ from evalstats.core.summary import print_brief_summary
442
+ print_brief_summary(analysis)
443
+ else:
444
+ print_analysis_summary(analysis, top_pairwise=args.top_pairwise)
445
+ summary_text = summary_buffer.getvalue()
446
+ print(summary_text, end="")
447
+
448
+ out_paths = getattr(args, "out", None)
449
+ if out_paths:
450
+ if ci is None:
451
+ from evalstats.config import get_alpha_ci
452
+
453
+ ci_for_outputs = 1.0 - get_alpha_ci()
454
+ else:
455
+ ci_for_outputs = ci
456
+ _write_outputs(
457
+ out_paths=out_paths,
458
+ summary_text=summary_text,
459
+ analysis=analysis,
460
+ reference=args.reference,
461
+ n_bootstrap=args.n_bootstrap,
462
+ ci=ci_for_outputs,
463
+ )
464
+
465
+
466
+ # ---------------------------------------------------------------------------
467
+ # File loading
468
+ # ---------------------------------------------------------------------------
469
+
470
+ def _parse_sheet(s: str) -> Union[int, str]:
471
+ """Convert a sheet argument to int if it looks like a number, else str."""
472
+ try:
473
+ return int(s)
474
+ except (ValueError, TypeError):
475
+ return s
476
+
477
+
478
+ def _load_file(path: Path, sheet: Union[int, str] = 0) -> pd.DataFrame:
479
+ suffix = path.suffix.lower()
480
+ if suffix == ".csv":
481
+ return pd.read_csv(path)
482
+ elif suffix in (".xlsx", ".xls", ".ods"):
483
+ return pd.read_excel(path, sheet_name=sheet)
484
+ else:
485
+ raise ValueError(
486
+ f"Unsupported file type '{suffix}'. "
487
+ "Accepted formats: .csv, .xlsx, .xls, .ods"
488
+ )
489
+
490
+
491
+ def _die(msg: str) -> None:
492
+ sys.stdout.flush()
493
+ print(f"evalstats error: {msg}", file=sys.stderr)
494
+ sys.exit(1)
495
+
496
+
497
+ def _to_builtin(value):
498
+ if is_dataclass(value):
499
+ return _to_builtin(asdict(value))
500
+ if isinstance(value, dict):
501
+ return {str(k): _to_builtin(v) for k, v in value.items()}
502
+ if isinstance(value, (list, tuple)):
503
+ return [_to_builtin(v) for v in value]
504
+ if isinstance(value, np.ndarray):
505
+ return value.tolist()
506
+ if isinstance(value, np.generic):
507
+ return value.item()
508
+ return value
509
+
510
+
511
+ def _write_outputs(
512
+ *,
513
+ out_paths: list[str],
514
+ summary_text: str,
515
+ analysis,
516
+ reference: str,
517
+ n_bootstrap: int,
518
+ ci: float,
519
+ ) -> None:
520
+ from evalstats.core.router import AnalysisBundle, MultiModelBundle
521
+ from evalstats.vis.point_estimates import plot_point_estimates
522
+
523
+ for raw in out_paths:
524
+ out_path = Path(raw).expanduser().resolve()
525
+ out_path.parent.mkdir(parents=True, exist_ok=True)
526
+ suffix = out_path.suffix.lower()
527
+
528
+ if suffix in {".txt", ".md"}:
529
+ if suffix == ".md":
530
+ content = "# evalstats analysis\n\n```text\n" + summary_text.rstrip() + "\n```\n"
531
+ else:
532
+ content = summary_text
533
+ out_path.write_text(content, encoding="utf-8")
534
+ print(f"Wrote summary: {out_path}")
535
+ continue
536
+
537
+ if suffix == ".json":
538
+ payload = {
539
+ "type": "evalstats.analysis",
540
+ "summary": summary_text,
541
+ "analysis": _to_builtin(analysis),
542
+ }
543
+ out_path.write_text(json.dumps(payload, indent=2), encoding="utf-8")
544
+ print(f"Wrote JSON: {out_path}")
545
+ continue
546
+
547
+ if suffix == ".png":
548
+ if isinstance(analysis, AnalysisBundle):
549
+ fig = plot_point_estimates(
550
+ analysis.benchmark,
551
+ n_bootstrap=n_bootstrap,
552
+ ci=ci,
553
+ )
554
+ fig.savefig(out_path, dpi=150, bbox_inches="tight")
555
+ print(f"Wrote plot: {out_path}")
556
+ continue
557
+ if isinstance(analysis, MultiModelBundle):
558
+ fig = plot_point_estimates(
559
+ analysis.model_level.benchmark,
560
+ n_bootstrap=n_bootstrap,
561
+ ci=ci,
562
+ title="Model-Level Robustness Intervals",
563
+ )
564
+ fig.savefig(out_path, dpi=150, bbox_inches="tight")
565
+ print(f"Wrote plot: {out_path}")
566
+ continue
567
+ if isinstance(analysis, dict):
568
+ base = out_path.with_suffix("")
569
+ for evaluator_name, evaluator_analysis in analysis.items():
570
+ target = base.with_name(f"{base.name}_{evaluator_name}").with_suffix(".png")
571
+ if isinstance(evaluator_analysis, MultiModelBundle):
572
+ fig = plot_point_estimates(
573
+ evaluator_analysis.model_level.benchmark,
574
+ n_bootstrap=n_bootstrap,
575
+ ci=ci,
576
+ title=f"Model-Level Robustness Intervals ({evaluator_name})",
577
+ )
578
+ else:
579
+ fig = plot_point_estimates(
580
+ evaluator_analysis.benchmark,
581
+ n_bootstrap=n_bootstrap,
582
+ ci=ci,
583
+ title=f"Robustness Intervals ({evaluator_name})",
584
+ )
585
+ fig.savefig(target, dpi=150, bbox_inches="tight")
586
+ print(f"Wrote plot: {target}")
587
+ continue
588
+
589
+ _die(
590
+ f"unsupported output file extension for '{out_path.name}'. "
591
+ "Use one of: .txt, .md, .json, .png"
592
+ )
593
+
594
+
595
+ if __name__ == "__main__":
596
+ main()