reliability-lab 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.
Files changed (51) hide show
  1. reliability_lab-0.1.0/.gitattributes +2 -0
  2. reliability_lab-0.1.0/.github/workflows/ci.yml +50 -0
  3. reliability_lab-0.1.0/.github/workflows/release.yml +65 -0
  4. reliability_lab-0.1.0/.gitignore +28 -0
  5. reliability_lab-0.1.0/.python-version +1 -0
  6. reliability_lab-0.1.0/CHANGELOG.md +41 -0
  7. reliability_lab-0.1.0/LICENSE +28 -0
  8. reliability_lab-0.1.0/PKG-INFO +178 -0
  9. reliability_lab-0.1.0/README.md +148 -0
  10. reliability_lab-0.1.0/docs/design/framing-v2.md +323 -0
  11. reliability_lab-0.1.0/docs/releasing.md +32 -0
  12. reliability_lab-0.1.0/examples/llm_judge.py +87 -0
  13. reliability_lab-0.1.0/examples/quickstart.py +58 -0
  14. reliability_lab-0.1.0/examples/splits.py +101 -0
  15. reliability_lab-0.1.0/pyproject.toml +107 -0
  16. reliability_lab-0.1.0/src/reliability_lab/__init__.py +68 -0
  17. reliability_lab-0.1.0/src/reliability_lab/_version.py +1 -0
  18. reliability_lab-0.1.0/src/reliability_lab/alpha.py +290 -0
  19. reliability_lab-0.1.0/src/reliability_lab/data.py +294 -0
  20. reliability_lab-0.1.0/src/reliability_lab/design.py +308 -0
  21. reliability_lab-0.1.0/src/reliability_lab/exceptions.py +47 -0
  22. reliability_lab-0.1.0/src/reliability_lab/inference.py +724 -0
  23. reliability_lab-0.1.0/src/reliability_lab/llm.py +560 -0
  24. reliability_lab-0.1.0/src/reliability_lab/py.typed +0 -0
  25. reliability_lab-0.1.0/src/reliability_lab/report.py +779 -0
  26. reliability_lab-0.1.0/src/reliability_lab/split.py +338 -0
  27. reliability_lab-0.1.0/src/reliability_lab/study.py +1034 -0
  28. reliability_lab-0.1.0/tests/fixtures/__init__.py +0 -0
  29. reliability_lab-0.1.0/tests/fixtures/krippendorff2011.py +52 -0
  30. reliability_lab-0.1.0/tests/fixtures/llm_logs.py +181 -0
  31. reliability_lab-0.1.0/tests/helpers.py +57 -0
  32. reliability_lab-0.1.0/tests/naive_bootstrap.py +90 -0
  33. reliability_lab-0.1.0/tests/reference.py +73 -0
  34. reliability_lab-0.1.0/tests/test_alpha.py +146 -0
  35. reliability_lab-0.1.0/tests/test_bca.py +83 -0
  36. reliability_lab-0.1.0/tests/test_compare.py +136 -0
  37. reliability_lab-0.1.0/tests/test_data.py +106 -0
  38. reliability_lab-0.1.0/tests/test_design.py +218 -0
  39. reliability_lab-0.1.0/tests/test_inference_units.py +227 -0
  40. reliability_lab-0.1.0/tests/test_kalpha.py +133 -0
  41. reliability_lab-0.1.0/tests/test_llm_end_to_end.py +281 -0
  42. reliability_lab-0.1.0/tests/test_parity.py +95 -0
  43. reliability_lab-0.1.0/tests/test_properties.py +130 -0
  44. reliability_lab-0.1.0/tests/test_report.py +75 -0
  45. reliability_lab-0.1.0/tests/test_report_io.py +172 -0
  46. reliability_lab-0.1.0/tests/test_retries.py +123 -0
  47. reliability_lab-0.1.0/tests/test_smoke.py +6 -0
  48. reliability_lab-0.1.0/tests/test_split.py +361 -0
  49. reliability_lab-0.1.0/tests/test_status_policies.py +200 -0
  50. reliability_lab-0.1.0/tests/test_trace.py +292 -0
  51. reliability_lab-0.1.0/uv.lock +1473 -0
