synthbench-eval 0.4.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.
- synthbench/__init__.py +16 -0
- synthbench/__main__.py +5 -0
- synthbench/adapter.py +131 -0
- synthbench/anomaly.py +503 -0
- synthbench/baseline_floors.py +213 -0
- synthbench/baselines.py +902 -0
- synthbench/cli.py +2929 -0
- synthbench/config_id.py +424 -0
- synthbench/contamination.py +783 -0
- synthbench/convergence/__init__.py +67 -0
- synthbench/convergence/baseline.py +172 -0
- synthbench/convergence/bootstrap.py +60 -0
- synthbench/convergence/cli_report.py +556 -0
- synthbench/convergence/curves.py +111 -0
- synthbench/convergence/real_sampling.py +146 -0
- synthbench/convergence/thresholds.py +49 -0
- synthbench/datasets/__init__.py +37 -0
- synthbench/datasets/base.py +147 -0
- synthbench/datasets/eurobarometer.py +335 -0
- synthbench/datasets/globalopinionqa.py +252 -0
- synthbench/datasets/gss.py +323 -0
- synthbench/datasets/michigan.py +505 -0
- synthbench/datasets/ntia.py +357 -0
- synthbench/datasets/opinionsqa.py +412 -0
- synthbench/datasets/pewtech.py +334 -0
- synthbench/datasets/policy.py +142 -0
- synthbench/datasets/subpop.py +302 -0
- synthbench/datasets/wvs.py +229 -0
- synthbench/findings.py +988 -0
- synthbench/holdout.py +347 -0
- synthbench/human_distributions.py +243 -0
- synthbench/leaderboard.py +715 -0
- synthbench/leaderboard_pr.py +274 -0
- synthbench/metrics/__init__.py +38 -0
- synthbench/metrics/composite.py +72 -0
- synthbench/metrics/conditioning.py +39 -0
- synthbench/metrics/distributional.py +41 -0
- synthbench/metrics/ranking.py +37 -0
- synthbench/metrics/refusal.py +270 -0
- synthbench/metrics/subgroup.py +58 -0
- synthbench/private_holdout.py +240 -0
- synthbench/providers/__init__.py +43 -0
- synthbench/providers/_parsing.py +150 -0
- synthbench/providers/_retry.py +107 -0
- synthbench/providers/base.py +212 -0
- synthbench/providers/http.py +108 -0
- synthbench/providers/majority_baseline.py +23 -0
- synthbench/providers/ollama.py +109 -0
- synthbench/providers/openrouter.py +144 -0
- synthbench/providers/population_baseline.py +87 -0
- synthbench/providers/random_baseline.py +24 -0
- synthbench/providers/raw_anthropic.py +149 -0
- synthbench/providers/raw_gemini.py +145 -0
- synthbench/providers/raw_openai.py +139 -0
- synthbench/providers/synthpanel.py +978 -0
- synthbench/publish.py +2772 -0
- synthbench/r2_upload.py +178 -0
- synthbench/recompute.py +235 -0
- synthbench/report.py +493 -0
- synthbench/run_hash.py +111 -0
- synthbench/run_validity.py +232 -0
- synthbench/runner.py +834 -0
- synthbench/stats.py +1492 -0
- synthbench/submission.py +284 -0
- synthbench/submission_pr.py +436 -0
- synthbench/submit_adapter.py +748 -0
- synthbench/suite.py +424 -0
- synthbench/suites/__init__.py +94 -0
- synthbench/topics.py +256 -0
- synthbench/user_config.py +409 -0
- synthbench/validation.py +1556 -0
- synthbench/visualize.py +455 -0
- synthbench_eval-0.4.0.dist-info/METADATA +279 -0
- synthbench_eval-0.4.0.dist-info/RECORD +78 -0
- synthbench_eval-0.4.0.dist-info/WHEEL +5 -0
- synthbench_eval-0.4.0.dist-info/entry_points.txt +2 -0
- synthbench_eval-0.4.0.dist-info/licenses/LICENSE +21 -0
- synthbench_eval-0.4.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
"""Bootstrap convergence analysis over aggregate human distributions.
|
|
2
|
+
|
|
3
|
+
Given a Question with a known ``human_distribution``, this module computes a
|
|
4
|
+
theoretical ~1/sqrt(n) convergence curve by multinomial subsampling:
|
|
5
|
+
|
|
6
|
+
curve(n) = distribution over B bootstrap samples of
|
|
7
|
+
JSD(empirical_sample_of_size_n, full_distribution)
|
|
8
|
+
|
|
9
|
+
Three layers:
|
|
10
|
+
|
|
11
|
+
* :mod:`synthbench.convergence.bootstrap` -- multinomial sub-sampling primitives
|
|
12
|
+
* :mod:`synthbench.convergence.curves` -- curve computation
|
|
13
|
+
(n -> JSD_mean / JSD_p10 / JSD_p90) across sample sizes
|
|
14
|
+
* :mod:`synthbench.convergence.thresholds` -- "convergence n" finder: smallest
|
|
15
|
+
n at which the curve stabilizes below epsilon with a flat tail
|
|
16
|
+
|
|
17
|
+
The output is the theoretical lower bound an idealized i.i.d. sampler from the
|
|
18
|
+
aggregate distribution would achieve. It is NOT an estimate of real-population
|
|
19
|
+
convergence (population heterogeneity requires microdata; see the microdata
|
|
20
|
+
bead). Treat this curve as the floor everything else gets compared to.
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
from __future__ import annotations
|
|
24
|
+
|
|
25
|
+
from synthbench.convergence.baseline import (
|
|
26
|
+
BaselineGatedError,
|
|
27
|
+
BaselineUnavailable,
|
|
28
|
+
load_convergence_baseline,
|
|
29
|
+
)
|
|
30
|
+
from synthbench.convergence.bootstrap import (
|
|
31
|
+
bootstrap_sample,
|
|
32
|
+
empirical_distribution,
|
|
33
|
+
)
|
|
34
|
+
from synthbench.convergence.curves import (
|
|
35
|
+
DEFAULT_BOOTSTRAP_B,
|
|
36
|
+
DEFAULT_SAMPLE_SIZES,
|
|
37
|
+
CurvePoint,
|
|
38
|
+
compute_curve,
|
|
39
|
+
)
|
|
40
|
+
from synthbench.convergence.real_sampling import (
|
|
41
|
+
compute_real_curve,
|
|
42
|
+
respondent_distribution,
|
|
43
|
+
subsample_distribution,
|
|
44
|
+
)
|
|
45
|
+
from synthbench.convergence.thresholds import (
|
|
46
|
+
DEFAULT_DELTA,
|
|
47
|
+
DEFAULT_EPSILON,
|
|
48
|
+
find_convergence_n,
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
__all__ = [
|
|
52
|
+
"BaselineGatedError",
|
|
53
|
+
"BaselineUnavailable",
|
|
54
|
+
"load_convergence_baseline",
|
|
55
|
+
"bootstrap_sample",
|
|
56
|
+
"empirical_distribution",
|
|
57
|
+
"compute_curve",
|
|
58
|
+
"CurvePoint",
|
|
59
|
+
"DEFAULT_SAMPLE_SIZES",
|
|
60
|
+
"DEFAULT_BOOTSTRAP_B",
|
|
61
|
+
"compute_real_curve",
|
|
62
|
+
"respondent_distribution",
|
|
63
|
+
"subsample_distribution",
|
|
64
|
+
"find_convergence_n",
|
|
65
|
+
"DEFAULT_EPSILON",
|
|
66
|
+
"DEFAULT_DELTA",
|
|
67
|
+
]
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
"""Stable baseline-loader export for synthpanel's ``--calibrate-against`` flag.
|
|
2
|
+
|
|
3
|
+
synthpanel (>= 0.9) probes ``synthbench`` at runtime for a
|
|
4
|
+
``load_convergence_baseline`` callable when a user passes
|
|
5
|
+
``--calibrate-against DATASET:QUESTION``. This module is the single
|
|
6
|
+
owner of that export.
|
|
7
|
+
|
|
8
|
+
Policy:
|
|
9
|
+
|
|
10
|
+
* ``full``-tier datasets (currently ``gss`` and ``ntia``) return the
|
|
11
|
+
aggregate ``human_distribution`` directly — the underlying license is
|
|
12
|
+
unambiguously public.
|
|
13
|
+
* ``gated``-tier datasets raise :class:`BaselineGatedError`. Serving
|
|
14
|
+
those payloads requires hitting the authenticated R2 origin with
|
|
15
|
+
credentials, which is out of scope for the in-process loader.
|
|
16
|
+
* ``aggregates_only`` / ``citation_only`` datasets raise
|
|
17
|
+
:class:`BaselineUnavailable` — synthbench has no per-question
|
|
18
|
+
redistribution rights for them at all.
|
|
19
|
+
* Unknown datasets raise :class:`BaselineUnavailable`; the policy
|
|
20
|
+
lookup is conservative (defaults to ``aggregates_only``) and a typo
|
|
21
|
+
should not silently leak or crash downstream.
|
|
22
|
+
|
|
23
|
+
The return shape is fixed by ``docs/convergence-analysis.md`` — synthpanel
|
|
24
|
+
attaches ``human_distribution`` to every matching question's
|
|
25
|
+
``calibration`` sub-object and computes JSD with its own local metric.
|
|
26
|
+
|
|
27
|
+
For datasets whose policy permits redistributing the question wording
|
|
28
|
+
(``full`` / ``citation_only``), the payload additionally carries
|
|
29
|
+
``question_text`` and ``options`` so synthpanel can present the *real*
|
|
30
|
+
survey item to its panel instead of fabricating a placeholder prompt.
|
|
31
|
+
These fields are additive; ``human_distribution`` remains the only
|
|
32
|
+
load-bearing key.
|
|
33
|
+
"""
|
|
34
|
+
|
|
35
|
+
from __future__ import annotations
|
|
36
|
+
|
|
37
|
+
from typing import Any
|
|
38
|
+
|
|
39
|
+
from synthbench.datasets import DATASETS
|
|
40
|
+
from synthbench.datasets.policy import policy_for
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
class BaselineUnavailable(LookupError):
|
|
44
|
+
"""Raised when no per-question baseline can be returned for this dataset.
|
|
45
|
+
|
|
46
|
+
Covers unknown datasets and the ``aggregates_only`` / ``citation_only``
|
|
47
|
+
tiers where synthbench ships no per-question artifact at all.
|
|
48
|
+
"""
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
class BaselineGatedError(BaselineUnavailable):
|
|
52
|
+
"""Raised when the dataset is ``gated`` and requires authenticated access.
|
|
53
|
+
|
|
54
|
+
The in-process loader cannot vend gated-tier baselines — those payloads
|
|
55
|
+
live behind the JWT-authenticated R2 origin. synthpanel can surface this
|
|
56
|
+
to the user as an "authenticate to calibrate against <dataset>" hint.
|
|
57
|
+
"""
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def load_convergence_baseline(
|
|
61
|
+
dataset: str,
|
|
62
|
+
question_key: str | None = None,
|
|
63
|
+
) -> dict[str, Any]:
|
|
64
|
+
"""Return the aggregate baseline payload for one question.
|
|
65
|
+
|
|
66
|
+
Args:
|
|
67
|
+
dataset: Adapter name as registered in
|
|
68
|
+
:data:`synthbench.datasets.DATASETS` (e.g. ``"gss"``, ``"ntia"``).
|
|
69
|
+
A filter suffix like ``"gss (2018)"`` is accepted and resolves
|
|
70
|
+
to the base-name adapter.
|
|
71
|
+
question_key: Question identifier. Accepts both the canonical
|
|
72
|
+
:attr:`Question.key` (e.g. ``"GSS_SPKATH"``) and the bare
|
|
73
|
+
upstream id (``"SPKATH"``). Required.
|
|
74
|
+
|
|
75
|
+
Returns:
|
|
76
|
+
Dict shaped per ``docs/convergence-analysis.md`` with keys:
|
|
77
|
+
``dataset``, ``question_key``, ``human_distribution``,
|
|
78
|
+
``redistribution_policy``, ``license_url``, ``citation``.
|
|
79
|
+
|
|
80
|
+
When the dataset policy permits redistributing question text
|
|
81
|
+
(``full`` / ``citation_only``), two additive keys are also
|
|
82
|
+
present: ``question_text`` (the survey item's wording) and
|
|
83
|
+
``options`` (the ordered answer labels, aligned with the keys of
|
|
84
|
+
``human_distribution``). Callers must treat both as optional —
|
|
85
|
+
older payloads and policy-withholding datasets omit them.
|
|
86
|
+
|
|
87
|
+
Raises:
|
|
88
|
+
BaselineGatedError: dataset tier is ``gated`` — authenticated
|
|
89
|
+
access required.
|
|
90
|
+
BaselineUnavailable: dataset is unknown or its tier is
|
|
91
|
+
``aggregates_only`` / ``citation_only``; or the requested
|
|
92
|
+
question key does not resolve in the adapter.
|
|
93
|
+
ValueError: ``question_key`` was not provided.
|
|
94
|
+
"""
|
|
95
|
+
if not question_key:
|
|
96
|
+
raise ValueError(
|
|
97
|
+
"question_key is required: load_convergence_baseline needs a "
|
|
98
|
+
"specific question to return human_distribution for"
|
|
99
|
+
)
|
|
100
|
+
|
|
101
|
+
policy = policy_for(dataset)
|
|
102
|
+
|
|
103
|
+
if policy.redistribution_policy == "gated":
|
|
104
|
+
raise BaselineGatedError(
|
|
105
|
+
f"dataset {policy.name!r} is gated-tier; baseline requires "
|
|
106
|
+
"authenticated access to the R2 origin and cannot be loaded "
|
|
107
|
+
"from the in-process synthbench API"
|
|
108
|
+
)
|
|
109
|
+
|
|
110
|
+
if policy.redistribution_policy != "full":
|
|
111
|
+
raise BaselineUnavailable(
|
|
112
|
+
f"dataset {dataset!r} has no per-question baseline available "
|
|
113
|
+
f"(tier={policy.redistribution_policy!r})"
|
|
114
|
+
)
|
|
115
|
+
|
|
116
|
+
adapter_cls = DATASETS.get(policy.name)
|
|
117
|
+
if adapter_cls is None:
|
|
118
|
+
# policy_for() falls back to "aggregates_only" for unknown names,
|
|
119
|
+
# so we'd have bailed above. This is defensive: a future adapter
|
|
120
|
+
# whose policy is declared elsewhere could theoretically skip
|
|
121
|
+
# registration.
|
|
122
|
+
raise BaselineUnavailable(f"no registered adapter for dataset {dataset!r}")
|
|
123
|
+
|
|
124
|
+
question = _resolve_question(adapter_cls(), question_key)
|
|
125
|
+
|
|
126
|
+
payload: dict[str, Any] = {
|
|
127
|
+
"dataset": policy.name,
|
|
128
|
+
"question_key": question.key,
|
|
129
|
+
"human_distribution": dict(question.human_distribution),
|
|
130
|
+
"redistribution_policy": policy.redistribution_policy,
|
|
131
|
+
"license_url": policy.license_url,
|
|
132
|
+
"citation": policy.citation,
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
# Additive, policy-gated metadata: the survey item's wording and its
|
|
136
|
+
# answer labels. synthpanel's calibration panel needs the real question
|
|
137
|
+
# text (rather than a fabricated placeholder) and needs option labels that
|
|
138
|
+
# line up with the ``human_distribution`` keys so its pick_one extraction
|
|
139
|
+
# aligns with ground truth. Only surfaced where the redistribution policy
|
|
140
|
+
# permits distributing question text (``full`` / ``citation_only``); this
|
|
141
|
+
# is a strictly narrower disclosure than ``human_distribution``.
|
|
142
|
+
if policy.allows_question_text:
|
|
143
|
+
payload["question_text"] = question.text
|
|
144
|
+
payload["options"] = list(question.options)
|
|
145
|
+
|
|
146
|
+
return payload
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
def _resolve_question(dataset, question_key: str):
|
|
150
|
+
"""Locate a Question by key; accept bare or adapter-prefixed forms.
|
|
151
|
+
|
|
152
|
+
Mirrors the CLI's :func:`convergence.cli_report._resolve_question`
|
|
153
|
+
behavior so synthpanel callers can pass either the canonical
|
|
154
|
+
``Question.key`` or the bare upstream id.
|
|
155
|
+
"""
|
|
156
|
+
questions = dataset.load()
|
|
157
|
+
for q in questions:
|
|
158
|
+
if q.key == question_key:
|
|
159
|
+
return q
|
|
160
|
+
suffixed = [q for q in questions if q.key.endswith(f"_{question_key}")]
|
|
161
|
+
if len(suffixed) == 1:
|
|
162
|
+
return suffixed[0]
|
|
163
|
+
raise BaselineUnavailable(
|
|
164
|
+
f"no question with key {question_key!r} in dataset {dataset.name!r}"
|
|
165
|
+
)
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
__all__ = [
|
|
169
|
+
"load_convergence_baseline",
|
|
170
|
+
"BaselineUnavailable",
|
|
171
|
+
"BaselineGatedError",
|
|
172
|
+
]
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
"""Multinomial sub-sampling primitives.
|
|
2
|
+
|
|
3
|
+
The aggregate ``human_distribution`` on a Question is the population proportion
|
|
4
|
+
vector. Drawing n responses from an i.i.d. population with that distribution is
|
|
5
|
+
a multinomial draw. This module provides the narrow primitives -- actual curve
|
|
6
|
+
computation lives in :mod:`synthbench.convergence.curves`.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import numpy as np
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def bootstrap_sample(
|
|
15
|
+
distribution: dict[str, float],
|
|
16
|
+
n: int,
|
|
17
|
+
rng: np.random.Generator | None = None,
|
|
18
|
+
) -> dict[str, int]:
|
|
19
|
+
"""Draw a multinomial sample of size ``n`` from ``distribution``.
|
|
20
|
+
|
|
21
|
+
Args:
|
|
22
|
+
distribution: Option -> probability. Need not be exactly normalized --
|
|
23
|
+
zero-sum inputs return an empty count vector.
|
|
24
|
+
n: Number of responses to draw. Must be non-negative.
|
|
25
|
+
rng: Optional numpy ``Generator`` for reproducibility. If omitted, a
|
|
26
|
+
fresh default generator is used (non-deterministic).
|
|
27
|
+
|
|
28
|
+
Returns:
|
|
29
|
+
Option -> integer count. Counts sum to ``n`` when the input has positive
|
|
30
|
+
mass; sum to 0 for an empty / zero-mass distribution.
|
|
31
|
+
"""
|
|
32
|
+
if n < 0:
|
|
33
|
+
raise ValueError(f"n must be non-negative, got {n}")
|
|
34
|
+
if rng is None:
|
|
35
|
+
rng = np.random.default_rng()
|
|
36
|
+
|
|
37
|
+
keys = list(distribution.keys())
|
|
38
|
+
if not keys or n == 0:
|
|
39
|
+
return {k: 0 for k in keys}
|
|
40
|
+
|
|
41
|
+
probs = np.array([max(0.0, distribution[k]) for k in keys], dtype=np.float64)
|
|
42
|
+
total = probs.sum()
|
|
43
|
+
if total <= 0.0:
|
|
44
|
+
return {k: 0 for k in keys}
|
|
45
|
+
probs /= total
|
|
46
|
+
|
|
47
|
+
counts = rng.multinomial(n, probs)
|
|
48
|
+
return {k: int(c) for k, c in zip(keys, counts)}
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def empirical_distribution(sample: dict[str, int]) -> dict[str, float]:
|
|
52
|
+
"""Normalize a count vector into an empirical distribution.
|
|
53
|
+
|
|
54
|
+
Zero-total samples return a dict with the same keys but zero probabilities,
|
|
55
|
+
preserving key alignment for downstream JSD.
|
|
56
|
+
"""
|
|
57
|
+
total = sum(sample.values())
|
|
58
|
+
if total <= 0:
|
|
59
|
+
return {k: 0.0 for k in sample}
|
|
60
|
+
return {k: v / total for k, v in sample.items()}
|