evalstats 0.2.0__tar.gz → 0.2.2__tar.gz

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.
Files changed (66) hide show
  1. {evalstats-0.2.0 → evalstats-0.2.2}/PKG-INFO +180 -11
  2. {evalstats-0.2.0 → evalstats-0.2.2}/README.md +179 -10
  3. {evalstats-0.2.0 → evalstats-0.2.2}/evalstats/__init__.py +25 -11
  4. evalstats-0.2.2/evalstats/alignment.py +560 -0
  5. evalstats-0.2.2/evalstats/api.py +1881 -0
  6. {evalstats-0.2.0 → evalstats-0.2.2}/evalstats/cli.py +19 -5
  7. evalstats-0.2.2/evalstats/config.py +251 -0
  8. {evalstats-0.2.0 → evalstats-0.2.2}/evalstats/core/bundles.py +21 -0
  9. {evalstats-0.2.0 → evalstats-0.2.2}/evalstats/core/mixed_effects.py +330 -0
  10. {evalstats-0.2.0 → evalstats-0.2.2}/evalstats/core/paired.py +529 -278
  11. {evalstats-0.2.0 → evalstats-0.2.2}/evalstats/core/ranking.py +92 -3
  12. {evalstats-0.2.0 → evalstats-0.2.2}/evalstats/core/resampling.py +116 -4
  13. {evalstats-0.2.0 → evalstats-0.2.2}/evalstats/core/router.py +89 -33
  14. evalstats-0.2.2/evalstats/core/stats_utils.py +128 -0
  15. {evalstats-0.2.0 → evalstats-0.2.2}/evalstats/core/summary.py +492 -106
  16. {evalstats-0.2.0 → evalstats-0.2.2}/evalstats/core/types.py +0 -1
  17. {evalstats-0.2.0 → evalstats-0.2.2}/evalstats/core/variance.py +79 -13
  18. evalstats-0.2.2/evalstats/loader.py +697 -0
  19. evalstats-0.2.2/evalstats/ppi.py +375 -0
  20. evalstats-0.2.2/evalstats/tests/__init__.py +3141 -0
  21. {evalstats-0.2.0 → evalstats-0.2.2}/evalstats/vis/critical_difference.py +0 -1
  22. {evalstats-0.2.0 → evalstats-0.2.2}/evalstats/vis/forest.py +3 -5
  23. {evalstats-0.2.0 → evalstats-0.2.2}/evalstats/vis/scoreboard.py +8 -9
  24. {evalstats-0.2.0 → evalstats-0.2.2}/evalstats.egg-info/PKG-INFO +180 -11
  25. {evalstats-0.2.0 → evalstats-0.2.2}/evalstats.egg-info/SOURCES.txt +10 -3
  26. {evalstats-0.2.0 → evalstats-0.2.2}/pyproject.toml +1 -1
  27. evalstats-0.2.2/tests/test_alignment.py +995 -0
  28. {evalstats-0.2.0 → evalstats-0.2.2}/tests/test_analyze.py +24 -1
  29. {evalstats-0.2.0 → evalstats-0.2.2}/tests/test_bayes_binary_routing.py +19 -36
  30. evalstats-0.2.2/tests/test_bootstrap_t_pairwise_ranking.py +39 -0
  31. {evalstats-0.2.0 → evalstats-0.2.2}/tests/test_cli.py +93 -10
  32. evalstats-0.2.2/tests/test_compare.py +409 -0
  33. {evalstats-0.2.0 → evalstats-0.2.2}/tests/test_critical_difference_plot.py +10 -9
  34. evalstats-0.2.2/tests/test_ppi_core.py +151 -0
  35. evalstats-0.2.2/tests/test_ppi_corrections.py +2510 -0
  36. {evalstats-0.2.0 → evalstats-0.2.2}/tests/test_resampling.py +2 -2
  37. {evalstats-0.2.0 → evalstats-0.2.2}/tests/test_scoreboard_plot.py +23 -17
  38. {evalstats-0.2.0 → evalstats-0.2.2}/tests/test_set_alpha_ci.py +2 -2
  39. {evalstats-0.2.0 → evalstats-0.2.2}/tests/test_simultaneous_ci.py +39 -20
  40. {evalstats-0.2.0 → evalstats-0.2.2}/tests/test_wilson_newcombe.py +67 -56
  41. evalstats-0.2.0/evalstats/compare.py +0 -900
  42. evalstats-0.2.0/evalstats/config.py +0 -29
  43. evalstats-0.2.0/evalstats/core/stats_utils.py +0 -47
  44. evalstats-0.2.0/tests/test_compare_models.py +0 -385
  45. evalstats-0.2.0/tests/test_compare_prompts.py +0 -567
  46. {evalstats-0.2.0 → evalstats-0.2.2}/LICENSE +0 -0
  47. {evalstats-0.2.0 → evalstats-0.2.2}/evalstats/core/__init__.py +0 -0
  48. {evalstats-0.2.0 → evalstats-0.2.2}/evalstats/core/bayes_evals.py +0 -0
  49. {evalstats-0.2.0 → evalstats-0.2.2}/evalstats/io.py +0 -0
  50. {evalstats-0.2.0 → evalstats-0.2.2}/evalstats/vis/__init__.py +0 -0
  51. {evalstats-0.2.0 → evalstats-0.2.2}/evalstats/vis/heatmap.py +0 -0
  52. {evalstats-0.2.0 → evalstats-0.2.2}/evalstats/vis/point_estimates.py +0 -0
  53. {evalstats-0.2.0 → evalstats-0.2.2}/evalstats.egg-info/dependency_links.txt +0 -0
  54. {evalstats-0.2.0 → evalstats-0.2.2}/evalstats.egg-info/entry_points.txt +0 -0
  55. {evalstats-0.2.0 → evalstats-0.2.2}/evalstats.egg-info/requires.txt +0 -0
  56. {evalstats-0.2.0 → evalstats-0.2.2}/evalstats.egg-info/top_level.txt +0 -0
  57. {evalstats-0.2.0 → evalstats-0.2.2}/setup.cfg +0 -0
  58. {evalstats-0.2.0 → evalstats-0.2.2}/tests/test_analyze_factorial.py +0 -0
  59. {evalstats-0.2.0 → evalstats-0.2.2}/tests/test_auto_ci_routing.py +0 -0
  60. {evalstats-0.2.0 → evalstats-0.2.2}/tests/test_io.py +0 -0
  61. {evalstats-0.2.0 → evalstats-0.2.2}/tests/test_lmm.py +0 -0
  62. {evalstats-0.2.0 → evalstats-0.2.2}/tests/test_lmm_backend_parity.py +0 -0
  63. {evalstats-0.2.0 → evalstats-0.2.2}/tests/test_lmm_statsmodels.py +0 -0
  64. {evalstats-0.2.0 → evalstats-0.2.2}/tests/test_nig_ci_methods.py +0 -0
  65. {evalstats-0.2.0 → evalstats-0.2.2}/tests/test_p_values.py +0 -0
  66. {evalstats-0.2.0 → evalstats-0.2.2}/tests/test_permutation.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: evalstats
