reliability-lab 0.1.2__tar.gz → 0.1.3__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.
- {reliability_lab-0.1.2 → reliability_lab-0.1.3}/CHANGELOG.md +13 -0
- {reliability_lab-0.1.2 → reliability_lab-0.1.3}/PKG-INFO +7 -6
- {reliability_lab-0.1.2 → reliability_lab-0.1.3}/README.md +5 -5
- {reliability_lab-0.1.2 → reliability_lab-0.1.3}/pyproject.toml +1 -0
- reliability_lab-0.1.3/src/reliability_lab/_version.py +1 -0
- {reliability_lab-0.1.2 → reliability_lab-0.1.3}/src/reliability_lab/inference.py +117 -97
- {reliability_lab-0.1.2 → reliability_lab-0.1.3}/tests/test_bca.py +83 -20
- {reliability_lab-0.1.2 → reliability_lab-0.1.3}/tests/test_compare.py +51 -0
- {reliability_lab-0.1.2 → reliability_lab-0.1.3}/tests/test_inference_units.py +18 -0
- reliability_lab-0.1.2/src/reliability_lab/_version.py +0 -1
- {reliability_lab-0.1.2 → reliability_lab-0.1.3}/.gitattributes +0 -0
- {reliability_lab-0.1.2 → reliability_lab-0.1.3}/.github/workflows/ci.yml +0 -0
- {reliability_lab-0.1.2 → reliability_lab-0.1.3}/.github/workflows/release.yml +0 -0
- {reliability_lab-0.1.2 → reliability_lab-0.1.3}/.gitignore +0 -0
- {reliability_lab-0.1.2 → reliability_lab-0.1.3}/.python-version +0 -0
- {reliability_lab-0.1.2 → reliability_lab-0.1.3}/LICENSE +0 -0
- {reliability_lab-0.1.2 → reliability_lab-0.1.3}/docs/design/framing-v2.md +0 -0
- {reliability_lab-0.1.2 → reliability_lab-0.1.3}/docs/guidance/bootstrap-coverage.md +0 -0
- {reliability_lab-0.1.2 → reliability_lab-0.1.3}/docs/guidance/figures/coverage_by_factor-dark.png +0 -0
- {reliability_lab-0.1.2 → reliability_lab-0.1.3}/docs/guidance/figures/coverage_by_factor-light.png +0 -0
- {reliability_lab-0.1.2 → reliability_lab-0.1.3}/docs/guidance/figures/coverage_by_n-dark.png +0 -0
- {reliability_lab-0.1.2 → reliability_lab-0.1.3}/docs/guidance/figures/coverage_by_n-light.png +0 -0
- {reliability_lab-0.1.2 → reliability_lab-0.1.3}/docs/guidance/figures/coverage_by_spread-dark.png +0 -0
- {reliability_lab-0.1.2 → reliability_lab-0.1.3}/docs/guidance/figures/coverage_by_spread-light.png +0 -0
- {reliability_lab-0.1.2 → reliability_lab-0.1.3}/docs/releasing.md +0 -0
- {reliability_lab-0.1.2 → reliability_lab-0.1.3}/examples/llm_judge.py +0 -0
- {reliability_lab-0.1.2 → reliability_lab-0.1.3}/examples/quickstart.py +0 -0
- {reliability_lab-0.1.2 → reliability_lab-0.1.3}/examples/splits.py +0 -0
- {reliability_lab-0.1.2 → reliability_lab-0.1.3}/simulations/__init__.py +0 -0
- {reliability_lab-0.1.2 → reliability_lab-0.1.3}/simulations/coders.py +0 -0
- {reliability_lab-0.1.2 → reliability_lab-0.1.3}/simulations/coverage.py +0 -0
- {reliability_lab-0.1.2 → reliability_lab-0.1.3}/simulations/grid.py +0 -0
- {reliability_lab-0.1.2 → reliability_lab-0.1.3}/simulations/report.py +0 -0
- {reliability_lab-0.1.2 → reliability_lab-0.1.3}/simulations/results/coders/coverage.csv +0 -0
- {reliability_lab-0.1.2 → reliability_lab-0.1.3}/simulations/results/coders/run_info.json +0 -0
- {reliability_lab-0.1.2 → reliability_lab-0.1.3}/simulations/results/coverage.csv +0 -0
- {reliability_lab-0.1.2 → reliability_lab-0.1.3}/simulations/results/run_info.json +0 -0
- {reliability_lab-0.1.2 → reliability_lab-0.1.3}/simulations/run_coverage.py +0 -0
- {reliability_lab-0.1.2 → reliability_lab-0.1.3}/simulations/templates/bootstrap-coverage.md +0 -0
- {reliability_lab-0.1.2 → reliability_lab-0.1.3}/src/reliability_lab/__init__.py +0 -0
- {reliability_lab-0.1.2 → reliability_lab-0.1.3}/src/reliability_lab/alpha.py +0 -0
- {reliability_lab-0.1.2 → reliability_lab-0.1.3}/src/reliability_lab/data.py +0 -0
- {reliability_lab-0.1.2 → reliability_lab-0.1.3}/src/reliability_lab/design.py +0 -0
- {reliability_lab-0.1.2 → reliability_lab-0.1.3}/src/reliability_lab/exceptions.py +0 -0
- {reliability_lab-0.1.2 → reliability_lab-0.1.3}/src/reliability_lab/llm.py +0 -0
- {reliability_lab-0.1.2 → reliability_lab-0.1.3}/src/reliability_lab/py.typed +0 -0
- {reliability_lab-0.1.2 → reliability_lab-0.1.3}/src/reliability_lab/report.py +0 -0
- {reliability_lab-0.1.2 → reliability_lab-0.1.3}/src/reliability_lab/split.py +0 -0
- {reliability_lab-0.1.2 → reliability_lab-0.1.3}/src/reliability_lab/study.py +0 -0
- {reliability_lab-0.1.2 → reliability_lab-0.1.3}/tests/fixtures/__init__.py +0 -0
- {reliability_lab-0.1.2 → reliability_lab-0.1.3}/tests/fixtures/krippendorff2011.py +0 -0
- {reliability_lab-0.1.2 → reliability_lab-0.1.3}/tests/fixtures/llm_logs.py +0 -0
- {reliability_lab-0.1.2 → reliability_lab-0.1.3}/tests/helpers.py +0 -0
- {reliability_lab-0.1.2 → reliability_lab-0.1.3}/tests/naive_bootstrap.py +0 -0
- {reliability_lab-0.1.2 → reliability_lab-0.1.3}/tests/reference.py +0 -0
- {reliability_lab-0.1.2 → reliability_lab-0.1.3}/tests/test_alpha.py +0 -0
- {reliability_lab-0.1.2 → reliability_lab-0.1.3}/tests/test_coder_jackknife.py +0 -0
- {reliability_lab-0.1.2 → reliability_lab-0.1.3}/tests/test_coder_resampling.py +0 -0
- {reliability_lab-0.1.2 → reliability_lab-0.1.3}/tests/test_data.py +0 -0
- {reliability_lab-0.1.2 → reliability_lab-0.1.3}/tests/test_design.py +0 -0
- {reliability_lab-0.1.2 → reliability_lab-0.1.3}/tests/test_kalpha.py +0 -0
- {reliability_lab-0.1.2 → reliability_lab-0.1.3}/tests/test_llm_end_to_end.py +0 -0
- {reliability_lab-0.1.2 → reliability_lab-0.1.3}/tests/test_parity.py +0 -0
- {reliability_lab-0.1.2 → reliability_lab-0.1.3}/tests/test_properties.py +0 -0
- {reliability_lab-0.1.2 → reliability_lab-0.1.3}/tests/test_report.py +0 -0
- {reliability_lab-0.1.2 → reliability_lab-0.1.3}/tests/test_report_io.py +0 -0
- {reliability_lab-0.1.2 → reliability_lab-0.1.3}/tests/test_retries.py +0 -0
- {reliability_lab-0.1.2 → reliability_lab-0.1.3}/tests/test_simulations.py +0 -0
- {reliability_lab-0.1.2 → reliability_lab-0.1.3}/tests/test_smoke.py +0 -0
- {reliability_lab-0.1.2 → reliability_lab-0.1.3}/tests/test_split.py +0 -0
- {reliability_lab-0.1.2 → reliability_lab-0.1.3}/tests/test_status_policies.py +0 -0
- {reliability_lab-0.1.2 → reliability_lab-0.1.3}/tests/test_trace.py +0 -0
- {reliability_lab-0.1.2 → reliability_lab-0.1.3}/uv.lock +0 -0
|
@@ -2,6 +2,19 @@
|
|
|
2
2
|
|
|
3
3
|
## Unreleased
|
|
4
4
|
|
|
5
|
+
## 0.1.3
|
|
6
|
+
|
|
7
|
+
Faster bootstrap.
|
|
8
|
+
|
|
9
|
+
- Every scheme is faster. With 2,000 resamples of 5,000 units and 5 coders (ordinal, 3 estimates):
|
|
10
|
+
- units, percentile: 1.4 s → 0.5 s;
|
|
11
|
+
- units, BCa: 6.4 s → 0.5 s, and `compare(method="bca")`: 18 s → 0.5 s;
|
|
12
|
+
- kalpha: 5.1 s → 2.8 s;
|
|
13
|
+
- coders: 8.2 s → 1.4 s, and two_way: 10 s → 2.1 s.
|
|
14
|
+
- BCa intervals no longer go through `scipy.stats.bootstrap`. The jackknife leaves each cluster out in closed form, instead of recomputing alpha from k × k resample weights, so BCa now runs on large studies: 20,000 units take 3.7 s, where they previously ran out of memory. The resamples and the interval are still the ones `scipy.stats.bootstrap` gives, and the tests check this.
|
|
15
|
+
- The random draws are unchanged: a given `random_state` gives the same intervals as in 0.1.2, up to floating-point rounding.
|
|
16
|
+
- The README links to the docs and examples on GitHub, so the links work on PyPI. The project links on PyPI include the changelog.
|
|
17
|
+
|
|
5
18
|
## 0.1.2
|
|
6
19
|
|
|
7
20
|
Generalising over coders.
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: reliability-lab
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.3
|
|
4
4
|
Summary: Study-design library for the reliability of LLM and human judgments, built on Krippendorff's alpha.
|
|
5
5
|
Project-URL: Repository, https://github.com/Sathapana/reliability-lab
|
|
6
6
|
Project-URL: Issues, https://github.com/Sathapana/reliability-lab/issues
|
|
7
|
+
Project-URL: Changelog, https://github.com/Sathapana/reliability-lab/blob/main/CHANGELOG.md
|
|
7
8
|
Author: Sathapana Chawananon
|
|
8
9
|
License-Expression: BSD-3-Clause
|
|
9
10
|
License-File: LICENSE
|
|
@@ -30,7 +31,7 @@ Description-Content-Type: text/markdown
|
|
|
30
31
|
|
|
31
32
|
# reliability-lab
|
|
32
33
|
|
|
33
|
-
**Status: alpha (0.1.
|
|
34
|
+
**Status: alpha (0.1.3).** The API may still change between releases.
|
|
34
35
|
|
|
35
36
|
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
|
|
|
@@ -45,7 +46,7 @@ reliability-lab is a study-design library for measuring the reliability of LLM a
|
|
|
45
46
|
- versioned JSON and Parquet reports with provenance;
|
|
46
47
|
- 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).
|
|
47
48
|
|
|
48
|
-
Mixed human-LLM panels are next on the roadmap ([design document](docs/design/framing-v2.md)).
|
|
49
|
+
Mixed human-LLM panels are next on the roadmap ([design document](https://github.com/Sathapana/reliability-lab/blob/main/docs/design/framing-v2.md)).
|
|
49
50
|
|
|
50
51
|
> **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.
|
|
51
52
|
|
|
@@ -111,7 +112,7 @@ print(jk.summary()) # interval, standard error, and the most influential coders
|
|
|
111
112
|
|
|
112
113
|
When coders are resampled, the copies of one coder are never paired with each other, since a coder always agrees with itself. These schemes need at least 3 coders and give percentile intervals.
|
|
113
114
|
|
|
114
|
-
Which scheme to trust, and when, is measured by simulation in the [bootstrap coverage guidance](docs/guidance/bootstrap-coverage.md).
|
|
115
|
+
Which scheme to trust, and when, is measured by simulation in the [bootstrap coverage guidance](https://github.com/Sathapana/reliability-lab/blob/main/docs/guidance/bootstrap-coverage.md).
|
|
115
116
|
|
|
116
117
|
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:
|
|
117
118
|
|
|
@@ -150,7 +151,7 @@ dev2, test2 = later_study.apply_manifest("split.json") # same units, new judge
|
|
|
150
151
|
test.manifest.assert_no_leakage(few_shot_units) # raises LeakageError
|
|
151
152
|
```
|
|
152
153
|
|
|
153
|
-
See [`examples/splits.py`](examples/splits.py) for a full run. Releases are described in [docs/releasing.md](docs/releasing.md).
|
|
154
|
+
See [`examples/splits.py`](https://github.com/Sathapana/reliability-lab/blob/main/examples/splits.py) for a full run. Releases are described in [docs/releasing.md](https://github.com/Sathapana/reliability-lab/blob/main/docs/releasing.md).
|
|
154
155
|
|
|
155
156
|
## LLM judge workflows
|
|
156
157
|
|
|
@@ -190,7 +191,7 @@ result.trace_findings # confounds and caveats from the trace
|
|
|
190
191
|
|
|
191
192
|
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.
|
|
192
193
|
|
|
193
|
-
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).
|
|
194
|
+
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`](https://github.com/Sathapana/reliability-lab/blob/main/examples/llm_judge.py).
|
|
194
195
|
|
|
195
196
|
## Licence
|
|
196
197
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# reliability-lab
|
|
2
2
|
|
|
3
|
-
**Status: alpha (0.1.
|
|
3
|
+
**Status: alpha (0.1.3).** The API may still change between releases.
|
|
4
4
|
|
|
5
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
6
|
|
|
@@ -15,7 +15,7 @@ reliability-lab is a study-design library for measuring the reliability of LLM a
|
|
|
15
15
|
- versioned JSON and Parquet reports with provenance;
|
|
16
16
|
- 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).
|
|
17
17
|
|
|
18
|
-
Mixed human-LLM panels are next on the roadmap ([design document](docs/design/framing-v2.md)).
|
|
18
|
+
Mixed human-LLM panels are next on the roadmap ([design document](https://github.com/Sathapana/reliability-lab/blob/main/docs/design/framing-v2.md)).
|
|
19
19
|
|
|
20
20
|
> **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.
|
|
21
21
|
|
|
@@ -81,7 +81,7 @@ print(jk.summary()) # interval, standard error, and the most influential coders
|
|
|
81
81
|
|
|
82
82
|
When coders are resampled, the copies of one coder are never paired with each other, since a coder always agrees with itself. These schemes need at least 3 coders and give percentile intervals.
|
|
83
83
|
|
|
84
|
-
Which scheme to trust, and when, is measured by simulation in the [bootstrap coverage guidance](docs/guidance/bootstrap-coverage.md).
|
|
84
|
+
Which scheme to trust, and when, is measured by simulation in the [bootstrap coverage guidance](https://github.com/Sathapana/reliability-lab/blob/main/docs/guidance/bootstrap-coverage.md).
|
|
85
85
|
|
|
86
86
|
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:
|
|
87
87
|
|
|
@@ -120,7 +120,7 @@ dev2, test2 = later_study.apply_manifest("split.json") # same units, new judge
|
|
|
120
120
|
test.manifest.assert_no_leakage(few_shot_units) # raises LeakageError
|
|
121
121
|
```
|
|
122
122
|
|
|
123
|
-
See [`examples/splits.py`](examples/splits.py) for a full run. Releases are described in [docs/releasing.md](docs/releasing.md).
|
|
123
|
+
See [`examples/splits.py`](https://github.com/Sathapana/reliability-lab/blob/main/examples/splits.py) for a full run. Releases are described in [docs/releasing.md](https://github.com/Sathapana/reliability-lab/blob/main/docs/releasing.md).
|
|
124
124
|
|
|
125
125
|
## LLM judge workflows
|
|
126
126
|
|
|
@@ -160,7 +160,7 @@ result.trace_findings # confounds and caveats from the trace
|
|
|
160
160
|
|
|
161
161
|
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.
|
|
162
162
|
|
|
163
|
-
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).
|
|
163
|
+
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`](https://github.com/Sathapana/reliability-lab/blob/main/examples/llm_judge.py).
|
|
164
164
|
|
|
165
165
|
## Licence
|
|
166
166
|
|
|
@@ -44,6 +44,7 @@ parquet = ["pyarrow>=15"]
|
|
|
44
44
|
[project.urls]
|
|
45
45
|
Repository = "https://github.com/Sathapana/reliability-lab"
|
|
46
46
|
Issues = "https://github.com/Sathapana/reliability-lab/issues"
|
|
47
|
+
Changelog = "https://github.com/Sathapana/reliability-lab/blob/main/CHANGELOG.md"
|
|
47
48
|
|
|
48
49
|
[dependency-groups]
|
|
49
50
|
dev = [
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "0.1.3"
|
|
@@ -23,15 +23,14 @@ configurations, so the interval falls on the difference alpha_b - alpha_a.
|
|
|
23
23
|
|
|
24
24
|
from __future__ import annotations
|
|
25
25
|
|
|
26
|
-
import
|
|
27
|
-
from collections.abc import Callable, Mapping
|
|
26
|
+
from collections.abc import Mapping
|
|
28
27
|
from dataclasses import dataclass
|
|
29
28
|
from typing import Any
|
|
30
29
|
|
|
31
30
|
import numpy as np
|
|
32
31
|
import numpy.typing as npt
|
|
33
32
|
import pandas as pd
|
|
34
|
-
from scipy import sparse, stats
|
|
33
|
+
from scipy import sparse, special, stats
|
|
35
34
|
|
|
36
35
|
from reliability_lab.alpha import ORDINAL, Distance, compute_alpha, per_unit_coincidences
|
|
37
36
|
from reliability_lab.data import preview
|
|
@@ -218,17 +217,41 @@ def replicate_alphas(
|
|
|
218
217
|
distance: Distance,
|
|
219
218
|
) -> FloatArray:
|
|
220
219
|
"""Alpha for each row of cluster weights; NaN where alpha is undefined."""
|
|
221
|
-
w = np.atleast_2d(np.asarray(weights
|
|
220
|
+
w = np.atleast_2d(np.asarray(weights))
|
|
222
221
|
n_values = len(values)
|
|
222
|
+
n_flat = n_values * n_values
|
|
223
|
+
# A dense product is one BLAS call; large value domains stay sparse.
|
|
224
|
+
dense = clusters.shape[0] * n_flat <= BATCH_FLOATS
|
|
225
|
+
contributions = clusters.toarray() if dense else sparse.csr_array(clusters.T)
|
|
223
226
|
out = np.empty(len(w))
|
|
224
|
-
batch = max(1, BATCH_FLOATS // max(
|
|
227
|
+
batch = max(1, BATCH_FLOATS // max(n_flat, clusters.shape[0], 1))
|
|
225
228
|
for start in range(0, len(w), batch):
|
|
226
|
-
wb = w[start : start + batch]
|
|
227
|
-
|
|
229
|
+
wb = np.asarray(w[start : start + batch], dtype=float)
|
|
230
|
+
if dense:
|
|
231
|
+
flat = wb @ contributions
|
|
232
|
+
else:
|
|
233
|
+
flat = np.asarray(contributions @ np.asfortranarray(wb.T)).T
|
|
234
|
+
o = flat.reshape(len(wb), n_values, n_values)
|
|
228
235
|
out[start : start + len(wb)] = alphas_from_coincidences(o, values, distance)
|
|
229
236
|
return out
|
|
230
237
|
|
|
231
238
|
|
|
239
|
+
def jackknife_alphas(
|
|
240
|
+
clusters: sparse.csr_array, values: tuple[Any, ...], distance: Distance
|
|
241
|
+
) -> FloatArray:
|
|
242
|
+
"""Alpha without each cluster in turn: the coincidences less that cluster's."""
|
|
243
|
+
n_values = len(values)
|
|
244
|
+
n_flat = n_values * n_values
|
|
245
|
+
total = np.asarray(clusters.sum(axis=0), dtype=float).ravel()
|
|
246
|
+
out = np.empty(clusters.shape[0])
|
|
247
|
+
batch = max(1, BATCH_FLOATS // max(n_flat, 1))
|
|
248
|
+
for start in range(0, clusters.shape[0], batch):
|
|
249
|
+
part = clusters[start : start + batch].toarray()
|
|
250
|
+
o = (total - part).reshape(len(part), n_values, n_values)
|
|
251
|
+
out[start : start + len(part)] = alphas_from_coincidences(o, values, distance)
|
|
252
|
+
return out
|
|
253
|
+
|
|
254
|
+
|
|
232
255
|
def alphas_from_coincidences(
|
|
233
256
|
o: FloatArray, values: tuple[Any, ...], distance: Distance
|
|
234
257
|
) -> FloatArray:
|
|
@@ -285,25 +308,26 @@ def two_way_alphas(
|
|
|
285
308
|
``(n_u n_u' - diag n_u) / (m_u - 1)``.
|
|
286
309
|
"""
|
|
287
310
|
v = np.atleast_2d(np.asarray(coder_weights, dtype=float))
|
|
288
|
-
n_units,
|
|
289
|
-
w = (
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
)
|
|
311
|
+
n_units, n_coders, n_values = onehot.shape
|
|
312
|
+
w = None if unit_weights is None else np.atleast_2d(np.asarray(unit_weights))
|
|
313
|
+
# Every sum over coders is a matrix product with the coder weights.
|
|
314
|
+
by_coder = onehot.transpose(1, 0, 2).reshape(n_coders, n_units * n_values)
|
|
315
|
+
rated = onehot.sum(axis=2).T
|
|
294
316
|
out = np.empty(len(v))
|
|
295
317
|
per_replicate = max(n_units * n_values * 2, n_values * n_values, 1)
|
|
296
318
|
batch = max(1, BATCH_FLOATS // per_replicate)
|
|
297
319
|
for start in range(0, len(v), batch):
|
|
298
|
-
vb
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
320
|
+
vb = v[start : start + batch]
|
|
321
|
+
v2 = vb * vb
|
|
322
|
+
wb = 1.0 if w is None else np.asarray(w[start : start + batch], dtype=float)
|
|
323
|
+
counts = (vb @ by_coder).reshape(len(vb), n_units, n_values)
|
|
324
|
+
squares = (v2 @ by_coder).reshape(len(vb), n_units, n_values)
|
|
325
|
+
m = vb @ rated
|
|
326
|
+
pairs = m * m - v2 @ rated
|
|
303
327
|
with np.errstate(divide="ignore", invalid="ignore"):
|
|
304
328
|
scale = np.where(pairs > 0, wb * m / pairs, 0.0)
|
|
305
|
-
o =
|
|
306
|
-
diagonal =
|
|
329
|
+
o = (counts * scale[:, :, None]).transpose(0, 2, 1) @ counts
|
|
330
|
+
diagonal = (scale[:, None, :] @ squares)[:, 0, :]
|
|
307
331
|
o[:, np.arange(n_values), np.arange(n_values)] -= diagonal
|
|
308
332
|
out[start : start + len(vb)] = alphas_from_coincidences(o, values, distance)
|
|
309
333
|
return out
|
|
@@ -389,12 +413,14 @@ def kalpha_replicates(pool: KalphaPool, draws: IntArray, redraws: IntArray) -> F
|
|
|
389
413
|
``redraws`` (replicates x len(second_pick)) replaces a unit's second draw when
|
|
390
414
|
it repeats the first.
|
|
391
415
|
"""
|
|
392
|
-
d = np.
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
416
|
+
d = np.asarray(draws, dtype=np.intp)
|
|
417
|
+
sp = pool.second_pick
|
|
418
|
+
alpha = 1.0 - np.take(pool.errors, d) @ pool.weights
|
|
419
|
+
if len(sp):
|
|
420
|
+
# Repeats are rare, so their redraws correct the sums afterwards.
|
|
421
|
+
rows, cols = np.nonzero(d[:, sp - 1] == d[:, sp])
|
|
422
|
+
redrawn = pool.errors[np.asarray(redraws)[rows, cols]] - pool.errors[d[rows, sp[cols]]]
|
|
423
|
+
np.subtract.at(alpha, rows, redrawn * pool.weights[sp[cols]])
|
|
398
424
|
return np.asarray(np.maximum(alpha, -1.0), dtype=float)
|
|
399
425
|
|
|
400
426
|
|
|
@@ -452,37 +478,34 @@ def percentile_interval(
|
|
|
452
478
|
def bca_interval(
|
|
453
479
|
name: str,
|
|
454
480
|
point: float,
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
481
|
+
distribution: FloatArray,
|
|
482
|
+
theta_hat: float,
|
|
483
|
+
jackknife: FloatArray,
|
|
458
484
|
confidence: float,
|
|
459
|
-
seed: int,
|
|
460
485
|
alpha_min: float | None = None,
|
|
461
486
|
) -> Interval:
|
|
462
|
-
"""BCa interval
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
# BootstrapWarning, which names the estimate and the fallback.
|
|
469
|
-
with np.errstate(divide="ignore", invalid="ignore"), warnings.catch_warnings():
|
|
470
|
-
warnings.simplefilter("ignore", stats.DegenerateDataWarning)
|
|
471
|
-
res = stats.bootstrap(
|
|
472
|
-
(np.arange(n_clusters),),
|
|
473
|
-
vectorised,
|
|
474
|
-
n_resamples=n_resamples,
|
|
475
|
-
confidence_level=confidence,
|
|
476
|
-
method="BCa",
|
|
477
|
-
vectorized=True,
|
|
478
|
-
rng=np.random.default_rng(seed),
|
|
479
|
-
)
|
|
480
|
-
distribution = np.asarray(res.bootstrap_distribution, dtype=float)
|
|
487
|
+
"""BCa interval (Efron and Tibshirani 1993, 14.3 and 15.4), as ``scipy.stats.bootstrap`` does.
|
|
488
|
+
|
|
489
|
+
``theta_hat`` is the statistic on the full sample, computed like the replicates,
|
|
490
|
+
and ``jackknife`` the statistic without each cluster in turn.
|
|
491
|
+
"""
|
|
492
|
+
n_resamples = len(distribution)
|
|
481
493
|
failed = np.isnan(distribution)
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
494
|
+
z_alpha = float(special.ndtri((1.0 - confidence) / 2.0))
|
|
495
|
+
with np.errstate(divide="ignore", invalid="ignore"):
|
|
496
|
+
below_hat = (distribution < theta_hat).sum() + (distribution <= theta_hat).sum()
|
|
497
|
+
z0 = special.ndtri(below_hat / (2 * n_resamples))
|
|
498
|
+
u = jackknife.mean() - jackknife
|
|
499
|
+
a_hat = (u**3).sum() / (6.0 * (u**2).sum() ** 1.5)
|
|
500
|
+
num = z0 + np.array([z_alpha, -z_alpha])
|
|
501
|
+
levels = special.ndtr(z0 + num / (1.0 - a_hat * num))
|
|
502
|
+
low = high = np.nan
|
|
503
|
+
reason: str | None = (
|
|
504
|
+
"the BCa interval is undefined (undefined replicates or a degenerate jackknife)"
|
|
505
|
+
)
|
|
506
|
+
if not failed.any() and np.isfinite(levels).all():
|
|
507
|
+
low, high = (float(q) for q in np.quantile(distribution, levels))
|
|
508
|
+
reason = None
|
|
486
509
|
defined = distribution[~failed]
|
|
487
510
|
below = float((defined < alpha_min).mean()) if alpha_min is not None and len(defined) else None
|
|
488
511
|
return Interval(
|
|
@@ -490,6 +513,30 @@ def bca_interval(
|
|
|
490
513
|
)
|
|
491
514
|
|
|
492
515
|
|
|
516
|
+
def bca_weights(seed: int, n_clusters: int, n_resamples: int) -> npt.NDArray[np.int64]:
|
|
517
|
+
"""Cluster weights of the BCa resamples (n x clusters).
|
|
518
|
+
|
|
519
|
+
The indices are the ones ``scipy.stats.bootstrap`` draws from the same seed,
|
|
520
|
+
drawn in batches (which leaves the stream unchanged) and counted.
|
|
521
|
+
"""
|
|
522
|
+
rng = np.random.default_rng(seed)
|
|
523
|
+
batch = max(1, BATCH_FLOATS // max(n_clusters, 1))
|
|
524
|
+
parts = []
|
|
525
|
+
for start in range(0, n_resamples, batch):
|
|
526
|
+
size = min(batch, n_resamples - start)
|
|
527
|
+
idx = rng.integers(0, n_clusters, (size, n_clusters))
|
|
528
|
+
parts.append(weights_from_indices(idx, n_clusters))
|
|
529
|
+
return np.concatenate(parts)
|
|
530
|
+
|
|
531
|
+
|
|
532
|
+
def bca_parts(
|
|
533
|
+
clusters: sparse.csr_array, values: tuple[Any, ...], distance: Distance
|
|
534
|
+
) -> tuple[float, FloatArray]:
|
|
535
|
+
"""The full-sample statistic and the leave-one-cluster-out jackknife for BCa."""
|
|
536
|
+
theta_hat = replicate_alphas(clusters, np.ones(clusters.shape[0]), values, distance)[0]
|
|
537
|
+
return float(theta_hat), jackknife_alphas(clusters, values, distance)
|
|
538
|
+
|
|
539
|
+
|
|
493
540
|
def weights_from_indices(idx: IntArray, n_clusters: int) -> npt.NDArray[np.int64]:
|
|
494
541
|
"""Count cluster indices along the last axis: (..., k) -> (prod(...), n_clusters)."""
|
|
495
542
|
flat = np.asarray(idx, dtype=np.intp).reshape(-1, np.shape(idx)[-1])
|
|
@@ -498,17 +545,6 @@ def weights_from_indices(idx: IntArray, n_clusters: int) -> npt.NDArray[np.int64
|
|
|
498
545
|
return counts.reshape(len(flat), n_clusters)
|
|
499
546
|
|
|
500
547
|
|
|
501
|
-
def _alpha_statistic(
|
|
502
|
-
clusters: sparse.csr_array, n_clusters: int, values: tuple[Any, ...], distance: Distance
|
|
503
|
-
) -> Callable[[IntArray], FloatArray]:
|
|
504
|
-
def statistic(idx: IntArray) -> FloatArray:
|
|
505
|
-
lead = np.shape(idx)[:-1]
|
|
506
|
-
w = weights_from_indices(idx, n_clusters)
|
|
507
|
-
return replicate_alphas(clusters, w, values, distance).reshape(lead)
|
|
508
|
-
|
|
509
|
-
return statistic
|
|
510
|
-
|
|
511
|
-
|
|
512
548
|
# -- entry points ---------------------------------------------------------------------
|
|
513
549
|
|
|
514
550
|
|
|
@@ -624,35 +660,27 @@ def bootstrap(
|
|
|
624
660
|
recorded.append(emit(BootstrapWarning, level_warning, stacklevel=3))
|
|
625
661
|
if method == "bca" and strata:
|
|
626
662
|
raise DesignError(
|
|
627
|
-
"BCa intervals cannot resample within strata
|
|
628
|
-
"strata); use method='percentile'"
|
|
663
|
+
"BCa intervals cannot resample within strata; use method='percentile'"
|
|
629
664
|
)
|
|
630
665
|
plan = cluster_plan(sample, level_used, strata)
|
|
631
666
|
n_clusters = plan.n_clusters
|
|
632
667
|
weights = (
|
|
633
668
|
draw_weights(np.random.default_rng(seed), plan, n_resamples)
|
|
634
669
|
if method == "percentile"
|
|
635
|
-
else
|
|
670
|
+
else bca_weights(seed, n_clusters, n_resamples)
|
|
636
671
|
)
|
|
637
672
|
for name in names:
|
|
638
673
|
values = sample.inputs[name].values
|
|
639
674
|
clusters = cluster_coincidences(sample, name, plan)
|
|
640
|
-
|
|
641
|
-
|
|
675
|
+
distribution = replicate_alphas(clusters, weights, values, sample.distance)
|
|
676
|
+
if method == "percentile":
|
|
642
677
|
intervals[name] = percentile_interval(
|
|
643
678
|
name, points[name], distribution, confidence, alpha_min
|
|
644
679
|
)
|
|
645
680
|
else:
|
|
646
|
-
|
|
681
|
+
theta_hat, jackknife = bca_parts(clusters, values, sample.distance)
|
|
647
682
|
intervals[name] = bca_interval(
|
|
648
|
-
name,
|
|
649
|
-
points[name],
|
|
650
|
-
statistic,
|
|
651
|
-
n_clusters,
|
|
652
|
-
n_resamples,
|
|
653
|
-
confidence,
|
|
654
|
-
seed,
|
|
655
|
-
alpha_min,
|
|
683
|
+
name, points[name], distribution, theta_hat, jackknife, confidence, alpha_min
|
|
656
684
|
)
|
|
657
685
|
notes.append(UNITS_NOTE.format(level=level_used, within=" within strata" if strata else ""))
|
|
658
686
|
if len(names) > 1:
|
|
@@ -795,7 +823,7 @@ def compare(
|
|
|
795
823
|
weights = (
|
|
796
824
|
draw_weights(np.random.default_rng(seed), plan_a, n_resamples)
|
|
797
825
|
if method == "percentile"
|
|
798
|
-
else
|
|
826
|
+
else bca_weights(seed, plan_a.n_clusters, n_resamples)
|
|
799
827
|
)
|
|
800
828
|
|
|
801
829
|
arm_intervals: dict[str, dict[str, Interval]] = {"a": {}, "b": {}}
|
|
@@ -806,30 +834,22 @@ def compare(
|
|
|
806
834
|
clusters_a = cluster_coincidences(sample_a, name, plan_a)
|
|
807
835
|
clusters_b = cluster_coincidences(sample_b, name, plan_b)
|
|
808
836
|
values_a, values_b = sample_a.inputs[name].values, sample_b.inputs[name].values
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
837
|
+
dist_a = replicate_alphas(clusters_a, weights, values_a, sample_a.distance)
|
|
838
|
+
dist_b = replicate_alphas(clusters_b, weights, values_b, sample_b.distance)
|
|
839
|
+
if method == "percentile":
|
|
812
840
|
arm_intervals["a"][name] = percentile_interval(name, point_a, dist_a, confidence)
|
|
813
841
|
arm_intervals["b"][name] = percentile_interval(name, point_b, dist_b, confidence)
|
|
814
842
|
differences[name] = percentile_interval(
|
|
815
843
|
name, point_b - point_a, dist_b - dist_a, confidence
|
|
816
844
|
)
|
|
817
845
|
continue
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
stat_b: Callable[[IntArray], FloatArray] = stat_b,
|
|
826
|
-
) -> FloatArray:
|
|
827
|
-
return stat_b(idx) - stat_a(idx)
|
|
828
|
-
|
|
829
|
-
args = (k, n_resamples, confidence, seed)
|
|
830
|
-
arm_intervals["a"][name] = bca_interval(name, point_a, stat_a, *args)
|
|
831
|
-
arm_intervals["b"][name] = bca_interval(name, point_b, stat_b, *args)
|
|
832
|
-
differences[name] = bca_interval(name, point_b - point_a, stat_diff, *args)
|
|
846
|
+
theta_a, jack_a = bca_parts(clusters_a, values_a, sample_a.distance)
|
|
847
|
+
theta_b, jack_b = bca_parts(clusters_b, values_b, sample_b.distance)
|
|
848
|
+
arm_intervals["a"][name] = bca_interval(name, point_a, dist_a, theta_a, jack_a, confidence)
|
|
849
|
+
arm_intervals["b"][name] = bca_interval(name, point_b, dist_b, theta_b, jack_b, confidence)
|
|
850
|
+
differences[name] = bca_interval(
|
|
851
|
+
name, point_b - point_a, dist_b - dist_a, theta_b - theta_a, jack_b - jack_a, confidence
|
|
852
|
+
)
|
|
833
853
|
|
|
834
854
|
recorded.extend(_interval_warnings(differences, n_resamples))
|
|
835
855
|
within = " within strata" if strata else ""
|
|
@@ -6,10 +6,11 @@ from fixtures.krippendorff2011 import FOUR_CODERS_MISSING
|
|
|
6
6
|
from helpers import human_study
|
|
7
7
|
from scipy import stats
|
|
8
8
|
|
|
9
|
-
from reliability_lab import BootstrapWarning, DesignError
|
|
9
|
+
from reliability_lab import AlphaResult, BootstrapWarning, DesignError
|
|
10
10
|
from reliability_lab.inference import (
|
|
11
11
|
cluster_coincidences,
|
|
12
12
|
cluster_plan,
|
|
13
|
+
jackknife_alphas,
|
|
13
14
|
replicate_alphas,
|
|
14
15
|
weights_from_indices,
|
|
15
16
|
)
|
|
@@ -17,6 +18,35 @@ from reliability_lab.inference import (
|
|
|
17
18
|
F4 = FOUR_CODERS_MISSING
|
|
18
19
|
|
|
19
20
|
|
|
21
|
+
def direct_scipy_bca(
|
|
22
|
+
result: AlphaResult, name: str, n_resamples: int, seed: int
|
|
23
|
+
) -> tuple[float, float]:
|
|
24
|
+
"""The BCa interval of a direct ``scipy.stats.bootstrap`` call over cluster indices."""
|
|
25
|
+
sample = result._sample
|
|
26
|
+
assert sample is not None
|
|
27
|
+
plan = cluster_plan(sample, "unit", strata=False)
|
|
28
|
+
clusters = cluster_coincidences(sample, name, plan)
|
|
29
|
+
values = sample.inputs[name].values
|
|
30
|
+
|
|
31
|
+
def statistic(idx: np.ndarray, axis: int = -1) -> np.ndarray:
|
|
32
|
+
idx = np.moveaxis(idx, axis, -1)
|
|
33
|
+
w = weights_from_indices(idx, plan.n_clusters)
|
|
34
|
+
return replicate_alphas(clusters, w, values, sample.distance).reshape(idx.shape[:-1])
|
|
35
|
+
|
|
36
|
+
with np.errstate(divide="ignore", invalid="ignore"), warnings.catch_warnings():
|
|
37
|
+
warnings.simplefilter("ignore", stats.DegenerateDataWarning)
|
|
38
|
+
direct = stats.bootstrap(
|
|
39
|
+
(np.arange(plan.n_clusters),),
|
|
40
|
+
statistic,
|
|
41
|
+
n_resamples=n_resamples,
|
|
42
|
+
confidence_level=0.95,
|
|
43
|
+
method="BCa",
|
|
44
|
+
vectorized=True,
|
|
45
|
+
rng=np.random.default_rng(seed),
|
|
46
|
+
)
|
|
47
|
+
return float(direct.confidence_interval.low), float(direct.confidence_interval.high)
|
|
48
|
+
|
|
49
|
+
|
|
20
50
|
def test_bca_matches_a_direct_scipy_call() -> None:
|
|
21
51
|
result = human_study(F4["matrix"], "interval", F4["values"]).alpha()
|
|
22
52
|
with warnings.catch_warnings():
|
|
@@ -25,30 +55,63 @@ def test_bca_matches_a_direct_scipy_call() -> None:
|
|
|
25
55
|
assert not boot.experimental
|
|
26
56
|
assert "[experimental]" not in boot.summary()
|
|
27
57
|
|
|
58
|
+
low, high = direct_scipy_bca(result, "as_missing", 999, 5)
|
|
59
|
+
interval = boot["as_missing"]
|
|
60
|
+
assert interval.low == pytest.approx(low)
|
|
61
|
+
assert interval.high == pytest.approx(high)
|
|
62
|
+
assert interval.low < interval.point < interval.high
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
@pytest.mark.filterwarnings("ignore::reliability_lab.CategorySkewWarning")
|
|
66
|
+
@pytest.mark.filterwarnings("ignore::reliability_lab.BootstrapWarning")
|
|
67
|
+
@pytest.mark.parametrize(
|
|
68
|
+
("matrix", "n_resamples", "seed"),
|
|
69
|
+
[
|
|
70
|
+
# Undefined replicates: scipy gives no interval.
|
|
71
|
+
({"a": ["x"] * 19 + ["y"], "b": ["x"] * 20}, 500, 1),
|
|
72
|
+
({"a": ["x"] * 9 + ["y"], "b": ["x"] * 9 + ["y"]}, 50, 3),
|
|
73
|
+
# Every replicate defined in a short run, undefined ones in a long run.
|
|
74
|
+
(
|
|
75
|
+
{"a": ["x"] * 5 + ["y"] * 5, "b": ["x"] * 4 + ["y"] * 6, "c": ["x"] * 6 + ["y"] * 4},
|
|
76
|
+
50,
|
|
77
|
+
4,
|
|
78
|
+
),
|
|
79
|
+
(
|
|
80
|
+
{"a": ["x"] * 5 + ["y"] * 5, "b": ["x"] * 4 + ["y"] * 6, "c": ["x"] * 6 + ["y"] * 4},
|
|
81
|
+
500,
|
|
82
|
+
4,
|
|
83
|
+
),
|
|
84
|
+
],
|
|
85
|
+
)
|
|
86
|
+
def test_bca_matches_scipy_on_degenerate_data(
|
|
87
|
+
matrix: dict[str, list[str]], n_resamples: int, seed: int
|
|
88
|
+
) -> None:
|
|
89
|
+
result = human_study(matrix, "nominal", ["x", "y"]).alpha()
|
|
90
|
+
boot = result.bootstrap(
|
|
91
|
+
scheme="units", method="bca", n_resamples=n_resamples, random_state=seed
|
|
92
|
+
)
|
|
93
|
+
low, high = direct_scipy_bca(result, "as_missing", n_resamples, seed)
|
|
94
|
+
interval = boot["as_missing"]
|
|
95
|
+
np.testing.assert_allclose([interval.low, interval.high], [low, high], equal_nan=True)
|
|
96
|
+
assert interval.defined == np.isfinite(low)
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
@pytest.mark.parametrize("budget", [None, 20])
|
|
100
|
+
def test_jackknife_alphas_leave_out_each_cluster(
|
|
101
|
+
budget: int | None, monkeypatch: pytest.MonkeyPatch
|
|
102
|
+
) -> None:
|
|
103
|
+
if budget is not None:
|
|
104
|
+
monkeypatch.setattr("reliability_lab.inference.BATCH_FLOATS", budget)
|
|
105
|
+
result = human_study(F4["matrix"], "interval", F4["values"]).alpha()
|
|
28
106
|
sample = result._sample
|
|
29
107
|
assert sample is not None
|
|
30
108
|
plan = cluster_plan(sample, "unit", strata=False)
|
|
31
109
|
clusters = cluster_coincidences(sample, "as_missing", plan)
|
|
32
110
|
values = sample.inputs["as_missing"].values
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
return replicate_alphas(clusters, w, values, sample.distance).reshape(idx.shape[:-1])
|
|
38
|
-
|
|
39
|
-
direct = stats.bootstrap(
|
|
40
|
-
(np.arange(plan.n_clusters),),
|
|
41
|
-
statistic,
|
|
42
|
-
n_resamples=999,
|
|
43
|
-
confidence_level=0.95,
|
|
44
|
-
method="BCa",
|
|
45
|
-
vectorized=True,
|
|
46
|
-
rng=np.random.default_rng(5),
|
|
47
|
-
)
|
|
48
|
-
interval = boot["as_missing"]
|
|
49
|
-
assert interval.low == pytest.approx(direct.confidence_interval.low)
|
|
50
|
-
assert interval.high == pytest.approx(direct.confidence_interval.high)
|
|
51
|
-
assert interval.low < interval.point < interval.high
|
|
111
|
+
k = plan.n_clusters
|
|
112
|
+
left_out = replicate_alphas(clusters, np.ones((k, k)) - np.eye(k), values, sample.distance)
|
|
113
|
+
jackknife = jackknife_alphas(clusters, values, sample.distance)
|
|
114
|
+
np.testing.assert_allclose(jackknife, left_out, atol=1e-12, equal_nan=True)
|
|
52
115
|
|
|
53
116
|
|
|
54
117
|
@pytest.mark.filterwarnings("ignore::reliability_lab.CategorySkewWarning")
|
|
@@ -1,12 +1,22 @@
|
|
|
1
1
|
import json
|
|
2
|
+
from collections.abc import Callable
|
|
2
3
|
from typing import Any
|
|
3
4
|
|
|
4
5
|
import numpy as np
|
|
5
6
|
import pandas as pd
|
|
6
7
|
import pytest
|
|
7
8
|
from reference import reference_alpha
|
|
9
|
+
from scipy import stats
|
|
8
10
|
|
|
9
11
|
from reliability_lab import DesignError, RatingFrame, ReliabilityStudy
|
|
12
|
+
from reliability_lab.inference import (
|
|
13
|
+
ClusterPlan,
|
|
14
|
+
StudySample,
|
|
15
|
+
cluster_coincidences,
|
|
16
|
+
cluster_plan,
|
|
17
|
+
replicate_alphas,
|
|
18
|
+
weights_from_indices,
|
|
19
|
+
)
|
|
10
20
|
|
|
11
21
|
pytestmark = pytest.mark.filterwarnings("ignore::reliability_lab.CategorySkewWarning")
|
|
12
22
|
|
|
@@ -135,3 +145,44 @@ def test_bca_comparison() -> None:
|
|
|
135
145
|
assert not [w for w in diff.warnings if w.category == "BootstrapWarning"]
|
|
136
146
|
d = diff["as_missing"]
|
|
137
147
|
assert d.low <= d.point <= d.high
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
def test_bca_comparison_matches_direct_scipy_calls() -> None:
|
|
151
|
+
diff = study_of(panel_matrix()).compare(A, B, method="bca", n_resamples=500, random_state=2)
|
|
152
|
+
sample_a, sample_b = diff.result_a._sample, diff.result_b._sample
|
|
153
|
+
assert sample_a is not None and sample_b is not None
|
|
154
|
+
plan_a = cluster_plan(sample_a, "unit", strata=False)
|
|
155
|
+
plan_b = cluster_plan(sample_b, "unit", strata=False, universe=plan_a.labels)
|
|
156
|
+
k = plan_a.n_clusters
|
|
157
|
+
|
|
158
|
+
def arm(sample: StudySample, plan: ClusterPlan) -> Callable[[np.ndarray], np.ndarray]:
|
|
159
|
+
clusters = cluster_coincidences(sample, "as_missing", plan)
|
|
160
|
+
values = sample.inputs["as_missing"].values
|
|
161
|
+
|
|
162
|
+
def statistic(idx: np.ndarray, axis: int = -1) -> np.ndarray:
|
|
163
|
+
idx = np.moveaxis(idx, axis, -1)
|
|
164
|
+
w = weights_from_indices(idx, k)
|
|
165
|
+
return replicate_alphas(clusters, w, values, sample.distance).reshape(idx.shape[:-1])
|
|
166
|
+
|
|
167
|
+
return statistic
|
|
168
|
+
|
|
169
|
+
stat_a, stat_b = arm(sample_a, plan_a), arm(sample_b, plan_b)
|
|
170
|
+
|
|
171
|
+
def stat_diff(idx: np.ndarray, axis: int = -1) -> np.ndarray:
|
|
172
|
+
return stat_b(idx, axis) - stat_a(idx, axis)
|
|
173
|
+
|
|
174
|
+
for statistic, interval in [
|
|
175
|
+
(stat_a, diff.arm_a["as_missing"]),
|
|
176
|
+
(stat_b, diff.arm_b["as_missing"]),
|
|
177
|
+
(stat_diff, diff["as_missing"]),
|
|
178
|
+
]:
|
|
179
|
+
direct = stats.bootstrap(
|
|
180
|
+
(np.arange(k),),
|
|
181
|
+
statistic,
|
|
182
|
+
n_resamples=500,
|
|
183
|
+
method="BCa",
|
|
184
|
+
vectorized=True,
|
|
185
|
+
rng=np.random.default_rng(2),
|
|
186
|
+
)
|
|
187
|
+
assert interval.low == pytest.approx(direct.confidence_interval.low)
|
|
188
|
+
assert interval.high == pytest.approx(direct.confidence_interval.high)
|
|
@@ -76,6 +76,24 @@ def test_vectorised_units_scheme_matches_naive_loop(level: str, resample: str) -
|
|
|
76
76
|
np.testing.assert_allclose(fast, slow, atol=1e-12, equal_nan=True)
|
|
77
77
|
|
|
78
78
|
|
|
79
|
+
@pytest.mark.parametrize("level", LEVELS)
|
|
80
|
+
def test_sparse_batched_product_matches_naive_loop(
|
|
81
|
+
level: str, monkeypatch: pytest.MonkeyPatch
|
|
82
|
+
) -> None:
|
|
83
|
+
# A tiny budget keeps the cluster contributions sparse, with a few replicates per batch.
|
|
84
|
+
monkeypatch.setattr("reliability_lab.inference.BATCH_FLOATS", 50)
|
|
85
|
+
matrix, study = nested_study(level)
|
|
86
|
+
sample = study.alpha()._sample
|
|
87
|
+
assert sample is not None
|
|
88
|
+
plan = cluster_plan(sample, "unit", strata=False)
|
|
89
|
+
weights = draw_weights(np.random.default_rng(11), plan, 40)
|
|
90
|
+
clusters = cluster_coincidences(sample, "as_missing", plan)
|
|
91
|
+
fast = replicate_alphas(clusters, weights, sample.inputs["as_missing"].values, sample.distance)
|
|
92
|
+
mapping = {int(label): i for i, label in enumerate(sample.unit_labels)}
|
|
93
|
+
slow = naive_units_alphas(matrix, level, VALUES, mapping, weights)
|
|
94
|
+
np.testing.assert_allclose(fast, slow, atol=1e-12, equal_nan=True)
|
|
95
|
+
|
|
96
|
+
|
|
79
97
|
def test_custom_marginal_dependent_distance_matches_naive_loop() -> None:
|
|
80
98
|
custom = Distance("my_ordinal", ORDINAL.fn, depends_on_marginals=True)
|
|
81
99
|
matrix, study = nested_study(custom)
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
__version__ = "0.1.2"
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{reliability_lab-0.1.2 → reliability_lab-0.1.3}/docs/guidance/figures/coverage_by_factor-dark.png
RENAMED
|
File without changes
|
{reliability_lab-0.1.2 → reliability_lab-0.1.3}/docs/guidance/figures/coverage_by_factor-light.png
RENAMED
|
File without changes
|
{reliability_lab-0.1.2 → reliability_lab-0.1.3}/docs/guidance/figures/coverage_by_n-dark.png
RENAMED
|
File without changes
|
{reliability_lab-0.1.2 → reliability_lab-0.1.3}/docs/guidance/figures/coverage_by_n-light.png
RENAMED
|
File without changes
|
{reliability_lab-0.1.2 → reliability_lab-0.1.3}/docs/guidance/figures/coverage_by_spread-dark.png
RENAMED
|
File without changes
|
{reliability_lab-0.1.2 → reliability_lab-0.1.3}/docs/guidance/figures/coverage_by_spread-light.png
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|