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 +82 -0
- evalstats/cli.py +596 -0
- evalstats/compare.py +902 -0
- evalstats/config.py +29 -0
- evalstats/core/__init__.py +0 -0
- evalstats/core/bayes_evals.py +498 -0
- evalstats/core/bundles.py +164 -0
- evalstats/core/mixed_effects.py +1928 -0
- evalstats/core/paired.py +1608 -0
- evalstats/core/ranking.py +322 -0
- evalstats/core/resampling.py +860 -0
- evalstats/core/router.py +1034 -0
- evalstats/core/stats_utils.py +47 -0
- evalstats/core/summary.py +2404 -0
- evalstats/core/types.py +594 -0
- evalstats/core/variance.py +381 -0
- evalstats/io.py +493 -0
- evalstats/vis/__init__.py +13 -0
- evalstats/vis/critical_difference.py +430 -0
- evalstats/vis/forest.py +272 -0
- evalstats/vis/heatmap.py +247 -0
- evalstats/vis/point_estimates.py +260 -0
- evalstats/vis/scoreboard.py +255 -0
- evalstats-0.1.9.dist-info/METADATA +364 -0
- evalstats-0.1.9.dist-info/RECORD +29 -0
- evalstats-0.1.9.dist-info/WHEEL +5 -0
- evalstats-0.1.9.dist-info/entry_points.txt +2 -0
- evalstats-0.1.9.dist-info/licenses/LICENSE +23 -0
- evalstats-0.1.9.dist-info/top_level.txt +1 -0
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()
|