evalaudit 0.1.0__tar.gz → 0.2.0__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 (26) hide show
  1. {evalaudit-0.1.0 → evalaudit-0.2.0}/PKG-INFO +69 -13
  2. {evalaudit-0.1.0 → evalaudit-0.2.0}/README.md +286 -231
  3. {evalaudit-0.1.0 → evalaudit-0.2.0}/evalaudit/__init__.py +56 -51
  4. {evalaudit-0.1.0 → evalaudit-0.2.0}/evalaudit/_types.py +1572 -1344
  5. evalaudit-0.2.0/evalaudit/pairwise.py +682 -0
  6. {evalaudit-0.1.0 → evalaudit-0.2.0}/pyproject.toml +47 -38
  7. evalaudit-0.2.0/tests/test_pairwise.py +1451 -0
  8. evalaudit-0.2.0/tests/test_readme.py +98 -0
  9. {evalaudit-0.1.0 → evalaudit-0.2.0}/.github/workflows/tests.yml +0 -0
  10. {evalaudit-0.1.0 → evalaudit-0.2.0}/.gitignore +0 -0
  11. {evalaudit-0.1.0 → evalaudit-0.2.0}/LICENSE +0 -0
  12. {evalaudit-0.1.0 → evalaudit-0.2.0}/evalaudit/agreement.py +0 -0
  13. {evalaudit-0.1.0 → evalaudit-0.2.0}/evalaudit/audit.py +0 -0
  14. {evalaudit-0.1.0 → evalaudit-0.2.0}/evalaudit/compare.py +0 -0
  15. {evalaudit-0.1.0 → evalaudit-0.2.0}/evalaudit/judge.py +0 -0
  16. {evalaudit-0.1.0 → evalaudit-0.2.0}/evalaudit/power.py +0 -0
  17. {evalaudit-0.1.0 → evalaudit-0.2.0}/evalaudit/scores.py +0 -0
  18. {evalaudit-0.1.0 → evalaudit-0.2.0}/examples/.gitkeep +0 -0
  19. {evalaudit-0.1.0 → evalaudit-0.2.0}/examples/interval_method_study.py +0 -0
  20. {evalaudit-0.1.0 → evalaudit-0.2.0}/tests/__init__.py +0 -0
  21. {evalaudit-0.1.0 → evalaudit-0.2.0}/tests/test_agreement.py +0 -0
  22. {evalaudit-0.1.0 → evalaudit-0.2.0}/tests/test_audit.py +0 -0
  23. {evalaudit-0.1.0 → evalaudit-0.2.0}/tests/test_compare.py +0 -0
  24. {evalaudit-0.1.0 → evalaudit-0.2.0}/tests/test_judge.py +0 -0
  25. {evalaudit-0.1.0 → evalaudit-0.2.0}/tests/test_power.py +0 -0
  26. {evalaudit-0.1.0 → evalaudit-0.2.0}/tests/test_scores.py +0 -0
@@ -1,9 +1,9 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: evalaudit
3
- Version: 0.1.0
3
+ Version: 0.2.0
4
4
  Summary: Statistical validity checks for human-graded AI evaluations. Rater agreement, judge validation, and whether your result holds up.
5
- Project-URL: Homepage, https://github.com/ondibahezron-glitch/evalaudit
6
- Project-URL: Issues, https://github.com/ondibahezron-glitch/evalaudit/issues
5
+ Project-URL: Homepage, https://github.com/mokayahezron/evalaudit
6
+ Project-URL: Issues, https://github.com/mokayahezron/evalaudit/issues
7
7
  Author-email: Hezron Mokaya <ondibahezron@gmail.com>
8
8
  License: MIT
9
9
  License-File: LICENSE
@@ -18,6 +18,7 @@ Requires-Dist: numpy>=1.22
18
18
  Requires-Dist: pandas>=1.5
19
19
  Requires-Dist: scipy>=1.9
20
20
  Provides-Extra: dev
