evalaudit 0.0.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
evalaudit/__init__.py ADDED
@@ -0,0 +1,7 @@
1
+ """evalaudit: statistical validity checks for AI evaluations."""
2
+
3
+ __version__ = "0.1.0"
4
+
5
+ from .scores import score_ci
6
+
7
+ __all__ = ["score_ci"]
evalaudit/_types.py ADDED
@@ -0,0 +1,66 @@
1
+ """Return types for evalaudit.
2
+
3
+ Every public function returns one of these. Each carries the numbers and a
4
+ ``summary()`` that states, in plain English, what the numbers do and do not
5
+ support. The summary is the point of the library: scipy already computes
6
+ McNemar, but nothing tells a product manager their result does not hold.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ from dataclasses import dataclass
12
+
13
+
14
+ def _pct(x: float) -> str:
15
+ return f"{x * 100:.1f}%"
16
+
17
+
18
+ @dataclass(frozen=True)
19
+ class ScoreCI:
20
+ """Confidence interval on a single eval score."""
21
+
22
+ estimate: float
23
+ ci_low: float
24
+ ci_high: float
25
+ n: int
26
+ method: str
27
+ binary: bool
28
+ confidence: float = 0.95
29
+
30
+ @property
31
+ def margin(self) -> float:
32
+ """Half-width of the interval."""
33
+ return (self.ci_high - self.ci_low) / 2
34
+
35
+ @property
36
+ def width(self) -> float:
37
+ return self.ci_high - self.ci_low
38
+
39
+ def summary(self) -> str:
40
+ conf = f"{self.confidence * 100:.0f}%"
41
+ if self.binary:
42
+ head = (
43
+ f"{_pct(self.estimate)} pass rate "
44
+ f"({conf} CI: {_pct(self.ci_low)}-{_pct(self.ci_high)}, n={self.n})."
45
+ )
46
+ span = f"{self.width * 100:.0f} points"
47
+ else:
48
+ head = (
49
+ f"Mean score {self.estimate:.3f} "
50
+ f"({conf} CI: {self.ci_low:.3f}-{self.ci_high:.3f}, n={self.n})."
51
+ )
52
+ span = f"{self.width:.3f}"
53
+
54
+ tail = (
55
+ f" The interval spans {span}; treat differences smaller than that "
56
+ f"as unresolved."
57
+ )
58
+ if self.n < 30:
59
+ tail += (
60
+ f" With only {self.n} observations this estimate is weak "
61
+ f"regardless of the point value."
62
+ )
63
+ return head + tail
64
+
65
+ def __str__(self) -> str: # pragma: no cover
66
+ return self.summary()
evalaudit/scores.py ADDED
@@ -0,0 +1,136 @@
1
+ """Confidence intervals on a single eval score.
2
+
3
+ The most common missing piece in published eval results: a pass rate with no
4
+ interval around it. At n=50 the interval is roughly +/- 10 points, which is
5
+ usually wider than the difference being claimed.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ from typing import Optional, Sequence
11
+
12
+ import numpy as np
13
+ from scipy import stats as _stats
14
+
15
+ from ._types import ScoreCI
16
+
17
+ __all__ = ["score_ci"]
18
+
19
+ _VALID_METHODS = {"auto", "wilson", "bootstrap", "t"}
20
+
21
+
22
+ def score_ci(
23
+ scores: Sequence[float],
24
+ method: str = "auto",
25
+ confidence: float = 0.95,
26
+ n_boot: int = 10000,
27
+ seed: Optional[int] = None,
28
+ ) -> ScoreCI:
29
+ """Confidence interval on an eval score.
30
+
31
+ Parameters
32
+ ----------
33
+ scores
34
+ Binary (0/1) or continuous values, one per evaluated item.
35
+ method
36
+ ``"auto"`` picks Wilson for binary data and bootstrap for continuous.
37
+ ``"wilson"`` is the score interval for a proportion. Use it rather than
38
+ the normal approximation, which is visibly wrong at small n and near
39
+ the boundaries, and can return bounds outside [0, 1].
40
+ ``"bootstrap"`` is the percentile bootstrap of the mean.
41
+ ``"t"`` is the Student-t interval on the mean.
42
+ confidence
43
+ Nominal coverage, default 0.95.
44
+ n_boot
45
+ Bootstrap resamples. 1000 is enough for a consulting report;
46
+ the default is generous.
47
+ seed
48
+ Seeds the bootstrap so results are reproducible. Always set this in
49
+ anything you publish.
50
+
51
+ Returns
52
+ -------
53
+ ScoreCI
54
+
55
+ Examples
56
+ --------
57
+ >>> score_ci([1] * 42 + [0] * 8).summary()
58
+ '84.0% pass rate (95% CI: 71.5%-91.7%, n=50). ...'
59
+ """
60
+ x = np.asarray(scores, dtype=float)
61
+ n = len(x)
62
+ if n == 0:
63
+ raise ValueError("scores must not be empty")
64
+ if method not in _VALID_METHODS:
65
+ raise ValueError(f"method must be one of {_VALID_METHODS}, got {method!r}")
66
+
67
+ binary = _is_binary(x)
68
+
69
+ if method == "auto":
70
+ method = "wilson" if binary else "bootstrap"
71
+
72
+ estimate = float(np.mean(x))
73
+
74
+ if method == "wilson":
75
+ k = int(np.sum(x))
76
+ ci_low, ci_high = _wilson(k, n, confidence)
77
+ elif method == "bootstrap":
78
+ ci_low, ci_high = _bootstrap_mean(x, confidence, n_boot, seed)
79
+ else: # "t"
80
+ alpha = 1 - confidence
81
+ sem = float(np.std(x, ddof=1) / np.sqrt(n))
82
+ t_crit = _stats.t.ppf(1 - alpha / 2, df=n - 1)
83
+ ci_low = estimate - t_crit * sem
84
+ ci_high = estimate + t_crit * sem
85
+
86
+ return ScoreCI(
87
+ estimate=estimate,
88
+ ci_low=ci_low,
89
+ ci_high=ci_high,
90
+ n=n,
91
+ method=method,
92
+ binary=binary,
93
+ confidence=confidence,
94
+ )
95
+
96
+
97
+ def _is_binary(x: np.ndarray) -> bool:
98
+ """True when every value is 0 or 1."""
99
+ return bool(np.all((x == 0) | (x == 1)))
100
+
101
+
102
+ def _wilson(k: int, n: int, confidence: float) -> tuple[float, float]:
103
+ """Wilson score interval for a proportion.
104
+
105
+ (p + z^2/2n +/- z * sqrt( p(1-p)/n + z^2/4n^2 )) / (1 + z^2/n)
106
+
107
+ Clip to [0, 1] at the end.
108
+ """
109
+ p = k / n
110
+ z = _stats.norm.ppf(1 - (1 - confidence) / 2)
111
+ z2 = z * z
112
+ denom = 1 + z2 / n
113
+ centre = (p + z2 / (2 * n)) / denom
114
+ margin = (z / denom) * np.sqrt(p * (1 - p) / n + z2 / (4 * n * n))
115
+ lo = float(np.clip(centre - margin, 0.0, 1.0))
116
+ hi = float(np.clip(centre + margin, 0.0, 1.0))
117
+ return lo, hi
118
+
119
+
120
+ def _bootstrap_mean(
121
+ x: np.ndarray, confidence: float, n_boot: int, seed: Optional[int]
122
+ ) -> tuple[float, float]:
123
+ """Percentile bootstrap of the mean.
124
+
125
+ Vectorise this. Draw an (n_boot, n) index matrix with
126
+ ``rng.integers(0, n, size=(n_boot, n))`` and take row means in one pass.
127
+ A Python loop here is the difference between instant and slow on a laptop.
128
+ """
129
+ rng = np.random.default_rng(seed)
130
+ n = len(x)
131
+ idx = rng.integers(0, n, size=(n_boot, n))
132
+ means = x[idx].mean(axis=1)
133
+ alpha = 1 - confidence
134
+ lo = float(np.percentile(means, 100 * alpha / 2))
135
+ hi = float(np.percentile(means, 100 * (1 - alpha / 2)))
136
+ return lo, hi
@@ -0,0 +1,134 @@
1
+ Metadata-Version: 2.5
2
+ Name: evalaudit
3
+ Version: 0.0.1
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
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: pytest-cov; extra == 'dev'
22
+ Requires-Dist: pytest>=7.0; extra == 'dev'
23
+ Description-Content-Type: text/markdown
24
+
25
+ # evalaudit
26
+
27
+ [![Tests](https://github.com/ondibahezron-glitch/evalaudit/actions/workflows/tests.yml/badge.svg)](https://github.com/ondibahezron-glitch/evalaudit/actions/workflows/tests.yml)
28
+
29
+ **Statistical validity checks for human-graded AI evaluations.**
30
+
31
+ Three experts graded 200 items. One of them disagreed with the other two on
32
+ almost everything. Nobody noticed, because nobody computed agreement — and
33
+ half the items were only graded once, so it could not have been computed.
34
+
35
+ That evaluation produced a number. The number is now in a slide deck.
36
+
37
+ `evalaudit` is the set of checks that should have run first.
38
+
39
+ ---
40
+
41
+ ## Start here
42
+
43
+ ```python
44
+ from evalaudit import score_ci
45
+
46
+ wins = [1] * 58 + [0] * 42
47
+ print(score_ci(wins).summary())
48
+ ```
49
+
50
+ ```
51
+ 58.0% pass rate (95% CI: 48.2%-67.2%, n=100). The interval spans
52
+ 19 points; treat differences smaller than that as unresolved.
53
+ ```
54
+
55
+ The interval crosses 50%. On this data the new model might be worse than the
56
+ old one. That evaluation cannot support the decision being made with it.
57
+
58
+ ---
59
+
60
+ ## Why this exists
61
+
62
+ `scipy` computes McNemar's test. `sklearn` computes Cohen's kappa. Several
63
+ libraries now put bootstrapped intervals on automated eval scores.
64
+
65
+ What is missing is anything built for the **human** side of evaluation: the
66
+ graders, the rubric, the LLM judge standing in for both. That is where the
67
+ expensive failures happen, and it is where almost no tooling exists.
68
+
69
+ Every function returns an object with a `.summary()`. The numbers are the
70
+ input to the audit. The sentence is the output.
71
+
72
+ Two rules the library follows throughout:
73
+
74
+ - **Never report a bare p-value.** Always an effect size and an interval.
75
+ - **State what the data cannot support**, not only what it can.
76
+
77
+ ---
78
+
79
+ ## What it checks
80
+
81
+ | Module | Question |
82
+ |---|---|
83
+ | `agreement` | Do your graders agree? Which grader is the outlier? Which items are ambiguous rather than hard? |
84
+ | `judge` | Does your LLM judge track your humans — and on which slices does it stop? Is it biased by position or length? |
85
+ | `pairwise` | In a blind pairwise comparison, which models are actually separable? |
86
+ | `power` | Could this many comparisons ever have detected the effect you care about? |
87
+ | `scores` | Is there an interval around your headline number, and how wide? |
88
+ | `compare` | Does the margin between two systems survive a paired test? |
89
+ | `audit` | All of the above, as a report ranked by what changes the conclusion. |
90
+
91
+ The first four are the point. The last three are the foundation they stand on.
92
+
93
+ ---
94
+
95
+ ## Status
96
+
97
+ Early. `scores` is implemented. `agreement`, `judge` and `pairwise` are
98
+ specified and landing over the coming weeks.
99
+
100
+ ---
101
+
102
+ ## Coverage
103
+
104
+ A statistics package earns trust by showing its intervals cover at the rate
105
+ they claim. Every interval here is tested by simulation: generate 1,000
106
+ datasets with a known truth, confirm the 95% interval contains it about 95% of
107
+ the time. Those tests run in CI and you can read them in `tests/`.
108
+
109
+ ---
110
+
111
+ ## Related
112
+
113
+ [`promptstats`](https://pypi.org/project/promptstats/) covers the automated
114
+ side well — prompt sensitivity, model comparison across prompt variations,
115
+ bootstrapped intervals on benchmark scores. If your evaluation has no human
116
+ graders in it, start there.
117
+
118
+ ---
119
+
120
+ ## Install
121
+
122
+ ```bash
123
+ git clone https://github.com/ondibahezron-glitch/evalaudit.git
124
+ cd evalaudit
125
+ pip install -e ".[dev]"
126
+ ```
127
+
128
+ Python 3.9+, numpy, scipy, pandas. Nothing else. PyPI package coming soon.
129
+
130
+ ---
131
+
132
+ ## Licence
133
+
134
+ MIT.
@@ -0,0 +1,7 @@
1
+ evalaudit/__init__.py,sha256=xDC1rHAi-QtzJNx-hZ2zZXA8hrajKAKoT20w9lOo42g,142
2
+ evalaudit/_types.py,sha256=_G2Q5_UZsIWKXKrEF8ZNQI3WpmQ5Sx0RojZTOHw48Ic,1890
3
+ evalaudit/scores.py,sha256=diNHQpVWOgNQC1CXI-w_b7XDyl0T_w8Uq7cBx18IpV0,4099
4
+ evalaudit-0.0.1.dist-info/METADATA,sha256=3CLJxaioUBFr_RsdiYjzoZj1vFnGik8gSAnr72jLVYM,4524
5
+ evalaudit-0.0.1.dist-info/WHEEL,sha256=zOwg4jB6zX2kU910N-cMawjivD6tO8NEWvE12je1bVk,87
6
+ evalaudit-0.0.1.dist-info/licenses/LICENSE,sha256=JI27FMe4Ywi4XKsJPsHc4eYV0M71wU9_8FypQkia0XA,1070
7
+ evalaudit-0.0.1.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.32.0
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +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.