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.
@@ -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."""