reliability-lab 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- reliability_lab/__init__.py +68 -0
- reliability_lab/_version.py +1 -0
- reliability_lab/alpha.py +290 -0
- reliability_lab/data.py +294 -0
- reliability_lab/design.py +308 -0
- reliability_lab/exceptions.py +47 -0
- reliability_lab/inference.py +724 -0
- reliability_lab/llm.py +560 -0
- reliability_lab/py.typed +0 -0
- reliability_lab/report.py +779 -0
- reliability_lab/split.py +338 -0
- reliability_lab/study.py +1034 -0
- reliability_lab-0.1.0.dist-info/METADATA +178 -0
- reliability_lab-0.1.0.dist-info/RECORD +16 -0
- reliability_lab-0.1.0.dist-info/WHEEL +4 -0
- reliability_lab-0.1.0.dist-info/licenses/LICENSE +28 -0
|
@@ -0,0 +1,308 @@
|
|
|
1
|
+
"""Coder definitions and design labels.
|
|
2
|
+
|
|
3
|
+
The coder definition decides which columns of the long-form log identify a coder:
|
|
4
|
+
a human id, a model, a prompt condition, a run. Together with the design label it
|
|
5
|
+
fixes which quantity alpha measures. Different designs answer different questions,
|
|
6
|
+
so the library never pools them into one number.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
from collections.abc import Sequence
|
|
12
|
+
from dataclasses import dataclass
|
|
13
|
+
from enum import StrEnum
|
|
14
|
+
from typing import Any
|
|
15
|
+
|
|
16
|
+
import numpy as np
|
|
17
|
+
import numpy.typing as npt
|
|
18
|
+
import pandas as pd
|
|
19
|
+
|
|
20
|
+
from reliability_lab.data import RatingFrame, preview
|
|
21
|
+
from reliability_lab.exceptions import (
|
|
22
|
+
CoderDependenceWarning,
|
|
23
|
+
DesignError,
|
|
24
|
+
ReliabilityWarning,
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class Design(StrEnum):
|
|
29
|
+
"""What the coders are, and therefore what alpha measures."""
|
|
30
|
+
|
|
31
|
+
HUMAN_PANEL = "human_panel"
|
|
32
|
+
REPEATABILITY = "repeatability"
|
|
33
|
+
PROMPT_ROBUSTNESS = "prompt_robustness"
|
|
34
|
+
INTER_MODEL = "inter_model"
|
|
35
|
+
CONFIGURATION_CHANGE = "configuration_change"
|
|
36
|
+
ORDER_ROBUSTNESS = "order_robustness"
|
|
37
|
+
HUMAN_LLM_ALIGNMENT = "human_llm_alignment"
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
#: Fields a CoderSpec may combine.
|
|
41
|
+
CODER_FIELDS: tuple[str, ...] = ("rater_id", "model_id", "model_family", "condition_id", "run_id")
|
|
42
|
+
|
|
43
|
+
LLM_DESIGNS = frozenset(
|
|
44
|
+
{
|
|
45
|
+
Design.REPEATABILITY,
|
|
46
|
+
Design.PROMPT_ROBUSTNESS,
|
|
47
|
+
Design.INTER_MODEL,
|
|
48
|
+
Design.ORDER_ROBUSTNESS,
|
|
49
|
+
}
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
#: What each implemented design measures, attached to every result.
|
|
53
|
+
DESIGN_NOTES: dict[Design, str] = {
|
|
54
|
+
Design.HUMAN_PANEL: "Inter-coder reliability of a human panel.",
|
|
55
|
+
Design.REPEATABILITY: (
|
|
56
|
+
"Repeatability of one judge configuration across repeated runs. Runs of one "
|
|
57
|
+
"configuration are not independent coders, so this is not inter-coder reliability, "
|
|
58
|
+
"and a repeatable judge can still be consistently wrong."
|
|
59
|
+
),
|
|
60
|
+
Design.PROMPT_ROBUSTNESS: (
|
|
61
|
+
"Agreement of one model across prompt conditions. All coders share one model, so "
|
|
62
|
+
"their errors are correlated; the result holds only if the prompts keep the "
|
|
63
|
+
"rubric's meaning."
|
|
64
|
+
),
|
|
65
|
+
Design.INTER_MODEL: (
|
|
66
|
+
"Agreement among different models. It is not agreement with human experts or "
|
|
67
|
+
"with ground truth."
|
|
68
|
+
),
|
|
69
|
+
Design.ORDER_ROBUSTNESS: (
|
|
70
|
+
"Agreement of one judge with itself when the candidates' presentation order is "
|
|
71
|
+
"swapped. It applies to pairwise judgments whose labels name the winning candidate, "
|
|
72
|
+
"not its position."
|
|
73
|
+
),
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
_NOT_YET: dict[Design, str] = {
|
|
77
|
+
Design.HUMAN_LLM_ALIGNMENT: "human-LLM alignment (mixed panels) is planned for v0.2",
|
|
78
|
+
Design.CONFIGURATION_CHANGE: (
|
|
79
|
+
"configuration_change is a paired comparison, not a single-alpha design: declare the "
|
|
80
|
+
"design each configuration is measured under and call study.compare(a=..., b=...)"
|
|
81
|
+
),
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
@dataclass(frozen=True, init=False)
|
|
86
|
+
class CoderSpec:
|
|
87
|
+
"""Which canonical RatingFrame fields together identify a coder.
|
|
88
|
+
|
|
89
|
+
``CoderSpec(by="rater_id")`` treats each human annotator as a coder;
|
|
90
|
+
``CoderSpec(by=["model_id", "condition_id"])`` treats each model and prompt
|
|
91
|
+
combination as a coder.
|
|
92
|
+
"""
|
|
93
|
+
|
|
94
|
+
by: tuple[str, ...]
|
|
95
|
+
|
|
96
|
+
def __init__(self, by: str | Sequence[str]) -> None:
|
|
97
|
+
fields = (by,) if isinstance(by, str) else tuple(by)
|
|
98
|
+
if not fields:
|
|
99
|
+
raise DesignError("CoderSpec needs at least one field")
|
|
100
|
+
unknown = [f for f in fields if f not in CODER_FIELDS]
|
|
101
|
+
if unknown:
|
|
102
|
+
raise DesignError(
|
|
103
|
+
f"CoderSpec fields must be among {list(CODER_FIELDS)}; got {preview(unknown)}"
|
|
104
|
+
)
|
|
105
|
+
if len(set(fields)) != len(fields):
|
|
106
|
+
raise DesignError(f"CoderSpec fields repeat: {list(fields)}")
|
|
107
|
+
object.__setattr__(self, "by", fields)
|
|
108
|
+
|
|
109
|
+
def labels(self, ratings: RatingFrame) -> npt.NDArray[np.object_]:
|
|
110
|
+
"""One coder label per row of ``ratings``."""
|
|
111
|
+
data = ratings._data
|
|
112
|
+
for field in self.by:
|
|
113
|
+
if field not in ratings.declared:
|
|
114
|
+
raise DesignError(
|
|
115
|
+
f"CoderSpec uses {field!r}, which was not mapped in RatingFrame.from_long"
|
|
116
|
+
)
|
|
117
|
+
missing = data.index[data[field].isna()]
|
|
118
|
+
if len(missing):
|
|
119
|
+
raise DesignError(
|
|
120
|
+
f"coder field {field!r} is missing in {len(missing)} rows "
|
|
121
|
+
f"{preview(missing)}; every row needs a value for each coder field"
|
|
122
|
+
)
|
|
123
|
+
parts = [data[f].map(_label).to_numpy(dtype=object) for f in self.by]
|
|
124
|
+
if len(parts) == 1:
|
|
125
|
+
return parts[0]
|
|
126
|
+
return np.array([" | ".join(row) for row in zip(*parts, strict=True)], dtype=object)
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def _label(x: Any) -> str:
|
|
130
|
+
"""Readable label: integral floats (from columns with gaps) print as integers."""
|
|
131
|
+
if isinstance(x, float | np.floating) and float(x).is_integer():
|
|
132
|
+
return str(int(x))
|
|
133
|
+
return str(x)
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
@dataclass(frozen=True)
|
|
137
|
+
class DesignCheck:
|
|
138
|
+
"""Outcome of checking a design against the data.
|
|
139
|
+
|
|
140
|
+
``configurations`` lists the judge configurations of a single-configuration
|
|
141
|
+
design (one tuple of key values each). A study holding several can be
|
|
142
|
+
compared configuration by configuration, but has no alpha of its own.
|
|
143
|
+
"""
|
|
144
|
+
|
|
145
|
+
notes: tuple[str, ...]
|
|
146
|
+
warnings: tuple[tuple[type[ReliabilityWarning], str], ...]
|
|
147
|
+
configurations: tuple[tuple[Any, ...], ...] = ()
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
#: Designs measured on one judge configuration, and the fields that identify it.
|
|
151
|
+
SINGLE_CONFIGURATION: dict[Design, tuple[str, ...]] = {
|
|
152
|
+
Design.REPEATABILITY: ("model_id", "condition_id"),
|
|
153
|
+
Design.PROMPT_ROBUSTNESS: ("model_id",),
|
|
154
|
+
Design.ORDER_ROBUSTNESS: ("model_id",),
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
def configuration_masks(
|
|
159
|
+
design: Design, data: pd.DataFrame
|
|
160
|
+
) -> list[tuple[tuple[Any, ...], npt.NDArray[np.bool_]]]:
|
|
161
|
+
"""Each judge configuration of a single-configuration design, with its row mask.
|
|
162
|
+
|
|
163
|
+
Other designs, and data with one configuration, give a single all-rows entry.
|
|
164
|
+
"""
|
|
165
|
+
keys = SINGLE_CONFIGURATION.get(design)
|
|
166
|
+
if keys is None:
|
|
167
|
+
return [((), np.ones(len(data), dtype=bool))]
|
|
168
|
+
frame = data[list(keys)].astype(object)
|
|
169
|
+
codes, uniques = pd.factorize(pd.MultiIndex.from_frame(frame), sort=False)
|
|
170
|
+
return [(tuple(_label(v) for v in key), codes == i) for i, key in enumerate(uniques)]
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
def check_design(
|
|
174
|
+
design: Design,
|
|
175
|
+
ratings: RatingFrame,
|
|
176
|
+
coders: CoderSpec,
|
|
177
|
+
coder_labels: npt.NDArray[np.object_],
|
|
178
|
+
) -> DesignCheck:
|
|
179
|
+
"""Refuse designs the data cannot support, and collect the caveats of the rest.
|
|
180
|
+
|
|
181
|
+
A single-configuration design (repeatability, prompt or order robustness)
|
|
182
|
+
holding several configurations is checked configuration by configuration.
|
|
183
|
+
"""
|
|
184
|
+
if design in _NOT_YET:
|
|
185
|
+
raise NotImplementedError(_NOT_YET[design])
|
|
186
|
+
data = ratings._data
|
|
187
|
+
is_llm = ratings.is_llm
|
|
188
|
+
if is_llm.any() and not is_llm.all():
|
|
189
|
+
raise DesignError(
|
|
190
|
+
f"the ratings mix {int((~is_llm).sum())} human rows and {int(is_llm.sum())} LLM "
|
|
191
|
+
"rows; mixed human-LLM panels are planned for v0.2. Filter the ratings to one kind."
|
|
192
|
+
)
|
|
193
|
+
if design is Design.HUMAN_PANEL and is_llm.any():
|
|
194
|
+
raise DesignError("human_panel was declared but the rows carry a model_id (LLM rows)")
|
|
195
|
+
if design in LLM_DESIGNS and not is_llm.all():
|
|
196
|
+
raise DesignError(f"{design.value} needs LLM rows (rows with a model_id)")
|
|
197
|
+
|
|
198
|
+
configurations = configuration_masks(design, data)
|
|
199
|
+
if len(configurations) > 1:
|
|
200
|
+
checks = [
|
|
201
|
+
check_design(design, ratings._subset(mask), coders, coder_labels[mask])
|
|
202
|
+
for _, mask in configurations
|
|
203
|
+
]
|
|
204
|
+
return DesignCheck(
|
|
205
|
+
notes=checks[0].notes,
|
|
206
|
+
warnings=tuple(dict.fromkeys(w for c in checks for w in c.warnings)),
|
|
207
|
+
configurations=tuple(key for key, _ in configurations),
|
|
208
|
+
)
|
|
209
|
+
|
|
210
|
+
warnings: list[tuple[type[ReliabilityWarning], str]] = []
|
|
211
|
+
if design is Design.REPEATABILITY:
|
|
212
|
+
if "run_id" not in coders.by:
|
|
213
|
+
raise DesignError(
|
|
214
|
+
"repeatability coders are repeated runs: include 'run_id' in CoderSpec(by=...)"
|
|
215
|
+
)
|
|
216
|
+
elif design is Design.PROMPT_ROBUSTNESS:
|
|
217
|
+
if "condition_id" not in coders.by:
|
|
218
|
+
raise DesignError(
|
|
219
|
+
"prompt_robustness coders are prompt conditions: include 'condition_id' in "
|
|
220
|
+
"CoderSpec(by=...)"
|
|
221
|
+
)
|
|
222
|
+
elif design is Design.INTER_MODEL:
|
|
223
|
+
if "model_id" not in coders.by:
|
|
224
|
+
raise DesignError(
|
|
225
|
+
"inter_model coders are models: include 'model_id' in CoderSpec(by=...)"
|
|
226
|
+
)
|
|
227
|
+
if data["model_id"].nunique() < 2:
|
|
228
|
+
raise DesignError("inter_model needs at least two distinct model_id values")
|
|
229
|
+
elif design is Design.ORDER_ROBUSTNESS and "condition_id" not in coders.by:
|
|
230
|
+
raise DesignError(
|
|
231
|
+
"order_robustness coders are candidate-order conditions: include "
|
|
232
|
+
"'condition_id' in CoderSpec(by=...)"
|
|
233
|
+
)
|
|
234
|
+
|
|
235
|
+
coders_present = pd.unique(coder_labels)
|
|
236
|
+
if len(coders_present) < 2:
|
|
237
|
+
raise DesignError(
|
|
238
|
+
f"alpha needs at least two coders; CoderSpec(by={list(coders.by)}) yields "
|
|
239
|
+
f"{len(coders_present)}"
|
|
240
|
+
)
|
|
241
|
+
if design is Design.INTER_MODEL:
|
|
242
|
+
warnings.extend(_dependence_warnings(ratings, coder_labels))
|
|
243
|
+
|
|
244
|
+
_require_one_rating_per_coder(data["unit_id"].to_numpy(), coder_labels)
|
|
245
|
+
notes = (DESIGN_NOTES[design],) if design in DESIGN_NOTES else ()
|
|
246
|
+
return DesignCheck(
|
|
247
|
+
notes=notes,
|
|
248
|
+
warnings=tuple(warnings),
|
|
249
|
+
configurations=tuple(key for key, _ in configurations),
|
|
250
|
+
)
|
|
251
|
+
|
|
252
|
+
|
|
253
|
+
def _dependence_warnings(
|
|
254
|
+
ratings: RatingFrame, coder_labels: npt.NDArray[np.object_]
|
|
255
|
+
) -> list[tuple[type[ReliabilityWarning], str]]:
|
|
256
|
+
data = ratings._data
|
|
257
|
+
found: list[tuple[type[ReliabilityWarning], str]] = []
|
|
258
|
+
coder_models = pd.DataFrame({"coder": coder_labels, "model": data["model_id"].to_numpy()})
|
|
259
|
+
coder_models = coder_models.drop_duplicates()
|
|
260
|
+
shared_model = coder_models.groupby("model", sort=False)["coder"].agg(list)
|
|
261
|
+
for model, members in shared_model.items():
|
|
262
|
+
if len(members) > 1:
|
|
263
|
+
found.append(
|
|
264
|
+
(
|
|
265
|
+
CoderDependenceWarning,
|
|
266
|
+
f"coders {preview(members)} use the same model {model!r}; their errors are "
|
|
267
|
+
"likely correlated and alpha may be inflated",
|
|
268
|
+
)
|
|
269
|
+
)
|
|
270
|
+
|
|
271
|
+
if "model_family" not in ratings.declared or data["model_family"].isna().any():
|
|
272
|
+
unknown = pd.unique(data.loc[data["model_family"].isna(), "model_id"])
|
|
273
|
+
found.append(
|
|
274
|
+
(
|
|
275
|
+
CoderDependenceWarning,
|
|
276
|
+
f"model_family is missing for models {preview(unknown)}, so coder independence "
|
|
277
|
+
"across model families cannot be checked",
|
|
278
|
+
)
|
|
279
|
+
)
|
|
280
|
+
return found
|
|
281
|
+
|
|
282
|
+
families = data[["model_id", "model_family"]].drop_duplicates()
|
|
283
|
+
per_family = families.groupby("model_family", sort=False)["model_id"].agg(list)
|
|
284
|
+
for family, models in per_family.items():
|
|
285
|
+
if len(models) > 1:
|
|
286
|
+
found.append(
|
|
287
|
+
(
|
|
288
|
+
CoderDependenceWarning,
|
|
289
|
+
f"models {preview(models)} share model family {family!r}; same-family "
|
|
290
|
+
"models share errors, so alpha may be inflated",
|
|
291
|
+
)
|
|
292
|
+
)
|
|
293
|
+
return found
|
|
294
|
+
|
|
295
|
+
|
|
296
|
+
def _require_one_rating_per_coder(
|
|
297
|
+
units: npt.NDArray[Any], coder_labels: npt.NDArray[np.object_]
|
|
298
|
+
) -> None:
|
|
299
|
+
keys = pd.DataFrame({"unit": units, "coder": coder_labels})
|
|
300
|
+
duplicated = keys[keys.duplicated(keep="first")].drop_duplicates()
|
|
301
|
+
if len(duplicated):
|
|
302
|
+
pairs = list(zip(duplicated["unit"], duplicated["coder"], strict=True))
|
|
303
|
+
raise DesignError(
|
|
304
|
+
f"{len(pairs)} (unit, coder) pairs have more than one row {preview(pairs)}. Each "
|
|
305
|
+
"coder may rate a unit once: for retried calls, map an attempt column and declare "
|
|
306
|
+
"retries='last' or retries='first_observed'; for repeated runs, add run_id to the "
|
|
307
|
+
"CoderSpec."
|
|
308
|
+
)
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
"""Errors and warnings raised by reliability-lab."""
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class ReliabilityLabError(Exception):
|
|
5
|
+
"""Base class for every error raised by reliability-lab."""
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class SchemaError(ReliabilityLabError, ValueError):
|
|
9
|
+
"""The ratings table does not satisfy the long-form schema."""
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class DesignError(ReliabilityLabError, ValueError):
|
|
13
|
+
"""The declared study design is incomplete, inconsistent with the data, or unsupported."""
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class LeakageError(DesignError):
|
|
17
|
+
"""Data meant for tuning or few-shot examples reaches the test partition."""
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class UndefinedAlphaError(ReliabilityLabError, ValueError):
|
|
21
|
+
"""Alpha is undefined: no pairable values, or zero expected disagreement."""
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class ReliabilityWarning(UserWarning):
|
|
25
|
+
"""Base class for warnings about how a result should be read."""
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class CoderDependenceWarning(ReliabilityWarning):
|
|
29
|
+
"""Coders may not be independent, so alpha's chance model may not hold."""
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class CategorySkewWarning(ReliabilityWarning):
|
|
33
|
+
"""One category dominates, so alpha can be low even when agreement is high."""
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class DomainSizeWarning(ReliabilityWarning):
|
|
37
|
+
"""The value domain is large enough to make V x V matrices costly."""
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
class TraceWarning(ReliabilityWarning):
|
|
41
|
+
"""The trace sidecar shows a confound: configurations differ in more than the
|
|
42
|
+
design compares (for example temperature as well as the prompt)."""
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
class BootstrapWarning(ReliabilityWarning):
|
|
46
|
+
"""An interval needs care: failed replicates, a resampling level that ignores
|
|
47
|
+
declared groups, or an experimental method."""
|