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.
- consequa_x/__init__.py +22 -0
- consequa_x/__main__.py +3 -0
- consequa_x/adapters/__init__.py +5 -0
- consequa_x/adapters/ollama.py +350 -0
- consequa_x/attribution.py +407 -0
- consequa_x/benchmark/__init__.py +1 -0
- consequa_x/benchmark/assessment_metrics.py +54 -0
- consequa_x/benchmark/calibration.py +787 -0
- consequa_x/benchmark/calibration_export.py +139 -0
- consequa_x/benchmark/calibration_mechanisms.py +266 -0
- consequa_x/benchmark/decision_coalition.py +254 -0
- consequa_x/benchmark/decision_contrastive.py +448 -0
- consequa_x/benchmark/decision_replay.py +329 -0
- consequa_x/benchmark/decision_shapley.py +628 -0
- consequa_x/benchmark/evaluation.py +187 -0
- consequa_x/benchmark/interaction_export.py +220 -0
- consequa_x/benchmark/interaction_mechanisms.py +192 -0
- consequa_x/benchmark/interval_comparison.py +276 -0
- consequa_x/benchmark/intervals.py +164 -0
- consequa_x/benchmark/payments.py +426 -0
- consequa_x/benchmark/policy_experiment.py +298 -0
- consequa_x/benchmark/policy_runner.py +234 -0
- consequa_x/benchmark/runner.py +241 -0
- consequa_x/benchmark/study.py +183 -0
- consequa_x/benchmark/targets.py +149 -0
- consequa_x/benchmark/templates.py +102 -0
- consequa_x/cli.py +1191 -0
- consequa_x/explanation.py +130 -0
- consequa_x/graph.py +142 -0
- consequa_x/models.py +532 -0
- consequa_x/policies.py +248 -0
- consequa_x/py.typed +0 -0
- consequa_x/schema.py +159 -0
- consequa_x/schemas/trace-v0.1.json +543 -0
- consequa_x/verification.py +192 -0
- consequa_x-0.1.0a2.dist-info/METADATA +303 -0
- consequa_x-0.1.0a2.dist-info/RECORD +41 -0
- consequa_x-0.1.0a2.dist-info/WHEEL +5 -0
- consequa_x-0.1.0a2.dist-info/entry_points.txt +2 -0
- consequa_x-0.1.0a2.dist-info/licenses/LICENSE +21 -0
- 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)
|