@@ -0,0 +1,2 @@
1
+ # Auto detect text files and perform LF normalization
2
+ * text=auto
@@ -0,0 +1,50 @@
1
+ name: CI
2
+
3
+ on:
4
+ push:
5
+ branches: ["**"]
6
+ pull_request:
7
+
8
+ jobs:
9
+ test:
10
+ name: ${{ matrix.os }} / py${{ matrix.python-version }}
11
+ runs-on: ${{ matrix.os }}
12
+ strategy:
13
+ fail-fast: false
14
+ matrix:
15
+ os: [ubuntu-latest, windows-latest, macos-latest]
16
+ python-version: ["3.11", "3.12", "3.13"]
17
+ steps:
18
+ - uses: actions/checkout@v5
19
+ - uses: astral-sh/setup-uv@v6
20
+ with:
21
+ python-version: ${{ matrix.python-version }}
22
+ - name: Install
23
+ run: uv sync --all-groups --locked
24
+ - name: Lint
25
+ run: uv run ruff check
26
+ - name: Format
27
+ run: uv run ruff format --check
28
+ - name: Type-check
29
+ run: uv run mypy
30
+ - name: Test
31
+ run: uv run pytest
32
+
33
+ # irrCAC pins scipy==1.12.0, below this package's floor (1.15, for the bootstrap's
34
+ # rng keyword). Its parity test only needs the point estimates, so it runs in a
35
+ # throwaway environment with the package installed without its dependencies.
36
+ parity-irrcac:
37
+ name: irrCAC parity (isolated)
38
+ runs-on: ubuntu-latest
39
+ steps:
40
+ - uses: actions/checkout@v5
41
+ - uses: astral-sh/setup-uv@v6
42
+ - name: Build the isolated environment
43
+ run: |
44
+ uv venv .venv-irrcac --python 3.12
45
+ uv pip install --python .venv-irrcac irrcac pytest hypothesis
46
+ uv pip install --python .venv-irrcac --no-deps .
47
+ - name: Run irrCAC parity
48
+ run: >-
49
+ .venv-irrcac/bin/python -m pytest tests/test_parity.py -m parity -k irrcac
50
+ -p no:cacheprovider
@@ -0,0 +1,65 @@
1
+ name: Release
2
+
3
+ # Publishes on a version tag (v0.1.0a1, v0.1.0, ...) with PyPI trusted publishing:
4
+ # no API tokens are stored. See docs/releasing.md for the one-time setup.
5
+ on:
6
+ push:
7
+ tags: ["v*"]
8
+
9
+ jobs:
10
+ build:
11
+ name: Build and check the distributions
12
+ runs-on: ubuntu-latest
13
+ steps:
14
+ - uses: actions/checkout@v5
15
+ - uses: astral-sh/setup-uv@v6
16
+ - name: Check the tag matches the package version
17
+ run: |
18
+ version=$(sed -n 's/^__version__ = "\(.*\)"$/\1/p' src/reliability_lab/_version.py)
19
+ if [ "v$version" != "$GITHUB_REF_NAME" ]; then
20
+ echo "tag $GITHUB_REF_NAME does not match package version $version" >&2
21
+ exit 1
22
+ fi
23
+ - name: Build
24
+ run: uv build
25
+ - name: Check metadata
26
+ run: uvx twine check --strict dist/*
27
+ - uses: actions/upload-artifact@v4
28
+ with:
29
+ name: dist
30
+ path: dist/
31
+
32
+ publish-testpypi:
33
+ name: Publish to TestPyPI
34
+ needs: build
35
+ runs-on: ubuntu-latest
36
+ environment:
37
+ name: testpypi
38
+ url: https://test.pypi.org/p/reliability-lab
39
+ permissions:
40
+ id-token: write
41
+ steps:
42
+ - uses: actions/download-artifact@v4
43
+ with:
44
+ name: dist
45
+ path: dist/
46
+ - uses: pypa/gh-action-pypi-publish@release/v1
47
+ with:
48
+ repository-url: https://test.pypi.org/legacy/
49
+
50
+ publish-pypi:
51
+ name: Publish to PyPI
52
+ needs: publish-testpypi
53
+ runs-on: ubuntu-latest
54
+ # Give this environment a required reviewer to approve each release by hand.
55
+ environment:
56
+ name: pypi
57
+ url: https://pypi.org/p/reliability-lab
58
+ permissions:
59
+ id-token: write
60
+ steps:
61
+ - uses: actions/download-artifact@v4
62
+ with:
63
+ name: dist
64
+ path: dist/
65
+ - uses: pypa/gh-action-pypi-publish@release/v1
@@ -0,0 +1,28 @@
1
+ # Byte-compiled / cache
2
+ __pycache__/
3
+ *.py[cod]
4
+ .mypy_cache/
5
+ .pytest_cache/
6
+ .ruff_cache/
7
+ .hypothesis/
8
+
9
+ # Environments
10
+ .venv/
11
+ venv/
12
+
13
+ # Build artefacts
14
+ build/
15
+ dist/
16
+ *.egg-info/
17
+
18
+ # Coverage
19
+ .coverage
20
+ .coverage.*
21
+ htmlcov/
22
+ coverage.xml
23
+
24
+ # Editors / OS
25
+ .vscode/
26
+ .idea/
27
+ .DS_Store
28
+ Thumbs.db
@@ -0,0 +1 @@
1
+ 3.12
@@ -0,0 +1,41 @@
1
+ # Changelog
2
+
3
+ ## Unreleased
4
+
5
+ ## 0.1.0
6
+
7
+ LLM judge workflows.
8
+
9
+ - `TraceSidecar`: one row per judge configuration (`condition_id`). It records provider, prompt hash, rubric and parser versions, temperature, seed, determinism, candidate order, request time and retry policy.
10
+ - Each study checks its trace against its design. Configurations that differ in more than the design compares raise a `TraceWarning`; missing conditions or fields become provenance gaps. Comparisons report the model and trace fields that changed between the arms (`configuration_diff`).
11
+ - The single-configuration designs (repeatability, prompt robustness, order robustness) can hold several judge configurations for `compare()`. Each configuration is checked on its own, and `alpha()` refuses to pool them.
12
+ - `order_robustness` design for pairwise judges whose candidates are shown in swapped orders.
13
+ - Diagnostics on every result (`AlphaResult.diagnostics`):
14
+ - `coder_agreement` (pairwise exact agreement);
15
+ - `run_agreement` (repeatability);
16
+ - `order_swap` (reversal rate, and position bias with a Wilson interval).
17
+ - Retries: a new `attempt` field in `RatingFrame.from_long`. Retried calls must be declared with `retries="last"` or `retries="first_observed"`. Attempts are grouped by call (unit, rater, model, condition, run), so repeated runs are never treated as retries. Coverage reports attempt-level and final error rates.
18
+ - Provenance records the trace rows of the conditions in use and the retries declaration.
19
+
20
+ ## 0.1.0a1
21
+
22
+ First public release.
23
+
24
+ - Project scaffold: packaging, BSD-3-Clause licence, CI on Linux, Windows and macOS.
25
+ - Core engine: `RatingFrame` long-form schema with rating status, `CoderSpec` and design labels, Krippendorff's alpha (nominal, ordinal, interval, ratio, custom distances) with white-box diagnostics, and missing-data policies (`as_missing`, `as_category`, `sensitivity`).
26
+ - Bootstrap inference: `AlphaResult.bootstrap(scheme=...)` with a required scheme.
27
+ - `units` resamples units, or groups, optionally within strata, and recomputes D_e per replicate.
28
+ - `kalpha` follows Krippendorff's algorithm, resampling pairs of values with D_e fixed.
29
+ - Percentile intervals are the default; BCa is opt-in and flagged experimental.
30
+ - Failed replicates are counted and reported, the seed is always recorded, and an optional `alpha_min` reports how many replicates fall below it.
31
+ - Paired comparisons: `ReliabilityStudy.compare(a=..., b=...)` places the interval on Δα = α_b − α_a, using the same resampled units for both configurations.
32
+ - Group-safe splits:
33
+ - `ReliabilityStudy.holdout()` and `cv()` take any scikit-learn style splitter. The splitter sees one sample per group, so groups and repeated LLM calls never cross partitions.
34
+ - When a group's units span several strata, `group_stratum` must be given.
35
+ - `SplitManifest` saves partition ids, the splitter, realised sizes and fingerprints; `apply_manifest()` reapplies them to a later log.
36
+ - `assert_no_leakage()` checks that few-shot examples come from the dev partition.
37
+ - Reports:
38
+ - versioned JSON (`to_json`) with provenance for every result type: software versions, ratings fingerprint, declarations, split and known gaps;
39
+ - tidy tables (`tables`) and Parquet output (`to_parquet`, with the `parquet` extra).
40
+ - Tag-triggered release workflow using PyPI trusted publishing.
41
+ - New exceptions and warnings: `BootstrapWarning`, `LeakageError`. The scipy floor is now 1.15.
@@ -0,0 +1,28 @@
1
+ BSD 3-Clause License
2
+
3
+ Copyright (c) 2026, Sathapana Chawananon
4
+
5
+ Redistribution and use in source and binary forms, with or without
6
+ modification, are permitted provided that the following conditions are met:
7
+
8
+ 1. Redistributions of source code must retain the above copyright notice, this
9
+ list of conditions and the following disclaimer.
10
+
11
+ 2. Redistributions in binary form must reproduce the above copyright notice,
12
+ this list of conditions and the following disclaimer in the documentation
13
+ and/or other materials provided with the distribution.
14
+
15
+ 3. Neither the name of the copyright holder nor the names of its
16
+ contributors may be used to endorse or promote products derived from
17
+ this software without specific prior written permission.
18
+
19
+ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
20
+ AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
21
+ IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
22
+ DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
23
+ FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
24
+ DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
25
+ SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
26
+ CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
27
+ OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
28
+ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
@@ -0,0 +1,178 @@
1
+ Metadata-Version: 2.5
2
+ Name: reliability-lab
3
+ Version: 0.1.0
4
+ Summary: Study-design library for the reliability of LLM and human judgments, built on Krippendorff's alpha.
5
+ Project-URL: Repository, https://github.com/Sathapana/reliability-lab
6
+ Project-URL: Issues, https://github.com/Sathapana/reliability-lab/issues
7
+ Author: Sathapana Chawananon
8
+ License-Expression: BSD-3-Clause
9
+ License-File: LICENSE
10
+ Keywords: alpha,annotation,evaluation,inter-rater reliability,krippendorff,llm-as-a-judge
11
+ Classifier: Development Status :: 3 - Alpha
12
+ Classifier: Intended Audience :: Science/Research
13
+ Classifier: Operating System :: OS Independent
14
+ Classifier: Programming Language :: Python :: 3
15
+ Classifier: Programming Language :: Python :: 3 :: Only
16
+ Classifier: Programming Language :: Python :: 3.11
17
+ Classifier: Programming Language :: Python :: 3.12
18
+ Classifier: Programming Language :: Python :: 3.13
19
+ Classifier: Topic :: Scientific/Engineering :: Information Analysis
20
+ Classifier: Typing :: Typed
21
+ Requires-Python: >=3.11
22
+ Requires-Dist: numpy>=1.26
23
+ Requires-Dist: pandas>=2.1
24
+ Requires-Dist: scipy>=1.15
25
+ Provides-Extra: parquet
26
+ Requires-Dist: pyarrow>=15; extra == 'parquet'
27
+ Provides-Extra: sklearn
28
+ Requires-Dist: scikit-learn>=1.4; extra == 'sklearn'
29
+ Description-Content-Type: text/markdown
30
+
31
+ # reliability-lab
32
+
33
+ **Status: alpha (0.1.0).** The API may still change between releases.
34
+
35
+ reliability-lab is a study-design library for measuring the reliability of LLM and human judgments. It is built on Krippendorff's alpha. Alpha implementations already exist in Python. This package adds the workflow around the coefficient:
36
+
37
+ - a validated long-form ratings schema in which every rating has an explicit status (`observed`, `abstained`, `inapplicable`, `parse_error`, `provider_error`);
38
+ - declared measurement semantics: level, value domain, coder definition and design label;
39
+ - white-box output: observed and expected disagreement, coincidence matrices and per-unit contributions;
40
+ - explicit missing-data policies, because LLM failures are rarely unrelated to the item;
41
+ - bootstrap intervals by a scheme you name: whole units or groups resampled (`scheme="units"`), or Krippendorff's KALPHA algorithm (`scheme="kalpha"`);
42
+ - paired comparisons of two configurations on the same units, with the interval on the difference;
43
+ - group-safe train/test splits that work with any scikit-learn splitter, with a saved manifest that reproduces the split;
44
+ - versioned JSON and Parquet reports with provenance;
45
+ - LLM judge workflows: a trace sidecar of judge configurations, declared retry handling, and evidence specific to each design (run agreement, order swaps and position bias).
46
+
47
+ Mixed human-LLM panels and coverage simulations for the bootstrap schemes are next on the roadmap ([design document](docs/design/framing-v2.md)).
48
+
49
+ > **Reliability is not validity.** Coders can agree consistently and still be wrong in the same way, for example because of a flawed rubric or a bias they share. When results come from different designs (repeatability, inter-model agreement, human panels), they are reported as different quantities and are never pooled.
50
+
51
+ ## Install
52
+
53
+ ```bash
54
+ pip install "reliability-lab[sklearn,parquet]"
55
+ ```
56
+
57
+ The extras are optional. `sklearn` provides the splitters, although any object with a scikit-learn style `split` method works; `parquet` enables `to_parquet`. For development, run `uv sync` and then `uv run pytest`.
58
+
59
+ ## Example
60
+
61
+ ```python
62
+ import pandas as pd
63
+ from reliability_lab import CoderSpec, RatingFrame, ReliabilityStudy
64
+
65
+ df = pd.DataFrame(
66
+ {
67
+ "item": ["u1", "u1", "u2", "u2", "u3", "u3"],
68
+ "annotator": ["ann", "bob", "ann", "bob", "ann", "bob"],
69
+ "label": ["pos", "pos", "neg", "pos", "neg", "neg"],
70
+ }
71
+ )
72
+
73
+ ratings = RatingFrame.from_long(df, unit="item", rater="annotator", value="label")
74
+ study = ReliabilityStudy(
75
+ ratings,
76
+ measurement="nominal",
77
+ values=["neg", "pos"],
78
+ coders=CoderSpec(by="rater_id"),
79
+ design="human_panel",
80
+ )
81
+ result = study.alpha()
82
+ print(result.summary())
83
+ ```
84
+
85
+ ## Intervals and comparisons
86
+
87
+ Every interval names its scheme, because the two schemes answer different questions. `scheme` is a required argument.
88
+
89
+ ```python
90
+ boot = result.bootstrap(scheme="units", n_resamples=2000, random_state=7)
91
+ print(boot.summary()) # 95% percentile intervals for every missing-data policy
92
+ ```
93
+
94
+ - `scheme="units"` resamples whole units, and whole groups when a `group` column is declared. It can also resample within declared strata (`strata=True`), and it recomputes the expected disagreement in every replicate. `method="bca"` is available as an experimental option.
95
+ - `scheme="kalpha"` reproduces Krippendorff's KALPHA algorithm. It resamples pairs of values and holds the expected disagreement fixed.
96
+
97
+ To compare two configurations of a judge, for example two prompts, select each one's rows. Both must cover the same units. Every replicate resamples the same units for both, and the interval is on the difference:
98
+
99
+ ```python
100
+ diff = study.compare(
101
+ a={"condition_id": "prompt_v1"},
102
+ b={"condition_id": "prompt_v2"},
103
+ n_resamples=2000,
104
+ random_state=7,
105
+ )
106
+ print(diff.summary()) # alpha_b - alpha_a with a paired interval
107
+ ```
108
+
109
+ ## Splits and reports
110
+
111
+ Teams tune the judge (prompt, few-shot examples, rubric) and should report reliability on units the tuning never saw. So split before tuning, freeze the judge, then estimate on the test partition.
112
+
113
+ When groups are declared, the splitter sees one sample per group (a conversation, say), so groups and repeated LLM calls never cross partitions:
114
+
115
+ ```python
116
+ from sklearn.model_selection import StratifiedGroupKFold
117
+
118
+ dev, test = study.holdout(
119
+ StratifiedGroupKFold(n_splits=4, shuffle=True, random_state=7),
120
+ group_stratum="majority", # needed only when a group's units span several strata
121
+ )
122
+ test.save_manifest("split.json") # the ids of every partition, not just the seed
123
+
124
+ # ... tune and freeze the judge on dev ...
125
+
126
+ result = test.alpha()
127
+ result.to_json("alpha_test.json") # strict JSON with provenance
128
+ result.to_parquet("alpha_test/") # tidy tables
129
+
130
+ dev2, test2 = later_study.apply_manifest("split.json") # same units, new judge runs
131
+ test.manifest.assert_no_leakage(few_shot_units) # raises LeakageError
132
+ ```
133
+
134
+ See [`examples/splits.py`](examples/splits.py) for a full run. Releases are described in [docs/releasing.md](docs/releasing.md).
135
+
136
+ ## LLM judge workflows
137
+
138
+ A `condition_id` names a frozen judge configuration. A trace sidecar records what each configuration was: provider, prompt hash, rubric and parser versions, temperature, seed, and candidate order for pairwise judges. With it, a study checks that its coders differ only in what the design compares. For example, if prompt-robustness conditions also differ in temperature, it raises a `TraceWarning`. Whatever the trace leaves out is listed among the provenance gaps.
139
+
140
+ ```python
141
+ from reliability_lab import TraceSidecar
142
+
143
+ trace = TraceSidecar.from_frame(
144
+ configs,
145
+ condition="condition_id",
146
+ prompt_hash="prompt_sha",
147
+ temperature="temperature",
148
+ candidate_order="order", # "A,B" / "B,A"
149
+ )
150
+ study = ReliabilityStudy(
151
+ ratings, # RatingFrame.from_long(..., attempt="attempt")
152
+ measurement="nominal",
153
+ values=["A", "tie", "B"], # labels name the winning candidate, not its position
154
+ coders="condition_id",
155
+ design="order_robustness",
156
+ trace=trace,
157
+ retries="last", # required when calls were retried
158
+ )
159
+ result = study.alpha()
160
+ result.diagnostics["order_swap"].values # reversal rate, P(first shown wins) with a Wilson interval
161
+ result.trace_findings # confounds and caveats from the trace
162
+ ```
163
+
164
+ | Design | Coders are | Evidence beside alpha |
165
+ | --- | --- | --- |
166
+ | `repeatability` | repeated runs of one configuration (`run_id`) | run agreement: units where every run agrees |
167
+ | `prompt_robustness` | prompt conditions of one model | pairwise agreement between conditions |
168
+ | `inter_model` | different models | pairwise agreement; family and configuration checks |
169
+ | `order_robustness` | candidate orders of one pairwise judge | reversal rate and position bias |
170
+ | configuration change | two configurations, via `study.compare(a=..., b=...)` | paired interval on the difference; the model and trace fields that changed |
171
+
172
+ The single-configuration designs (repeatability, prompt robustness, order robustness) can hold several configurations to compare, such as an old and a new judge. Each configuration is checked on its own. `alpha()` refuses to pool them; `study.compare(a={"condition_id": "old"}, b={"condition_id": "new"})` reports each one's alpha and diagnostics, with a paired interval on the difference.
173
+
174
+ Retried calls are never resolved silently. Number the attempts with an `attempt` column, then declare `retries="last"` or `retries="first_observed"`. Coverage then reports error rates both per attempt and for the attempts that count. See [`examples/llm_judge.py`](examples/llm_judge.py).
175
+
176
+ ## Licence
177
+
178
+ BSD-3-Clause. Alpha is implemented independently from Krippendorff's papers. The GPL-licensed `krippendorff` package is used only in the test suite, as a parity check.
@@ -0,0 +1,148 @@
1
+ # reliability-lab
2
+
3
+ **Status: alpha (0.1.0).** The API may still change between releases.
4
+
5
+ reliability-lab is a study-design library for measuring the reliability of LLM and human judgments. It is built on Krippendorff's alpha. Alpha implementations already exist in Python. This package adds the workflow around the coefficient:
6
+
7
+ - a validated long-form ratings schema in which every rating has an explicit status (`observed`, `abstained`, `inapplicable`, `parse_error`, `provider_error`);
8
+ - declared measurement semantics: level, value domain, coder definition and design label;
9
+ - white-box output: observed and expected disagreement, coincidence matrices and per-unit contributions;
10
+ - explicit missing-data policies, because LLM failures are rarely unrelated to the item;
11
+ - bootstrap intervals by a scheme you name: whole units or groups resampled (`scheme="units"`), or Krippendorff's KALPHA algorithm (`scheme="kalpha"`);
12
+ - paired comparisons of two configurations on the same units, with the interval on the difference;
13
+ - group-safe train/test splits that work with any scikit-learn splitter, with a saved manifest that reproduces the split;
14
+ - versioned JSON and Parquet reports with provenance;
15
+ - LLM judge workflows: a trace sidecar of judge configurations, declared retry handling, and evidence specific to each design (run agreement, order swaps and position bias).
16
+
17
+ Mixed human-LLM panels and coverage simulations for the bootstrap schemes are next on the roadmap ([design document](docs/design/framing-v2.md)).
18
+
19
+ > **Reliability is not validity.** Coders can agree consistently and still be wrong in the same way, for example because of a flawed rubric or a bias they share. When results come from different designs (repeatability, inter-model agreement, human panels), they are reported as different quantities and are never pooled.
20
+
21
+ ## Install
22
+
23
+ ```bash
24
+ pip install "reliability-lab[sklearn,parquet]"
25
+ ```
26
+
27
+ The extras are optional. `sklearn` provides the splitters, although any object with a scikit-learn style `split` method works; `parquet` enables `to_parquet`. For development, run `uv sync` and then `uv run pytest`.
28
+
29
+ ## Example
30
+
31
+ ```python
32
+ import pandas as pd
33
+ from reliability_lab import CoderSpec, RatingFrame, ReliabilityStudy
34
+
35
+ df = pd.DataFrame(
36
+ {
37
+ "item": ["u1", "u1", "u2", "u2", "u3", "u3"],
38
+ "annotator": ["ann", "bob", "ann", "bob", "ann", "bob"],
39
+ "label": ["pos", "pos", "neg", "pos", "neg", "neg"],
40
+ }
41
+ )
42
+
43
+ ratings = RatingFrame.from_long(df, unit="item", rater="annotator", value="label")
44
+ study = ReliabilityStudy(
45
+ ratings,
46
+ measurement="nominal",
47
+ values=["neg", "pos"],
48
+ coders=CoderSpec(by="rater_id"),
49
+ design="human_panel",
50
+ )
51
+ result = study.alpha()
52
+ print(result.summary())
53
+ ```
54
+
55
+ ## Intervals and comparisons
56
+
57
+ Every interval names its scheme, because the two schemes answer different questions. `scheme` is a required argument.
58
+
59
+ ```python
60
+ boot = result.bootstrap(scheme="units", n_resamples=2000, random_state=7)
61
+ print(boot.summary()) # 95% percentile intervals for every missing-data policy
62
+ ```
63
+
64
+ - `scheme="units"` resamples whole units, and whole groups when a `group` column is declared. It can also resample within declared strata (`strata=True`), and it recomputes the expected disagreement in every replicate. `method="bca"` is available as an experimental option.
65
+ - `scheme="kalpha"` reproduces Krippendorff's KALPHA algorithm. It resamples pairs of values and holds the expected disagreement fixed.
66
+
67
+ To compare two configurations of a judge, for example two prompts, select each one's rows. Both must cover the same units. Every replicate resamples the same units for both, and the interval is on the difference:
68
+
69
+ ```python
70
+ diff = study.compare(
71
+ a={"condition_id": "prompt_v1"},
72
+ b={"condition_id": "prompt_v2"},
73
+ n_resamples=2000,
74
+ random_state=7,
75
+ )
76
+ print(diff.summary()) # alpha_b - alpha_a with a paired interval
77
+ ```
78
+
79
+ ## Splits and reports
80
+
81
+ Teams tune the judge (prompt, few-shot examples, rubric) and should report reliability on units the tuning never saw. So split before tuning, freeze the judge, then estimate on the test partition.
82
+
83
+ When groups are declared, the splitter sees one sample per group (a conversation, say), so groups and repeated LLM calls never cross partitions:
84
+
85
+ ```python
86
+ from sklearn.model_selection import StratifiedGroupKFold
87
+
88
+ dev, test = study.holdout(
89
+ StratifiedGroupKFold(n_splits=4, shuffle=True, random_state=7),
90
+ group_stratum="majority", # needed only when a group's units span several strata
91
+ )
92
+ test.save_manifest("split.json") # the ids of every partition, not just the seed
93
+
94
+ # ... tune and freeze the judge on dev ...
95
+
96
+ result = test.alpha()
97
+ result.to_json("alpha_test.json") # strict JSON with provenance
98
+ result.to_parquet("alpha_test/") # tidy tables
99
+
100
+ dev2, test2 = later_study.apply_manifest("split.json") # same units, new judge runs
101
+ test.manifest.assert_no_leakage(few_shot_units) # raises LeakageError
102
+ ```
103
+
104
+ See [`examples/splits.py`](examples/splits.py) for a full run. Releases are described in [docs/releasing.md](docs/releasing.md).
105
+
106
+ ## LLM judge workflows
107
+
108
+ A `condition_id` names a frozen judge configuration. A trace sidecar records what each configuration was: provider, prompt hash, rubric and parser versions, temperature, seed, and candidate order for pairwise judges. With it, a study checks that its coders differ only in what the design compares. For example, if prompt-robustness conditions also differ in temperature, it raises a `TraceWarning`. Whatever the trace leaves out is listed among the provenance gaps.
109
+
110
+ ```python
111
+ from reliability_lab import TraceSidecar
112
+
113
+ trace = TraceSidecar.from_frame(
114
+ configs,
115
+ condition="condition_id",
116
+ prompt_hash="prompt_sha",
117
+ temperature="temperature",
118
+ candidate_order="order", # "A,B" / "B,A"
119
+ )
120
+ study = ReliabilityStudy(
121
+ ratings, # RatingFrame.from_long(..., attempt="attempt")
122
+ measurement="nominal",
123
+ values=["A", "tie", "B"], # labels name the winning candidate, not its position
124
+ coders="condition_id",
125
+ design="order_robustness",
126
+ trace=trace,
127
+ retries="last", # required when calls were retried
128
+ )
129
+ result = study.alpha()
130
+ result.diagnostics["order_swap"].values # reversal rate, P(first shown wins) with a Wilson interval
131
+ result.trace_findings # confounds and caveats from the trace
132
+ ```
133
+
134
+ | Design | Coders are | Evidence beside alpha |
135
+ | --- | --- | --- |
136
+ | `repeatability` | repeated runs of one configuration (`run_id`) | run agreement: units where every run agrees |
137
+ | `prompt_robustness` | prompt conditions of one model | pairwise agreement between conditions |
138
+ | `inter_model` | different models | pairwise agreement; family and configuration checks |
139
+ | `order_robustness` | candidate orders of one pairwise judge | reversal rate and position bias |
140
+ | configuration change | two configurations, via `study.compare(a=..., b=...)` | paired interval on the difference; the model and trace fields that changed |
141
+
142
+ The single-configuration designs (repeatability, prompt robustness, order robustness) can hold several configurations to compare, such as an old and a new judge. Each configuration is checked on its own. `alpha()` refuses to pool them; `study.compare(a={"condition_id": "old"}, b={"condition_id": "new"})` reports each one's alpha and diagnostics, with a paired interval on the difference.
143
+
144
+ Retried calls are never resolved silently. Number the attempts with an `attempt` column, then declare `retries="last"` or `retries="first_observed"`. Coverage then reports error rates both per attempt and for the attempts that count. See [`examples/llm_judge.py`](examples/llm_judge.py).
145
+
146
+ ## Licence
147
+
148
+ BSD-3-Clause. Alpha is implemented independently from Krippendorff's papers. The GPL-licensed `krippendorff` package is used only in the test suite, as a parity check.