3
- Version: 0.2.0
3
+ Version: 0.2.2
4
4
  Summary: Statistically sane analysis methods for comparing AI model and prompt performance.
5
5
  Author: Ian Arawjo
6
6
  License-Expression: MIT
@@ -81,7 +81,7 @@ the lessons hold regardless of implementation).
81
81
 
82
82
  ## Sample output
83
83
 
84
- Running `estats.analyze()` and then `estats.print_analysis_summary(analysis)` prints a full statistical report to the terminal, including confidence interval line plots, pairwise comparisons between prompt templates, and per-input stability across runs (how stable the model is across multiple runs for the same input). Below is example excerpt from an analysis of a 4-template sentiment-classification benchmark (GPT-4.1-nano, 27 inputs, 3 runs, 3 evaluators):
84
+ Running `es.compare(evaldata, factors="prompt")` and then `result.summary()` prints a full statistical report to the terminal, including confidence interval line plots, pairwise comparisons between prompt templates, and per-input stability across runs (how stable the model is across multiple runs for the same input). Below is example excerpt from an analysis of a 4-template sentiment-classification benchmark (GPT-4.1-nano, 27 inputs, 3 runs, 3 evaluators):
85
85
 
86
86
  ![Example terminal output](docs/example-output.png)
87
87
 
