consequa-x 0.1.0a2__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 (41) hide show
  1. consequa_x/__init__.py +22 -0
  2. consequa_x/__main__.py +3 -0
  3. consequa_x/adapters/__init__.py +5 -0
  4. consequa_x/adapters/ollama.py +350 -0
  5. consequa_x/attribution.py +407 -0
  6. consequa_x/benchmark/__init__.py +1 -0
  7. consequa_x/benchmark/assessment_metrics.py +54 -0
  8. consequa_x/benchmark/calibration.py +787 -0
  9. consequa_x/benchmark/calibration_export.py +139 -0
  10. consequa_x/benchmark/calibration_mechanisms.py +266 -0
  11. consequa_x/benchmark/decision_coalition.py +254 -0
  12. consequa_x/benchmark/decision_contrastive.py +448 -0
  13. consequa_x/benchmark/decision_replay.py +329 -0
  14. consequa_x/benchmark/decision_shapley.py +628 -0
  15. consequa_x/benchmark/evaluation.py +187 -0
  16. consequa_x/benchmark/interaction_export.py +220 -0
  17. consequa_x/benchmark/interaction_mechanisms.py +192 -0
  18. consequa_x/benchmark/interval_comparison.py +276 -0
  19. consequa_x/benchmark/intervals.py +164 -0
  20. consequa_x/benchmark/payments.py +426 -0
  21. consequa_x/benchmark/policy_experiment.py +298 -0
  22. consequa_x/benchmark/policy_runner.py +234 -0
  23. consequa_x/benchmark/runner.py +241 -0
  24. consequa_x/benchmark/study.py +183 -0
  25. consequa_x/benchmark/targets.py +149 -0
  26. consequa_x/benchmark/templates.py +102 -0
  27. consequa_x/cli.py +1191 -0
  28. consequa_x/explanation.py +130 -0
  29. consequa_x/graph.py +142 -0
  30. consequa_x/models.py +532 -0
  31. consequa_x/policies.py +248 -0
  32. consequa_x/py.typed +0 -0
  33. consequa_x/schema.py +159 -0
  34. consequa_x/schemas/trace-v0.1.json +543 -0
  35. consequa_x/verification.py +192 -0
  36. consequa_x-0.1.0a2.dist-info/METADATA +303 -0
  37. consequa_x-0.1.0a2.dist-info/RECORD +41 -0
  38. consequa_x-0.1.0a2.dist-info/WHEEL +5 -0
  39. consequa_x-0.1.0a2.dist-info/entry_points.txt +2 -0
  40. consequa_x-0.1.0a2.dist-info/licenses/LICENSE +21 -0
  41. consequa_x-0.1.0a2.dist-info/top_level.txt +1 -0
