views-frames 1.8.0__tar.gz → 1.9.0__tar.gz
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.
- {views_frames-1.8.0 → views_frames-1.9.0}/.gitignore +3 -0
- {views_frames-1.8.0 → views_frames-1.9.0}/PKG-INFO +10 -3
- {views_frames-1.8.0 → views_frames-1.9.0}/README.md +9 -2
- {views_frames-1.8.0 → views_frames-1.9.0}/pyproject.toml +1 -1
- {views_frames-1.8.0 → views_frames-1.9.0}/src/views_frames/conformance/__init__.py +18 -0
- {views_frames-1.8.0 → views_frames-1.9.0}/src/views_frames/index.py +5 -0
- {views_frames-1.8.0 → views_frames-1.9.0}/src/views_frames_reconcile/conformance.py +14 -0
- {views_frames-1.8.0 → views_frames-1.9.0}/src/views_frames_reconcile/proportional.py +28 -5
- {views_frames-1.8.0 → views_frames-1.9.0}/src/views_frames_summarize/config.py +9 -3
- {views_frames-1.8.0 → views_frames-1.9.0}/src/views_frames_summarize/conformance.py +40 -0
- {views_frames-1.8.0 → views_frames-1.9.0}/src/views_frames_summarize/tower_point.py +9 -3
- {views_frames-1.8.0 → views_frames-1.9.0}/LICENSE +0 -0
- {views_frames-1.8.0 → views_frames-1.9.0}/src/views_frames/__init__.py +0 -0
- {views_frames-1.8.0 → views_frames-1.9.0}/src/views_frames/_typing.py +0 -0
- {views_frames-1.8.0 → views_frames-1.9.0}/src/views_frames/_validation.py +0 -0
- {views_frames-1.8.0 → views_frames-1.9.0}/src/views_frames/feature_frame.py +0 -0
- {views_frames-1.8.0 → views_frames-1.9.0}/src/views_frames/io/__init__.py +0 -0
- {views_frames-1.8.0 → views_frames-1.9.0}/src/views_frames/io/arrow.py +0 -0
- {views_frames-1.8.0 → views_frames-1.9.0}/src/views_frames/io/npz.py +0 -0
- {views_frames-1.8.0 → views_frames-1.9.0}/src/views_frames/metadata.py +0 -0
- {views_frames-1.8.0 → views_frames-1.9.0}/src/views_frames/prediction_frame.py +0 -0
- {views_frames-1.8.0 → views_frames-1.9.0}/src/views_frames/protocols.py +0 -0
- {views_frames-1.8.0 → views_frames-1.9.0}/src/views_frames/py.typed +0 -0
- {views_frames-1.8.0 → views_frames-1.9.0}/src/views_frames/spatial_level.py +0 -0
- {views_frames-1.8.0 → views_frames-1.9.0}/src/views_frames/target_frame.py +0 -0
- {views_frames-1.8.0 → views_frames-1.9.0}/src/views_frames_reconcile/__init__.py +0 -0
- {views_frames-1.8.0 → views_frames-1.9.0}/src/views_frames_reconcile/frames.py +0 -0
- {views_frames-1.8.0 → views_frames-1.9.0}/src/views_frames_reconcile/grouping.py +0 -0
- {views_frames-1.8.0 → views_frames-1.9.0}/src/views_frames_reconcile/module.py +0 -0
- {views_frames-1.8.0 → views_frames-1.9.0}/src/views_frames_reconcile/py.typed +0 -0
- {views_frames-1.8.0 → views_frames-1.9.0}/src/views_frames_reconcile/result.py +0 -0
- {views_frames-1.8.0 → views_frames-1.9.0}/src/views_frames_reconcile/validation.py +0 -0
- {views_frames-1.8.0 → views_frames-1.9.0}/src/views_frames_summarize/__init__.py +0 -0
- {views_frames-1.8.0 → views_frames-1.9.0}/src/views_frames_summarize/_common.py +0 -0
- {views_frames-1.8.0 → views_frames-1.9.0}/src/views_frames_summarize/aggregate.py +0 -0
- {views_frames-1.8.0 → views_frames-1.9.0}/src/views_frames_summarize/bimodality.py +0 -0
- {views_frames-1.8.0 → views_frames-1.9.0}/src/views_frames_summarize/collapse.py +0 -0
- {views_frames-1.8.0 → views_frames-1.9.0}/src/views_frames_summarize/exceedance.py +0 -0
- {views_frames-1.8.0 → views_frames-1.9.0}/src/views_frames_summarize/expected_shortfall.py +0 -0
- {views_frames-1.8.0 → views_frames-1.9.0}/src/views_frames_summarize/interval.py +0 -0
- {views_frames-1.8.0 → views_frames-1.9.0}/src/views_frames_summarize/point.py +0 -0
- {views_frames-1.8.0 → views_frames-1.9.0}/src/views_frames_summarize/py.typed +0 -0
- {views_frames-1.8.0 → views_frames-1.9.0}/src/views_frames_summarize/summarize_tower.py +0 -0
- {views_frames-1.8.0 → views_frames-1.9.0}/src/views_frames_summarize/tower.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: views-frames
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.9.0
|
|
4
4
|
Summary: The VIEWS platform data-contract layer: immutable array+identifier frames (numpy only, root of the dependency DAG).
|
|
5
5
|
Project-URL: Homepage, https://github.com/views-platform/views-frames
|
|
6
6
|
Project-URL: Repository, https://github.com/views-platform/views-frames
|
|
@@ -36,14 +36,21 @@ Description-Content-Type: text/markdown
|
|
|
36
36
|
> containers (`FeatureFrame`, `PredictionFrame`, and their anticipated siblings)
|
|
37
37
|
> that every other repo depends on and that depends on nothing internal.
|
|
38
38
|
>
|
|
39
|
-
> **Status:** **v1.
|
|
39
|
+
> **Status:** **v1.9.0 — frozen API, published to PyPI** (frozen since v1.0.0, ADR-018; the
|
|
40
|
+
> v1.1 surface is
|
|
40
41
|
> purely additive — the coherent posterior summary, ADR-019; v1.2.0 rebuilt the tower
|
|
41
42
|
> `outside-in`, C-44; v1.3.0 makes the tower summary distribution-agnostic — no magnitude
|
|
42
43
|
> zeroing by default, register C-45; v1.4.0 adds generic provenance to `FrameMetadata`
|
|
43
44
|
> (`run_id`/`data_version`) and publishes the shared `assert_frame_envelope` checker,
|
|
44
45
|
> ADR-020; v1.5.0 adds the threshold **exceedance** estimator `P(Y > c)`, ADR-021; v1.6.0
|
|
45
46
|
> adds the worst-case **expected_shortfall** estimator, ADR-022; v1.7.0 adds a third
|
|
46
|
-
> sibling package **`views_frames_reconcile`** — forecast reconciliation, ADR-023
|
|
47
|
+
> sibling package **`views_frames_reconcile`** — forecast reconciliation, ADR-023; v1.8.0
|
|
48
|
+
> adds the native point-country broadcast + the self-describing reconciliation mode,
|
|
49
|
+
> the three showcase notebooks, and the frames-family hardening pass — Reconcile.md CIC,
|
|
50
|
+
> ADR-025 immutability-by-convention, the adversarial red-test batch; v1.9.0 moves the
|
|
51
|
+
> tower-tip MAP to the **top floor** of the published tower — `tip_mass` 0.25 with the
|
|
52
|
+
> **MAP-containment law** in the conformance suite, ADR-019 Amendment 3 — and adds the
|
|
53
|
+
> `research/figures/` tower-figure toolkit). This
|
|
47
54
|
> README is the design
|
|
48
55
|
> bible; the contract it specifies is realised in `src/views_frames/` (index, frames,
|
|
49
56
|
> io, conformance suite) plus the `src/views_frames_summarize/` sibling package
|
|
@@ -4,14 +4,21 @@
|
|
|
4
4
|
> containers (`FeatureFrame`, `PredictionFrame`, and their anticipated siblings)
|
|
5
5
|
> that every other repo depends on and that depends on nothing internal.
|
|
6
6
|
>
|
|
7
|
-
> **Status:** **v1.
|
|
7
|
+
> **Status:** **v1.9.0 — frozen API, published to PyPI** (frozen since v1.0.0, ADR-018; the
|
|
8
|
+
> v1.1 surface is
|
|
8
9
|
> purely additive — the coherent posterior summary, ADR-019; v1.2.0 rebuilt the tower
|
|
9
10
|
> `outside-in`, C-44; v1.3.0 makes the tower summary distribution-agnostic — no magnitude
|
|
10
11
|
> zeroing by default, register C-45; v1.4.0 adds generic provenance to `FrameMetadata`
|
|
11
12
|
> (`run_id`/`data_version`) and publishes the shared `assert_frame_envelope` checker,
|
|
12
13
|
> ADR-020; v1.5.0 adds the threshold **exceedance** estimator `P(Y > c)`, ADR-021; v1.6.0
|
|
13
14
|
> adds the worst-case **expected_shortfall** estimator, ADR-022; v1.7.0 adds a third
|
|
14
|
-
> sibling package **`views_frames_reconcile`** — forecast reconciliation, ADR-023
|
|
15
|
+
> sibling package **`views_frames_reconcile`** — forecast reconciliation, ADR-023; v1.8.0
|
|
16
|
+
> adds the native point-country broadcast + the self-describing reconciliation mode,
|
|
17
|
+
> the three showcase notebooks, and the frames-family hardening pass — Reconcile.md CIC,
|
|
18
|
+
> ADR-025 immutability-by-convention, the adversarial red-test batch; v1.9.0 moves the
|
|
19
|
+
> tower-tip MAP to the **top floor** of the published tower — `tip_mass` 0.25 with the
|
|
20
|
+
> **MAP-containment law** in the conformance suite, ADR-019 Amendment 3 — and adds the
|
|
21
|
+
> `research/figures/` tower-figure toolkit). This
|
|
15
22
|
> README is the design
|
|
16
23
|
> bible; the contract it specifies is realised in `src/views_frames/` (index, frames,
|
|
17
24
|
> io, conformance suite) plus the `src/views_frames_summarize/` sibling package
|
|
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "views-frames"
|
|
7
|
-
version = "1.
|
|
7
|
+
version = "1.9.0"
|
|
8
8
|
description = "The VIEWS platform data-contract layer: immutable array+identifier frames (numpy only, root of the dependency DAG)."
|
|
9
9
|
authors = [
|
|
10
10
|
{ name = "Simon Polichinel von der Maase", email = "simmaa@prio.org" },
|
|
@@ -40,6 +40,20 @@ __all__ = [
|
|
|
40
40
|
]
|
|
41
41
|
|
|
42
42
|
|
|
43
|
+
def _require_assertions() -> None:
|
|
44
|
+
"""Fail loud if assertions are stripped (``python -O``/``-OO``).
|
|
45
|
+
|
|
46
|
+
The suite is built from ``assert`` statements; under optimized bytecode every
|
|
47
|
+
check would silently pass regardless of conformance — a verification tool that
|
|
48
|
+
reports green while dead (falsify audit 2026-07, F3). Refuse to run instead.
|
|
49
|
+
"""
|
|
50
|
+
if not __debug__: # pragma: no cover — pytest always runs with assertions on
|
|
51
|
+
raise RuntimeError(
|
|
52
|
+
"the views-frames conformance suite requires assertions; run without "
|
|
53
|
+
"python -O/-OO (PYTHONOPTIMIZE), otherwise every check silently passes"
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
|
|
43
57
|
def assert_frame_envelope(frame: Any) -> None:
|
|
44
58
|
"""Assert ``frame`` satisfies the shared **frame envelope**.
|
|
45
59
|
|
|
@@ -56,6 +70,7 @@ def assert_frame_envelope(frame: Any) -> None:
|
|
|
56
70
|
Raises:
|
|
57
71
|
AssertionError: any part of the envelope is violated.
|
|
58
72
|
"""
|
|
73
|
+
_require_assertions()
|
|
59
74
|
values = frame.values
|
|
60
75
|
assert isinstance(values, np.ndarray), "values must be a numpy array"
|
|
61
76
|
assert values.dtype == np.float32, f"values must be float32, got {values.dtype}"
|
|
@@ -77,6 +92,7 @@ def assert_frame_contract(frame: Any) -> None:
|
|
|
77
92
|
Raises:
|
|
78
93
|
AssertionError: the envelope or the spatiotemporal identifier rule is violated.
|
|
79
94
|
"""
|
|
95
|
+
_require_assertions()
|
|
80
96
|
assert_frame_envelope(frame)
|
|
81
97
|
|
|
82
98
|
ids = frame.identifiers
|
|
@@ -110,6 +126,7 @@ def assert_index_alignment_laws(index_a: Any, index_b: Any) -> None:
|
|
|
110
126
|
Raises:
|
|
111
127
|
AssertionError: a law is violated.
|
|
112
128
|
"""
|
|
129
|
+
_require_assertions()
|
|
113
130
|
assert index_a.intersect(index_b) == index_b.intersect(index_a), (
|
|
114
131
|
"intersect must be commutative"
|
|
115
132
|
)
|
|
@@ -140,6 +157,7 @@ def assert_cross_level_alignment_law(
|
|
|
140
157
|
AssertionError: the remap disagrees with the mapping, drops time, or
|
|
141
158
|
produces the wrong level.
|
|
142
159
|
"""
|
|
160
|
+
_require_assertions()
|
|
143
161
|
aligned = index.cross_level_align(mapping, target_level)
|
|
144
162
|
assert aligned.level is target_level, "cross_level_align must carry target_level"
|
|
145
163
|
assert np.array_equal(aligned.time, index.time), "cross_level_align must keep time"
|
|
@@ -132,6 +132,11 @@ class SpatioTemporalIndex:
|
|
|
132
132
|
The pure-numpy analogue of ``pd.Index.get_indexer``: a same-level join.
|
|
133
133
|
"""
|
|
134
134
|
self._require_same_level(other)
|
|
135
|
+
if self.n_rows == 0:
|
|
136
|
+
# An empty index contains nothing: every row of `other` is absent. The
|
|
137
|
+
# general path below would clip positions to -1 and crash with an
|
|
138
|
+
# obscure IndexError on the empty array (falsify audit 2026-07, F4).
|
|
139
|
+
return np.full(other.n_rows, -1, dtype=np.intp)
|
|
135
140
|
self_rows = self._row_view(self._keys())
|
|
136
141
|
other_rows = self._row_view(other._keys())
|
|
137
142
|
order = np.argsort(self_rows, kind="stable")
|
|
@@ -20,6 +20,19 @@ from views_frames import PredictionFrame, SpatialLevel
|
|
|
20
20
|
from views_frames_reconcile.module import ReconciliationModule
|
|
21
21
|
|
|
22
22
|
|
|
23
|
+
def _require_assertions() -> None:
|
|
24
|
+
"""Fail loud if assertions are stripped (``python -O``/``-OO``).
|
|
25
|
+
|
|
26
|
+
Mirrors ``views_frames.conformance._require_assertions`` (falsify audit 2026-07,
|
|
27
|
+
F3): under optimized bytecode every ``assert`` silently passes — refuse to run.
|
|
28
|
+
"""
|
|
29
|
+
if not __debug__: # pragma: no cover — pytest always runs with assertions on
|
|
30
|
+
raise RuntimeError(
|
|
31
|
+
"the reconcile conformance suite requires assertions; run without "
|
|
32
|
+
"python -O/-OO (PYTHONOPTIMIZE), otherwise every check silently passes"
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
|
|
23
36
|
def assert_reconcile_contract(
|
|
24
37
|
cm_frame: PredictionFrame,
|
|
25
38
|
pgm_frame: PredictionFrame,
|
|
@@ -35,6 +48,7 @@ def assert_reconcile_contract(
|
|
|
35
48
|
Raises:
|
|
36
49
|
AssertionError: a contract law is violated.
|
|
37
50
|
"""
|
|
51
|
+
_require_assertions()
|
|
38
52
|
mk = np.asarray(map_keys)
|
|
39
53
|
mv = np.asarray(map_vals)
|
|
40
54
|
out = ReconciliationModule(mk, mv).reconcile(cm_frame, pgm_frame)
|
|
@@ -9,6 +9,11 @@ stay zero; country totals are authoritative; the result is non-negative.
|
|
|
9
9
|
This is a *faithful, numpy-only* port of views-reporting's
|
|
10
10
|
``ForecastReconciler.reconcile_forecast`` (torch), migrated here because the
|
|
11
11
|
algorithm belongs in post-processing, not reporting (views-reporting issue #72).
|
|
12
|
+
One deliberate deviation (falsify audit 2026-07): the original's ``+ 1e-8``
|
|
13
|
+
denominator epsilon is replaced by an explicit all-zero-draw guard — bit-identical
|
|
14
|
+
on all realistic data (the epsilon was a float32 no-op for draw sums ≳ 0.1) but
|
|
15
|
+
exactly conserving for tiny nonzero sums, where the epsilon silently deflated the
|
|
16
|
+
scale factor. Negative country totals now fail loud instead of silently clamping.
|
|
12
17
|
It is intentionally the **same** method — a pragmatic per-draw approximation, not
|
|
13
18
|
principled joint probabilistic reconciliation. The upgrade to the latter is designed
|
|
14
19
|
in **ADR-024** (register **C-62**; the cross-repo lineage is views-postprocessing
|
|
@@ -27,8 +32,6 @@ from typing import Any
|
|
|
27
32
|
import numpy as np
|
|
28
33
|
from numpy.typing import NDArray
|
|
29
34
|
|
|
30
|
-
_EPS = np.float32(1e-8)
|
|
31
|
-
|
|
32
35
|
|
|
33
36
|
def reconcile_proportional(
|
|
34
37
|
grid: NDArray[np.floating[Any]] | object,
|
|
@@ -49,7 +52,8 @@ def reconcile_proportional(
|
|
|
49
52
|
are clamped to be non-negative.
|
|
50
53
|
|
|
51
54
|
Raises:
|
|
52
|
-
ValueError: the grid and country sample counts disagree
|
|
55
|
+
ValueError: the grid and country sample counts disagree, or any country
|
|
56
|
+
total is negative (cannot be conserved under the non-negativity clamp).
|
|
53
57
|
"""
|
|
54
58
|
grid_arr = np.asarray(grid, dtype=np.float32)
|
|
55
59
|
is_point = grid_arr.ndim == 1
|
|
@@ -65,13 +69,32 @@ def reconcile_proportional(
|
|
|
65
69
|
f"Mismatch in sample count: grid has {grid_arr.shape[0]}, "
|
|
66
70
|
f"country has {country_arr.shape[0]}"
|
|
67
71
|
)
|
|
72
|
+
if bool((country_arr < 0).any()):
|
|
73
|
+
# A negative total cannot be conserved under the non-negativity clamp — the
|
|
74
|
+
# output would silently sum to 0, not the total (falsify audit 2026-07, F8).
|
|
75
|
+
raise ValueError(
|
|
76
|
+
"country totals must be non-negative; got a negative total "
|
|
77
|
+
f"(min={float(country_arr.min())}). A negative forecast is an upstream "
|
|
78
|
+
"bug; reconciliation cannot conserve it under the non-negativity clamp."
|
|
79
|
+
)
|
|
68
80
|
|
|
69
81
|
# Preserve zeros: only strictly-positive cells carry probability mass.
|
|
70
82
|
nonzero = np.where(grid_arr > 0, grid_arr, np.float32(0.0))
|
|
71
83
|
|
|
72
|
-
# Per-draw proportional scaling to the (authoritative) country total.
|
|
84
|
+
# Per-draw proportional scaling to the (authoritative) country total. Guard the
|
|
85
|
+
# all-zero draws explicitly (they carry no mass and stay zero — the documented
|
|
86
|
+
# edge) instead of an additive epsilon in the denominator: `+ 1e-8` was a no-op
|
|
87
|
+
# in float32 for sums ≳ 0.1 (machine eps exceeds it) but silently deflated the
|
|
88
|
+
# scale factor for tiny nonzero sums — at a draw sum of 1e-8 a country total of
|
|
89
|
+
# 100 reconciled to 50 with no error signal (falsify audit 2026-07, F1). The
|
|
90
|
+
# exact division below is bit-identical to the epsilon form on all realistic
|
|
91
|
+
# data (torch-oracle parity preserved) and conserves exactly for any nonzero sum.
|
|
73
92
|
sum_nonzero = nonzero.sum(axis=1, keepdims=True) # (S, 1)
|
|
74
|
-
|
|
93
|
+
has_mass = sum_nonzero > 0
|
|
94
|
+
safe_sum = np.where(has_mass, sum_nonzero, np.float32(1.0))
|
|
95
|
+
scaling = np.where(
|
|
96
|
+
has_mass, country_arr.reshape(-1, 1) / safe_sum, np.float32(0.0)
|
|
97
|
+
) # (S, 1)
|
|
75
98
|
adjusted = np.clip(nonzero * scaling, 0.0, None).astype(np.float32)
|
|
76
99
|
|
|
77
100
|
return np.asarray(adjusted[0] if is_point else adjusted, dtype=np.float32)
|
|
@@ -28,9 +28,15 @@ TOWER_CONFIG: Final[dict[str, Any]] = {
|
|
|
28
28
|
[round(0.05 * i, 2) for i in range(1, 19)] # 0.05 … 0.90
|
|
29
29
|
+ [0.92, 0.94, 0.95, 0.96, 0.97, 0.98, 0.99] # fine high-mass tail
|
|
30
30
|
),
|
|
31
|
-
# The tower-tip reads the median of the floor at this mass — the
|
|
32
|
-
#
|
|
33
|
-
|
|
31
|
+
# The tower-tip reads the median of the floor at this mass — the top floor of the
|
|
32
|
+
# published tower. 0.25 (the top-quartile floor; ADR-019 amendment 2026-07-24,
|
|
33
|
+
# evidence in research/map_hdi/tip_mass_study.py): lower bias than the 0.5 shorth
|
|
34
|
+
# everywhere, RMSE-superior at pooled S, exact on zero-inflated cells, and it
|
|
35
|
+
# passes the real-cell C-44 gate with margin — the tip floor's median is safe as
|
|
36
|
+
# long as a duplicate stack is under half the floor (k > 2·duplicates; at S=32,
|
|
37
|
+
# k=8 tolerates the observed 2-3-zero stacks). Containment law: every HDI of mass
|
|
38
|
+
# > tip_mass/2 provably contains the tip (asserted in conformance.py).
|
|
39
|
+
"tip_mass": 0.25,
|
|
34
40
|
# Optional magnitude zero rule, OFF by default (``None``). When set to a float, a
|
|
35
41
|
# row whose every draw is <= it collapses to 0 — a *count* opt-in (sub-1 ⇒ 0).
|
|
36
42
|
# Left ``None`` for any non-count target: zero-inflation is otherwise handled by the
|
|
@@ -22,12 +22,26 @@ from views_frames_summarize.tower import hdi_tower
|
|
|
22
22
|
from views_frames_summarize.tower_point import tower_point
|
|
23
23
|
|
|
24
24
|
|
|
25
|
+
def _require_assertions() -> None:
|
|
26
|
+
"""Fail loud if assertions are stripped (``python -O``/``-OO``).
|
|
27
|
+
|
|
28
|
+
Mirrors ``views_frames.conformance._require_assertions`` (falsify audit 2026-07,
|
|
29
|
+
F3): under optimized bytecode every ``assert`` silently passes — refuse to run.
|
|
30
|
+
"""
|
|
31
|
+
if not __debug__: # pragma: no cover — pytest always runs with assertions on
|
|
32
|
+
raise RuntimeError(
|
|
33
|
+
"the summarize conformance suite requires assertions; run without "
|
|
34
|
+
"python -O/-OO (PYTHONOPTIMIZE), otherwise every check silently passes"
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
|
|
25
38
|
def assert_summarizer_contract(frame: AnyFrame) -> None:
|
|
26
39
|
"""Assert the summarizers behave on ``frame``.
|
|
27
40
|
|
|
28
41
|
Raises:
|
|
29
42
|
AssertionError: a summarizer violates its output contract.
|
|
30
43
|
"""
|
|
44
|
+
_require_assertions()
|
|
31
45
|
n = frame.n_rows
|
|
32
46
|
|
|
33
47
|
point = collapse(frame, np.mean)
|
|
@@ -112,6 +126,32 @@ def _assert_tower_contract(frame: AnyFrame, n: int) -> None:
|
|
|
112
126
|
assert (tip.values[..., 0] >= tlo - 1e-6).all(), "tip below the tip_mass floor"
|
|
113
127
|
assert (tip.values[..., 0] <= thi + 1e-6).all(), "tip above the tip_mass floor"
|
|
114
128
|
|
|
129
|
+
# MAP-containment law (ADR-019 amendment 2026-07-24). Wider-than-tip_mass floors
|
|
130
|
+
# contain the tip by nesting. Below tip_mass, a nested floor still contains it
|
|
131
|
+
# whenever it holds MORE THAN HALF the tip floor's draws: a contiguous sub-window
|
|
132
|
+
# longer than half the parent cannot trim away the parent's middle draw(s), and
|
|
133
|
+
# the tip is their median/average. A floor of mass m spans floor(m·S)+1 draws
|
|
134
|
+
# (the `_ks` value counts inter-draw steps), so the exact condition is
|
|
135
|
+
# 2·(floor(m·S)+1) > floor(tip_mass·S)+1 — asymptotically mass > tip_mass/2.
|
|
136
|
+
# Floors below it carry NO guarantee and are below platform sample resolution
|
|
137
|
+
# (see tower_point.py / research/map_hdi/tip_mass_study.py).
|
|
138
|
+
s_count = int(frame.values.shape[-1])
|
|
139
|
+
n_tip = int(np.floor(tip_mass * s_count)) + 1
|
|
140
|
+
guaranteed = tuple(
|
|
141
|
+
float(m)
|
|
142
|
+
for m in config.canonical_floors()
|
|
143
|
+
if 2 * (int(np.floor(float(m) * s_count)) + 1) > n_tip
|
|
144
|
+
)
|
|
145
|
+
law_tower = hdi_tower(frame, masses=guaranteed)
|
|
146
|
+
for j, m in enumerate(guaranteed):
|
|
147
|
+
glo, ghi = law_tower[..., j, 0], law_tower[..., j, 1]
|
|
148
|
+
assert (tip.values[..., 0] >= glo - 1e-6).all(), (
|
|
149
|
+
f"MAP-containment violated: tip below the {m:.2f} floor"
|
|
150
|
+
)
|
|
151
|
+
assert (tip.values[..., 0] <= ghi + 1e-6).all(), (
|
|
152
|
+
f"MAP-containment violated: tip above the {m:.2f} floor"
|
|
153
|
+
)
|
|
154
|
+
|
|
115
155
|
# Reproducibility law: the 50% HDI is independent of the other requested masses.
|
|
116
156
|
just_50 = hdi_tower(frame, masses=(0.5,))
|
|
117
157
|
assert np.array_equal(just_50[..., 0, :], tower[..., 0, :]), (
|
|
@@ -2,15 +2,21 @@
|
|
|
2
2
|
|
|
3
3
|
`tower_point` is the "most likely single value" we report to a consumer: the median of
|
|
4
4
|
the draws inside the **`tip_mass` floor** of the nested tower (config-driven, default
|
|
5
|
-
0.
|
|
6
|
-
|
|
5
|
+
0.25 — the top-quartile floor, i.e. the top floor of the published tower; ADR-019
|
|
6
|
+
amendment 2026-07-24, evidence in `research/map_hdi/tip_mass_study.py`). Zero-inflation
|
|
7
|
+
is handled by that floor's density (a zero-majority row reads 0); the optional,
|
|
8
|
+
off-by-default magnitude cutoff applies too if set (C-45). Containment law: every HDI
|
|
9
|
+
whose floor holds more than half the tip floor's draws provably contains the tip
|
|
10
|
+
(asymptotically, mass > `tip_mass`/2 — a nested contiguous window longer than half
|
|
11
|
+
the parent cannot trim away the parent's median); narrower floors carry no guarantee
|
|
12
|
+
and are below platform sample resolution. Asserted in ``conformance.py``.
|
|
7
13
|
|
|
8
14
|
It is a *new* estimator, deliberately distinct from the frozen `map_estimate` (ADR-018):
|
|
9
15
|
`map_estimate` is a binned histogram mode with a zero-*mass*-fraction rule and a
|
|
10
16
|
lowest-index tie-break that is directionally biased on right-skewed, zero-inflated,
|
|
11
17
|
low-sample posteriors (register C-32). The tower tip is unbinned — it reads the median
|
|
12
18
|
of a mass-aware floor — so it carries no histogram tie-break **and** is robust to
|
|
13
|
-
minority duplicated draws (register C-44; a lonely outlier cannot define a
|
|
19
|
+
minority duplicated draws (register C-44; a lonely outlier cannot define a 25%-mass
|
|
14
20
|
floor). On a genuinely multi-peaked row the tip is, like any point, ambiguous; pair it
|
|
15
21
|
with `bimodality` to detect that case rather than collapse it silently.
|
|
16
22
|
"""
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|