@@ -99,10 +99,10 @@ You can also plot within notebook environments (although this feature is being a
99
99
 
100
100
  ## Statistics
101
101
 
102
- The specific statistical tests the `evalstats.analyze()` method runs are:
102
+ The specific statistical tests that `evalstats.compare()` runs (via the lower-level `analyze()` engine underneath it) are:
103
103
 
104
104
  - **All pairwise prompt comparisons (paired by input)** via `all_pairwise(...)`:
105
- - Computes mean or median difference (mean by default), bootstrapped 99% confidence interval, and p-value for every prompt template pair.
105
+ - Computes mean or median difference (mean by default), bootstrapped 95% confidence interval, and p-value for every prompt template pair.
106
106
  - Comparison method defaults to `method="auto"`:
107
107
  - **Smoothed bootstrap with a Gaussian KDE** (`method="smooth_bootstrap"`) in situations of non-binary data. It has been verified in our simulations that for eval-type data and small sample sizes especially, smoothed is superior to the other bootstrap methods considered (percentile, BCa, Bayesian).
108
108
  - **Bayesian pairwise from [`bayes_evals`](https://github.com/sambowyer/bayes_evals/tree/main) and McNemar's test**: Default methods for binary scores (0 or 1 only). Our simulations showed Bayesian pairwise was superior to bootstrap at small N. Note that Bayesian methods should technically be called credible intervals, but they estimate the confidence interval very closely.
@@ -144,13 +144,64 @@ From the command line, `evalstats` can read a CSV or Excel file directly and pri
144
144
  evalstats analyze results.csv
145
145
  ```
146
146
 
147
- The input file should have columns `template`, `input`, and `score` (run and evaluator columns are optional). Run `evalstats analyze --help` for the full list of options and supported column aliases.
147
+ The input file should have a prompt/template column, an item/input column, and a score column (model, run, and evaluator columns are optional) — see the column alias table in the [Python API](#python-api) section below for recognized names. Run `evalstats analyze --help` for the full list of options and supported column aliases.
148
148
 
149
149
  For more complex statistical analysis with mixed effects models, use `method="lmm"`. The default `statsmodels` backend works out of the box; for the optional R-based backend, see below.
150
150
 
151
151
  ## Python API
152
152
 
153
- `evalstats` main use case is as a Python API, which provides a similar entry point, the `analyze()` function. Simply pass your benchmark data in the correct format, and pass it to `analyze` to get a battery of results:
153
+ The main entry point is `load_from()` + `compare()`: parse your data once into an `EvalResults` object, then run comparisons against it.
154
+
155
+ ```python
156
+ import pandas as pd
157
+ import evalstats as es
158
+
159
+ df = pd.read_csv("results.csv") # columns: prompt, item, score (model optional)
160
+
161
+ evaldata = es.load_from(df)
162
+ evaldata.summary() # inspect detected structure/column assignments before analyzing
163
+
164
+ result = es.compare(evaldata, factors="prompt")
165
+ result.summary() # full terminal report: CIs, pairwise tests, rank probabilities
166
+ ```
167
+
168
+ `evalstats` expects **long-format** data: one row per (item, score) observation, plus whichever axis you want to compare — `model`, `prompt`, or both — and optionally `run` for repeated runs. Only `item` and `score` are strictly required; you need at least one of `model`/`prompt` too, whichever you pass to `compare(factors=...)`. `load_from()` auto-detects each column's role by matching its name (case-insensitively) against this table:
169
+
170
+ | Role | Canonical name | Recognized aliases | Required? |
171
+ |----------|-----------------|----------------------------------------|------------------------------------------------------|
172
+ | model | `model` | `model_label`, `model_name` | Optional — needed to compare models (`factors="model"`) |
173
+ | prompt | `prompt` | `template`, `prompt_template` | Optional — needed to compare prompts (`factors="prompt"`) |
174
+ | item | `item` | `input`, `example`, `id`, `input_label`| Yes |
175
+ | score | `score` | `value`, `result`, `metric` | Yes |
176
+ | run | `run` | `seed`, `repeat`, `run_id`, `trial` | Optional — add if you have repeated runs per (model/prompt, item) |
177
+
178
+ For example, a minimal CSV comparing prompts:
179
+
180
+ | prompt | item | score |
181
+ |-------------|------|-------|
182
+ | Minimal | q1 | 0.82 |
183
+ | Instructive | q1 | 0.91 |
184
+ | Minimal | q2 | 0.75 |
185
+ | Instructive | q2 | 0.88 |
186
+
187
+ If your columns don't match any of the aliases above, remap them explicitly with `col_map`:
188
+
189
+ ```python
190
+ evaldata = es.load_from(df, col_map={"llm": "model", "variant": "prompt", "q_id": "item"})
191
+ ```
192
+
193
+ `compare()` also handles:
194
+
195
+ - **Comparing models**: `factors="model"`
196
+ - **Factorial designs** (model × prompt): `factors=["model", "prompt"]` (routes to an LMM backend)
197
+ - **Filtering**: any keyword matching a column name acts as a row filter, e.g. `es.compare(evaldata, factors="model", split="test")`
198
+ - **PPI-corrected inference** for noisy LLM-judge scores against a smaller human-labeled subset — see [PPI-Corrected Inference](#ppi-corrected-inference-means-cis-and-tests) below
199
+
200
+ The returned `result` is a `ComparisonResult`. Besides `.summary()`, it has `.to_frame()` / `.to_dict()` for programmatic access, `.plot(method="bar" | "forest" | "cd")` for charts, and `.disagreements()` to surface the items entities disagree on most.
201
+
202
+ ### Advanced: raw score arrays (low-level engine)
203
+
204
+ `compare()` is a wrapper around a lower-level engine, `analyze()`, which operates directly on `BenchmarkResult` / `MultiModelBenchmark` objects (numpy score arrays) rather than a DataFrame. Reach for this path only if you already have scores as arrays and don't want to build a DataFrame first — most use cases should use `compare()` above.
154
205
 
155
206
  ```python
156
207
  import numpy as np
@@ -177,10 +228,10 @@ result = estats.BenchmarkResult(
177
228
  )
178
229
 
179
230
  analysis = estats.analyze(result, reference="grand_mean", n_bootstrap=5_000)
180
- estats.print_analysis_summary(analysis)
231
+ analysis.summary() # same terminal report as ComparisonResult.summary()
181
232
  ```
182
233
 
183
- If your source data is already in a pandas DataFrame (possibly with noisy values), you can parse it directly and inspect a coercion report:
234
+ If you want this lower-level path from a DataFrame (e.g. to inspect the raw `BenchmarkResult` object, or to fine-tune `strict_complete_design`), use `from_dataframe()` instead of `load_from()`. It returns the array-based `BenchmarkResult` / `MultiModelBenchmark` that `analyze()` expects, plus an optional `DataLoadReport` — a data-quality log of coercions/repairs made while parsing (not a statistical report):
184
235
 
185
236
  ```python
186
237
  import evalstats as estats
@@ -197,15 +248,133 @@ for line in load_report.to_lines():
197
248
  print(line)
198
249
 
199
250
  analysis = estats.analyze(benchmark)
251
+ analysis.summary()
200
252
  ```
201
253
 
202
- To visualize absolute prompt performance with bootstrapped 99% confidence intervals:
254
+ To visualize absolute prompt performance directly from a `BenchmarkResult`, bypassing `analyze()` (use `result.plot()` above instead if you're on the `compare()` path):
203
255
 
204
256
  ```python
205
257
  fig = estats.plot_point_estimates(result)
206
258
  fig.savefig("mean_performance.png", dpi=150, bbox_inches="tight")
207
259
  ```
208
260
 
261
+ ## PPI-Corrected Inference (Means, CIs, and Tests)
262
+
263
+ `evalstats` supports PPI-corrected inference for means, confidence intervals, and common statistical tests.
264
+
265
+ PPI (Prediction-Powered Inference) lets you use lots of cheap LLM
266
+ judgments plus a smaller set of human labels to correct measurement error from the LLM
267
+ judge. This gives you corrected estimates and uncertainty that better reflect what you
268
+ would have gotten from a fully human-labeled study (Angelopoulos et al., 2023).
269
+
270
+ Most PPI correction methods use PPIBoot (bootstrap variant of PPI; Zrnic, 2024).
271
+ Implemented corrections have been battle-tested via simulations (see `simulations/sim_type_i_calibration.py`).
272
+
273
+ ### Example: Comparing models with corrected LLM judge evals via `compare(..., alignment=...)`
274
+
275
+ ```python
276
+ import evalstats as es
277
+
278
+ # Dataframe columns include:
279
+ # model item llm_score human_score (NaN for unlabeled rows)
280
+ evaldata = es.load_from(df)
281
+
282
+ # Compute alignment between LLM and human judges
283
+ alignment = es.validate_alignment(
284
+ evaldata,
285
+ llm_metric="llm_score",
286
+ human_groundtruth="human_score",
287
+ )
288
+
289
+ # Compare models, using PPI to correct for bias/misalignment with human graders
290
+ result = es.compare(
291
+ evaldata,
292
+ factors="model",
293
+ metric="llm_score",
294
+ alignment={"llm_score": alignment},
295
+ )
296
+
297
+ result.summary()
298
+ ```
299
+
300
+ ### Example: T-test PPI-correction via `evalstats.tests.ttest`
301
+
302
+ Use this for a t-test of mean differences between two groups (or two paired
303
+ conditions when `paired=True`).
304
+
305
+ ```python
306
+ import evalstats as es
307
+
308
+ res = es.tests.ttest(
309
+ a=llm_a,
310
+ b=llm_b,
311
+ a_lab=human_a, # same length as llm_a, NaN where unlabeled
312
+ b_lab=human_b, # same length as llm_b, NaN where unlabeled
313
+ paired=False,
314
+ print_result=False,
315
+ )
316
+
317
+ print(res.p_value, res.corrected_p_value, res.corrected_ci)
318
+ ```
319
+
320
+ ### Example: Mann-Whitney U test PPI-correction via `evalstats.tests.mannwhitney`
321
+
322
+ Use this for a Mann-Whitney U test, a nonparametric two-group comparison based
323
+ on relative ranks rather than assuming normally distributed scores.
324
+
325
+ ```python
326
+ import evalstats as es
327
+
328
+ res = es.tests.mannwhitney(
329
+ x=llm_x,
330
+ y=llm_y,
331
+ x_lab=human_x,
332
+ y_lab=human_y,
333
+ print_result=False,
334
+ )
335
+
336
+ print(res.p_value, res.corrected_p_value, res.corrected_ci)
337
+ ```
338
+
339
+ ### Example: Wilcoxon signed-ranks test PPI-correction via `evalstats.tests.wilcoxon` (paired)
340
+
341
+ Use this for a Wilcoxon signed-rank test, a nonparametric paired test for
342
+ matched observations (before/after, A/B on the same items, etc.).
343
+
344
+ ```python
345
+ import evalstats as es
346
+
347
+ res = es.tests.wilcoxon(
348
+ x=llm_before,
349
+ y=llm_after,
350
+ x_lab=human_before,
351
+ y_lab=human_after,
352
+ print_result=False,
353
+ )
354
+
355
+ print(res.p_value, res.corrected_p_value, res.corrected_ci)
356
+ ```
357
+
358
+ ### Example: One-way ANOVA PPI-correction via `evalstats.tests.anova_oneway`
359
+
360
+ Use this for one-way ANOVA when comparing more than two groups, with
361
+ `repeated=True` for repeated-measures (same subjects across conditions).
362
+
363
+ ```python
364
+ import evalstats as es
365
+
366
+ res = es.tests.anova_oneway(
367
+ llm_g1,
368
+ llm_g2,
369
+ llm_g3,
370
+ groups_lab=[human_g1, human_g2, human_g3],
371
+ repeated=False,
372
+ print_result=False,
373
+ )
374
+
375
+ print(res.p_value, res.corrected_p_value, res.corrected_ci)
376
+ ```
377
+
209
378
  ## Motivation
210
379
 
211
380
  Most eval tools in the LLM evaluation space don't help users perform _any_ statistical tests, let alone showcase variances in performance between prompts or models. They instead present bar charts of average performance. Developers then glance at the bar chart and decide that "prompt/model A is better than B." But was it really?
@@ -220,7 +389,7 @@ Why do people do evals this way? Well, they don't have the time, tools, or knowl
220
389
 
221
390
  ### Is one prompt "better" than others? Quantify uncertainty
222
391
 
223
- When you have scores for multiple prompt templates across a set of inputs, `evalstats` computes bootstrapped 99% confidence intervals and pairwise significance tests so you can see not just which prompt scored highest on average, but how certain you can be about that ranking. It plots these to the terminal so you can check at a glance:
392
+ When you have scores for multiple prompt templates across a set of inputs, `evalstats` computes bootstrapped 95% confidence intervals and pairwise significance tests so you can see not just which prompt scored highest on average, but how certain you can be about that ranking. It plots these to the terminal so you can check at a glance:
224
393
 
225
394
  ![Comparing across prompts output](docs/compare-prompts-output.png)
226
395
 
@@ -228,7 +397,7 @@ When you have scores for multiple prompt templates across a set of inputs, `eval
228
397
 
229
398
  A common failure mode in LLM benchmarking, both in academic papers and practitioner evaluations, is testing each model with a single prompt template and reporting the resulting scores as if they reflect stable model capabilities. In reality, model rankings can flip under semantically equivalent paraphrases of the same instruction. A benchmark result that says "Model A beats Model B" may be an artifact of prompt phrasing, not a meaningful capability difference.
230
399
 
231
- Here, we can see the difference between OpenAI's `gpt-4.1-nano` and MistralAI's `ministral-8b-2512` on a small sentiment classification benchmark, quantified by bootstrapped 99% confidence intervals:
400
+ Here, we can see the difference between OpenAI's `gpt-4.1-nano` and MistralAI's `ministral-8b-2512` on a small sentiment classification benchmark, quantified by bootstrapped 95% confidence intervals:
232
401
 
233
402
  ![Comparing across models output](docs/compare-models-output.png)
234
403
 
@@ -27,7 +27,7 @@ the lessons hold regardless of implementation).
27
27
 
28
28
  ## Sample output
29
29
 
30
- Running `estats.analyze()` and then `estats.print_analysis_summary(analysis)` prints a full statistical report to the terminal, including confidence interval line plots, pairwise comparisons between prompt templates, and per-input stability across runs (how stable the model is across multiple runs for the same input). Below is example excerpt from an analysis of a 4-template sentiment-classification benchmark (GPT-4.1-nano, 27 inputs, 3 runs, 3 evaluators):
30
+ Running `es.compare(evaldata, factors="prompt")` and then `result.summary()` prints a full statistical report to the terminal, including confidence interval line plots, pairwise comparisons between prompt templates, and per-input stability across runs (how stable the model is across multiple runs for the same input). Below is example excerpt from an analysis of a 4-template sentiment-classification benchmark (GPT-4.1-nano, 27 inputs, 3 runs, 3 evaluators):
31
31
 
32
32
  ![Example terminal output](docs/example-output.png)
33
33
 
@@ -45,10 +45,10 @@ You can also plot within notebook environments (although this feature is being a
45
45
 
46
46
  ## Statistics
47
47
 
48
- The specific statistical tests the `evalstats.analyze()` method runs are:
48
+ The specific statistical tests that `evalstats.compare()` runs (via the lower-level `analyze()` engine underneath it) are:
49
49
 
50
50
  - **All pairwise prompt comparisons (paired by input)** via `all_pairwise(...)`:
51
- - Computes mean or median difference (mean by default), bootstrapped 99% confidence interval, and p-value for every prompt template pair.
51
+ - Computes mean or median difference (mean by default), bootstrapped 95% confidence interval, and p-value for every prompt template pair.
52
52
  - Comparison method defaults to `method="auto"`:
53
53
  - **Smoothed bootstrap with a Gaussian KDE** (`method="smooth_bootstrap"`) in situations of non-binary data. It has been verified in our simulations that for eval-type data and small sample sizes especially, smoothed is superior to the other bootstrap methods considered (percentile, BCa, Bayesian).
54
54
  - **Bayesian pairwise from [`bayes_evals`](https://github.com/sambowyer/bayes_evals/tree/main) and McNemar's test**: Default methods for binary scores (0 or 1 only). Our simulations showed Bayesian pairwise was superior to bootstrap at small N. Note that Bayesian methods should technically be called credible intervals, but they estimate the confidence interval very closely.
@@ -90,13 +90,64 @@ From the command line, `evalstats` can read a CSV or Excel file directly and pri
90
90
  evalstats analyze results.csv
91
91
  ```
92
92
 
93
- The input file should have columns `template`, `input`, and `score` (run and evaluator columns are optional). Run `evalstats analyze --help` for the full list of options and supported column aliases.
93
+ The input file should have a prompt/template column, an item/input column, and a score column (model, run, and evaluator columns are optional) — see the column alias table in the [Python API](#python-api) section below for recognized names. Run `evalstats analyze --help` for the full list of options and supported column aliases.
94
94
 
95
95
  For more complex statistical analysis with mixed effects models, use `method="lmm"`. The default `statsmodels` backend works out of the box; for the optional R-based backend, see below.
96
96
 
97
97
  ## Python API
98
98
 
99
- `evalstats` main use case is as a Python API, which provides a similar entry point, the `analyze()` function. Simply pass your benchmark data in the correct format, and pass it to `analyze` to get a battery of results:
99
+ The main entry point is `load_from()` + `compare()`: parse your data once into an `EvalResults` object, then run comparisons against it.
100
+
101
+ ```python
102
+ import pandas as pd
103
+ import evalstats as es
104
+
105
+ df = pd.read_csv("results.csv") # columns: prompt, item, score (model optional)
106
+
107
+ evaldata = es.load_from(df)
108
+ evaldata.summary() # inspect detected structure/column assignments before analyzing
109
+
110
+ result = es.compare(evaldata, factors="prompt")
111
+ result.summary() # full terminal report: CIs, pairwise tests, rank probabilities
112
+ ```
113
+
114
+ `evalstats` expects **long-format** data: one row per (item, score) observation, plus whichever axis you want to compare — `model`, `prompt`, or both — and optionally `run` for repeated runs. Only `item` and `score` are strictly required; you need at least one of `model`/`prompt` too, whichever you pass to `compare(factors=...)`. `load_from()` auto-detects each column's role by matching its name (case-insensitively) against this table:
115
+
116
+ | Role | Canonical name | Recognized aliases | Required? |
117
+ |----------|-----------------|----------------------------------------|------------------------------------------------------|
118
+ | model | `model` | `model_label`, `model_name` | Optional — needed to compare models (`factors="model"`) |
119
+ | prompt | `prompt` | `template`, `prompt_template` | Optional — needed to compare prompts (`factors="prompt"`) |
120
+ | item | `item` | `input`, `example`, `id`, `input_label`| Yes |
121
+ | score | `score` | `value`, `result`, `metric` | Yes |
122
+ | run | `run` | `seed`, `repeat`, `run_id`, `trial` | Optional — add if you have repeated runs per (model/prompt, item) |
123
+
124
+ For example, a minimal CSV comparing prompts:
125
+
126
+ | prompt | item | score |
127
+ |-------------|------|-------|
128
+ | Minimal | q1 | 0.82 |
129
+ | Instructive | q1 | 0.91 |
130
+ | Minimal | q2 | 0.75 |
131
+ | Instructive | q2 | 0.88 |
132
+
133
+ If your columns don't match any of the aliases above, remap them explicitly with `col_map`:
134
+
135
+ ```python
136
+ evaldata = es.load_from(df, col_map={"llm": "model", "variant": "prompt", "q_id": "item"})
137
+ ```
138
+
139
+ `compare()` also handles:
140
+
141
+ - **Comparing models**: `factors="model"`
142
+ - **Factorial designs** (model × prompt): `factors=["model", "prompt"]` (routes to an LMM backend)
143
+ - **Filtering**: any keyword matching a column name acts as a row filter, e.g. `es.compare(evaldata, factors="model", split="test")`
144
+ - **PPI-corrected inference** for noisy LLM-judge scores against a smaller human-labeled subset — see [PPI-Corrected Inference](#ppi-corrected-inference-means-cis-and-tests) below
145
+
146
+ The returned `result` is a `ComparisonResult`. Besides `.summary()`, it has `.to_frame()` / `.to_dict()` for programmatic access, `.plot(method="bar" | "forest" | "cd")` for charts, and `.disagreements()` to surface the items entities disagree on most.
147
+
148
+ ### Advanced: raw score arrays (low-level engine)
149
+
150
+ `compare()` is a wrapper around a lower-level engine, `analyze()`, which operates directly on `BenchmarkResult` / `MultiModelBenchmark` objects (numpy score arrays) rather than a DataFrame. Reach for this path only if you already have scores as arrays and don't want to build a DataFrame first — most use cases should use `compare()` above.
100
151
 
101
152
  ```python
102
153
  import numpy as np
@@ -123,10 +174,10 @@ result = estats.BenchmarkResult(
123
174
  )
124
175
 
125
176
  analysis = estats.analyze(result, reference="grand_mean", n_bootstrap=5_000)
126
- estats.print_analysis_summary(analysis)
177
+ analysis.summary() # same terminal report as ComparisonResult.summary()
127
178
  ```
128
179
 
129
- If your source data is already in a pandas DataFrame (possibly with noisy values), you can parse it directly and inspect a coercion report:
180
+ If you want this lower-level path from a DataFrame (e.g. to inspect the raw `BenchmarkResult` object, or to fine-tune `strict_complete_design`), use `from_dataframe()` instead of `load_from()`. It returns the array-based `BenchmarkResult` / `MultiModelBenchmark` that `analyze()` expects, plus an optional `DataLoadReport` — a data-quality log of coercions/repairs made while parsing (not a statistical report):
130
181
 
131
182
  ```python
132
183
  import evalstats as estats
@@ -143,15 +194,133 @@ for line in load_report.to_lines():
143
194
  print(line)
144
195
 
145
196
  analysis = estats.analyze(benchmark)
197
+ analysis.summary()
146
198
  ```
147
199
 
148
- To visualize absolute prompt performance with bootstrapped 99% confidence intervals:
200
+ To visualize absolute prompt performance directly from a `BenchmarkResult`, bypassing `analyze()` (use `result.plot()` above instead if you're on the `compare()` path):
149
201
 
150
202
  ```python
151
203
  fig = estats.plot_point_estimates(result)
152
204
  fig.savefig("mean_performance.png", dpi=150, bbox_inches="tight")
153
205
  ```
154
206
 
207
+ ## PPI-Corrected Inference (Means, CIs, and Tests)
208
+
209
+ `evalstats` supports PPI-corrected inference for means, confidence intervals, and common statistical tests.
210
+
211
+ PPI (Prediction-Powered Inference) lets you use lots of cheap LLM
212
+ judgments plus a smaller set of human labels to correct measurement error from the LLM
213
+ judge. This gives you corrected estimates and uncertainty that better reflect what you
214
+ would have gotten from a fully human-labeled study (Angelopoulos et al., 2023).
215
+
216
+ Most PPI correction methods use PPIBoot (bootstrap variant of PPI; Zrnic, 2024).
217
+ Implemented corrections have been battle-tested via simulations (see `simulations/sim_type_i_calibration.py`).
218
+
219
+ ### Example: Comparing models with corrected LLM judge evals via `compare(..., alignment=...)`
220
+
221
+ ```python
222
+ import evalstats as es
223
+
224
+ # Dataframe columns include:
225
+ # model item llm_score human_score (NaN for unlabeled rows)
226
+ evaldata = es.load_from(df)
227
+
228
+ # Compute alignment between LLM and human judges
229
+ alignment = es.validate_alignment(
230
+ evaldata,
231
+ llm_metric="llm_score",
232
+ human_groundtruth="human_score",
233
+ )
234
+
235
+ # Compare models, using PPI to correct for bias/misalignment with human graders
236
+ result = es.compare(
237
+ evaldata,
238
+ factors="model",
239
+ metric="llm_score",
240
+ alignment={"llm_score": alignment},
241
+ )
242
+
243
+ result.summary()
244
+ ```
245
+
246
+ ### Example: T-test PPI-correction via `evalstats.tests.ttest`
247
+
248
+ Use this for a t-test of mean differences between two groups (or two paired
249
+ conditions when `paired=True`).
250
+
251
+ ```python
252
+ import evalstats as es
253
+
254
+ res = es.tests.ttest(
255
+ a=llm_a,
256
+ b=llm_b,
257
+ a_lab=human_a, # same length as llm_a, NaN where unlabeled
258
+ b_lab=human_b, # same length as llm_b, NaN where unlabeled
259
+ paired=False,
260
+ print_result=False,
261
+ )
262
+
263
+ print(res.p_value, res.corrected_p_value, res.corrected_ci)
264
+ ```
265
+
266
+ ### Example: Mann-Whitney U test PPI-correction via `evalstats.tests.mannwhitney`
267
+
268
+ Use this for a Mann-Whitney U test, a nonparametric two-group comparison based
269
+ on relative ranks rather than assuming normally distributed scores.
270
+
271
+ ```python
272
+ import evalstats as es
273
+
274
+ res = es.tests.mannwhitney(
275
+ x=llm_x,
276
+ y=llm_y,
277
+ x_lab=human_x,
278
+ y_lab=human_y,
279
+ print_result=False,
280
+ )
281
+
282
+ print(res.p_value, res.corrected_p_value, res.corrected_ci)
283
+ ```
284
+
285
+ ### Example: Wilcoxon signed-ranks test PPI-correction via `evalstats.tests.wilcoxon` (paired)
286
+
287
+ Use this for a Wilcoxon signed-rank test, a nonparametric paired test for
288
+ matched observations (before/after, A/B on the same items, etc.).
289
+
290
+ ```python
291
+ import evalstats as es
292
+
293
+ res = es.tests.wilcoxon(
294
+ x=llm_before,
295
+ y=llm_after,
296
+ x_lab=human_before,
297
+ y_lab=human_after,
298
+ print_result=False,
299
+ )
300
+
301
+ print(res.p_value, res.corrected_p_value, res.corrected_ci)
302
+ ```
303
+
304
+ ### Example: One-way ANOVA PPI-correction via `evalstats.tests.anova_oneway`
305
+
306
+ Use this for one-way ANOVA when comparing more than two groups, with
307
+ `repeated=True` for repeated-measures (same subjects across conditions).
308
+
309
+ ```python
310
+ import evalstats as es
311
+
312
+ res = es.tests.anova_oneway(
313
+ llm_g1,
314
+ llm_g2,
315
+ llm_g3,
316
+ groups_lab=[human_g1, human_g2, human_g3],
317
+ repeated=False,
318
+ print_result=False,
319
+ )
320
+
321
+ print(res.p_value, res.corrected_p_value, res.corrected_ci)
322
+ ```
323
+
155
324
  ## Motivation
156
325
 
157
326
  Most eval tools in the LLM evaluation space don't help users perform _any_ statistical tests, let alone showcase variances in performance between prompts or models. They instead present bar charts of average performance. Developers then glance at the bar chart and decide that "prompt/model A is better than B." But was it really?
@@ -166,7 +335,7 @@ Why do people do evals this way? Well, they don't have the time, tools, or knowl
166
335
 
167
336
  ### Is one prompt "better" than others? Quantify uncertainty
168
337
 
169
- When you have scores for multiple prompt templates across a set of inputs, `evalstats` computes bootstrapped 99% confidence intervals and pairwise significance tests so you can see not just which prompt scored highest on average, but how certain you can be about that ranking. It plots these to the terminal so you can check at a glance:
338
+ When you have scores for multiple prompt templates across a set of inputs, `evalstats` computes bootstrapped 95% confidence intervals and pairwise significance tests so you can see not just which prompt scored highest on average, but how certain you can be about that ranking. It plots these to the terminal so you can check at a glance:
170
339
 
171
340
  ![Comparing across prompts output](docs/compare-prompts-output.png)
172
341
 
@@ -174,7 +343,7 @@ When you have scores for multiple prompt templates across a set of inputs, `eval
174
343
 
175
344
  A common failure mode in LLM benchmarking, both in academic papers and practitioner evaluations, is testing each model with a single prompt template and reporting the resulting scores as if they reflect stable model capabilities. In reality, model rankings can flip under semantically equivalent paraphrases of the same instruction. A benchmark result that says "Model A beats Model B" may be an artifact of prompt phrasing, not a meaningful capability difference.
176
345
 
177
- Here, we can see the difference between OpenAI's `gpt-4.1-nano` and MistralAI's `ministral-8b-2512` on a small sentiment classification benchmark, quantified by bootstrapped 99% confidence intervals:
346
+ Here, we can see the difference between OpenAI's `gpt-4.1-nano` and MistralAI's `ministral-8b-2512` on a small sentiment classification benchmark, quantified by bootstrapped 95% confidence intervals:
178
347
 
179
348
  ![Comparing across models output](docs/compare-models-output.png)
180
349
 
@@ -1,5 +1,6 @@
1
1
  """evalstats: Statistical analysis and visualization for prompt benchmarking."""
2
2
 
3
+ # ── Core types and analysis engine ───────────────────────────────────────────
3
4
  from evalstats.core.types import BenchmarkResult, MultiModelBenchmark
4
5
  from evalstats.core.paired import pairwise_differences, all_pairwise, vs_baseline, friedman_nemenyi, FriedmanResult
5
6
  from evalstats.core.ranking import bootstrap_ranks
@@ -24,17 +25,34 @@ from evalstats.vis.scoreboard import plot_accuracy_bar
24
25
  from evalstats.io import from_dataframe, DataLoadReport
25
26
  from evalstats.core.resampling import bayes_binary_ci_1d, bayes_paired_diff_ci
26
27
  from evalstats.core import bayes_evals
27
- from evalstats.compare import (
28
- compare_prompts,
29
- compare_models,
30
- CompareReport,
31
- EntityStats,
32
- )
33
28
  from evalstats.config import set_alpha_ci, get_alpha_ci
34
29
 
35
- __version__ = "0.2.0"
30
+ # ── High-level spec API ───────────────────────────────────────────────────────
31
+ # Must come after all other imports: importing evalstats.api triggers
32
+ # evalstats.compare submodule registration, which would shadow a bare
33
+ # "compare" name if it were imported before the submodule.
34
+ from evalstats.loader import load_from, EvalResults, EvalLoadError
35
+ from evalstats.api import compare, compare_models, compare_prompts, ComparisonResult
36
+ from evalstats.alignment import validate_alignment, AlignmentResult
37
+ from evalstats import ppi
38
+ from evalstats import tests
39
+
40
+ __version__ = "0.2.2"
36
41
 
37
42
  __all__ = [
43
+ # High-level spec API
44
+ "load_from",
45
+ "validate_alignment",
46
+ "AlignmentResult",
47
+ "ppi",
48
+ "tests",
49
+ "EvalResults",
50
+ "EvalLoadError",
51
+ "compare",
52
+ "compare_models",
53
+ "compare_prompts",
54
+ "ComparisonResult",
55
+ # Core types
38
56
  "BenchmarkResult",
39
57
  "MultiModelBenchmark",
40
58
  "pairwise_differences",
@@ -62,10 +80,6 @@ __all__ = [
62
80
  "bayes_binary_ci_1d",
63
81
  "bayes_paired_diff_ci",
64
82
  "bayes_evals",
65
- "compare_prompts",
66
- "compare_models",
67
- "CompareReport",
68
- "EntityStats",
69
83
  "analyze_factorial",
70
84
  "set_alpha_ci",
71
85
  "get_alpha_ci",