judgepanel 0.1.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.
- judgepanel-0.1.0/CITATION.cff +30 -0
- judgepanel-0.1.0/LICENSE +21 -0
- judgepanel-0.1.0/PKG-INFO +171 -0
- judgepanel-0.1.0/README.md +148 -0
- judgepanel-0.1.0/pyproject.toml +65 -0
- judgepanel-0.1.0/src/judgepanel/__init__.py +39 -0
- judgepanel-0.1.0/src/judgepanel/agreement.py +80 -0
- judgepanel-0.1.0/src/judgepanel/cli.py +274 -0
- judgepanel-0.1.0/src/judgepanel/em.py +202 -0
- judgepanel-0.1.0/src/judgepanel/io.py +76 -0
- judgepanel-0.1.0/src/judgepanel/panel.py +107 -0
- judgepanel-0.1.0/src/judgepanel/py.typed +0 -0
- judgepanel-0.1.0/src/judgepanel/simulate.py +151 -0
- judgepanel-0.1.0/src/judgepanel/uncertainty.py +95 -0
- judgepanel-0.1.0/src/judgepanel/vote.py +60 -0
- judgepanel-0.1.0/tests/test_agreement.py +97 -0
- judgepanel-0.1.0/tests/test_em.py +131 -0
- judgepanel-0.1.0/tests/test_io_cli.py +150 -0
- judgepanel-0.1.0/tests/test_package.py +26 -0
- judgepanel-0.1.0/tests/test_panel.py +73 -0
- judgepanel-0.1.0/tests/test_simulate.py +73 -0
- judgepanel-0.1.0/tests/test_uncertainty.py +69 -0
- judgepanel-0.1.0/tests/test_vote.py +66 -0
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
cff-version: 1.2.0
|
|
2
|
+
message: "If you use this software, please cite it as below."
|
|
3
|
+
title: "judgepanel: estimating LLM-judge accuracy without gold labels"
|
|
4
|
+
type: software
|
|
5
|
+
version: "0.1.0"
|
|
6
|
+
date-released: "2026-08-05"
|
|
7
|
+
license: MIT
|
|
8
|
+
repository-code: "https://github.com/mohammadi-hadi/judgepanel"
|
|
9
|
+
url: "https://github.com/mohammadi-hadi/judgepanel"
|
|
10
|
+
authors:
|
|
11
|
+
- given-names: "Hadi"
|
|
12
|
+
family-names: "Mohammadi"
|
|
13
|
+
orcid: "https://orcid.org/0000-0003-0860-9200"
|
|
14
|
+
affiliation: "Utrecht University"
|
|
15
|
+
keywords:
|
|
16
|
+
- "LLM-as-judge"
|
|
17
|
+
- "Dawid-Skene"
|
|
18
|
+
- "label aggregation"
|
|
19
|
+
- "inter-rater agreement"
|
|
20
|
+
- "evaluation"
|
|
21
|
+
- "large language models"
|
|
22
|
+
abstract: >-
|
|
23
|
+
Estimates each judge's confusion matrix, the class prevalence, and a
|
|
24
|
+
posterior over every item's true label from a panel of imperfect judges
|
|
25
|
+
labelling the same items, with no gold labels, via the Dawid-Skene model
|
|
26
|
+
fitted by EM. Includes majority-vote baselines, Cohen's and Fleiss' kappa,
|
|
27
|
+
bootstrap confidence intervals, and panel simulators. Validated on a real
|
|
28
|
+
five-judge panel over 400 agent trajectories where the estimated judge
|
|
29
|
+
accuracies can be checked against construction-time gold labels, including
|
|
30
|
+
an honest account of the correlated-judge failure mode.
|
judgepanel-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Hadi Mohammadi
|
|
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.
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: judgepanel
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Estimate LLM-judge accuracy without gold labels and aggregate judge panels: Dawid-Skene EM, agreement statistics, bootstrap uncertainty
|
|
5
|
+
Project-URL: Repository, https://github.com/mohammadi-hadi/judgepanel
|
|
6
|
+
Project-URL: Issues, https://github.com/mohammadi-hadi/judgepanel/issues
|
|
7
|
+
Author: Hadi Mohammadi
|
|
8
|
+
License-Expression: MIT
|
|
9
|
+
License-File: LICENSE
|
|
10
|
+
Keywords: annotation,dawid-skene,evaluation,inter-rater-agreement,label-aggregation,llm-as-judge
|
|
11
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
14
|
+
Requires-Python: >=3.12
|
|
15
|
+
Requires-Dist: numpy>=1.26
|
|
16
|
+
Provides-Extra: dev
|
|
17
|
+
Requires-Dist: build>=1.2; extra == 'dev'
|
|
18
|
+
Requires-Dist: mypy>=1.10; extra == 'dev'
|
|
19
|
+
Requires-Dist: pytest>=8.0; extra == 'dev'
|
|
20
|
+
Requires-Dist: ruff>=0.5; extra == 'dev'
|
|
21
|
+
Requires-Dist: twine>=6.1; extra == 'dev'
|
|
22
|
+
Description-Content-Type: text/markdown
|
|
23
|
+
|
|
24
|
+
# judgepanel
|
|
25
|
+
|
|
26
|
+
[](https://github.com/mohammadi-hadi/judgepanel/actions/workflows/ci.yml)
|
|
27
|
+
|
|
28
|
+
**Estimate how accurate your LLM judges are — without gold labels — and aggregate
|
|
29
|
+
their verdicts better than majority vote.**
|
|
30
|
+
|
|
31
|
+
When several imperfect judges label the same items, the disagreement pattern
|
|
32
|
+
itself reveals who is reliable. judgepanel fits the classic Dawid–Skene model
|
|
33
|
+
(1979) to a judge panel and recovers, jointly:
|
|
34
|
+
|
|
35
|
+
- each judge's **confusion matrix** (sensitivity/specificity in the binary case),
|
|
36
|
+
- the **class prevalence**,
|
|
37
|
+
- a **posterior over every item's true label**, weighted by estimated judge
|
|
38
|
+
quality instead of one-judge-one-vote.
|
|
39
|
+
|
|
40
|
+
Around the core: majority-vote baselines with explicit tie handling,
|
|
41
|
+
chance-corrected agreement (Cohen's and Fleiss' kappa), bootstrap confidence
|
|
42
|
+
intervals, and simulators for planning panels and studying failure modes.
|
|
43
|
+
Pure NumPy; no model calls; every fit deterministic.
|
|
44
|
+
|
|
45
|
+
## Install
|
|
46
|
+
|
|
47
|
+
```bash
|
|
48
|
+
pip install git+https://github.com/mohammadi-hadi/judgepanel
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
## Sixty seconds
|
|
52
|
+
|
|
53
|
+
Your data is one JSON object per (item, judge) verdict:
|
|
54
|
+
|
|
55
|
+
```json
|
|
56
|
+
{"item": "case-0017", "judge": "gpt-strict", "label": "faulty"}
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
```bash
|
|
60
|
+
judgepanel fit examples/data/demo_panel.jsonl --positive faulty --bootstrap 200
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
```
|
|
64
|
+
panel: 300 items, 5 judges, 2 classes (clean, faulty)
|
|
65
|
+
converged after 55 iterations
|
|
66
|
+
estimated prevalence: clean=0.577, faulty=0.423
|
|
67
|
+
|
|
68
|
+
judge sens spec youden
|
|
69
|
+
gpt-strict 0.906 0.931 0.836
|
|
70
|
+
gpt-lenient 0.715 0.999 0.714
|
|
71
|
+
small-model 0.692 0.710 0.402
|
|
72
|
+
rules 0.562 0.996 0.559
|
|
73
|
+
flag-happy 0.953 0.394 0.347
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
The demo panel is synthetic with known truth (`examples/data/demo_gold.jsonl`):
|
|
77
|
+
the true parameters are (0.92, 0.94), (0.70, 0.97), (0.66, 0.72), (0.55, 0.99)
|
|
78
|
+
and (0.97, 0.35) — every estimate above lands within 0.04 without ever seeing a
|
|
79
|
+
gold label, and the flag-everything judge is exposed immediately.
|
|
80
|
+
|
|
81
|
+
Same thing in Python:
|
|
82
|
+
|
|
83
|
+
```python
|
|
84
|
+
from judgepanel import fit_dawid_skene, read_panel_jsonl
|
|
85
|
+
|
|
86
|
+
panel = read_panel_jsonl("panel.jsonl")
|
|
87
|
+
result = fit_dawid_skene(panel)
|
|
88
|
+
result.sensitivity("faulty") # {"gpt-strict": 0.906, ...}
|
|
89
|
+
result.labels() # posterior-aggregated label per item
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
Keys are configurable, so existing verdict files work directly
|
|
93
|
+
(`--item-key trajectory_id --judge-key judge_id --label-key faulty`; boolean
|
|
94
|
+
labels become `"true"`/`"false"`).
|
|
95
|
+
|
|
96
|
+
## Validation on a real judge panel
|
|
97
|
+
|
|
98
|
+
The [trajectory-judge](https://github.com/mohammadi-hadi/trajectory-judge)
|
|
99
|
+
study had five LLM judges label the same 400 agent trajectories as faulty or
|
|
100
|
+
clean, with gold labels known by construction. Fitting judgepanel on the
|
|
101
|
+
verdicts alone (gold hidden), then unblinding
|
|
102
|
+
(`python examples/case_study.py --raw .../results/raw`):
|
|
103
|
+
|
|
104
|
+
| judge | est sens | true sens | est spec | true spec |
|
|
105
|
+
|---|---|---|---|---|
|
|
106
|
+
| programmatic rules | 0.771 | 0.667 | 0.993 | 1.000 |
|
|
107
|
+
| outcome-only (14B) | 0.639 | 0.613 | 0.633 | 0.670 |
|
|
108
|
+
| step-rubric (14B) | 0.995 | 0.857 | 1.000 | 1.000 |
|
|
109
|
+
| step-rubric (8B) | 0.988 | 0.990 | 0.000 | 0.000 |
|
|
110
|
+
| self-consistency k=3 (14B) | 0.980 | 0.843 | 0.994 | 0.990 |
|
|
111
|
+
|
|
112
|
+
Three honest findings:
|
|
113
|
+
|
|
114
|
+
1. **The spammer is exposed exactly.** The 8B judge flags nearly everything;
|
|
115
|
+
its estimated specificity is 0.000 — matching the truth — with no gold
|
|
116
|
+
label in sight. The judge quality *ranking* is right across the board, and
|
|
117
|
+
the production-default outcome judge is estimated within 0.04.
|
|
118
|
+
2. **Correlated judges inflate their own estimates.** Self-consistency is the
|
|
119
|
+
step judge sampled three times, and the two agree at kappa 0.96. Dawid–Skene
|
|
120
|
+
assumes conditionally independent judges, so this pair drags the latent
|
|
121
|
+
truth toward its shared verdicts: both get overestimated by up to +0.14,
|
|
122
|
+
and estimated prevalence drops to 0.645 against a true 0.750. The library
|
|
123
|
+
prints the kappa matrix precisely so you check this before believing the fit.
|
|
124
|
+
3. **No aggregation recovers what every judge misses.** Faults that leave the
|
|
125
|
+
final answer correct are invisible to the whole panel, so posterior
|
|
126
|
+
aggregation (0.895 accuracy) cannot beat majority (0.897) here. Weighted
|
|
127
|
+
aggregation wins when judge quality is *uneven*, not when errors are shared.
|
|
128
|
+
|
|
129
|
+
## When not to trust it
|
|
130
|
+
|
|
131
|
+
- **Fewer than 3 judges**: weakly identified; the fit warns.
|
|
132
|
+
- **Highly correlated judges** (same base model, ensembles of one judge,
|
|
133
|
+
shared blind spots): estimates bias toward the correlated cluster — check
|
|
134
|
+
`judgepanel agree` first, and prefer diverse judge families.
|
|
135
|
+
- **A panel that is wrong on average**: if most judges are worse than chance,
|
|
136
|
+
the labels-and-mirror solution is indistinguishable without gold.
|
|
137
|
+
- **Tiny or extremely imbalanced panels**: run `judgepanel simulate` with your
|
|
138
|
+
expected operating points to see the estimation error you should expect
|
|
139
|
+
before you rely on the numbers.
|
|
140
|
+
|
|
141
|
+
## API
|
|
142
|
+
|
|
143
|
+
| call | what it does |
|
|
144
|
+
|---|---|
|
|
145
|
+
| `read_panel_jsonl(path, ...)` | long-format JSONL → `Panel` |
|
|
146
|
+
| `fit_dawid_skene(panel)` | EM fit → confusions, prevalence, posteriors |
|
|
147
|
+
| `bootstrap_judges(panel, n_boot=200)` | percentile CIs for judge parameters |
|
|
148
|
+
| `majority_vote(panel)` | baseline aggregation, ties made explicit |
|
|
149
|
+
| `cohen_kappa`, `fleiss_kappa`, `pairwise_cohen_kappa` | agreement diagnostics |
|
|
150
|
+
| `simulate_panel`, `simulate_confusion_panel` | synthetic panels with known truth |
|
|
151
|
+
|
|
152
|
+
CLI: `judgepanel fit`, `judgepanel agree`, `judgepanel simulate`,
|
|
153
|
+
`judgepanel version` (each supports `--json`).
|
|
154
|
+
|
|
155
|
+
## Development
|
|
156
|
+
|
|
157
|
+
```bash
|
|
158
|
+
python -m venv .venv && .venv/bin/pip install -e ".[dev]"
|
|
159
|
+
.venv/bin/pytest # 52 tests: hand-computed values, seeded recovery, CLI
|
|
160
|
+
.venv/bin/ruff check src tests && .venv/bin/mypy src
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
The suite checks statistical behaviour, not just plumbing: kappa against
|
|
164
|
+
hand-computed tables, EM recovery on seeded synthetic panels, log-likelihood
|
|
165
|
+
monotonicity, bootstrap coverage, and determinism.
|
|
166
|
+
|
|
167
|
+
## Citation
|
|
168
|
+
|
|
169
|
+
If you use judgepanel, please cite it (see `CITATION.cff`).
|
|
170
|
+
|
|
171
|
+
MIT license.
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
# judgepanel
|
|
2
|
+
|
|
3
|
+
[](https://github.com/mohammadi-hadi/judgepanel/actions/workflows/ci.yml)
|
|
4
|
+
|
|
5
|
+
**Estimate how accurate your LLM judges are — without gold labels — and aggregate
|
|
6
|
+
their verdicts better than majority vote.**
|
|
7
|
+
|
|
8
|
+
When several imperfect judges label the same items, the disagreement pattern
|
|
9
|
+
itself reveals who is reliable. judgepanel fits the classic Dawid–Skene model
|
|
10
|
+
(1979) to a judge panel and recovers, jointly:
|
|
11
|
+
|
|
12
|
+
- each judge's **confusion matrix** (sensitivity/specificity in the binary case),
|
|
13
|
+
- the **class prevalence**,
|
|
14
|
+
- a **posterior over every item's true label**, weighted by estimated judge
|
|
15
|
+
quality instead of one-judge-one-vote.
|
|
16
|
+
|
|
17
|
+
Around the core: majority-vote baselines with explicit tie handling,
|
|
18
|
+
chance-corrected agreement (Cohen's and Fleiss' kappa), bootstrap confidence
|
|
19
|
+
intervals, and simulators for planning panels and studying failure modes.
|
|
20
|
+
Pure NumPy; no model calls; every fit deterministic.
|
|
21
|
+
|
|
22
|
+
## Install
|
|
23
|
+
|
|
24
|
+
```bash
|
|
25
|
+
pip install git+https://github.com/mohammadi-hadi/judgepanel
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
## Sixty seconds
|
|
29
|
+
|
|
30
|
+
Your data is one JSON object per (item, judge) verdict:
|
|
31
|
+
|
|
32
|
+
```json
|
|
33
|
+
{"item": "case-0017", "judge": "gpt-strict", "label": "faulty"}
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
```bash
|
|
37
|
+
judgepanel fit examples/data/demo_panel.jsonl --positive faulty --bootstrap 200
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
```
|
|
41
|
+
panel: 300 items, 5 judges, 2 classes (clean, faulty)
|
|
42
|
+
converged after 55 iterations
|
|
43
|
+
estimated prevalence: clean=0.577, faulty=0.423
|
|
44
|
+
|
|
45
|
+
judge sens spec youden
|
|
46
|
+
gpt-strict 0.906 0.931 0.836
|
|
47
|
+
gpt-lenient 0.715 0.999 0.714
|
|
48
|
+
small-model 0.692 0.710 0.402
|
|
49
|
+
rules 0.562 0.996 0.559
|
|
50
|
+
flag-happy 0.953 0.394 0.347
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
The demo panel is synthetic with known truth (`examples/data/demo_gold.jsonl`):
|
|
54
|
+
the true parameters are (0.92, 0.94), (0.70, 0.97), (0.66, 0.72), (0.55, 0.99)
|
|
55
|
+
and (0.97, 0.35) — every estimate above lands within 0.04 without ever seeing a
|
|
56
|
+
gold label, and the flag-everything judge is exposed immediately.
|
|
57
|
+
|
|
58
|
+
Same thing in Python:
|
|
59
|
+
|
|
60
|
+
```python
|
|
61
|
+
from judgepanel import fit_dawid_skene, read_panel_jsonl
|
|
62
|
+
|
|
63
|
+
panel = read_panel_jsonl("panel.jsonl")
|
|
64
|
+
result = fit_dawid_skene(panel)
|
|
65
|
+
result.sensitivity("faulty") # {"gpt-strict": 0.906, ...}
|
|
66
|
+
result.labels() # posterior-aggregated label per item
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
Keys are configurable, so existing verdict files work directly
|
|
70
|
+
(`--item-key trajectory_id --judge-key judge_id --label-key faulty`; boolean
|
|
71
|
+
labels become `"true"`/`"false"`).
|
|
72
|
+
|
|
73
|
+
## Validation on a real judge panel
|
|
74
|
+
|
|
75
|
+
The [trajectory-judge](https://github.com/mohammadi-hadi/trajectory-judge)
|
|
76
|
+
study had five LLM judges label the same 400 agent trajectories as faulty or
|
|
77
|
+
clean, with gold labels known by construction. Fitting judgepanel on the
|
|
78
|
+
verdicts alone (gold hidden), then unblinding
|
|
79
|
+
(`python examples/case_study.py --raw .../results/raw`):
|
|
80
|
+
|
|
81
|
+
| judge | est sens | true sens | est spec | true spec |
|
|
82
|
+
|---|---|---|---|---|
|
|
83
|
+
| programmatic rules | 0.771 | 0.667 | 0.993 | 1.000 |
|
|
84
|
+
| outcome-only (14B) | 0.639 | 0.613 | 0.633 | 0.670 |
|
|
85
|
+
| step-rubric (14B) | 0.995 | 0.857 | 1.000 | 1.000 |
|
|
86
|
+
| step-rubric (8B) | 0.988 | 0.990 | 0.000 | 0.000 |
|
|
87
|
+
| self-consistency k=3 (14B) | 0.980 | 0.843 | 0.994 | 0.990 |
|
|
88
|
+
|
|
89
|
+
Three honest findings:
|
|
90
|
+
|
|
91
|
+
1. **The spammer is exposed exactly.** The 8B judge flags nearly everything;
|
|
92
|
+
its estimated specificity is 0.000 — matching the truth — with no gold
|
|
93
|
+
label in sight. The judge quality *ranking* is right across the board, and
|
|
94
|
+
the production-default outcome judge is estimated within 0.04.
|
|
95
|
+
2. **Correlated judges inflate their own estimates.** Self-consistency is the
|
|
96
|
+
step judge sampled three times, and the two agree at kappa 0.96. Dawid–Skene
|
|
97
|
+
assumes conditionally independent judges, so this pair drags the latent
|
|
98
|
+
truth toward its shared verdicts: both get overestimated by up to +0.14,
|
|
99
|
+
and estimated prevalence drops to 0.645 against a true 0.750. The library
|
|
100
|
+
prints the kappa matrix precisely so you check this before believing the fit.
|
|
101
|
+
3. **No aggregation recovers what every judge misses.** Faults that leave the
|
|
102
|
+
final answer correct are invisible to the whole panel, so posterior
|
|
103
|
+
aggregation (0.895 accuracy) cannot beat majority (0.897) here. Weighted
|
|
104
|
+
aggregation wins when judge quality is *uneven*, not when errors are shared.
|
|
105
|
+
|
|
106
|
+
## When not to trust it
|
|
107
|
+
|
|
108
|
+
- **Fewer than 3 judges**: weakly identified; the fit warns.
|
|
109
|
+
- **Highly correlated judges** (same base model, ensembles of one judge,
|
|
110
|
+
shared blind spots): estimates bias toward the correlated cluster — check
|
|
111
|
+
`judgepanel agree` first, and prefer diverse judge families.
|
|
112
|
+
- **A panel that is wrong on average**: if most judges are worse than chance,
|
|
113
|
+
the labels-and-mirror solution is indistinguishable without gold.
|
|
114
|
+
- **Tiny or extremely imbalanced panels**: run `judgepanel simulate` with your
|
|
115
|
+
expected operating points to see the estimation error you should expect
|
|
116
|
+
before you rely on the numbers.
|
|
117
|
+
|
|
118
|
+
## API
|
|
119
|
+
|
|
120
|
+
| call | what it does |
|
|
121
|
+
|---|---|
|
|
122
|
+
| `read_panel_jsonl(path, ...)` | long-format JSONL → `Panel` |
|
|
123
|
+
| `fit_dawid_skene(panel)` | EM fit → confusions, prevalence, posteriors |
|
|
124
|
+
| `bootstrap_judges(panel, n_boot=200)` | percentile CIs for judge parameters |
|
|
125
|
+
| `majority_vote(panel)` | baseline aggregation, ties made explicit |
|
|
126
|
+
| `cohen_kappa`, `fleiss_kappa`, `pairwise_cohen_kappa` | agreement diagnostics |
|
|
127
|
+
| `simulate_panel`, `simulate_confusion_panel` | synthetic panels with known truth |
|
|
128
|
+
|
|
129
|
+
CLI: `judgepanel fit`, `judgepanel agree`, `judgepanel simulate`,
|
|
130
|
+
`judgepanel version` (each supports `--json`).
|
|
131
|
+
|
|
132
|
+
## Development
|
|
133
|
+
|
|
134
|
+
```bash
|
|
135
|
+
python -m venv .venv && .venv/bin/pip install -e ".[dev]"
|
|
136
|
+
.venv/bin/pytest # 52 tests: hand-computed values, seeded recovery, CLI
|
|
137
|
+
.venv/bin/ruff check src tests && .venv/bin/mypy src
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
The suite checks statistical behaviour, not just plumbing: kappa against
|
|
141
|
+
hand-computed tables, EM recovery on seeded synthetic panels, log-likelihood
|
|
142
|
+
monotonicity, bootstrap coverage, and determinism.
|
|
143
|
+
|
|
144
|
+
## Citation
|
|
145
|
+
|
|
146
|
+
If you use judgepanel, please cite it (see `CITATION.cff`).
|
|
147
|
+
|
|
148
|
+
MIT license.
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "judgepanel"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "Estimate LLM-judge accuracy without gold labels and aggregate judge panels: Dawid-Skene EM, agreement statistics, bootstrap uncertainty"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
license = "MIT"
|
|
11
|
+
license-files = ["LICENSE"]
|
|
12
|
+
authors = [{ name = "Hadi Mohammadi" }]
|
|
13
|
+
requires-python = ">=3.12"
|
|
14
|
+
keywords = [
|
|
15
|
+
"llm-as-judge",
|
|
16
|
+
"dawid-skene",
|
|
17
|
+
"label-aggregation",
|
|
18
|
+
"inter-rater-agreement",
|
|
19
|
+
"annotation",
|
|
20
|
+
"evaluation",
|
|
21
|
+
]
|
|
22
|
+
classifiers = [
|
|
23
|
+
"Programming Language :: Python :: 3.12",
|
|
24
|
+
"Programming Language :: Python :: 3.13",
|
|
25
|
+
"Programming Language :: Python :: 3.14",
|
|
26
|
+
]
|
|
27
|
+
dependencies = ["numpy>=1.26"]
|
|
28
|
+
|
|
29
|
+
[project.urls]
|
|
30
|
+
Repository = "https://github.com/mohammadi-hadi/judgepanel"
|
|
31
|
+
Issues = "https://github.com/mohammadi-hadi/judgepanel/issues"
|
|
32
|
+
|
|
33
|
+
[project.optional-dependencies]
|
|
34
|
+
dev = [
|
|
35
|
+
"pytest>=8.0",
|
|
36
|
+
"ruff>=0.5",
|
|
37
|
+
"mypy>=1.10",
|
|
38
|
+
"build>=1.2",
|
|
39
|
+
"twine>=6.1",
|
|
40
|
+
]
|
|
41
|
+
|
|
42
|
+
[project.scripts]
|
|
43
|
+
judgepanel = "judgepanel.cli:main"
|
|
44
|
+
|
|
45
|
+
[tool.hatch.build.targets.wheel]
|
|
46
|
+
packages = ["src/judgepanel"]
|
|
47
|
+
|
|
48
|
+
[tool.hatch.build.targets.sdist]
|
|
49
|
+
include = ["src", "tests", "README.md", "LICENSE", "CITATION.cff"]
|
|
50
|
+
|
|
51
|
+
[tool.ruff]
|
|
52
|
+
line-length = 100
|
|
53
|
+
target-version = "py312"
|
|
54
|
+
|
|
55
|
+
[tool.ruff.lint]
|
|
56
|
+
select = ["E", "F", "I", "UP", "B", "SIM", "RUF"]
|
|
57
|
+
|
|
58
|
+
[tool.mypy]
|
|
59
|
+
python_version = "3.12"
|
|
60
|
+
strict = true
|
|
61
|
+
warn_unreachable = true
|
|
62
|
+
|
|
63
|
+
[tool.pytest.ini_options]
|
|
64
|
+
testpaths = ["tests"]
|
|
65
|
+
addopts = "-q"
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
"""Estimate LLM-judge accuracy without gold labels and aggregate judge panels.
|
|
2
|
+
|
|
3
|
+
The core is the Dawid-Skene model: several imperfect judges label the same
|
|
4
|
+
items, and EM jointly recovers each judge's confusion matrix, the class
|
|
5
|
+
prevalence, and a posterior over every item's true label - no gold labels
|
|
6
|
+
required. Around it: majority-vote baselines, chance-corrected agreement,
|
|
7
|
+
bootstrap confidence intervals, and simulators for planning panels and
|
|
8
|
+
studying failure modes such as correlated judges.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from judgepanel.agreement import cohen_kappa, fleiss_kappa, pairwise_cohen_kappa
|
|
12
|
+
from judgepanel.em import DawidSkeneResult, fit_dawid_skene
|
|
13
|
+
from judgepanel.io import read_panel_jsonl, write_labels_jsonl
|
|
14
|
+
from judgepanel.panel import MISSING, Panel
|
|
15
|
+
from judgepanel.simulate import SimJudge, simulate_confusion_panel, simulate_panel
|
|
16
|
+
from judgepanel.uncertainty import BootstrapResult, bootstrap_judges
|
|
17
|
+
from judgepanel.vote import MajorityResult, majority_vote
|
|
18
|
+
|
|
19
|
+
__version__ = "0.1.0"
|
|
20
|
+
|
|
21
|
+
__all__ = [
|
|
22
|
+
"MISSING",
|
|
23
|
+
"BootstrapResult",
|
|
24
|
+
"DawidSkeneResult",
|
|
25
|
+
"MajorityResult",
|
|
26
|
+
"Panel",
|
|
27
|
+
"SimJudge",
|
|
28
|
+
"__version__",
|
|
29
|
+
"bootstrap_judges",
|
|
30
|
+
"cohen_kappa",
|
|
31
|
+
"fit_dawid_skene",
|
|
32
|
+
"fleiss_kappa",
|
|
33
|
+
"majority_vote",
|
|
34
|
+
"pairwise_cohen_kappa",
|
|
35
|
+
"read_panel_jsonl",
|
|
36
|
+
"simulate_confusion_panel",
|
|
37
|
+
"simulate_panel",
|
|
38
|
+
"write_labels_jsonl",
|
|
39
|
+
]
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
"""Chance-corrected agreement between judges."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from collections.abc import Sequence
|
|
6
|
+
|
|
7
|
+
import numpy as np
|
|
8
|
+
import numpy.typing as npt
|
|
9
|
+
|
|
10
|
+
from judgepanel.panel import MISSING, Panel
|
|
11
|
+
|
|
12
|
+
FloatArray = npt.NDArray[np.float64]
|
|
13
|
+
|
|
14
|
+
_EPS = 1e-12
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def cohen_kappa(a: Sequence[object], b: Sequence[object]) -> float:
|
|
18
|
+
"""Cohen's kappa between two aligned label sequences.
|
|
19
|
+
|
|
20
|
+
Returns 1.0 when both raters agree everywhere with no variation, and NaN
|
|
21
|
+
when chance agreement is 1 but observed agreement is not (kappa undefined).
|
|
22
|
+
"""
|
|
23
|
+
if len(a) != len(b):
|
|
24
|
+
raise ValueError("sequences must be aligned")
|
|
25
|
+
if not a:
|
|
26
|
+
raise ValueError("no labels")
|
|
27
|
+
labels_a = list(a)
|
|
28
|
+
labels_b = list(b)
|
|
29
|
+
classes = sorted({*labels_a, *labels_b}, key=repr)
|
|
30
|
+
index = {c: k for k, c in enumerate(classes)}
|
|
31
|
+
n = len(labels_a)
|
|
32
|
+
table = np.zeros((len(classes), len(classes)), dtype=np.float64)
|
|
33
|
+
for x, y in zip(labels_a, labels_b, strict=True):
|
|
34
|
+
table[index[x], index[y]] += 1.0
|
|
35
|
+
table /= n
|
|
36
|
+
observed = float(np.trace(table))
|
|
37
|
+
expected = float(table.sum(axis=1) @ table.sum(axis=0))
|
|
38
|
+
if 1.0 - expected < _EPS:
|
|
39
|
+
return 1.0 if 1.0 - observed < _EPS else float("nan")
|
|
40
|
+
return (observed - expected) / (1.0 - expected)
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def pairwise_cohen_kappa(panel: Panel) -> FloatArray:
|
|
44
|
+
"""Cohen's kappa for every judge pair, on their co-observed items.
|
|
45
|
+
|
|
46
|
+
The diagonal is 1. A pair with fewer than 2 co-observed items is NaN.
|
|
47
|
+
"""
|
|
48
|
+
out = np.full((panel.n_judges, panel.n_judges), np.nan, dtype=np.float64)
|
|
49
|
+
np.fill_diagonal(out, 1.0)
|
|
50
|
+
labels = panel.labels
|
|
51
|
+
for j in range(panel.n_judges):
|
|
52
|
+
for m in range(j + 1, panel.n_judges):
|
|
53
|
+
both = (labels[:, j] != MISSING) & (labels[:, m] != MISSING)
|
|
54
|
+
if int(both.sum()) < 2:
|
|
55
|
+
continue
|
|
56
|
+
kappa = cohen_kappa(labels[both, j].tolist(), labels[both, m].tolist())
|
|
57
|
+
out[j, m] = out[m, j] = kappa
|
|
58
|
+
return out
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def fleiss_kappa(panel: Panel) -> float:
|
|
62
|
+
"""Fleiss' kappa over the panel, generalised to a variable rater count.
|
|
63
|
+
|
|
64
|
+
Items with fewer than 2 observed labels carry no pairwise-agreement
|
|
65
|
+
information and are skipped.
|
|
66
|
+
"""
|
|
67
|
+
counts = panel.counts().astype(np.float64)
|
|
68
|
+
per_item = counts.sum(axis=1)
|
|
69
|
+
usable = per_item >= 2
|
|
70
|
+
if not bool(usable.any()):
|
|
71
|
+
raise ValueError("no item has 2 or more labels")
|
|
72
|
+
counts = counts[usable]
|
|
73
|
+
per_item = per_item[usable]
|
|
74
|
+
agree = ((counts * (counts - 1)).sum(axis=1)) / (per_item * (per_item - 1))
|
|
75
|
+
observed = float(agree.mean())
|
|
76
|
+
shares = counts.sum(axis=0) / counts.sum()
|
|
77
|
+
expected = float(shares @ shares)
|
|
78
|
+
if 1.0 - expected < _EPS:
|
|
79
|
+
return 1.0 if 1.0 - observed < _EPS else float("nan")
|
|
80
|
+
return (observed - expected) / (1.0 - expected)
|