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.
Files changed (78) hide show
  1. synthbench/__init__.py +16 -0
  2. synthbench/__main__.py +5 -0
  3. synthbench/adapter.py +131 -0
  4. synthbench/anomaly.py +503 -0
  5. synthbench/baseline_floors.py +213 -0
  6. synthbench/baselines.py +902 -0
  7. synthbench/cli.py +2929 -0
  8. synthbench/config_id.py +424 -0
  9. synthbench/contamination.py +783 -0
  10. synthbench/convergence/__init__.py +67 -0
  11. synthbench/convergence/baseline.py +172 -0
  12. synthbench/convergence/bootstrap.py +60 -0
  13. synthbench/convergence/cli_report.py +556 -0
  14. synthbench/convergence/curves.py +111 -0
  15. synthbench/convergence/real_sampling.py +146 -0
  16. synthbench/convergence/thresholds.py +49 -0
  17. synthbench/datasets/__init__.py +37 -0
  18. synthbench/datasets/base.py +147 -0
  19. synthbench/datasets/eurobarometer.py +335 -0
  20. synthbench/datasets/globalopinionqa.py +252 -0
  21. synthbench/datasets/gss.py +323 -0
  22. synthbench/datasets/michigan.py +505 -0
  23. synthbench/datasets/ntia.py +357 -0
  24. synthbench/datasets/opinionsqa.py +412 -0
  25. synthbench/datasets/pewtech.py +334 -0
  26. synthbench/datasets/policy.py +142 -0
  27. synthbench/datasets/subpop.py +302 -0
  28. synthbench/datasets/wvs.py +229 -0
  29. synthbench/findings.py +988 -0
  30. synthbench/holdout.py +347 -0
  31. synthbench/human_distributions.py +243 -0
  32. synthbench/leaderboard.py +715 -0
  33. synthbench/leaderboard_pr.py +274 -0
  34. synthbench/metrics/__init__.py +38 -0
  35. synthbench/metrics/composite.py +72 -0
  36. synthbench/metrics/conditioning.py +39 -0
  37. synthbench/metrics/distributional.py +41 -0
  38. synthbench/metrics/ranking.py +37 -0
  39. synthbench/metrics/refusal.py +270 -0
  40. synthbench/metrics/subgroup.py +58 -0
  41. synthbench/private_holdout.py +240 -0
  42. synthbench/providers/__init__.py +43 -0
  43. synthbench/providers/_parsing.py +150 -0
  44. synthbench/providers/_retry.py +107 -0
  45. synthbench/providers/base.py +212 -0
  46. synthbench/providers/http.py +108 -0
  47. synthbench/providers/majority_baseline.py +23 -0
  48. synthbench/providers/ollama.py +109 -0
  49. synthbench/providers/openrouter.py +144 -0
  50. synthbench/providers/population_baseline.py +87 -0
  51. synthbench/providers/random_baseline.py +24 -0
  52. synthbench/providers/raw_anthropic.py +149 -0
  53. synthbench/providers/raw_gemini.py +145 -0
  54. synthbench/providers/raw_openai.py +139 -0
  55. synthbench/providers/synthpanel.py +978 -0
  56. synthbench/publish.py +2772 -0
  57. synthbench/r2_upload.py +178 -0
  58. synthbench/recompute.py +235 -0
  59. synthbench/report.py +493 -0
  60. synthbench/run_hash.py +111 -0
  61. synthbench/run_validity.py +232 -0
  62. synthbench/runner.py +834 -0
  63. synthbench/stats.py +1492 -0
  64. synthbench/submission.py +284 -0
  65. synthbench/submission_pr.py +436 -0
  66. synthbench/submit_adapter.py +748 -0
  67. synthbench/suite.py +424 -0
  68. synthbench/suites/__init__.py +94 -0
  69. synthbench/topics.py +256 -0
  70. synthbench/user_config.py +409 -0
  71. synthbench/validation.py +1556 -0
  72. synthbench/visualize.py +455 -0
  73. synthbench_eval-0.4.0.dist-info/METADATA +279 -0
  74. synthbench_eval-0.4.0.dist-info/RECORD +78 -0
  75. synthbench_eval-0.4.0.dist-info/WHEEL +5 -0
  76. synthbench_eval-0.4.0.dist-info/entry_points.txt +2 -0
  77. synthbench_eval-0.4.0.dist-info/licenses/LICENSE +21 -0
  78. 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()}