@@ -0,0 +1,276 @@
1
+ """Frozen, same-draw interval comparisons and finite binomial calculations."""
2
+
3
+ from contextlib import ExitStack
4
+ from dataclasses import replace
5
+ from fractions import Fraction
6
+ import gzip
7
+ from hashlib import sha256
8
+ import math
9
+ from pathlib import Path
10
+
11
+ from .. import __version__
12
+ from ..models import to_dict
13
+ from .calibration import (
14
+ CalibrationPlan, CalibrationResult, INTERVAL_COMPARISON_CASES,
15
+ analyze_calibration, calibration_protocol, compact_calibration_trial,
16
+ )
17
+ from .calibration_export import _json, _write_json, export_calibration
18
+ from .decision_contrastive import with_contrastive_interval
19
+ from .decision_shapley import with_shapley_intervals
20
+ from .intervals import binomial_interval
21
+
22
+
23
+ CONTRASTIVE_VARIANTS = ("percentile_bootstrap", "wilson", "clopper_pearson")
24
+ SHAPLEY_VARIANTS = ("normal", "hoeffding_exact_endpoints")
25
+ PROBABILITY_GRID = tuple(Fraction(value) for value in (
26
+ "0", ".01", ".05", ".1", ".25", ".5", ".75", ".9", ".95", ".99", "1"))
27
+ SAMPLE_GRID = (8, 16, 32, 64)
28
+
29
+
30
+ def comparison_plan(*, repeats=32, seed=2_000_000, **settings):
31
+ """Fresh integration defaults; the legacy replay methods supply shared draws."""
32
+ return CalibrationPlan(repeats=repeats, seed=seed, mechanisms=INTERVAL_COMPARISON_CASES,
33
+ **settings)
34
+
35
+
36
+ def _bootstrap_limit_interval(successes, total, confidence):
37
+ """Infinite-resample percentile limits for the empirical binomial law.
38
+
39
+ This is NOT exact coverage of a particular finite seeded bootstrap run.
40
+ Finite bootstrap intervals are separately compared on fresh actual traces.
41
+ """
42
+ p = Fraction(successes, total)
43
+ tail = Fraction.from_float((1-confidence)/2)
44
+ cumulative = Fraction(0)
45
+ lower = upper = None
46
+ for count in range(total+1):
47
+ cumulative += math.comb(total, count) * p**count * (1-p)**(total-count)
48
+ if lower is None and cumulative >= tail:
49
+ lower = count / total
50
+ if upper is None and cumulative >= 1-tail:
51
+ upper = count / total
52
+ return lower, upper
53
+
54
+
55
+ def finite_binomial_comparison(*, confidence=.95):
56
+ """Enumerate every success count using exact rational binomial weights."""
57
+ # Reuse public input validation before any artifact is created.
58
+ binomial_interval(0, 1, confidence=confidence)
59
+ rows = []
60
+ count_intervals = []
61
+ for total in SAMPLE_GRID:
62
+ methods = {
63
+ "percentile_bootstrap_limit": [_bootstrap_limit_interval(k, total, confidence)
64
+ for k in range(total+1)],
65
+ "wilson": [binomial_interval(k, total, confidence=confidence, method="wilson")
66
+ for k in range(total+1)],
67
+ "clopper_pearson": [binomial_interval(k, total, confidence=confidence)
68
+ for k in range(total+1)],
69
+ }
70
+ for method, intervals in methods.items():
71
+ count_intervals.append({"samples": total, "method": method,
72
+ "intervals_by_success_count": intervals})
73
+ for q in PROBABILITY_GRID:
74
+ weights = [math.comb(total, k)*q**k*(1-q)**(total-k) for k in range(total+1)]
75
+ assert sum(weights) == 1
76
+ covered = sum(weight for weight, (low, high) in zip(weights, intervals)
77
+ if low <= float(q) <= high)
78
+ degenerate = sum(weight for weight, (low, high) in zip(weights, intervals)
79
+ if low == high)
80
+ rows.append({"samples": total, "probability": float(q), "method": method,
81
+ "coverage": float(covered),
82
+ "coverage_fraction": [covered.numerator, covered.denominator],
83
+ "expected_width": math.fsum(float(weight)*(high-low)
84
+ for weight, (low, high) in zip(weights, intervals)),
85
+ "zero_width_probability": float(degenerate),
86
+ "zero_count_interval": intervals[0], "full_count_interval": intervals[-1],
87
+ "probability_mass": 1})
88
+ return {"kind": "finite_binomial_interval_comparison_v1", "confidence": confidence,
89
+ "sample_grid": SAMPLE_GRID, "probability_grid": [float(q) for q in PROBABILITY_GRID],
90
+ "rows": rows, "count_intervals": count_intervals,
91
+ "arithmetic": "exact rational probability weights; floating-point interval boundaries",
92
+ "bootstrap_scope": "limiting empirical bootstrap; finite seeded bootstrap is tested in integration",
93
+ "scope": "independent known Bernoulli draws, constant observed factual baseline",
94
+ "limits": ["No multiplicity correction, unknown evidence, model generalization or seed independence established.",
95
+ "Clopper-Pearson classification relies on numerically validated binomial inversion."]}
96
+
97
+
98
+ def _contrastive_record(result):
99
+ return {"summary": result.summary(), "method": result.method_record(),
100
+ "results": [item.to_dict() for item in result.results], "loci": to_dict(result.loci)}
101
+
102
+
103
+ def _shapley_record(result):
104
+ return {"summary": result.summary(), "method": result.method_record(),
105
+ "results": [item.to_dict() for item in result.results],
106
+ "endpoint_checks": to_dict(result.endpoint_checks)}
107
+
108
+
109
+ def _manifest(output):
110
+ artifacts = [{"path": path.relative_to(output).as_posix(),
111
+ "sha256": sha256(path.read_bytes()).hexdigest(), "bytes": path.stat().st_size}
112
+ for path in sorted(output.rglob("*")) if path.is_file() and path != output/"manifest.json"]
113
+ _write_json(output/"manifest.json", {"package_version": __version__, "status": "completed",
114
+ "artifacts": artifacts})
115
+
116
+
117
+ def export_interval_comparison(plan: CalibrationPlan, output, *, source_revision=None, theory_only=False):
118
+ """Declare first, collect one unchanged replay dataset, then compare intervals.
119
+
120
+ Full policy/world evidence is retained once under raw/. Every interval uses
121
+ exactly that dataset. Compact comparison receipts identify its trial and
122
+ cannot spend a second policy-call allowance or turn a refusal into a point.
123
+ """
124
+ protocol = calibration_protocol(plan)
125
+ if (plan.contrastive_interval_method != "percentile_bootstrap"
126
+ or plan.shapley_interval_method != "normal" or plan.endpoint_interval_method != "normal"):
127
+ raise ValueError("comparison collection must preserve the original bootstrap/normal methods")
128
+ if source_revision is not None and (type(source_revision) is not str or not source_revision.strip()):
129
+ raise ValueError("source_revision must be a nonempty identifier or None")
130
+ output = Path(output)
131
+ if output.exists() and (not output.is_dir() or any(output.iterdir())):
132
+ raise ValueError("compare-intervals requires a new or empty output directory")
133
+ output.mkdir(parents=True, exist_ok=True)
134
+ _write_json(output/"protocol.json", {"package_version": __version__, **protocol,
135
+ "study_kind": "same_draw_interval_comparison_v1", "source_revision": source_revision,
136
+ "theory_only": theory_only, "contrastive_variants": CONTRASTIVE_VARIANTS,
137
+ "shapley_variants": SHAPLEY_VARIANTS, "additional_policy_calls_for_comparison": 0,
138
+ "finite_sample_grid": SAMPLE_GRID, "finite_probability_grid": [float(q) for q in PROBABILITY_GRID],
139
+ "retention": "all raw trials plus all numerical variants; full raw evidence preselected at replicate 0"})
140
+ finite = finite_binomial_comparison(confidence=plan.confidence)
141
+ _write_json(output/"finite-binomial.json", finite)
142
+ if theory_only:
143
+ (output/"README.md").write_text(
144
+ "# Finite binomial interval comparison\n\n"
145
+ "Every binomial count was enumerated on the predeclared sample/probability grid. "
146
+ "Probability weights are exact rational values; interval boundaries use floating-point arithmetic. "
147
+ "The bootstrap column is its infinite-resample limit, not a finite seeded bootstrap guarantee. "
148
+ "No payment replay or model was run.\n", encoding="utf-8", newline="\n")
149
+ _manifest(output)
150
+ return {"finite_binomial": finite, "actual_policy_calls": 0}
151
+
152
+ compact_variants = {name: [] for name in CONTRASTIVE_VARIANTS + SHAPLEY_VARIANTS[1:]}
153
+ with ExitStack() as stack:
154
+ streams = {}
155
+ for name in plan.mechanisms:
156
+ path = output/"comparisons"/f"{name}.jsonl.gz"
157
+ path.parent.mkdir(parents=True, exist_ok=True)
158
+ raw = stack.enter_context(path.open("wb"))
159
+ streams[name] = stack.enter_context(gzip.GzipFile(filename="", fileobj=raw, mode="wb", mtime=0))
160
+
161
+ def compare(trial):
162
+ base = compact_calibration_trial(trial)
163
+ c_records = {}
164
+ s_records = {}
165
+ for name in CONTRASTIVE_VARIANTS:
166
+ if trial.contrastive is None:
167
+ compact_variants[name].append(base)
168
+ c_records[name] = None
169
+ continue
170
+ result = with_contrastive_interval(trial.contrastive, interval_method=name)
171
+ fields = _contrastive_record(result)
172
+ c_records[name] = fields
173
+ compact_variants[name].append(replace(base, contrastive={**base.contrastive, **fields}))
174
+ if trial.shapley is not None:
175
+ result = with_shapley_intervals(trial.shapley, interval_method="hoeffding",
176
+ endpoint_interval_method="clopper_pearson_difference")
177
+ fields = _shapley_record(result)
178
+ s_records["normal"] = _shapley_record(trial.shapley)
179
+ s_records["hoeffding_exact_endpoints"] = fields
180
+ compact_variants["hoeffding_exact_endpoints"].append(
181
+ replace(base, shapley={**base.shapley, **fields}))
182
+ else:
183
+ s_records = dict.fromkeys(SHAPLEY_VARIANTS)
184
+ compact_variants["hoeffding_exact_endpoints"].append(base)
185
+ record = {"seed_plan": to_dict(trial.seed_plan),
186
+ "raw_evidence": f"raw/trials/{trial.seed_plan.mechanism}.jsonl.gz",
187
+ "replicate": trial.seed_plan.replicate, "contrastive": c_records,
188
+ "shapley": s_records, "calls": base.to_dict()["calls"],
189
+ "additional_policy_calls": 0}
190
+ streams[trial.seed_plan.mechanism].write((_json(record)+"\n").encode("utf-8"))
191
+
192
+ raw_result = export_calibration(plan, output/"raw", source_revision=source_revision, on_trial=compare)
193
+
194
+ original = raw_result.summary()
195
+ summaries = {name: analyze_calibration(CalibrationResult(plan, tuple(trials)))
196
+ for name, trials in compact_variants.items()}
197
+ summary = {"package_version": __version__, "kind": "same_draw_interval_comparison_v1",
198
+ "source_revision": source_revision, "planned_trials": original["planned_trials"],
199
+ "actual_policy_calls": original["actual_policy_calls"],
200
+ "additional_policy_calls_for_comparison": 0,
201
+ "contrastive": {name: {"estimates": [row for row in summaries[name]["estimates"]
202
+ if row["method"] == "contrastive"],
203
+ "loci": summaries[name]["loci"]} for name in CONTRASTIVE_VARIANTS},
204
+ "shapley": {"normal": {"estimates": [row for row in original["estimates"]
205
+ if row["method"] == "shapley"],
206
+ "endpoints": original["endpoints"]},
207
+ "hoeffding_exact_endpoints": {
208
+ "estimates": [row for row in summaries["hoeffding_exact_endpoints"]["estimates"]
209
+ if row["method"] == "shapley"],
210
+ "endpoints": summaries["hoeffding_exact_endpoints"]["endpoints"]}},
211
+ "raw_summary": "raw/summary.json",
212
+ "limits": ["Every variant shares points, target labels, seeds, actual calls and all refusals.",
213
+ "Coverage requires analytic eligibility; a completed invalid-population game remains unscored.",
214
+ "Conservative intervals can be substantially wider; no model or task-generalization claim.",
215
+ "Within-method independence is an assumption; requested RNG keys do not prove it."]}
216
+ _write_json(output/"summary.json", summary)
217
+ _write_report(output, plan, summary, finite)
218
+ _manifest(output)
219
+ print(f"Saved same-draw interval comparison in {output.resolve()}")
220
+ return summary
221
+
222
+
223
+ def _write_report(output, plan, summary, finite):
224
+ def coverage(row):
225
+ value = row["interval_coverage"]
226
+ n, d = value["numerator"], value["denominator"]
227
+ return f"{n}/{d}" if d else "unavailable"
228
+
229
+ lines = ["# Same-draw interval comparison\n",
230
+ f"{summary['planned_trials']} predeclared synthetic trials, {plan.repeats} repetitions per case. "
231
+ "No language model was called. All variants reuse the same real refund-world evidence; "
232
+ "interval comparisons spent zero additional policy calls.\n",
233
+ "## Consequence coverage at the timeout position\n",
234
+ "| Mechanism | Bootstrap | Wilson | Exact binomial | Normal Shapley | Hoeffding Shapley |",
235
+ "| --- | --- | --- | --- | --- | --- |"]
236
+ for mechanism in plan.mechanisms:
237
+ row_values = []
238
+ for name in CONTRASTIVE_VARIANTS:
239
+ rows = summary["contrastive"][name]["estimates"]
240
+ row_values.append(coverage(next(r for r in rows if r["mechanism"] == mechanism
241
+ and r["target"] == "consequence" and r["decision_index"] == 1)))
242
+ for name in SHAPLEY_VARIANTS:
243
+ rows = summary["shapley"][name]["estimates"]
244
+ row_values.append(coverage(next(r for r in rows if r["mechanism"] == mechanism
245
+ and r["target"] == "consequence" and r["decision_index"] == 1)))
246
+ lines.append("| " + " | ".join([mechanism, *row_values]) + " |")
247
+ minima = {name: min(r["coverage"] for r in finite["rows"] if r["method"] == name)
248
+ for name in ("percentile_bootstrap_limit", "wilson", "clopper_pearson")}
249
+ lines += ["", "Coverage denominators include available eligible intervals only. Availability and "
250
+ "covered/planned eligible composites remain separate in `summary.json`. The state and "
251
+ "linked targets share evidence and are not pooled as independent observations.\n",
252
+ "## Finite distribution calculation\n",
253
+ "The predeclared grid uses n=8,16,32,64 and eleven probabilities including zero and one. "
254
+ "Minimum coverage over that grid: " + ", ".join(f"{name} {100*value:.2f}%" for name, value in minima.items()) + ". "
255
+ "The bootstrap entry is its infinite-resample limit; actual seeded bootstrap intervals "
256
+ "are separately measured above. Exact probability weights do not make a finite "
257
+ "observed coverage proportion a population guarantee.\n",
258
+ "## Width, refusals and interpretation\n",
259
+ "Exact binomial and Hoeffding options retain uncertainty at homogeneous samples and "
260
+ "can be much wider than bootstrap/normal intervals. Every row retains mean interval "
261
+ "width, point bias/RMSE, zero-width frequency and explicit refusal counts. "
262
+ "No point, draw, locus rule or budget was changed. Different interval bounds can "
263
+ "change nominal locus selections; all such selections are retained.\n",
264
+ "The strict three-position retry game can be population-undefined. Available vectors "
265
+ "there remain ineligible for numerical scoring. Unknown readback/provenance and "
266
+ "budget failures remain refusals under every method. Conservative bounds do not "
267
+ "repair undefined interventions or missing evidence.\n",
268
+ "Binomial bounds require independent known binary draws; Hoeffding requires "
269
+ "independent bounded pair observations from a defined game. Requested seeds do not "
270
+ "establish these assumptions for live models. Intervals remain individual, with "
271
+ "no multiplicity correction across decisions or targets.\n",
272
+ "Raw public traces and explanations for replicate 0 are under `raw/evidence/`; "
273
+ "simulator gold is separate. All planned raw draws and numerical variants are "
274
+ "compressed JSONL. Both protocols were saved before policy calls. Byte manifests "
275
+ "detect changes, not authenticity.\n"]
276
+ (output/"README.md").write_text("\n".join(lines), encoding="utf-8", newline="\n")
@@ -0,0 +1,164 @@
1
+ """Conditional intervals for known binary outcomes and bounded pair means.
2
+
3
+ These are independently implemented standard statistical methods. The binomial
4
+ and concentration statements require independent draws from the declared law;
5
+ requested seeds, or observing only complete replays, do not establish that law.
6
+ """
7
+
8
+ import math
9
+ from functools import lru_cache
10
+ from statistics import NormalDist
11
+ from typing import Iterable, Literal
12
+
13
+
14
+ BinomialMethod = Literal["wilson", "clopper_pearson"]
15
+ BINOMIAL_METHODS = ("wilson", "clopper_pearson")
16
+
17
+
18
+ def _confidence(value):
19
+ try:
20
+ valid = type(value) in (int, float) and math.isfinite(value) and 0 < value < 1
21
+ except OverflowError:
22
+ valid = False
23
+ if not valid:
24
+ raise ValueError("confidence must be finite and strictly between 0 and 1")
25
+
26
+
27
+ def _counts(successes, total):
28
+ if type(total) is not int or total < 1:
29
+ raise ValueError("total must be a positive integer")
30
+ if type(successes) is not int or not 0 <= successes <= total:
31
+ raise ValueError("successes must be an integer between zero and total")
32
+
33
+
34
+ def _log_binomial_sum(total, probability, first, last):
35
+ """Log probability on an inclusive count range, summed about its mode.
36
+
37
+ Adjacent weights are scaled by the largest term on the requested range.
38
+ Both tails are evaluated directly, avoiding subtraction of a tiny tail
39
+ from one. Ratios decrease away from the mode and never overflow.
40
+ """
41
+ if probability == 0:
42
+ return 0. if first == 0 else -math.inf
43
+ if probability == 1:
44
+ return 0. if last == total else -math.inf
45
+ mode = max(first, min(last, math.floor((total + 1) * probability)))
46
+ log_mass = (math.lgamma(total + 1) - math.lgamma(mode + 1)
47
+ - math.lgamma(total - mode + 1) + mode * math.log(probability)
48
+ + (total - mode) * math.log1p(-probability))
49
+
50
+ def weights():
51
+ yield 1.
52
+ term = 1.
53
+ for count in range(mode, first, -1):
54
+ term *= (count / (total - count + 1)) * ((1 - probability) / probability)
55
+ if term == 0:
56
+ break
57
+ yield term
58
+ term = 1.
59
+ for count in range(mode, last):
60
+ term *= ((total - count) / (count + 1)) * (probability / (1 - probability))
61
+ if term == 0:
62
+ break
63
+ yield term
64
+
65
+ return min(0., log_mass + math.log(math.fsum(weights())))
66
+
67
+
68
+ @lru_cache(maxsize=4096)
69
+ def _tail_root(total, successes, log_tail, *, lower):
70
+ lo, hi = 0., 1.
71
+ for _ in range(160):
72
+ mid = lo + (hi - lo) / 2
73
+ if mid == lo or mid == hi:
74
+ break
75
+ value = (_log_binomial_sum(total, mid, successes, total) if lower else
76
+ _log_binomial_sum(total, mid, 0, successes))
77
+ if (value >= log_tail) == lower:
78
+ hi = mid
79
+ else:
80
+ lo = mid
81
+ # Choose the outward root bracket. The finite numerical implementation is
82
+ # validated against independent high-precision binomial calculations.
83
+ return lo if lower else hi
84
+
85
+
86
+ def binomial_interval(successes: int, total: int, *, confidence: float = .95,
87
+ method: BinomialMethod = "clopper_pearson") -> tuple[float, float]:
88
+ """Interval for a known binomial probability; endpoints remain exactly 0/1.
89
+
90
+ Wilson is a score approximation. Equal-tailed Clopper–Pearson inverts the
91
+ binomial tails and is conservative under the independent binomial model.
92
+ It does not model an uncertain factual baseline or adjust multiplicity.
93
+ """
94
+ _counts(successes, total)
95
+ _confidence(confidence)
96
+ if type(method) is not str or method not in BINOMIAL_METHODS:
97
+ raise ValueError("method must be wilson or clopper_pearson")
98
+ probability = successes / total
99
+ tail = (1 - confidence) / 2
100
+ if method == "wilson":
101
+ z = -NormalDist().inv_cdf(tail)
102
+ scale = 1 + z * z / total
103
+ center = (probability + z * z / (2 * total)) / scale
104
+ margin = z / scale * math.sqrt(
105
+ probability * (1 - probability) / total + z * z / (4 * total * total))
106
+ return (0. if successes == 0 else max(0., center - margin),
107
+ 1. if successes == total else min(1., center + margin))
108
+ log_tail = math.log(tail)
109
+ if successes == 0:
110
+ upper = min(1., math.nextafter(-math.expm1(log_tail / total), 1.))
111
+ return 0., upper
112
+ if successes == total:
113
+ lower = max(0., math.nextafter(math.exp(log_tail / total), 0.))
114
+ return lower, 1.
115
+ return (_tail_root(total, successes, log_tail, lower=True),
116
+ _tail_root(total, successes, log_tail, lower=False))
117
+
118
+
119
+ def binomial_difference_interval(full_successes: int, full_total: int,
120
+ empty_successes: int, empty_total: int, *,
121
+ confidence: float = .95) -> tuple[float, float]:
122
+ """Full-minus-empty interval using two equal-tailed exact marginal intervals.
123
+
124
+ Each marginal receives half the failure allowance. A union bound covers
125
+ their difference at least nominally under each binomial marginal model;
126
+ independence BETWEEN marginals is not required for that union bound.
127
+ """
128
+ _counts(full_successes, full_total)
129
+ _counts(empty_successes, empty_total)
130
+ _confidence(confidence)
131
+ # Near confidence one, (1 + confidence)/2 can round to one. Compute the
132
+ # halved marginal tail directly instead of manufacturing an invalid level.
133
+ log_tail = math.log((1 - confidence) / 4)
134
+
135
+ def marginal(successes, total):
136
+ lower = (0. if successes == 0 else math.nextafter(math.exp(log_tail / total), 0.)
137
+ if successes == total else _tail_root(total, successes, log_tail, lower=True))
138
+ upper = (1. if successes == total else math.nextafter(-math.expm1(log_tail / total), 1.)
139
+ if successes == 0 else _tail_root(total, successes, log_tail, lower=False))
140
+ return max(0., lower), min(1., upper)
141
+
142
+ full = marginal(full_successes, full_total)
143
+ empty = marginal(empty_successes, empty_total)
144
+ return full[0] - empty[1], full[1] - empty[0]
145
+
146
+
147
+ def bounded_mean_interval(values: Iterable[float], *, confidence: float = .95,
148
+ lower: float = -1., upper: float = 1.) -> tuple[float, float]:
149
+ """Two-sided Hoeffding interval for an independent bounded observation mean.
150
+
151
+ One Shapley forward/reverse pair is one observation. This uses its declared
152
+ [-1,1] range and does not infer certainty from zero sample variance.
153
+ """
154
+ _confidence(confidence)
155
+ if (type(lower) not in (int, float) or type(upper) not in (int, float)
156
+ or not math.isfinite(lower) or not math.isfinite(upper) or lower >= upper):
157
+ raise ValueError("bounds must be finite numbers with lower < upper")
158
+ observed = tuple(values)
159
+ if not observed or any(type(value) not in (int, float) or not math.isfinite(value)
160
+ or not lower <= value <= upper for value in observed):
161
+ raise ValueError("values must be nonempty finite observations within the bounds")
162
+ point = math.fsum(value / len(observed) for value in observed)
163
+ width = (upper - lower) * math.sqrt(math.log(2 / (1 - confidence)) / (2 * len(observed)))
164
+ return max(lower, point - width), min(upper, point + width)