21
+ Requires-Dist: choix>=0.4; (python_version >= '3.10') and extra == 'dev'
21
22
  Requires-Dist: krippendorff>=0.6; extra == 'dev'
22
23
  Requires-Dist: pytest-cov; extra == 'dev'
23
24
  Requires-Dist: pytest>=7.0; extra == 'dev'
@@ -26,7 +27,7 @@ Description-Content-Type: text/markdown
26
27
 
27
28
  # evalaudit
28
29
 
29
- [![Tests](https://github.com/ondibahezron-glitch/evalaudit/actions/workflows/tests.yml/badge.svg)](https://github.com/ondibahezron-glitch/evalaudit/actions/workflows/tests.yml)
30
+ [![Tests](https://github.com/mokayahezron/evalaudit/actions/workflows/tests.yml/badge.svg)](https://github.com/mokayahezron/evalaudit/actions/workflows/tests.yml)
30
31
 
31
32
  **Statistical validity checks for human-graded AI evaluations.**
32
33
 
@@ -156,14 +157,20 @@ Two rules the library follows throughout:
156
157
  |---|---|
157
158
  | `agreement` | Do your graders agree? Which grader is the outlier? Which items are ambiguous rather than hard? |
158
159
  | `judge` | Does your LLM judge track your humans, and on which slices does it stop? Is it biased by position or length? |
159
- | `pairwise` | In a blind pairwise comparison, which models are actually separable? |
160
+ | `pairwise` | On a blind pairwise leaderboard, which pairs of models are actually separable and how many are not? Do the Elo gaps survive their intervals? |
160
161
  | `power` | Could this many comparisons ever have detected the effect you care about? |
161
162
  | `scores` | Is there an interval around your headline number, and how wide? |
162
163
  | `compare` | Does the margin between two systems survive a paired test? |
163
- | `audit` | All of the above, as a report ranked by what changes the conclusion. |
164
+ | `audit` | Everything except `pairwise`, as a report ranked by what changes the conclusion. |
165
+
166
+ `audit` is the deliverable. It runs five of these checks and orders what it
167
+ finds by what changes the conclusion. Each of those five also stands alone
168
+ when you want a single number instead of a report.
169
+
170
+ `pairwise` runs on its own. A leaderboard is a different question from a
171
+ single eval and arrives as a different frame, one row per comparison rather
172
+ than one row per graded item, so it is not folded into the report.
164
173
 
165
- `audit` is the deliverable and the other six are the checks it runs. Each one
166
- also stands alone when you want a single number instead of a report.
167
174
  Agreement, judge and pairwise are where human graded evaluation goes wrong.
168
175
  Scores, compare and power are the foundation those three stand on.
169
176
 
@@ -171,7 +178,8 @@ Scores, compare and power are the foundation those three stand on.
171
178
 
172
179
  ## Status
173
180
 
174
- Early, and filling in along the build order.
181
+ All seven modules are implemented. The examples below are run in CI, so what
182
+ they print here is what they print for you.
175
183
 
176
184
  | Module | State |
177
185
  |---|---|
@@ -181,7 +189,7 @@ Early, and filling in along the build order.
181
189
  | `judge` | Implemented |
182
190
  | `power` | Implemented |
183
191
  | `audit` | Implemented |
184
- | `pairwise` | Specified, next |
192
+ | `pairwise` | Implemented |
185
193
 
186
194
  ### What `scores` answers
187
195
 
@@ -222,14 +230,62 @@ the information of a 12 item study. When the rate is not supplied, a
222
230
  conservative 0.3 stands in and the summary says so rather than assuming it
223
231
  quietly.
224
232
 
233
+ ### What `pairwise` answers
234
+
235
+ ```python
236
+ import numpy as np
237
+ import pandas as pd
238
+ from evalaudit import bradley_terry
239
+
240
+ rng = np.random.default_rng(0)
241
+ models = ["m1", "m2", "m3", "m4", "m5", "m6"]
242
+ strength = dict(zip(models, [0.55, 0.35, 0.15, -0.05, -0.30, -0.70]))
243
+
244
+ rows = []
245
+ for i, first in enumerate(models):
246
+ for second in models[i + 1:]:
247
+ p = 1 / (1 + np.exp(-(strength[first] - strength[second])))
248
+ for _ in range(40):
249
+ rows.append((first, second, first if rng.random() < p else second))
250
+
251
+ comparisons = pd.DataFrame(rows, columns=["model_a", "model_b", "winner"])
252
+ comparisons["item_id"] = [f"q{i}" for i in range(len(rows))]
253
+
254
+ print(bradley_terry(comparisons, seed=0).summary())
255
+ ```
256
+
257
+ ```
258
+ Bradley-Terry ratings for 6 models from 600 comparisons (ties split). 5 of
259
+ 15 pairs are separable at 95%, meaning their intervals do not overlap. The
260
+ other 10 pairs cannot be ordered [...] Ratings are anchored on m1 at 0.
261
+ Only differences between models mean anything [...]
262
+ ```
263
+
264
+ Six models make fifteen pairs. Six hundred comparisons separate five of
265
+ them, and every one of those five is a pair containing the weakest model.
266
+ The top five are not told apart from each other at all. A published board
267
+ would still print them in a line, one through six, and readers would take
268
+ that order seriously.
269
+
270
+ `to_elo` puts the same fit on the 400 point scale people expect and carries
271
+ the intervals across with it. Leaderboards publish Elo without intervals,
272
+ which is how a 12 point gap gets read as a ranking.
273
+
274
+ Two shapes of data get no ratings at all rather than numbers that look like
275
+ a ranking. Models that split into groups which never met have no common
276
+ scale, and a model that never lost has no finite rating. Both are refused
277
+ and named.
278
+
225
279
  ---
226
280
 
227
281
  ## Coverage
228
282
 
229
283
  A statistics package earns trust by showing its intervals cover at the rate
230
- they claim. Every interval here is tested by simulation: generate 1,000
231
- datasets with a known truth, confirm the 95% interval contains it about 95% of
232
- the time. Those tests run in CI and you can read them in `tests/`.
284
+ they claim. Every interval here is tested by simulation. Generate datasets
285
+ with a known truth, then confirm the 95% interval contains it about 95% of
286
+ the time. Runs per test range from a hundred or so to a thousand, set by what
287
+ each simulation costs. Those tests run in CI and you can read them in
288
+ `tests/`.
233
289
 
234
290
  ---
235
291
 
@@ -1,231 +1,286 @@
1
- # evalaudit
2
-
3
- [![Tests](https://github.com/ondibahezron-glitch/evalaudit/actions/workflows/tests.yml/badge.svg)](https://github.com/ondibahezron-glitch/evalaudit/actions/workflows/tests.yml)
4
-
5
- **Statistical validity checks for human-graded AI evaluations.**
6
-
7
- Three experts graded 200 items. One of them disagreed with the other two on
8
- almost everything. Nobody noticed, because nobody computed agreement. Half the items were
9
- only graded once, so it could not have been computed anyway.
10
-
11
- That evaluation produced a number. The number is now in a slide deck.
12
-
13
- `evalaudit` is the set of checks that should have run first.
14
-
15
- ---
16
-
17
- ## Start here
18
-
19
- ```python
20
- import numpy as np
21
- import pandas as pd
22
- from evalaudit import audit
23
-
24
- rng = np.random.default_rng(0)
25
-
26
- new = (rng.random(120) < 0.58).astype(int)
27
- old = (rng.random(120) < 0.51).astype(int)
28
-
29
- ratings = pd.DataFrame({
30
- "item_id": [f"i{i}" for i in range(40) for _ in range(2)],
31
- "rater_id": ["ana", "raj"] * 40,
32
- "rating": rng.integers(0, 2, 80),
33
- })
34
-
35
- report = audit(
36
- scores={"new": new, "old": old},
37
- ratings=ratings,
38
- config={"claim": "the new model is better", "seed": 0},
39
- )
40
- print(report.to_markdown())
41
- ```
42
-
43
- ```
44
- # Eval audit
45
-
46
- Claim under audit: the new model is better. 2 critical findings, 1 warning
47
- and 2 for context. On the critical findings the conclusion this data is
48
- being asked to support does not hold as stated. 3 checks could not run for
49
- lack of data. They are listed at the end.
50
-
51
- ## Findings
52
-
53
- ### 1. [critical] The headline margin does not exclude zero
54
-
55
- This is the margin between new and old, which is the number the claim rests
56
- on. Difference 2.5% (95% CI: -11.6% to 16.5%, paired, n=120). The interval
57
- crosses zero, so the data cannot confirm that either system is better. 75 of
58
- 120 items changed between systems. Do not report a direction from this data.
59
- [...]
60
-
61
- ### 2. [critical] This eval could not have detected the effect at issue
62
-
63
- The effect at issue is 2.5 points, the margin this eval reports. This eval
64
- ran 120 paired comparisons. At 80% power and a 5% significance level the
65
- smallest difference it could have found is 20 points [...] No conclusion
66
- about an effect this size can be drawn from this data, in either direction.
67
- A null result here says the eval was too small and says nothing about the
68
- systems. [...]
69
-
70
- ### 3. [warning] Rater agreement is below the working threshold
71
-
72
- The grades under this eval reproduce less well than the 0.667 the report is
73
- holding them to. Krippendorff's alpha -0.235 (95% CI: -0.539 to 0.057,
74
- nominal, 40 of 40 items graded more than once). That is below 0.667, the
75
- conventional floor for drawing any conclusion from coded data. [...]
76
-
77
- ### 4. [info] Score interval for new
78
-
79
- The headline number for new, with the interval around it. 50.0% pass rate
80
- (95% CI: 41.2%-58.8%, n=120). [...]
81
-
82
- ### 5. [info] Score interval for old
83
-
84
- The headline number for old, with the interval around it. 47.5% pass rate
85
- (95% CI: 38.8%-56.4%, n=120). [...]
86
-
87
- ## Checks that could not run
88
-
89
- - **Judge against humans**: No judge labels supplied. Pass judge with human
90
- and judge label sequences for the same items [...]
91
- - **Position bias**: No pairwise judgements supplied. Pass comparisons as a
92
- frame with pair_id, option_a, option_b and winner columns [...]
93
- - **Length bias**: No judge preferences and lengths supplied. Pass judge
94
- with preferences and lengths for the same pairs [...]
95
- ```
96
-
97
- Two systems, a 2.5 point margin, and forty items graded twice. The margin
98
- does not survive the fit, the eval was never large enough to find a margin
99
- that size, and the graders underneath it did not agree with each other. The
100
- report opens with whichever of those changes the decision, and it says out
101
- loud which checks it could not run.
102
-
103
- Every check runs only on the data you passed. Nothing is inferred from data
104
- that is not there.
105
-
106
- ---
107
-
108
- ## Why this exists
109
-
110
- `scipy` computes McNemar's test. `sklearn` computes Cohen's kappa. Several
111
- libraries now put bootstrapped intervals on automated eval scores.
112
-
113
- What is missing is anything built for the **human** side of evaluation: the
114
- graders, the rubric, the LLM judge standing in for both. That is where the
115
- expensive failures happen, and it is where almost no tooling exists.
116
-
117
- Every function returns an object with a `.summary()`. The numbers are the
118
- input to the audit. The sentence is the output.
119
-
120
- Two rules the library follows throughout:
121
-
122
- - **Never report a bare p-value.** Always an effect size and an interval.
123
- - **State what the data cannot support**, not only what it can.
124
-
125
- ---
126
-
127
- ## What it checks
128
-
129
- | Module | Question |
130
- |---|---|
131
- | `agreement` | Do your graders agree? Which grader is the outlier? Which items are ambiguous rather than hard? |
132
- | `judge` | Does your LLM judge track your humans, and on which slices does it stop? Is it biased by position or length? |
133
- | `pairwise` | In a blind pairwise comparison, which models are actually separable? |
134
- | `power` | Could this many comparisons ever have detected the effect you care about? |
135
- | `scores` | Is there an interval around your headline number, and how wide? |
136
- | `compare` | Does the margin between two systems survive a paired test? |
137
- | `audit` | All of the above, as a report ranked by what changes the conclusion. |
138
-
139
- `audit` is the deliverable and the other six are the checks it runs. Each one
140
- also stands alone when you want a single number instead of a report.
141
- Agreement, judge and pairwise are where human graded evaluation goes wrong.
142
- Scores, compare and power are the foundation those three stand on.
143
-
144
- ---
145
-
146
- ## Status
147
-
148
- Early, and filling in along the build order.
149
-
150
- | Module | State |
151
- |---|---|
152
- | `scores` | Implemented |
153
- | `compare` | Implemented |
154
- | `agreement` | Implemented |
155
- | `judge` | Implemented |
156
- | `power` | Implemented |
157
- | `audit` | Implemented |
158
- | `pairwise` | Specified, next |
159
-
160
- ### What `scores` answers
161
-
162
- ```python
163
- from evalaudit import score_ci
164
-
165
- wins = [1] * 58 + [0] * 42
166
- print(score_ci(wins).summary())
167
- ```
168
-
169
- ```
170
- 58.0% pass rate (95% CI: 48.2%-67.2%, n=100). The interval spans
171
- 19 points; treat differences smaller than that as unresolved.
172
- ```
173
-
174
- The interval crosses 50%. On this data the new model might be worse than the
175
- old one. That evaluation cannot support the decision being made with it.
176
-
177
- ### What `power` answers
178
-
179
- ```python
180
- from evalaudit import detectable_effect
181
-
182
- print(detectable_effect(500, discordance_rate=12 / 500).summary())
183
- ```
184
-
185
- ```
186
- This eval ran 500 paired comparisons. At 80% power and a 5% significance
187
- level the smallest difference it could have found is 1.9 points [...] At a
188
- 2.4% discordance rate that is about 12 discordant pairs, and McNemar reads
189
- only those. The rest of the items agree across both systems and carry no
190
- information about which one is better.
191
- ```
192
-
193
- Paired binary power runs on the discordance rate, not on the item count and
194
- not on the pass rate. An eval with 500 items and 12 discordant pairs carries
195
- the information of a 12 item study. When the rate is not supplied, a
196
- conservative 0.3 stands in and the summary says so rather than assuming it
197
- quietly.
198
-
199
- ---
200
-
201
- ## Coverage
202
-
203
- A statistics package earns trust by showing its intervals cover at the rate
204
- they claim. Every interval here is tested by simulation: generate 1,000
205
- datasets with a known truth, confirm the 95% interval contains it about 95% of
206
- the time. Those tests run in CI and you can read them in `tests/`.
207
-
208
- ---
209
-
210
- ## Related
211
-
212
- [`promptstats`](https://pypi.org/project/promptstats/) covers the automated
213
- side well: prompt sensitivity, model comparison across prompt variations,
214
- bootstrapped intervals on benchmark scores. If your evaluation has no human
215
- graders in it, start there.
216
-
217
- ---
218
-
219
- ## Install
220
-
221
- ```bash
222
- pip install evalaudit
223
- ```
224
-
225
- Python 3.9+, numpy, scipy, pandas. Nothing else.
226
-
227
- ---
228
-
229
- ## Licence
230
-
231
- MIT.
1
+ # evalaudit
2
+
3
+ [![Tests](https://github.com/mokayahezron/evalaudit/actions/workflows/tests.yml/badge.svg)](https://github.com/mokayahezron/evalaudit/actions/workflows/tests.yml)
4
+
5
+ **Statistical validity checks for human-graded AI evaluations.**
6
+
7
+ Three experts graded 200 items. One of them disagreed with the other two on
8
+ almost everything. Nobody noticed, because nobody computed agreement. Half the items were
9
+ only graded once, so it could not have been computed anyway.
10
+
11
+ That evaluation produced a number. The number is now in a slide deck.
12
+
13
+ `evalaudit` is the set of checks that should have run first.
14
+
15
+ ---
16
+
17
+ ## Start here
18
+
19
+ ```python
20
+ import numpy as np
21
+ import pandas as pd
22
+ from evalaudit import audit
23
+
24
+ rng = np.random.default_rng(0)
25
+
26
+ new = (rng.random(120) < 0.58).astype(int)
27
+ old = (rng.random(120) < 0.51).astype(int)
28
+
29
+ ratings = pd.DataFrame({
30
+ "item_id": [f"i{i}" for i in range(40) for _ in range(2)],
31
+ "rater_id": ["ana", "raj"] * 40,
32
+ "rating": rng.integers(0, 2, 80),
33
+ })
34
+
35
+ report = audit(
36
+ scores={"new": new, "old": old},
37
+ ratings=ratings,
38
+ config={"claim": "the new model is better", "seed": 0},
39
+ )
40
+ print(report.to_markdown())
41
+ ```
42
+
43
+ ```
44
+ # Eval audit
45
+
46
+ Claim under audit: the new model is better. 2 critical findings, 1 warning
47
+ and 2 for context. On the critical findings the conclusion this data is
48
+ being asked to support does not hold as stated. 3 checks could not run for
49
+ lack of data. They are listed at the end.
50
+
51
+ ## Findings
52
+
53
+ ### 1. [critical] The headline margin does not exclude zero
54
+
55
+ This is the margin between new and old, which is the number the claim rests
56
+ on. Difference 2.5% (95% CI: -11.6% to 16.5%, paired, n=120). The interval
57
+ crosses zero, so the data cannot confirm that either system is better. 75 of
58
+ 120 items changed between systems. Do not report a direction from this data.
59
+ [...]
60
+
61
+ ### 2. [critical] This eval could not have detected the effect at issue
62
+
63
+ The effect at issue is 2.5 points, the margin this eval reports. This eval
64
+ ran 120 paired comparisons. At 80% power and a 5% significance level the
65
+ smallest difference it could have found is 20 points [...] No conclusion
66
+ about an effect this size can be drawn from this data, in either direction.
67
+ A null result here says the eval was too small and says nothing about the
68
+ systems. [...]
69
+
70
+ ### 3. [warning] Rater agreement is below the working threshold
71
+
72
+ The grades under this eval reproduce less well than the 0.667 the report is
73
+ holding them to. Krippendorff's alpha -0.235 (95% CI: -0.539 to 0.057,
74
+ nominal, 40 of 40 items graded more than once). That is below 0.667, the
75
+ conventional floor for drawing any conclusion from coded data. [...]
76
+
77
+ ### 4. [info] Score interval for new
78
+
79
+ The headline number for new, with the interval around it. 50.0% pass rate
80
+ (95% CI: 41.2%-58.8%, n=120). [...]
81
+
82
+ ### 5. [info] Score interval for old
83
+
84
+ The headline number for old, with the interval around it. 47.5% pass rate
85
+ (95% CI: 38.8%-56.4%, n=120). [...]
86
+
87
+ ## Checks that could not run
88
+
89
+ - **Judge against humans**: No judge labels supplied. Pass judge with human
90
+ and judge label sequences for the same items [...]
91
+ - **Position bias**: No pairwise judgements supplied. Pass comparisons as a
92
+ frame with pair_id, option_a, option_b and winner columns [...]
93
+ - **Length bias**: No judge preferences and lengths supplied. Pass judge
94
+ with preferences and lengths for the same pairs [...]
95
+ ```
96
+
97
+ Two systems, a 2.5 point margin, and forty items graded twice. The margin
98
+ does not survive the fit, the eval was never large enough to find a margin
99
+ that size, and the graders underneath it did not agree with each other. The
100
+ report opens with whichever of those changes the decision, and it says out
101
+ loud which checks it could not run.
102
+
103
+ Every check runs only on the data you passed. Nothing is inferred from data
104
+ that is not there.
105
+
106
+ ---
107
+
108
+ ## Why this exists
109
+
110
+ `scipy` computes McNemar's test. `sklearn` computes Cohen's kappa. Several
111
+ libraries now put bootstrapped intervals on automated eval scores.
112
+
113
+ What is missing is anything built for the **human** side of evaluation: the
114
+ graders, the rubric, the LLM judge standing in for both. That is where the
115
+ expensive failures happen, and it is where almost no tooling exists.
116
+
117
+ Every function returns an object with a `.summary()`. The numbers are the
118
+ input to the audit. The sentence is the output.
119
+
120
+ Two rules the library follows throughout:
121
+
122
+ - **Never report a bare p-value.** Always an effect size and an interval.
123
+ - **State what the data cannot support**, not only what it can.
124
+
125
+ ---
126
+
127
+ ## What it checks
128
+
129
+ | Module | Question |
130
+ |---|---|
131
+ | `agreement` | Do your graders agree? Which grader is the outlier? Which items are ambiguous rather than hard? |
132
+ | `judge` | Does your LLM judge track your humans, and on which slices does it stop? Is it biased by position or length? |
133
+ | `pairwise` | On a blind pairwise leaderboard, which pairs of models are actually separable and how many are not? Do the Elo gaps survive their intervals? |
134
+ | `power` | Could this many comparisons ever have detected the effect you care about? |
135
+ | `scores` | Is there an interval around your headline number, and how wide? |
136
+ | `compare` | Does the margin between two systems survive a paired test? |
137
+ | `audit` | Everything except `pairwise`, as a report ranked by what changes the conclusion. |
138
+
139
+ `audit` is the deliverable. It runs five of these checks and orders what it
140
+ finds by what changes the conclusion. Each of those five also stands alone
141
+ when you want a single number instead of a report.
142
+
143
+ `pairwise` runs on its own. A leaderboard is a different question from a
144
+ single eval and arrives as a different frame, one row per comparison rather
145
+ than one row per graded item, so it is not folded into the report.
146
+
147
+ Agreement, judge and pairwise are where human graded evaluation goes wrong.
148
+ Scores, compare and power are the foundation those three stand on.
149
+
150
+ ---
151
+
152
+ ## Status
153
+
154
+ All seven modules are implemented. The examples below are run in CI, so what
155
+ they print here is what they print for you.
156
+
157
+ | Module | State |
158
+ |---|---|
159
+ | `scores` | Implemented |
160
+ | `compare` | Implemented |
161
+ | `agreement` | Implemented |
162
+ | `judge` | Implemented |
163
+ | `power` | Implemented |
164
+ | `audit` | Implemented |
165
+ | `pairwise` | Implemented |
166
+
167
+ ### What `scores` answers
168
+
169
+ ```python
170
+ from evalaudit import score_ci
171
+
172
+ wins = [1] * 58 + [0] * 42
173
+ print(score_ci(wins).summary())
174
+ ```
175
+
176
+ ```
177
+ 58.0% pass rate (95% CI: 48.2%-67.2%, n=100). The interval spans
178
+ 19 points; treat differences smaller than that as unresolved.
179
+ ```
180
+
181
+ The interval crosses 50%. On this data the new model might be worse than the
182
+ old one. That evaluation cannot support the decision being made with it.
183
+
184
+ ### What `power` answers
185
+
186
+ ```python
187
+ from evalaudit import detectable_effect
188
+
189
+ print(detectable_effect(500, discordance_rate=12 / 500).summary())
190
+ ```
191
+
192
+ ```
193
+ This eval ran 500 paired comparisons. At 80% power and a 5% significance
194
+ level the smallest difference it could have found is 1.9 points [...] At a
195
+ 2.4% discordance rate that is about 12 discordant pairs, and McNemar reads
196
+ only those. The rest of the items agree across both systems and carry no
197
+ information about which one is better.
198
+ ```
199
+
200
+ Paired binary power runs on the discordance rate, not on the item count and
201
+ not on the pass rate. An eval with 500 items and 12 discordant pairs carries
202
+ the information of a 12 item study. When the rate is not supplied, a
203
+ conservative 0.3 stands in and the summary says so rather than assuming it
204
+ quietly.
205
+
206
+ ### What `pairwise` answers
207
+
208
+ ```python
209
+ import numpy as np
210
+ import pandas as pd
211
+ from evalaudit import bradley_terry
212
+
213
+ rng = np.random.default_rng(0)
214
+ models = ["m1", "m2", "m3", "m4", "m5", "m6"]
215
+ strength = dict(zip(models, [0.55, 0.35, 0.15, -0.05, -0.30, -0.70]))
216
+
217
+ rows = []
218
+ for i, first in enumerate(models):
219
+ for second in models[i + 1:]:
220
+ p = 1 / (1 + np.exp(-(strength[first] - strength[second])))
221
+ for _ in range(40):
222
+ rows.append((first, second, first if rng.random() < p else second))
223
+
224
+ comparisons = pd.DataFrame(rows, columns=["model_a", "model_b", "winner"])
225
+ comparisons["item_id"] = [f"q{i}" for i in range(len(rows))]
226
+
227
+ print(bradley_terry(comparisons, seed=0).summary())
228
+ ```
229
+
230
+ ```
231
+ Bradley-Terry ratings for 6 models from 600 comparisons (ties split). 5 of
232
+ 15 pairs are separable at 95%, meaning their intervals do not overlap. The
233
+ other 10 pairs cannot be ordered [...] Ratings are anchored on m1 at 0.
234
+ Only differences between models mean anything [...]
235
+ ```
236
+
237
+ Six models make fifteen pairs. Six hundred comparisons separate five of
238
+ them, and every one of those five is a pair containing the weakest model.
239
+ The top five are not told apart from each other at all. A published board
240
+ would still print them in a line, one through six, and readers would take
241
+ that order seriously.
242
+
243
+ `to_elo` puts the same fit on the 400 point scale people expect and carries
244
+ the intervals across with it. Leaderboards publish Elo without intervals,
245
+ which is how a 12 point gap gets read as a ranking.
246
+
247
+ Two shapes of data get no ratings at all rather than numbers that look like
248
+ a ranking. Models that split into groups which never met have no common
249
+ scale, and a model that never lost has no finite rating. Both are refused
250
+ and named.
251
+
252
+ ---
253
+
254
+ ## Coverage
255
+
256
+ A statistics package earns trust by showing its intervals cover at the rate
257
+ they claim. Every interval here is tested by simulation. Generate datasets
258
+ with a known truth, then confirm the 95% interval contains it about 95% of
259
+ the time. Runs per test range from a hundred or so to a thousand, set by what
260
+ each simulation costs. Those tests run in CI and you can read them in
261
+ `tests/`.
262
+
263
+ ---
264
+
265
+ ## Related
266
+
267
+ [`promptstats`](https://pypi.org/project/promptstats/) covers the automated
268
+ side well: prompt sensitivity, model comparison across prompt variations,
269
+ bootstrapped intervals on benchmark scores. If your evaluation has no human
270
+ graders in it, start there.
271
+
272
+ ---
273
+
274
+ ## Install
275
+
276
+ ```bash
277
+ pip install evalaudit
278
+ ```
279
+
280
+ Python 3.9+, numpy, scipy, pandas. Nothing else.
281
+
282
+ ---
283
+
284
+ ## Licence
285
+
286
+ MIT.