evalaudit 0.0.1__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.
- {evalaudit-0.0.1 → evalaudit-0.2.0}/.gitignore +19 -18
- {evalaudit-0.0.1 → evalaudit-0.2.0}/LICENSE +21 -21
- evalaudit-0.2.0/PKG-INFO +313 -0
- evalaudit-0.2.0/README.md +286 -0
- evalaudit-0.2.0/evalaudit/__init__.py +56 -0
- evalaudit-0.2.0/evalaudit/_types.py +1572 -0
- evalaudit-0.2.0/evalaudit/agreement.py +600 -0
- evalaudit-0.2.0/evalaudit/audit.py +821 -0
- evalaudit-0.2.0/evalaudit/compare.py +708 -0
- evalaudit-0.2.0/evalaudit/judge.py +744 -0
- evalaudit-0.2.0/evalaudit/pairwise.py +682 -0
- evalaudit-0.2.0/evalaudit/power.py +528 -0
- evalaudit-0.2.0/examples/interval_method_study.py +308 -0
- {evalaudit-0.0.1 → evalaudit-0.2.0}/pyproject.toml +47 -38
- evalaudit-0.2.0/tests/test_agreement.py +1633 -0
- evalaudit-0.2.0/tests/test_audit.py +1059 -0
- evalaudit-0.2.0/tests/test_compare.py +802 -0
- evalaudit-0.2.0/tests/test_judge.py +2211 -0
- evalaudit-0.2.0/tests/test_pairwise.py +1451 -0
- evalaudit-0.2.0/tests/test_power.py +987 -0
- evalaudit-0.2.0/tests/test_readme.py +98 -0
- evalaudit-0.0.1/CLAUDE.md +0 -39
- evalaudit-0.0.1/PKG-INFO +0 -134
- evalaudit-0.0.1/README.md +0 -110
- evalaudit-0.0.1/evalaudit/__init__.py +0 -7
- evalaudit-0.0.1/evalaudit/_types.py +0 -66
- {evalaudit-0.0.1 → evalaudit-0.2.0}/.github/workflows/tests.yml +0 -0
- {evalaudit-0.0.1 → evalaudit-0.2.0}/evalaudit/scores.py +0 -0
- {evalaudit-0.0.1 → evalaudit-0.2.0}/examples/.gitkeep +0 -0
- {evalaudit-0.0.1 → evalaudit-0.2.0}/tests/__init__.py +0 -0
- {evalaudit-0.0.1 → evalaudit-0.2.0}/tests/test_scores.py +0 -0
|
@@ -1,18 +1,19 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
venv/
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
.
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
.
|
|
14
|
-
.
|
|
15
|
-
.
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
1
|
+
CLAUDE.md
|
|
2
|
+
__pycache__/
|
|
3
|
+
*.py[cod]
|
|
4
|
+
.venv/
|
|
5
|
+
venv/
|
|
6
|
+
.env
|
|
7
|
+
build/
|
|
8
|
+
dist/
|
|
9
|
+
*.egg-info/
|
|
10
|
+
.pytest_cache/
|
|
11
|
+
.coverage
|
|
12
|
+
htmlcov/
|
|
13
|
+
.ipynb_checkpoints/
|
|
14
|
+
.DS_Store
|
|
15
|
+
.vscode/
|
|
16
|
+
.idea/
|
|
17
|
+
data/raw/
|
|
18
|
+
outputs/
|
|
19
|
+
*.jsonl
|
|
@@ -1,21 +1,21 @@
|
|
|
1
|
-
MIT License
|
|
2
|
-
|
|
3
|
-
Copyright (c) 2026 Hezron Mokaya
|
|
4
|
-
|
|
5
|
-
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
-
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
-
in the Software without restriction, including without limitation the rights
|
|
8
|
-
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
-
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
-
furnished to do so, subject to the following conditions:
|
|
11
|
-
|
|
12
|
-
The above copyright notice and this permission notice shall be included in all
|
|
13
|
-
copies or substantial portions of the Software.
|
|
14
|
-
|
|
15
|
-
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
-
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
-
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
-
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
-
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
-
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
-
SOFTWARE.
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Hezron Mokaya
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
evalaudit-0.2.0/PKG-INFO
ADDED
|
@@ -0,0 +1,313 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: evalaudit
|
|
3
|
+
Version: 0.2.0
|
|
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/mokayahezron/evalaudit
|
|
6
|
+
Project-URL: Issues, https://github.com/mokayahezron/evalaudit/issues
|
|
7
|
+
Author-email: Hezron Mokaya <ondibahezron@gmail.com>
|
|
8
|
+
License: MIT
|
|
9
|
+
License-File: LICENSE
|
|
10
|
+
Keywords: annotation-quality,bradley-terry,evaluation,inter-rater-reliability,krippendorff,llm,llm-as-judge,statistics
|
|
11
|
+
Classifier: Development Status :: 3 - Alpha
|
|
12
|
+
Classifier: Intended Audience :: Science/Research
|
|
13
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
14
|
+
Classifier: Programming Language :: Python :: 3
|
|
15
|
+
Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
|
|
16
|
+
Requires-Python: >=3.9
|
|
17
|
+
Requires-Dist: numpy>=1.22
|
|
18
|
+
Requires-Dist: pandas>=1.5
|
|
19
|
+
Requires-Dist: scipy>=1.9
|
|
20
|
+
Provides-Extra: dev
|
|
21
|
+
Requires-Dist: choix>=0.4; (python_version >= '3.10') and extra == 'dev'
|
|
22
|
+
Requires-Dist: krippendorff>=0.6; extra == 'dev'
|
|
23
|
+
Requires-Dist: pytest-cov; extra == 'dev'
|
|
24
|
+
Requires-Dist: pytest>=7.0; extra == 'dev'
|
|
25
|
+
Requires-Dist: statsmodels>=0.14; extra == 'dev'
|
|
26
|
+
Description-Content-Type: text/markdown
|
|
27
|
+
|
|
28
|
+
# evalaudit
|
|
29
|
+
|
|
30
|
+
[](https://github.com/mokayahezron/evalaudit/actions/workflows/tests.yml)
|
|
31
|
+
|
|
32
|
+
**Statistical validity checks for human-graded AI evaluations.**
|
|
33
|
+
|
|
34
|
+
Three experts graded 200 items. One of them disagreed with the other two on
|
|
35
|
+
almost everything. Nobody noticed, because nobody computed agreement. Half the items were
|
|
36
|
+
only graded once, so it could not have been computed anyway.
|
|
37
|
+
|
|
38
|
+
That evaluation produced a number. The number is now in a slide deck.
|
|
39
|
+
|
|
40
|
+
`evalaudit` is the set of checks that should have run first.
|
|
41
|
+
|
|
42
|
+
---
|
|
43
|
+
|
|
44
|
+
## Start here
|
|
45
|
+
|
|
46
|
+
```python
|
|
47
|
+
import numpy as np
|
|
48
|
+
import pandas as pd
|
|
49
|
+
from evalaudit import audit
|
|
50
|
+
|
|
51
|
+
rng = np.random.default_rng(0)
|
|
52
|
+
|
|
53
|
+
new = (rng.random(120) < 0.58).astype(int)
|
|
54
|
+
old = (rng.random(120) < 0.51).astype(int)
|
|
55
|
+
|
|
56
|
+
ratings = pd.DataFrame({
|
|
57
|
+
"item_id": [f"i{i}" for i in range(40) for _ in range(2)],
|
|
58
|
+
"rater_id": ["ana", "raj"] * 40,
|
|
59
|
+
"rating": rng.integers(0, 2, 80),
|
|
60
|
+
})
|
|
61
|
+
|
|
62
|
+
report = audit(
|
|
63
|
+
scores={"new": new, "old": old},
|
|
64
|
+
ratings=ratings,
|
|
65
|
+
config={"claim": "the new model is better", "seed": 0},
|
|
66
|
+
)
|
|
67
|
+
print(report.to_markdown())
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
```
|
|
71
|
+
# Eval audit
|
|
72
|
+
|
|
73
|
+
Claim under audit: the new model is better. 2 critical findings, 1 warning
|
|
74
|
+
and 2 for context. On the critical findings the conclusion this data is
|
|
75
|
+
being asked to support does not hold as stated. 3 checks could not run for
|
|
76
|
+
lack of data. They are listed at the end.
|
|
77
|
+
|
|
78
|
+
## Findings
|
|
79
|
+
|
|
80
|
+
### 1. [critical] The headline margin does not exclude zero
|
|
81
|
+
|
|
82
|
+
This is the margin between new and old, which is the number the claim rests
|
|
83
|
+
on. Difference 2.5% (95% CI: -11.6% to 16.5%, paired, n=120). The interval
|
|
84
|
+
crosses zero, so the data cannot confirm that either system is better. 75 of
|
|
85
|
+
120 items changed between systems. Do not report a direction from this data.
|
|
86
|
+
[...]
|
|
87
|
+
|
|
88
|
+
### 2. [critical] This eval could not have detected the effect at issue
|
|
89
|
+
|
|
90
|
+
The effect at issue is 2.5 points, the margin this eval reports. This eval
|
|
91
|
+
ran 120 paired comparisons. At 80% power and a 5% significance level the
|
|
92
|
+
smallest difference it could have found is 20 points [...] No conclusion
|
|
93
|
+
about an effect this size can be drawn from this data, in either direction.
|
|
94
|
+
A null result here says the eval was too small and says nothing about the
|
|
95
|
+
systems. [...]
|
|
96
|
+
|
|
97
|
+
### 3. [warning] Rater agreement is below the working threshold
|
|
98
|
+
|
|
99
|
+
The grades under this eval reproduce less well than the 0.667 the report is
|
|
100
|
+
holding them to. Krippendorff's alpha -0.235 (95% CI: -0.539 to 0.057,
|
|
101
|
+
nominal, 40 of 40 items graded more than once). That is below 0.667, the
|
|
102
|
+
conventional floor for drawing any conclusion from coded data. [...]
|
|
103
|
+
|
|
104
|
+
### 4. [info] Score interval for new
|
|
105
|
+
|
|
106
|
+
The headline number for new, with the interval around it. 50.0% pass rate
|
|
107
|
+
(95% CI: 41.2%-58.8%, n=120). [...]
|
|
108
|
+
|
|
109
|
+
### 5. [info] Score interval for old
|
|
110
|
+
|
|
111
|
+
The headline number for old, with the interval around it. 47.5% pass rate
|
|
112
|
+
(95% CI: 38.8%-56.4%, n=120). [...]
|
|
113
|
+
|
|
114
|
+
## Checks that could not run
|
|
115
|
+
|
|
116
|
+
- **Judge against humans**: No judge labels supplied. Pass judge with human
|
|
117
|
+
and judge label sequences for the same items [...]
|
|
118
|
+
- **Position bias**: No pairwise judgements supplied. Pass comparisons as a
|
|
119
|
+
frame with pair_id, option_a, option_b and winner columns [...]
|
|
120
|
+
- **Length bias**: No judge preferences and lengths supplied. Pass judge
|
|
121
|
+
with preferences and lengths for the same pairs [...]
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
Two systems, a 2.5 point margin, and forty items graded twice. The margin
|
|
125
|
+
does not survive the fit, the eval was never large enough to find a margin
|
|
126
|
+
that size, and the graders underneath it did not agree with each other. The
|
|
127
|
+
report opens with whichever of those changes the decision, and it says out
|
|
128
|
+
loud which checks it could not run.
|
|
129
|
+
|
|
130
|
+
Every check runs only on the data you passed. Nothing is inferred from data
|
|
131
|
+
that is not there.
|
|
132
|
+
|
|
133
|
+
---
|
|
134
|
+
|
|
135
|
+
## Why this exists
|
|
136
|
+
|
|
137
|
+
`scipy` computes McNemar's test. `sklearn` computes Cohen's kappa. Several
|
|
138
|
+
libraries now put bootstrapped intervals on automated eval scores.
|
|
139
|
+
|
|
140
|
+
What is missing is anything built for the **human** side of evaluation: the
|
|
141
|
+
graders, the rubric, the LLM judge standing in for both. That is where the
|
|
142
|
+
expensive failures happen, and it is where almost no tooling exists.
|
|
143
|
+
|
|
144
|
+
Every function returns an object with a `.summary()`. The numbers are the
|
|
145
|
+
input to the audit. The sentence is the output.
|
|
146
|
+
|
|
147
|
+
Two rules the library follows throughout:
|
|
148
|
+
|
|
149
|
+
- **Never report a bare p-value.** Always an effect size and an interval.
|
|
150
|
+
- **State what the data cannot support**, not only what it can.
|
|
151
|
+
|
|
152
|
+
---
|
|
153
|
+
|
|
154
|
+
## What it checks
|
|
155
|
+
|
|
156
|
+
| Module | Question |
|
|
157
|
+
|---|---|
|
|
158
|
+
| `agreement` | Do your graders agree? Which grader is the outlier? Which items are ambiguous rather than hard? |
|
|
159
|
+
| `judge` | Does your LLM judge track your humans, and on which slices does it stop? Is it biased by position or length? |
|
|
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? |
|
|
161
|
+
| `power` | Could this many comparisons ever have detected the effect you care about? |
|
|
162
|
+
| `scores` | Is there an interval around your headline number, and how wide? |
|
|
163
|
+
| `compare` | Does the margin between two systems survive a paired test? |
|
|
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.
|
|
173
|
+
|
|
174
|
+
Agreement, judge and pairwise are where human graded evaluation goes wrong.
|
|
175
|
+
Scores, compare and power are the foundation those three stand on.
|
|
176
|
+
|
|
177
|
+
---
|
|
178
|
+
|
|
179
|
+
## Status
|
|
180
|
+
|
|
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.
|
|
183
|
+
|
|
184
|
+
| Module | State |
|
|
185
|
+
|---|---|
|
|
186
|
+
| `scores` | Implemented |
|
|
187
|
+
| `compare` | Implemented |
|
|
188
|
+
| `agreement` | Implemented |
|
|
189
|
+
| `judge` | Implemented |
|
|
190
|
+
| `power` | Implemented |
|
|
191
|
+
| `audit` | Implemented |
|
|
192
|
+
| `pairwise` | Implemented |
|
|
193
|
+
|
|
194
|
+
### What `scores` answers
|
|
195
|
+
|
|
196
|
+
```python
|
|
197
|
+
from evalaudit import score_ci
|
|
198
|
+
|
|
199
|
+
wins = [1] * 58 + [0] * 42
|
|
200
|
+
print(score_ci(wins).summary())
|
|
201
|
+
```
|
|
202
|
+
|
|
203
|
+
```
|
|
204
|
+
58.0% pass rate (95% CI: 48.2%-67.2%, n=100). The interval spans
|
|
205
|
+
19 points; treat differences smaller than that as unresolved.
|
|
206
|
+
```
|
|
207
|
+
|
|
208
|
+
The interval crosses 50%. On this data the new model might be worse than the
|
|
209
|
+
old one. That evaluation cannot support the decision being made with it.
|
|
210
|
+
|
|
211
|
+
### What `power` answers
|
|
212
|
+
|
|
213
|
+
```python
|
|
214
|
+
from evalaudit import detectable_effect
|
|
215
|
+
|
|
216
|
+
print(detectable_effect(500, discordance_rate=12 / 500).summary())
|
|
217
|
+
```
|
|
218
|
+
|
|
219
|
+
```
|
|
220
|
+
This eval ran 500 paired comparisons. At 80% power and a 5% significance
|
|
221
|
+
level the smallest difference it could have found is 1.9 points [...] At a
|
|
222
|
+
2.4% discordance rate that is about 12 discordant pairs, and McNemar reads
|
|
223
|
+
only those. The rest of the items agree across both systems and carry no
|
|
224
|
+
information about which one is better.
|
|
225
|
+
```
|
|
226
|
+
|
|
227
|
+
Paired binary power runs on the discordance rate, not on the item count and
|
|
228
|
+
not on the pass rate. An eval with 500 items and 12 discordant pairs carries
|
|
229
|
+
the information of a 12 item study. When the rate is not supplied, a
|
|
230
|
+
conservative 0.3 stands in and the summary says so rather than assuming it
|
|
231
|
+
quietly.
|
|
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
|
+
|
|
279
|
+
---
|
|
280
|
+
|
|
281
|
+
## Coverage
|
|
282
|
+
|
|
283
|
+
A statistics package earns trust by showing its intervals cover at the rate
|
|
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/`.
|
|
289
|
+
|
|
290
|
+
---
|
|
291
|
+
|
|
292
|
+
## Related
|
|
293
|
+
|
|
294
|
+
[`promptstats`](https://pypi.org/project/promptstats/) covers the automated
|
|
295
|
+
side well: prompt sensitivity, model comparison across prompt variations,
|
|
296
|
+
bootstrapped intervals on benchmark scores. If your evaluation has no human
|
|
297
|
+
graders in it, start there.
|
|
298
|
+
|
|
299
|
+
---
|
|
300
|
+
|
|
301
|
+
## Install
|
|
302
|
+
|
|
303
|
+
```bash
|
|
304
|
+
pip install evalaudit
|
|
305
|
+
```
|
|
306
|
+
|
|
307
|
+
Python 3.9+, numpy, scipy, pandas. Nothing else.
|
|
308
|
+
|
|
309
|
+
---
|
|
310
|
+
|
|
311
|
+
## Licence
|
|
312
|
+
|
|
313
|
+
MIT.
|
|
@@ -0,0 +1,286 @@
|
|
|
1
|
+
# evalaudit
|
|
2
|
+
|
|
3
|
+
[](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.
|