evalaudit 0.2.0__tar.gz → 0.2.1__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.2.1/AUDIT.md +671 -0
- {evalaudit-0.2.0 → evalaudit-0.2.1}/PKG-INFO +1 -1
- {evalaudit-0.2.0 → evalaudit-0.2.1}/evalaudit/__init__.py +9 -1
- {evalaudit-0.2.0 → evalaudit-0.2.1}/evalaudit/_types.py +23 -5
- {evalaudit-0.2.0 → evalaudit-0.2.1}/evalaudit/audit.py +107 -18
- {evalaudit-0.2.0 → evalaudit-0.2.1}/pyproject.toml +1 -1
- {evalaudit-0.2.0 → evalaudit-0.2.1}/tests/test_agreement.py +58 -9
- {evalaudit-0.2.0 → evalaudit-0.2.1}/tests/test_audit.py +406 -1
- {evalaudit-0.2.0 → evalaudit-0.2.1}/tests/test_judge.py +66 -2
- evalaudit-0.2.1/tests/test_version.py +84 -0
- {evalaudit-0.2.0 → evalaudit-0.2.1}/.github/workflows/tests.yml +0 -0
- {evalaudit-0.2.0 → evalaudit-0.2.1}/.gitignore +0 -0
- {evalaudit-0.2.0 → evalaudit-0.2.1}/LICENSE +0 -0
- {evalaudit-0.2.0 → evalaudit-0.2.1}/README.md +0 -0
- {evalaudit-0.2.0 → evalaudit-0.2.1}/evalaudit/agreement.py +0 -0
- {evalaudit-0.2.0 → evalaudit-0.2.1}/evalaudit/compare.py +0 -0
- {evalaudit-0.2.0 → evalaudit-0.2.1}/evalaudit/judge.py +0 -0
- {evalaudit-0.2.0 → evalaudit-0.2.1}/evalaudit/pairwise.py +0 -0
- {evalaudit-0.2.0 → evalaudit-0.2.1}/evalaudit/power.py +0 -0
- {evalaudit-0.2.0 → evalaudit-0.2.1}/evalaudit/scores.py +0 -0
- {evalaudit-0.2.0 → evalaudit-0.2.1}/examples/.gitkeep +0 -0
- {evalaudit-0.2.0 → evalaudit-0.2.1}/examples/interval_method_study.py +0 -0
- {evalaudit-0.2.0 → evalaudit-0.2.1}/tests/__init__.py +0 -0
- {evalaudit-0.2.0 → evalaudit-0.2.1}/tests/test_compare.py +0 -0
- {evalaudit-0.2.0 → evalaudit-0.2.1}/tests/test_pairwise.py +0 -0
- {evalaudit-0.2.0 → evalaudit-0.2.1}/tests/test_power.py +0 -0
- {evalaudit-0.2.0 → evalaudit-0.2.1}/tests/test_readme.py +0 -0
- {evalaudit-0.2.0 → evalaudit-0.2.1}/tests/test_scores.py +0 -0
evalaudit-0.2.1/AUDIT.md
ADDED
|
@@ -0,0 +1,671 @@
|
|
|
1
|
+
# Codebase audit: evalaudit
|
|
2
|
+
|
|
3
|
+
Status, 2026-09-08. Findings 1, 2, 4 and 5 are fixed on main. The rest are
|
|
4
|
+
open. Findings 3, 8, 9, 10 and 12 are regression risks on code that is
|
|
5
|
+
correct as shipped, not live errors.
|
|
6
|
+
|
|
7
|
+
Audited at commit `d38c7a3` on branch `copilot/create-audit-report`, 2026-09-07.
|
|
8
|
+
Working tree was clean at the time of the audit. Nothing in the repository was
|
|
9
|
+
modified except this file.
|
|
10
|
+
|
|
11
|
+
Method. I read every source file, every test file, the README, `pyproject.toml`,
|
|
12
|
+
the CI workflow, `examples/`, and the git history. Where a formula names a
|
|
13
|
+
source I checked it against the source or, where that was not available,
|
|
14
|
+
against a brute-force numerical solution of the same estimation problem. Where I
|
|
15
|
+
suspected a test had no teeth I proved it by copying the repository to a
|
|
16
|
+
scratch directory, breaking the implementation there, and running the suite. The
|
|
17
|
+
suite passes clean as shipped, 767 passed and 2 skipped at the audited commit.
|
|
18
|
+
783 today.
|
|
19
|
+
|
|
20
|
+
Findings are ranked by consequence. Everything in "Definitely wrong" I have
|
|
21
|
+
demonstrated. Everything in "Suspected" I could not close out.
|
|
22
|
+
|
|
23
|
+
---
|
|
24
|
+
|
|
25
|
+
## Definitely wrong
|
|
26
|
+
|
|
27
|
+
### 1. Position bias in the both-orders design is not tested at all, and the one test that looks like it guards the verdict is matching the wrong words
|
|
28
|
+
|
|
29
|
+
`evalaudit/_types.py:886`, `PositionBias.has_position_effect`, both-orders branch.
|
|
30
|
+
|
|
31
|
+
Replace that line with `return False` and **all 767 tests still pass**. No test
|
|
32
|
+
anywhere asserts that a both-orders frame with a real position effect is
|
|
33
|
+
detected.
|
|
34
|
+
|
|
35
|
+
What that branch controls, on the suite's own worst-case fixture (a judge that
|
|
36
|
+
picked the first-shown output on 100 of 100 flips):
|
|
37
|
+
|
|
38
|
+
```
|
|
39
|
+
shipped: ... The flips have a direction, so this is position bias rather than
|
|
40
|
+
an unsteady judge. It reaches for whatever it sees first.
|
|
41
|
+
|
|
42
|
+
mutated: ... The flips split evenly across the two positions, so this is an
|
|
43
|
+
unsteady judge rather than a position-biased one. Inconsistency is
|
|
44
|
+
its own problem and does not become position bias without a direction.
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
The 100% position-biased judge is described as splitting evenly. `audit` also
|
|
48
|
+
drops from a `warning` to an `info` for that data.
|
|
49
|
+
|
|
50
|
+
The reason no test catches it is `tests/test_judge.py:1243`:
|
|
51
|
+
|
|
52
|
+
```python
|
|
53
|
+
assert "first" in r.summary().lower()
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
Both versions of the summary contain the phrase *"went to whichever output was
|
|
57
|
+
shown first"* one sentence earlier, so the assertion holds either way. This is
|
|
58
|
+
the substring assertion matching unintended text.
|
|
59
|
+
|
|
60
|
+
Why it matters. Position bias in the both-orders design is the flagship check of
|
|
61
|
+
the flagship module. The shipped code is correct. I verified the branch by
|
|
62
|
+
hand. Nothing stops it regressing, and the one guard is decorative.
|
|
63
|
+
|
|
64
|
+
Confidence: certain. Fix: assert `r.has_position_effect` in
|
|
65
|
+
`test_a_judge_that_always_picks_the_first_output_scores_zero`, assert `not
|
|
66
|
+
r.has_position_effect` in `test_flips_that_are_noise_split_evenly_across_positions`,
|
|
67
|
+
and replace the `"first" in summary` assertions with the distinguishing clause
|
|
68
|
+
(`"reaches for whatever it sees first"` / `"unsteady judge"`). Under an hour.
|
|
69
|
+
|
|
70
|
+
---
|
|
71
|
+
|
|
72
|
+
### 2. A judge that prefers *short* answers is reported as length-biased toward long ones, three times in the same paragraph
|
|
73
|
+
|
|
74
|
+
`evalaudit/audit.py:757-762` (title and lead), `evalaudit/audit.py:802`
|
|
75
|
+
(`_length_flagged`), `evalaudit/_types.py:1064` (`LengthBias._preference`).
|
|
76
|
+
|
|
77
|
+
`_length_flagged` returns `not low <= 0.0 <= high`, which is true when the
|
|
78
|
+
interval sits entirely **below** zero as well as above it. The finding built on
|
|
79
|
+
top of it hard-codes the long-answer direction. Real output from a judge fitted
|
|
80
|
+
on a corpus where it systematically picks the shorter answer:
|
|
81
|
+
|
|
82
|
+
```
|
|
83
|
+
severity: warning
|
|
84
|
+
TITLE : The judge is pulled by how long the answer is
|
|
85
|
+
DETAIL : A judge that rewards length ranks the wordier system higher whatever
|
|
86
|
+
it says, and it can do that while agreeing with humans on most pairs.
|
|
87
|
+
Judge preference against length: 283 characters of extra length
|
|
88
|
+
multiplies the odds the judge picks that answer by 0.13 (95% CI: 0.09
|
|
89
|
+
to 0.19, p=0.0000, 400 pairs). It picked the longer answer on 22.0% of
|
|
90
|
+
pairs. A positive coefficient here is not bias on its own, because
|
|
91
|
+
longer answers may simply be better. ...
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
Coefficient is **-0.0072**. Three separate client-facing sentences assert a
|
|
95
|
+
long-answer preference (title, lead, and the "positive coefficient" caveat)
|
|
96
|
+
while the numbers in between say the opposite, including "picked the longer
|
|
97
|
+
answer on 22.0% of pairs" sitting directly against the title.
|
|
98
|
+
|
|
99
|
+
Why it matters. This is the prose that reaches a client, in the audit module
|
|
100
|
+
that is the deliverable. A sceptical reader who checks one number against one
|
|
101
|
+
sentence finds the contradiction inside the same paragraph.
|
|
102
|
+
|
|
103
|
+
Confidence: certain, demonstrated. Fix: branch title, lead and caveat on
|
|
104
|
+
`sign(coefficient)`. Half a day including tests, and the tests do not exist.
|
|
105
|
+
No fixture in the suite uses a short-preferring judge (`_length_flagged` can be
|
|
106
|
+
narrowed to `low > 0.0` and the whole suite passes).
|
|
107
|
+
|
|
108
|
+
---
|
|
109
|
+
|
|
110
|
+
### 3. `position_bias` reports an interval and a p-value that disagree about a half, which is the one thing `compare` is built not to do
|
|
111
|
+
|
|
112
|
+
`evalaudit/judge.py:498`. `_proportion` returns a Wilson score interval and an
|
|
113
|
+
exact binomial p-value for the same data. `PositionBias.has_position_effect`
|
|
114
|
+
(randomised branch) reads the **interval**; the summary prints the **p-value**
|
|
115
|
+
beside it.
|
|
116
|
+
|
|
117
|
+
The two are different procedures. Sweeping every table:
|
|
118
|
+
|
|
119
|
+
```
|
|
120
|
+
378 tables with n between 5 and 144 where the Wilson interval clears 0.5 and
|
|
121
|
+
the exact binomial p reported next to it does not reject (or vice versa)
|
|
122
|
+
|
|
123
|
+
k=0 n=5 : CI=(0.0000,0.4345) clears 50% exact p=0.0625 does not reject
|
|
124
|
+
k=1 n=8 : CI=(0.0224,0.4709) clears 50% exact p=0.0703 does not reject
|
|
125
|
+
k=3 n=14 : CI=(0.0757,0.4759) clears 50% exact p=0.0574 does not reject
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
On those tables the summary reads *"The interval clears 50%, so the judge
|
|
129
|
+
favours whichever output it sees first (... exact binomial p=0.0625)"*, and
|
|
130
|
+
`audit` raises a position-bias warning. The disagreement dies out above about 144,
|
|
131
|
+
but judge-validation sets of 50 to 150 pairs are the normal case.
|
|
132
|
+
|
|
133
|
+
`compare` treats this exact property as load-bearing: `_paired_proportion_ci`
|
|
134
|
+
and `_score_diff_ci` both have long docstrings explaining that they invert the
|
|
135
|
+
test reported beside them, and `test_interval_agrees_with_test` and
|
|
136
|
+
`test_independent_interval_and_test_never_disagree` enforce it. `judge` does not
|
|
137
|
+
follow the same rule and does not say why.
|
|
138
|
+
|
|
139
|
+
Confidence: certain, demonstrated. Fix: either report Clopper-Pearson (which
|
|
140
|
+
inverts the exact test) or report the score p-value beside the Wilson interval,
|
|
141
|
+
and add the sweep test `compare` already has. A day.
|
|
142
|
+
|
|
143
|
+
---
|
|
144
|
+
|
|
145
|
+
### 4. `evalaudit.__version__` is wrong in the package that is on PyPI right now
|
|
146
|
+
|
|
147
|
+
`evalaudit/__init__.py:3` says `__version__ = "0.1.0"`. I downloaded both
|
|
148
|
+
published wheels:
|
|
149
|
+
|
|
150
|
+
```
|
|
151
|
+
evalaudit 0.1.0 on PyPI -> __version__ = "0.1.0"
|
|
152
|
+
evalaudit 0.2.0 on PyPI -> __version__ = "0.1.0"
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
Two releases report the same version. The repository has three different answers
|
|
156
|
+
in play: `pyproject.toml` at `HEAD` and at `origin/main` says `0.1.0`, the
|
|
157
|
+
un-pushed local commit `242e354` on `main` says `0.2.0`, and `__init__.py` says
|
|
158
|
+
`0.1.0` everywhere. Nothing keeps them in step and no test reads `__version__`.
|
|
159
|
+
|
|
160
|
+
Why it matters. It is the first thing anyone reproducing a consulting number
|
|
161
|
+
records, and it is live and wrong.
|
|
162
|
+
|
|
163
|
+
Confidence: certain, verified against PyPI. Fix: delete the literal and read it
|
|
164
|
+
with `importlib.metadata.version("evalaudit")`, or add a test asserting the two
|
|
165
|
+
agree. Under an hour.
|
|
166
|
+
|
|
167
|
+
---
|
|
168
|
+
|
|
169
|
+
### 5. Cohen's kappa can be computed from the wrong marginals and every test passes, because all three fixtures are balanced, including the one called "lopsided"
|
|
170
|
+
|
|
171
|
+
`evalaudit/agreement.py:538`:
|
|
172
|
+
|
|
173
|
+
```python
|
|
174
|
+
expected = float(table.sum(axis=1) @ table.sum(axis=0)) / (n * n)
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
Change `axis=0` to `axis=1`, squaring rater 1's marginals instead of crossing
|
|
178
|
+
the two raters', which is a textbook kappa error, and the **full 767-test suite
|
|
179
|
+
passes**.
|
|
180
|
+
|
|
181
|
+
The parametrised statsmodels check at `tests/test_agreement.py:1385` uses three
|
|
182
|
+
fixtures, ids `["binary", "three-way", "lopsided"]`:
|
|
183
|
+
|
|
184
|
+
| fixture | rater 1 marginals | rater 2 marginals | shipped κ | mutant κ |
|
|
185
|
+
|---|---|---|---|---|
|
|
186
|
+
| binary | `[5, 5]` | `[4, 6]` | 0.400000 | 0.400000 |
|
|
187
|
+
| three-way | `[4, 4, 4]` | `[3, 6, 3]` | 0.625000 | 0.625000 |
|
|
188
|
+
| lopsided | `[20, 20]` | `[20, 20]` | 0.500000 | 0.500000 |
|
|
189
|
+
|
|
190
|
+
Rater 1's counts are exactly uniform in all three, so the two expressions are
|
|
191
|
+
algebraically identical on every fixture. The fixture named "lopsided" is
|
|
192
|
+
perfectly balanced on **both** margins. The name claims the property the
|
|
193
|
+
fixture is there to test and does not have it. A 30/10 split catches the
|
|
194
|
+
mutation immediately (0.500000 vs 0.466667).
|
|
195
|
+
|
|
196
|
+
The other two kappa tests are blind for the same reason: `perfect_and_chance`
|
|
197
|
+
uses a symmetric table and 4000 coin flips.
|
|
198
|
+
|
|
199
|
+
Confidence: certain, demonstrated. Fix: add one unbalanced fixture. Ten minutes.
|
|
200
|
+
|
|
201
|
+
---
|
|
202
|
+
|
|
203
|
+
### 6. The audit titles an undefined agreement figure as "below the working threshold", and the detail immediately contradicts the title
|
|
204
|
+
|
|
205
|
+
`evalaudit/audit.py:588` (agreement) and `evalaudit/audit.py:662` (judge). Both
|
|
206
|
+
branches are entered by `if alpha != alpha or alpha < cfg.agreement_threshold`,
|
|
207
|
+
so a NaN alpha gets the below-threshold title and lead. Real output for thirty
|
|
208
|
+
items graded identically by two raters:
|
|
209
|
+
|
|
210
|
+
```
|
|
211
|
+
TITLE : Rater agreement is below the working threshold
|
|
212
|
+
DETAIL: The grades under this eval reproduce less well than the 0.667 the report
|
|
213
|
+
is holding them to. Krippendorff's alpha is undefined (nominal, 30 of 30
|
|
214
|
+
items graded more than once). Every rating that could be compared was
|
|
215
|
+
identical, so there is no disagreement to divide by. ...
|
|
216
|
+
```
|
|
217
|
+
|
|
218
|
+
The title and lead assert a comparison against 0.667 that was never made, and
|
|
219
|
+
the sentence after them says no number exists. `AgreementResult` goes to real
|
|
220
|
+
trouble to distinguish "no variance" from "too few items" and to say that
|
|
221
|
+
neither is perfect agreement; `audit` collapses both into a threshold claim.
|
|
222
|
+
|
|
223
|
+
Confidence: certain, demonstrated. Fix: a third branch with its own title
|
|
224
|
+
("Agreement could not be measured"). An hour, plus tests.
|
|
225
|
+
|
|
226
|
+
---
|
|
227
|
+
|
|
228
|
+
### 7. The Wilcoxon path emits a zero-width 95% interval on n=0, which is the exact artifact the package's own docstring says it refuses to produce
|
|
229
|
+
|
|
230
|
+
`evalaudit/compare.py:402`. When every item ties:
|
|
231
|
+
|
|
232
|
+
```
|
|
233
|
+
compare_paired([2.5,4.0,3.5,1.0,2.0], [2.5,4.0,3.5,1.0,2.0], method="wilcoxon")
|
|
234
|
+
-> "Difference 0.000 (95% CI: 0.000 to 0.000, paired, n=0). The interval
|
|
235
|
+
crosses zero, so the data cannot confirm that either system is better."
|
|
236
|
+
```
|
|
237
|
+
|
|
238
|
+
`_paired_proportion_ci`'s docstring rejects the Wald interval partly because
|
|
239
|
+
"with no discordant pairs its standard error is zero, so it returns a single
|
|
240
|
+
point and claims two systems are identical on the strength of an empty table.
|
|
241
|
+
The score interval stays wide there, which is the honest answer." The McNemar
|
|
242
|
+
path has a dedicated `n_discordant == 0` branch in `ComparisonResult.summary()`
|
|
243
|
+
saying the table is empty. The Wilcoxon path sets `n_discordant=None`, misses
|
|
244
|
+
that branch, and prints the artifact.
|
|
245
|
+
|
|
246
|
+
`test_wilcoxon_counts_nothing_when_every_item_ties` asserts `n == 0`,
|
|
247
|
+
`p == 1.0`, `difference == 0.0` and never looks at the sentence.
|
|
248
|
+
|
|
249
|
+
Confidence: certain, demonstrated. Fix: a refusal branch in `summary()` keyed on
|
|
250
|
+
`n == 0`. An hour.
|
|
251
|
+
|
|
252
|
+
---
|
|
253
|
+
|
|
254
|
+
### 8. Two public interval methods have no test that checks a number
|
|
255
|
+
|
|
256
|
+
- `score_ci(method="t")` is **never called anywhere** in the suite, in
|
|
257
|
+
`examples/`, or in the README.
|
|
258
|
+
- `compare_independent(method="t")` (Welch) is called once, at
|
|
259
|
+
`tests/test_compare.py:564`, only to assert that a single-item group raises.
|
|
260
|
+
|
|
261
|
+
I mutated `evalaudit/scores.py:81` to `ddof=0` **and** swapped the t critical
|
|
262
|
+
value for the normal, together, and all 767 tests pass. I replaced Welch's
|
|
263
|
+
Satterthwaite degrees of freedom at `evalaudit/compare.py:667` with the pooled
|
|
264
|
+
`n1 + n2 - 2` and all 767 tests pass.
|
|
265
|
+
|
|
266
|
+
Both are correct as shipped. I checked them against
|
|
267
|
+
`scipy.stats.t.interval` and `ttest_ind(...).confidence_interval()` and they
|
|
268
|
+
match to the last bit. They are simply unguarded, and the README (line 257)
|
|
269
|
+
says "Every interval here is tested by simulation."
|
|
270
|
+
|
|
271
|
+
Confidence: certain. Fix: two coverage tests, or two reference comparisons
|
|
272
|
+
against scipy. An hour.
|
|
273
|
+
|
|
274
|
+
---
|
|
275
|
+
|
|
276
|
+
### 9. The bootstrap p-value can be halved and nothing fails
|
|
277
|
+
|
|
278
|
+
`evalaudit/compare.py:699`, `_bootstrap_p`. Drop the factor of two (turning a
|
|
279
|
+
two-sided p-value into a one-sided one) and all 767 tests pass. This is the
|
|
280
|
+
p-value on the **default** path for continuous scores, for both `compare_paired`
|
|
281
|
+
and `compare_independent`. The implementation is correct; nothing pins it.
|
|
282
|
+
|
|
283
|
+
Confidence: certain, demonstrated. Fix: assert the bootstrap p against a known
|
|
284
|
+
two-sided reference on one fixture. Twenty minutes.
|
|
285
|
+
|
|
286
|
+
---
|
|
287
|
+
|
|
288
|
+
### 10. The constrained-MLE cubic can lose a term and move reported bounds by more than a point, silently
|
|
289
|
+
|
|
290
|
+
`evalaudit/compare.py:562`, the Farrington-Manning cubic coefficient. Drop the
|
|
291
|
+
`delta * delta` term and all 767 tests pass. Effect on reported intervals:
|
|
292
|
+
|
|
293
|
+
```
|
|
294
|
+
45/50 vs 30/50 : shipped (+0.1365,+0.4551) broken (+0.1277,+0.4684) 1.33 points
|
|
295
|
+
180/200 vs 150/200: shipped (+0.0771,+0.2241) broken (+0.0743,+0.2369) 1.28 points
|
|
296
|
+
30/50 vs 20/50 : shipped (+0.0040,+0.3812) broken (+0.0040,+0.3705) 1.07 points
|
|
297
|
+
```
|
|
298
|
+
|
|
299
|
+
The mutation survives because the tests pin only the property that survives it:
|
|
300
|
+
`test_independent_interval_and_test_never_disagree` checks that the interval and
|
|
301
|
+
the p-value agree about zero, and at δ=0 the dropped term vanishes. There is no
|
|
302
|
+
test against any external reference or published value for either score
|
|
303
|
+
interval.
|
|
304
|
+
|
|
305
|
+
I verified the shipped code is right (see "What came back clean"), so this is a
|
|
306
|
+
regression risk rather than a live error. But a 1.3-point shift is the size of
|
|
307
|
+
effect this package exists to adjudicate.
|
|
308
|
+
|
|
309
|
+
Confidence: certain that the gap exists; certain that the current code is
|
|
310
|
+
correct. Fix: pin `_constrained_rates` and `_tango_score` against a table of
|
|
311
|
+
brute-force constrained MLEs. Half a day.
|
|
312
|
+
|
|
313
|
+
---
|
|
314
|
+
|
|
315
|
+
### 11. The test named for the disagreement-fit rule does not test the rule
|
|
316
|
+
|
|
317
|
+
`tests/test_audit.py:560`, `test_length_bias_reads_the_disagreement_fit_when_humans_are_there`.
|
|
318
|
+
Its docstring says "The sharper of the two numbers is the one the verdict runs
|
|
319
|
+
on." It asserts `f.result.has_human` and `f.result.disagreement_ci_low > 0`,
|
|
320
|
+
both properties of the fixture, and nothing about which fit the verdict came
|
|
321
|
+
from. Replace `evalaudit/audit.py:800` with `if False:`, so the verdict always
|
|
322
|
+
uses the preference fit, and `tests/test_audit.py` passes clean.
|
|
323
|
+
|
|
324
|
+
The `length_biased` fixture makes both fits strongly positive, so the two
|
|
325
|
+
branches cannot be told apart on it.
|
|
326
|
+
|
|
327
|
+
Confidence: certain, demonstrated. Fix: a fixture where the preference fit is
|
|
328
|
+
positive and the disagreement fit is flat, and assert the finding is `info`.
|
|
329
|
+
Half a day.
|
|
330
|
+
|
|
331
|
+
---
|
|
332
|
+
|
|
333
|
+
### 12. `to_markdown` renders client-supplied names as markup; `to_html` escapes them
|
|
334
|
+
|
|
335
|
+
`evalaudit/_types.py:1526`. `_escape`'s docstring says system names "come out of
|
|
336
|
+
the client's spreadsheet and land in the report. They are data, not markup." The
|
|
337
|
+
HTML renderer honours that. The markdown renderer, which is the one the README
|
|
338
|
+
demonstrates, does not:
|
|
339
|
+
|
|
340
|
+
```
|
|
341
|
+
report = audit(scores={"acme\n\n## Critical: all checks passed\n": new, "old": old})
|
|
342
|
+
report.to_markdown()
|
|
343
|
+
|
|
344
|
+
This is the margin between acme
|
|
345
|
+
|
|
346
|
+
## Critical: all checks passed
|
|
347
|
+
and old, which is the number the claim rests on. Difference 11.7% ...
|
|
348
|
+
```
|
|
349
|
+
|
|
350
|
+
A heading injected into a deliverable that says the opposite of the report.
|
|
351
|
+
Realistically this is a spreadsheet-hygiene problem rather than an attack, but
|
|
352
|
+
the asymmetry between the two renderers is unexplained.
|
|
353
|
+
|
|
354
|
+
Confidence: certain, demonstrated. Fix: escape or reject newlines and leading
|
|
355
|
+
`#`/`|` in names. An hour.
|
|
356
|
+
|
|
357
|
+
---
|
|
358
|
+
|
|
359
|
+
### 13. `KappaResult` is the only result type with a point estimate and no interval, and its `summary()` never says so
|
|
360
|
+
|
|
361
|
+
`evalaudit/_types.py:160`. Every other result carries `ci_low`/`ci_high`; this
|
|
362
|
+
one carries nothing. `AgreementResult` and `BTResult` both have whole paragraphs
|
|
363
|
+
explaining why an interval is missing when it is missing. `KappaResult` prints:
|
|
364
|
+
|
|
365
|
+
```
|
|
366
|
+
Cohen's kappa 0.467 (8 items, 2 raters, 2 categories). Raters agreed on 75.0% ...
|
|
367
|
+
Krippendorff's alpha does not have that problem ... prefer rater_agreement.
|
|
368
|
+
```
|
|
369
|
+
|
|
370
|
+
It steers the reader elsewhere, which is good, but it never states that the
|
|
371
|
+
number it just printed has no sampling error attached. Against the README's
|
|
372
|
+
"Never report a bare p-value. Always an effect size and an interval", this is
|
|
373
|
+
the one place the package hands over a naked estimate.
|
|
374
|
+
|
|
375
|
+
Confidence: certain. Fix: one sentence in `summary()`, or a bootstrap interval.
|
|
376
|
+
An hour for the sentence.
|
|
377
|
+
|
|
378
|
+
---
|
|
379
|
+
|
|
380
|
+
### 14. Public API surface: things that will read wrong once people depend on them
|
|
381
|
+
|
|
382
|
+
All confirmed by inspection of the signatures.
|
|
383
|
+
|
|
384
|
+
- **`position_bias(comparisons, seed=None)`**. `seed` is documented as unused
|
|
385
|
+
("Accepted so the signature matches the rest of the package. Nothing here is
|
|
386
|
+
resampled."). Honest, and still a parameter that does nothing.
|
|
387
|
+
- **`position_bias` and `length_bias` take no `confidence`** while every other
|
|
388
|
+
public function does, and both return dataclasses carrying a `confidence`
|
|
389
|
+
field. The interval level is hard-coded twice (`judge.py:498` `_wilson(k, n,
|
|
390
|
+
0.95)`, `judge.py` `norm.ppf(0.975)`). A caller who wants a 90% or 99%
|
|
391
|
+
interval cannot have one, and a `PositionBias` constructed with
|
|
392
|
+
`confidence=0.99` would print "99% CI" over a 95% interval.
|
|
393
|
+
- **`audit(comparisons=...)` and `bradley_terry(comparisons=...)` take
|
|
394
|
+
incompatible frames** under the same parameter name: `pair_id/option_a/
|
|
395
|
+
option_b/winner` versus `item_id/model_a/model_b/winner`. Passing a
|
|
396
|
+
leaderboard frame to `audit` raises a message that names the expected columns
|
|
397
|
+
but never mentions `bradley_terry`.
|
|
398
|
+
- **`detectable_effect(n, baseline=0.5, ...)`**. The second positional argument
|
|
399
|
+
does nothing in the default (`paired=True`) mode, and it sits where a reader
|
|
400
|
+
who saw `detectable_effect(500, discordance_rate=12/500)` in the README would
|
|
401
|
+
naturally put the discordance rate. `detectable_effect(500, 0.3)` silently
|
|
402
|
+
means "baseline 0.3, discordance assumed".
|
|
403
|
+
|
|
404
|
+
Confidence: certain. Fix: add `confidence` to the two judge functions, rename
|
|
405
|
+
`audit(comparisons=)` to `judgements=` (or make the error message point at
|
|
406
|
+
`bradley_terry`), and make `baseline` keyword-only. Half a day, and the window
|
|
407
|
+
for it is closing since 0.1.0 and 0.2.0 are both on PyPI.
|
|
408
|
+
|
|
409
|
+
---
|
|
410
|
+
|
|
411
|
+
### 15. Two commit messages do not describe their contents
|
|
412
|
+
|
|
413
|
+
- `d38c7a3` **"point at the new account, bump to 0.2.0"**. `pyproject.toml` at
|
|
414
|
+
that commit still says `version = "0.1.0"`. The bump landed later, in the
|
|
415
|
+
un-pushed local commit `242e354` ("Version Update"). `origin/main` points at
|
|
416
|
+
`d38c7a3`, so the message is wrong on the published branch.
|
|
417
|
+
- `a488aa4` **"add audit module: the report the other five modules exist to
|
|
418
|
+
produce"** contains a single change, deleting `CLAUDE.md`. The commit that
|
|
419
|
+
actually adds the audit module is `0500f53`, which carries the identical
|
|
420
|
+
message.
|
|
421
|
+
|
|
422
|
+
Nothing secret is committed. I regex-scanned the full history for keys, tokens,
|
|
423
|
+
passwords and private-key headers and found none.
|
|
424
|
+
|
|
425
|
+
Confidence: certain. Fix: nothing to do on published history; worth noting.
|
|
426
|
+
|
|
427
|
+
---
|
|
428
|
+
|
|
429
|
+
### 16. Smaller documentation drift
|
|
430
|
+
|
|
431
|
+
- **README line 257: "Every interval here is tested by simulation."** Not true
|
|
432
|
+
of `score_ci(method="t")`, `compare_independent(method="t")`,
|
|
433
|
+
`compare_independent(method="score")`, `compare_independent(method="bootstrap")`,
|
|
434
|
+
the Wilson interval in `position_bias`, or the Wald interval in `length_bias`.
|
|
435
|
+
Four of those six are validated some other way (exhaustive test/interval
|
|
436
|
+
agreement, or a statsmodels reference), which is arguably better than
|
|
437
|
+
simulation. The sentence as written is still checkable and false. Six of the
|
|
438
|
+
package's intervals do have real simulation coverage tests.
|
|
439
|
+
- **README line 139: "It runs five of these checks"** against
|
|
440
|
+
`evalaudit/audit.py:9` "Seven checks run". Both are defensible (five modules,
|
|
441
|
+
seven checks) and they are eleven files apart saying different numbers.
|
|
442
|
+
- **`evalaudit/power.py:130`**, docstring example:
|
|
443
|
+
`>>> detectable_effect(220, discordance_rate=0.3).difference` → `0.10289...`.
|
|
444
|
+
The actual value is `0.10290198155585326`. As a doctest with ellipsis it would
|
|
445
|
+
fail. Doctests are not run: `pyproject.toml` sets `testpaths = ["tests"]` with
|
|
446
|
+
no `--doctest-modules`, so the four `Examples` blocks in `scores.py`,
|
|
447
|
+
`compare.py` and `power.py` are never executed. The other three are accurate.
|
|
448
|
+
- **`PowerResult._reach` prints half an item** on an odd independent n:
|
|
449
|
+
`detectable_effect(101, paired=False)` → "This eval ran 101 items, 50.5 in
|
|
450
|
+
each group."
|
|
451
|
+
- **`pytest-cov` is declared in `[dev]` and never used.** No `--cov` flag in
|
|
452
|
+
`pyproject.toml` or the workflow, no coverage step in CI.
|
|
453
|
+
|
|
454
|
+
Confidence: certain, all verified. Fix: an hour for the lot.
|
|
455
|
+
|
|
456
|
+
---
|
|
457
|
+
|
|
458
|
+
## Suspected, not proven
|
|
459
|
+
|
|
460
|
+
### A. `_fit` in `pairwise.py` has no convergence check on the primary fit
|
|
461
|
+
|
|
462
|
+
`evalaudit/pairwise.py`. Newton runs to `_MAX_NEWTON = 100` and breaks on the
|
|
463
|
+
gradient tolerance. If it never reaches tolerance it returns whatever it reached,
|
|
464
|
+
with no flag and no note. The docstring justifies the cap for resamples ("so a
|
|
465
|
+
resample that is nearly separable cannot spin") but the same code path fits the
|
|
466
|
+
full data.
|
|
467
|
+
|
|
468
|
+
Mitigating: Ford's condition is checked before the fit, so a finite maximum is
|
|
469
|
+
guaranteed to exist, and the regularised Hessian is well conditioned. I could
|
|
470
|
+
not construct a case that fails to converge in 100 steps. Newton without a line
|
|
471
|
+
search is not globally convergent on this likelihood in general, so I cannot
|
|
472
|
+
rule it out either. `judge.py:_fit_logistic` handles the same risk properly. It
|
|
473
|
+
returns `_NOTE_NO_CONVERGENCE` on the `for/else`. `pairwise._fit` does not.
|
|
474
|
+
|
|
475
|
+
Confidence: low that it bites; certain that the asymmetry with `judge.py` is
|
|
476
|
+
real. Fix: a `for/else` and a `converged` flag on `BTResult`. Half a day.
|
|
477
|
+
|
|
478
|
+
### B. The bootstrap in `pairwise` resamples comparisons, not items
|
|
479
|
+
|
|
480
|
+
Documented honestly in the docstring ("on a design where one prompt carries many
|
|
481
|
+
comparisons the interval will run slightly narrow"). I did not measure how
|
|
482
|
+
narrow, and the coverage test uses one comparison per row so it cannot show it.
|
|
483
|
+
A leaderboard where 20 model pairs are judged on the same 100 prompts is the
|
|
484
|
+
normal shape and would be understated. Whether the understatement is 1% or 20%
|
|
485
|
+
of the width, I do not know.
|
|
486
|
+
|
|
487
|
+
### C. The coverage bands are wider than they need to be
|
|
488
|
+
|
|
489
|
+
`test_bootstrap_coverage` in `test_scores.py` uses 400 trials and asserts
|
|
490
|
+
`0.91 <= rate <= 0.99`. Four standard errors either side. An interval built at
|
|
491
|
+
98.5% nominal instead of 95% would pass. `test_wilson_coverage` (1000 trials,
|
|
492
|
+
0.92 to 0.98) is about three standard errors and is tighter than it needs to be to
|
|
493
|
+
catch the mutations I tried. I killed both Wilson mutations easily. I did not
|
|
494
|
+
find a plausible implementation error that squeaks through any of these bands,
|
|
495
|
+
so I am listing this as a smell rather than a finding. The `pairwise` and
|
|
496
|
+
`compare` bands come with arithmetic in their docstrings justifying the width,
|
|
497
|
+
which is the right way to do it; `test_scores.py` does not.
|
|
498
|
+
|
|
499
|
+
---
|
|
500
|
+
|
|
501
|
+
## What came back clean
|
|
502
|
+
|
|
503
|
+
I looked hard at these and found nothing. Saying so is part of the report.
|
|
504
|
+
|
|
505
|
+
**Krippendorff's alpha (`agreement.py`), the strongest part of the package.**
|
|
506
|
+
Checked three independent ways: against the `krippendorff` package on PyPI
|
|
507
|
+
across fifteen parametrised settings including gapped domains, continuous
|
|
508
|
+
ratings and 50%-missing designs; against Krippendorff's own published worked
|
|
509
|
+
examples (the four-observer 0.743/0.815/0.849 and three-observer
|
|
510
|
+
0.691358/0.810845 figures both reproduce); and against a slow reference written
|
|
511
|
+
from the definition inside the test file, which is itself validated against the
|
|
512
|
+
reference package first. I re-derived the nominal, interval and ordinal
|
|
513
|
+
coincidence formulas by hand and they match the code, including the ordinal
|
|
514
|
+
metric's half-of-each-end term. Every mutation I tried was killed: `n` for
|
|
515
|
+
`n-1`, dropping the ordinal end term, `sizes` for `sizes-1`, the usable-share
|
|
516
|
+
floor, the two-item floor, the dropout sort direction, the item-table
|
|
517
|
+
normalisation. The `_MIN_USABLE_SHARE` refusal and the leave-one-out noise
|
|
518
|
+
guard are both tested in both directions.
|
|
519
|
+
|
|
520
|
+
**The two score intervals are formula-correct, and I verified them the hard
|
|
521
|
+
way.** For Tango's paired interval I solved the constrained multinomial
|
|
522
|
+
likelihood numerically and compared against the quadratic root the code uses.
|
|
523
|
+
They agree to 3e-9 across eight tables. For the independent interval I did the
|
|
524
|
+
same for the Farrington-Manning cubic:
|
|
525
|
+
|
|
526
|
+
```
|
|
527
|
+
case true (q1,q2) evalaudit statsmodels
|
|
528
|
+
45/50 30/50 d=+0.45 (0.930774,0.480774) (0.930774,0.480774) (0.858852,0.408852)
|
|
529
|
+
9/20 3/20 d=+0.40 (0.520234,0.120234) (0.520234,0.120234) (0.488953,0.088953)
|
|
530
|
+
```
|
|
531
|
+
|
|
532
|
+
evalaudit matches the true constrained MLE exactly on every case I tried;
|
|
533
|
+
`statsmodels.score_test_proportions_2indep` does not. I also confirmed the
|
|
534
|
+
δ=0 collapse in both cases. Tango's statistic reduces to `(b-c)/sqrt(b+c)` and
|
|
535
|
+
the independent one to the pooled two-proportion z, so the stated "the interval
|
|
536
|
+
inverts the test" property holds in `compare` as claimed.
|
|
537
|
+
|
|
538
|
+
**Wilcoxon.** `_signed_rank_counts` is a correct subset-sum DP; `_signed_rank_trim`
|
|
539
|
+
reproduces the published two-sided α=0.05 critical value table for n=6..18; the
|
|
540
|
+
tie-corrected variance matches scipy's; the Hodges-Lehmann interval indexing is
|
|
541
|
+
right. The refusal below three distinct non-zero differences, with the 41%
|
|
542
|
+
contradiction rate quoted in the error message, is a genuinely good piece of
|
|
543
|
+
design. Mutations to the trim and the tie correction were killed.
|
|
544
|
+
|
|
545
|
+
**`power.py`.** I re-derived the quadratic in `_paired_difference` from
|
|
546
|
+
`_power_paired` and it matches the docstring and the code, discriminant
|
|
547
|
+
included. `_paired_size` is Connor's published form. `_independent_size` is the
|
|
548
|
+
standard pooled two-proportion formula. Every mutation I tried was killed:
|
|
549
|
+
dropping the z_β term, dropping z_β² from the denominator, one-sided alpha,
|
|
550
|
+
unpooled null variance, changing the default discordance rate, the per-group
|
|
551
|
+
doubling. The roundtrip between the two entry points is tested from both
|
|
552
|
+
directions.
|
|
553
|
+
|
|
554
|
+
**`pairwise.py`.** Ford's condition, the disconnected-groups check, the
|
|
555
|
+
undefeated/winless refusal, the Elo affine transform, the reference-shifted
|
|
556
|
+
intervals, the tie policies, the resample usability filter and the coverage
|
|
557
|
+
simulation are all tested with teeth. Mutations killed: Newton step cap, Newton
|
|
558
|
+
tolerance, `log(10)` → `log(2)`, tie credit 0.5 → 0.4, dropping the reference
|
|
559
|
+
shift, dropping the strong-connectivity check, the usable-share floor. The
|
|
560
|
+
`ties="davidson"` `NotImplementedError` is a good call.
|
|
561
|
+
|
|
562
|
+
**`test_readme.py` is a real test.** It executes every python block that has an
|
|
563
|
+
output block, splits the shown output on `[...]`, and requires each fragment to
|
|
564
|
+
appear verbatim with whitespace collapsed. It guards its own regex with a block
|
|
565
|
+
count. I re-ran the pairwise README example independently and confirmed the
|
|
566
|
+
claim "every one of those five is a pair containing the weakest model". All
|
|
567
|
+
five separable pairs are `mN` vs `m6`.
|
|
568
|
+
|
|
569
|
+
**`examples/interval_method_study.py` reproduces its recorded output exactly**,
|
|
570
|
+
to the last digit of all four tables, including the coverage figures its
|
|
571
|
+
docstring quotes.
|
|
572
|
+
|
|
573
|
+
**Packaging and CI.** Dependencies are `numpy`, `scipy`, `pandas`, all three
|
|
574
|
+
imported, nothing else imported that is not stdlib, nothing declared and unused
|
|
575
|
+
except `pytest-cov`. `requires-python = ">=3.9"` is honest: every module has
|
|
576
|
+
`from __future__ import annotations`, there is no `match`, no PEP 604 union in a
|
|
577
|
+
runtime position. The `choix>=0.4; python_version>='3.10'` marker with the
|
|
578
|
+
documented fallback is a careful piece of work. GitHub Actions shows the
|
|
579
|
+
`tests` workflow green on `main` across 3.9/3.11/3.12 for the last six runs, so
|
|
580
|
+
the 3.9 job really does resolve `krippendorff` (0.8.1 is the last release
|
|
581
|
+
supporting 3.9) and really does run. The published sdist contains exactly what
|
|
582
|
+
it should: source, tests, examples, README, LICENSE, workflow. No stray data
|
|
583
|
+
files, no notebooks, no secrets.
|
|
584
|
+
|
|
585
|
+
**Prose style.** No em-dashes or en-dashes anywhere in `evalaudit/` or the
|
|
586
|
+
README. No "not X, but Y" constructions in the shipped summaries. No leaked
|
|
587
|
+
`repr` output in any summary I generated. `!r` is used deliberately and only on
|
|
588
|
+
slice labels, which is the right call. The `_escape` HTML path is correct.
|
|
589
|
+
|
|
590
|
+
**Design invariants.** All thirteen return types are `@dataclass(frozen=True)`.
|
|
591
|
+
All of them have `summary()` except `AuditConfig`, which is a config rather than
|
|
592
|
+
a result. No public function returns a bare p-value; every p-value reported has
|
|
593
|
+
an effect size and (except for `KappaResult`, finding 13) an interval beside it.
|
|
594
|
+
|
|
595
|
+
---
|
|
596
|
+
|
|
597
|
+
## What I could not check
|
|
598
|
+
|
|
599
|
+
1. **Tango (1998) and Mee (1984) against the actual papers.** I do not have
|
|
600
|
+
them. I verified both implementations against a brute-force numerical
|
|
601
|
+
maximisation of the same constrained likelihood, which establishes the
|
|
602
|
+
arithmetic is right, and I verified the δ=0 collapse the docstrings claim.
|
|
603
|
+
What I cannot confirm is that the citation is the right attribution for the
|
|
604
|
+
construction, or that the docstring's claim about
|
|
605
|
+
Miettinen-Nurminen, "Measured over 9213 tables the correction produced 4
|
|
606
|
+
disagreements where this version produces none ... about 0.3 to 1 percent
|
|
607
|
+
wider", reproduces. That measurement is not in the repository, and nothing
|
|
608
|
+
in `examples/` or `tests/` regenerates it. The comparable claim in
|
|
609
|
+
`compare.py`'s Wilcoxon docstring ("Measured over 600 binary samples it
|
|
610
|
+
reported a bound of exactly zero while the p-value rejected on 41% of them")
|
|
611
|
+
is in the same position. `examples/interval_method_study.py` is the model for
|
|
612
|
+
how to make a claim like that checkable, and neither of these two follows it.
|
|
613
|
+
|
|
614
|
+
2. **Connor (1987) against the paper.** Same situation. The formula matches the
|
|
615
|
+
version reproduced in every secondary source I know, and the algebra of the
|
|
616
|
+
inverse is self-consistent, but I did not read Connor.
|
|
617
|
+
|
|
618
|
+
3. **The power docstring's simulation claim.** "Measured over 200,000 simulated
|
|
619
|
+
evals the shipped test delivers about 0.784 where this formula promises
|
|
620
|
+
0.800." Not reproduced in the repository and not cheap to rerun. The
|
|
621
|
+
`_floor_sentence()` that reaches the client rests on it.
|
|
622
|
+
|
|
623
|
+
4. **The `pairwise` bootstrap's behaviour on clustered designs** (suspicion B).
|
|
624
|
+
Measuring it properly needs a simulation with repeated prompts, which I did
|
|
625
|
+
not build.
|
|
626
|
+
|
|
627
|
+
5. **Real client data.** Every judgement about the prose is on synthetic
|
|
628
|
+
fixtures. I have not seen a real ratings frame go through `audit`.
|
|
629
|
+
|
|
630
|
+
6. **Whether the `promptstats` link in the README's Related section still
|
|
631
|
+
describes that package accurately.** I did not fetch it.
|
|
632
|
+
|
|
633
|
+
---
|
|
634
|
+
|
|
635
|
+
## The three I would fix first
|
|
636
|
+
|
|
637
|
+
**1. Finding 2, the length-bias direction bug.** It is the only item on this
|
|
638
|
+
list that is producing a wrong sentence in a deliverable *right now*, on data a
|
|
639
|
+
real client could plausibly have. A judge that prefers terse answers is not
|
|
640
|
+
exotic; short-answer preference is a documented LLM-judge failure mode. The
|
|
641
|
+
report calls it the opposite, three times, in one paragraph, with its own
|
|
642
|
+
numbers sitting next to the contradiction. Everything else here is either a
|
|
643
|
+
latent regression risk or a metadata problem. This one is the failure a
|
|
644
|
+
sceptical reader actually catches, and catching it costs them nothing. They
|
|
645
|
+
just read the paragraph. Fix the sign branch, and add the short-preferring
|
|
646
|
+
fixture the suite has never had.
|
|
647
|
+
|
|
648
|
+
**2. Finding 1, the untestable position-bias verdict.** Not because the code is
|
|
649
|
+
wrong (it is not) but because of what the hole says about the rest. The both-
|
|
650
|
+
orders branch is the single most distinctive check in the package, the reason
|
|
651
|
+
someone would choose `evalaudit` over `scipy`, and the assertion guarding it is
|
|
652
|
+
matching the word "first" in a sentence that always contains it. The same
|
|
653
|
+
pattern is what let finding 5 hide for three commits. I would fix this one and
|
|
654
|
+
then re-read every `assert <string> in summary` in the suite with the same
|
|
655
|
+
suspicion. There are enough of them that at least one more is probably
|
|
656
|
+
decorative. Two hours of work that changes what the test suite is worth.
|
|
657
|
+
|
|
658
|
+
**3. Finding 4, the version.** Trivially small and disproportionately
|
|
659
|
+
expensive. The pitch is "the numbers hold up when someone checks them", and the
|
|
660
|
+
first check anyone runs when reproducing a consulting result is
|
|
661
|
+
`evalaudit.__version__`. It answers `0.1.0` from a package that says `0.2.0` on
|
|
662
|
+
its label. There is no argument to be had about severity, no statistics to
|
|
663
|
+
discuss, and it takes twenty minutes. Read it from `importlib.metadata` and add
|
|
664
|
+
the one test that would have caught it.
|
|
665
|
+
|
|
666
|
+
Finding 3 (the Wilson/exact mismatch) is the next one after these, and it is a
|
|
667
|
+
close fourth. It is the only finding where the shipped arithmetic is
|
|
668
|
+
genuinely inconsistent with the package's own stated rule rather than merely
|
|
669
|
+
untested. I left it out of the three because it needs a design decision
|
|
670
|
+
(Clopper-Pearson or a score p-value) rather than a fix, and because the tables
|
|
671
|
+
where the two disagree are concentrated at n below 145.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: evalaudit
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.1
|
|
4
4
|
Summary: Statistical validity checks for human-graded AI evaluations. Rater agreement, judge validation, and whether your result holds up.
|
|
5
5
|
Project-URL: Homepage, https://github.com/mokayahezron/evalaudit
|
|
6
6
|
Project-URL: Issues, https://github.com/mokayahezron/evalaudit/issues
|