SSTT-visuals 0.1.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.
- sstt_visuals-0.1.0/LICENSE +26 -0
- sstt_visuals-0.1.0/PKG-INFO +47 -0
- sstt_visuals-0.1.0/README.md +29 -0
- sstt_visuals-0.1.0/pyproject.toml +33 -0
- sstt_visuals-0.1.0/setup.cfg +4 -0
- sstt_visuals-0.1.0/src/SSTT_visuals/__init__.py +5 -0
- sstt_visuals-0.1.0/src/SSTT_visuals/clusterStats.py +298 -0
- sstt_visuals-0.1.0/src/SSTT_visuals/mos.py +293 -0
- sstt_visuals-0.1.0/src/SSTT_visuals/plot.py +283 -0
- sstt_visuals-0.1.0/src/SSTT_visuals/primeslist.py +705 -0
- sstt_visuals-0.1.0/src/SSTT_visuals/wordDB.json +121070 -0
- sstt_visuals-0.1.0/src/SSTT_visuals/wordGroupTest.json +44 -0
- sstt_visuals-0.1.0/src/SSTT_visuals.egg-info/PKG-INFO +47 -0
- sstt_visuals-0.1.0/src/SSTT_visuals.egg-info/SOURCES.txt +15 -0
- sstt_visuals-0.1.0/src/SSTT_visuals.egg-info/dependency_links.txt +1 -0
- sstt_visuals-0.1.0/src/SSTT_visuals.egg-info/requires.txt +4 -0
- sstt_visuals-0.1.0/src/SSTT_visuals.egg-info/top_level.txt +1 -0
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
BSD 3-Clause License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025, Ant-Presentation-Vis contributors
|
|
4
|
+
All rights reserved.
|
|
5
|
+
|
|
6
|
+
Redistribution and use in source and binary forms, with or without
|
|
7
|
+
modification, are permitted provided that the following conditions are met:
|
|
8
|
+
|
|
9
|
+
1. Redistributions of source code must retain the above copyright notice, this
|
|
10
|
+
list of conditions and the following disclaimer.
|
|
11
|
+
|
|
12
|
+
2. Redistributions in binary form must reproduce the above copyright notice,
|
|
13
|
+
this list of conditions and the following disclaimer in the documentation
|
|
14
|
+
and/or other materials provided with the distribution.
|
|
15
|
+
|
|
16
|
+
3. Neither the name of the copyright holder nor the names of its
|
|
17
|
+
contributors may be used to endorse or promote products derived from this
|
|
18
|
+
software without specific prior written permission.
|
|
19
|
+
|
|
20
|
+
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
|
21
|
+
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
|
22
|
+
IMPLIED WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
|
23
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE
|
|
24
|
+
FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
|
|
25
|
+
TORT or OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR
|
|
26
|
+
THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: SSTT-visuals
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Visualization utilities for the Social Science Trust Taxonomy.
|
|
5
|
+
Author-email: "Anthony E. D. Mobbs" <tony@mobbs.com.au>, "Redmond R. Scoble" <imashellio@gmail.com>
|
|
6
|
+
License-Expression: BSD-3-Clause
|
|
7
|
+
Project-URL: Homepage, https://github.com/AnthonyMobbs/SSTT-visuals
|
|
8
|
+
Classifier: Programming Language :: Python :: 3
|
|
9
|
+
Classifier: Operating System :: OS Independent
|
|
10
|
+
Requires-Python: >=3.13
|
|
11
|
+
Description-Content-Type: text/markdown
|
|
12
|
+
License-File: LICENSE
|
|
13
|
+
Requires-Dist: numpy>=1.24
|
|
14
|
+
Requires-Dist: matplotlib>=3.7
|
|
15
|
+
Requires-Dist: scipy>=1.10
|
|
16
|
+
Requires-Dist: gmpy2>=2.1
|
|
17
|
+
Dynamic: license-file
|
|
18
|
+
|
|
19
|
+
# SSTT-visuals
|
|
20
|
+
---
|
|
21
|
+
|
|
22
|
+
## Installation
|
|
23
|
+
|
|
24
|
+
Install the package with `pip`:
|
|
25
|
+
|
|
26
|
+
```bash
|
|
27
|
+
pip install SSTT-visuals
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
## Quick start
|
|
31
|
+
|
|
32
|
+
### 1. Produce visualisations
|
|
33
|
+
|
|
34
|
+
The `SSTTVisual()` function generates the visualisations used in [TBD]
|
|
35
|
+
|
|
36
|
+
```python
|
|
37
|
+
from SSTT_visuals.plot import SSTTVisual
|
|
38
|
+
|
|
39
|
+
SSTTVisual("SSTT_visuals/wordGroupTest.json")
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
## Citation
|
|
43
|
+
If you use SSTT_visuals in your research, please cite the original methodological validation paper:
|
|
44
|
+
[TBD]
|
|
45
|
+
|
|
46
|
+
## Core references
|
|
47
|
+
TBD
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# SSTT-visuals
|
|
2
|
+
---
|
|
3
|
+
|
|
4
|
+
## Installation
|
|
5
|
+
|
|
6
|
+
Install the package with `pip`:
|
|
7
|
+
|
|
8
|
+
```bash
|
|
9
|
+
pip install SSTT-visuals
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
## Quick start
|
|
13
|
+
|
|
14
|
+
### 1. Produce visualisations
|
|
15
|
+
|
|
16
|
+
The `SSTTVisual()` function generates the visualisations used in [TBD]
|
|
17
|
+
|
|
18
|
+
```python
|
|
19
|
+
from SSTT_visuals.plot import SSTTVisual
|
|
20
|
+
|
|
21
|
+
SSTTVisual("SSTT_visuals/wordGroupTest.json")
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
## Citation
|
|
25
|
+
If you use SSTT_visuals in your research, please cite the original methodological validation paper:
|
|
26
|
+
[TBD]
|
|
27
|
+
|
|
28
|
+
## Core references
|
|
29
|
+
TBD
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=77.0.3"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "SSTT-visuals"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
authors = [
|
|
9
|
+
{ name="Anthony E. D. Mobbs", email="tony@mobbs.com.au" },
|
|
10
|
+
{ name="Redmond R. Scoble", email="imashellio@gmail.com" },
|
|
11
|
+
]
|
|
12
|
+
description = "Visualization utilities for the Social Science Trust Taxonomy."
|
|
13
|
+
readme = "README.md"
|
|
14
|
+
requires-python = ">=3.13"
|
|
15
|
+
classifiers = [
|
|
16
|
+
"Programming Language :: Python :: 3",
|
|
17
|
+
"Operating System :: OS Independent",
|
|
18
|
+
]
|
|
19
|
+
|
|
20
|
+
license = "BSD-3-Clause"
|
|
21
|
+
|
|
22
|
+
dependencies = [
|
|
23
|
+
"numpy>=1.24",
|
|
24
|
+
"matplotlib>=3.7",
|
|
25
|
+
"scipy>=1.10",
|
|
26
|
+
"gmpy2>=2.1",
|
|
27
|
+
]
|
|
28
|
+
|
|
29
|
+
[tool.setuptools.package-data]
|
|
30
|
+
SSTT_visuals = ["*.json"]
|
|
31
|
+
|
|
32
|
+
[project.urls]
|
|
33
|
+
Homepage = "https://github.com/AnthonyMobbs/SSTT-visuals"
|
|
@@ -0,0 +1,298 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Two goodness-of-fit tests against the null hypothesis that a 5x5 array of
|
|
3
|
+
nonnegative-integer counts arises from N i.i.d. uniform draws over the 25
|
|
4
|
+
cells (equivalently: the count vector is Multinomial(N, p=1/25 each)).
|
|
5
|
+
|
|
6
|
+
1. occupancy_test: T = number of empty cells. Reports P(T >= t_obs), exact.
|
|
7
|
+
2. scan_statistic_test: Q = min over "2-by-n" rectangles of the rectangle's
|
|
8
|
+
own upper-tail binomial p-value. Reports P(Q <= q_obs) via simulation,
|
|
9
|
+
since the exact null distribution of a minimum over overlapping,
|
|
10
|
+
dependent windows has no tractable closed form.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from fractions import Fraction
|
|
14
|
+
from math import comb
|
|
15
|
+
|
|
16
|
+
import scipy.stats as stats
|
|
17
|
+
|
|
18
|
+
from .mos import normal_isf_asymptotic
|
|
19
|
+
|
|
20
|
+
import numpy as np
|
|
21
|
+
from scipy.stats import binom
|
|
22
|
+
|
|
23
|
+
GRID_SIZE = 5
|
|
24
|
+
CENTER = (GRID_SIZE // 2, GRID_SIZE // 2) # (2, 2), zero-indexed
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
# ---------------------------------------------------------------------------
|
|
28
|
+
# Test 1: Occupancy test
|
|
29
|
+
# ---------------------------------------------------------------------------
|
|
30
|
+
|
|
31
|
+
def _surjections(n: int, j: int) -> int:
|
|
32
|
+
"""Number of surjective functions from an n-set onto a j-set, via
|
|
33
|
+
inclusion-exclusion. Surj(n, 0) = 1 iff n == 0 (handled naturally by the
|
|
34
|
+
formula since 0**0 = 1 in Python)."""
|
|
35
|
+
total = 0
|
|
36
|
+
for i in range(j + 1):
|
|
37
|
+
total += (-1) ** i * comb(j, i) * (j - i) ** n
|
|
38
|
+
return total
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def occupancyStatisticTest(data: np.ndarray):
|
|
42
|
+
"""
|
|
43
|
+
Exact p-value P(T >= t_obs) for the occupancy test, where T is the
|
|
44
|
+
number of empty cells among the m = 25 cells of `data`, under the null
|
|
45
|
+
that N = sum(data) observations are i.i.d. uniform over the 25 cells.
|
|
46
|
+
|
|
47
|
+
Returns the p-value (float).
|
|
48
|
+
"""
|
|
49
|
+
data = np.asarray(data)
|
|
50
|
+
if data.shape != (GRID_SIZE, GRID_SIZE):
|
|
51
|
+
raise ValueError(f"expected a {GRID_SIZE}x{GRID_SIZE} array")
|
|
52
|
+
|
|
53
|
+
m = data.size # 25 boxes
|
|
54
|
+
n = int(data.sum()) # total balls
|
|
55
|
+
t_obs = int(np.sum(data == 0))
|
|
56
|
+
|
|
57
|
+
p_tail = Fraction(0)
|
|
58
|
+
for k in range(t_obs, m + 1):
|
|
59
|
+
j = m - k # number of "occupied" boxes required among the chosen k...
|
|
60
|
+
# careful: k here is the number of EMPTY boxes, so m-k boxes must
|
|
61
|
+
# all be nonempty -> surjection of n balls onto (m-k) boxes
|
|
62
|
+
occupied = m - k
|
|
63
|
+
surj = _surjections(n, occupied)
|
|
64
|
+
p_tail += Fraction(comb(m, k) * surj, m ** n)
|
|
65
|
+
|
|
66
|
+
p_value = float(p_tail)
|
|
67
|
+
if p_value <= 0:
|
|
68
|
+
z_score = normal_isf_asymptotic(p_tail)
|
|
69
|
+
lp = None
|
|
70
|
+
else:
|
|
71
|
+
z_score = stats.norm.isf(p_value)
|
|
72
|
+
lp = -np.log(p_value)
|
|
73
|
+
return (p_value, z_score, lp)
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
# ---------------------------------------------------------------------------
|
|
77
|
+
# Test 2: Scan statistic test
|
|
78
|
+
# ---------------------------------------------------------------------------
|
|
79
|
+
|
|
80
|
+
def _enumerate_rectangles(size: int = GRID_SIZE, center=CENTER):
|
|
81
|
+
"""
|
|
82
|
+
All axis-aligned rectangles with one side exactly length 2 and the other
|
|
83
|
+
side length 2..size (both orientations: 2-tall / n-wide, and n-tall /
|
|
84
|
+
2-wide, with n >= 2 so both dimensions are at least 2), excluding any
|
|
85
|
+
rectangle that contains `center`.
|
|
86
|
+
|
|
87
|
+
Returns a list of (r0, r1, c0, c1, flat_indices, p) tuples where
|
|
88
|
+
(r0..r1, c0..c1) are inclusive cell ranges, flat_indices is the list of
|
|
89
|
+
flattened (row-major) cell indices in the rectangle, and p = ncells/25
|
|
90
|
+
is the null probability mass covered by the rectangle.
|
|
91
|
+
"""
|
|
92
|
+
rects = set()
|
|
93
|
+
|
|
94
|
+
for w in range(2, size + 1):
|
|
95
|
+
h = 2
|
|
96
|
+
for r0 in range(size - h + 1):
|
|
97
|
+
for c0 in range(size - w + 1):
|
|
98
|
+
r1, c1 = r0 + h - 1, c0 + w - 1
|
|
99
|
+
if r0 <= center[0] <= r1 and c0 <= center[1] <= c1:
|
|
100
|
+
continue
|
|
101
|
+
rects.add((r0, r1, c0, c1))
|
|
102
|
+
|
|
103
|
+
for h in range(2, size + 1):
|
|
104
|
+
w = 2
|
|
105
|
+
for r0 in range(size - h + 1):
|
|
106
|
+
for c0 in range(size - w + 1):
|
|
107
|
+
r1, c1 = r0 + h - 1, c0 + w - 1
|
|
108
|
+
if r0 <= center[0] <= r1 and c0 <= center[1] <= c1:
|
|
109
|
+
continue
|
|
110
|
+
rects.add((r0, r1, c0, c1))
|
|
111
|
+
|
|
112
|
+
rect_info = []
|
|
113
|
+
for (r0, r1, c0, c1) in sorted(rects):
|
|
114
|
+
idx = [r * size + c for r in range(r0, r1 + 1) for c in range(c0, c1 + 1)]
|
|
115
|
+
p = len(idx) / (size * size)
|
|
116
|
+
rect_info.append((r0, r1, c0, c1, idx, p))
|
|
117
|
+
return rect_info
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
# These are the two composite regions in grid coordinates. Each tuple keeps
|
|
121
|
+
# the normal rectangle fields, followed by the cells in the union and the
|
|
122
|
+
# component rectangles used by the plotter for highlighting.
|
|
123
|
+
_SPECIAL_COMPOSITE_RECTS = [
|
|
124
|
+
(
|
|
125
|
+
0, 4, 0, 2,
|
|
126
|
+
[0, 1, 5, 6, 10, 11, 15, 16, 17, 20, 21, 22],
|
|
127
|
+
12 / 25,
|
|
128
|
+
((3, 4, 0, 2), (0, 2, 0, 1)),
|
|
129
|
+
),
|
|
130
|
+
(
|
|
131
|
+
0, 4, 2, 4,
|
|
132
|
+
[2, 3, 4, 7, 8, 9, 13, 14, 18, 19, 23, 24],
|
|
133
|
+
12 / 25,
|
|
134
|
+
((0, 1, 2, 4), (2, 4, 3, 4)),
|
|
135
|
+
),
|
|
136
|
+
]
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
_RECTS = _enumerate_rectangles() + _SPECIAL_COMPOSITE_RECTS
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
def _compute_Q(flat_counts: np.ndarray, n_total: int) -> float:
|
|
143
|
+
"""flat_counts: 1D array of length 25 (row-major). Returns Q = min_r q_r."""
|
|
144
|
+
q_min = 1.0
|
|
145
|
+
min_rect = None
|
|
146
|
+
for rect in _RECTS:
|
|
147
|
+
idx = rect[4]
|
|
148
|
+
p = rect[5]
|
|
149
|
+
s = flat_counts[idx].sum()
|
|
150
|
+
q_r = binom.sf(s - 1, n_total, p) # P(X >= s)
|
|
151
|
+
if q_r < q_min:
|
|
152
|
+
q_min = q_r
|
|
153
|
+
min_rect = rect
|
|
154
|
+
return q_min, min_rect
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
def _compute_Q_batch(flat_counts_batch: np.ndarray, n_total: int) -> np.ndarray:
|
|
158
|
+
"""Vectorised version: flat_counts_batch has shape (n_sims, 25)."""
|
|
159
|
+
n_sims = flat_counts_batch.shape[0]
|
|
160
|
+
q_min = np.ones(n_sims)
|
|
161
|
+
for rect in _RECTS:
|
|
162
|
+
idx = rect[4]
|
|
163
|
+
p = rect[5]
|
|
164
|
+
s = flat_counts_batch[:, idx].sum(axis=1)
|
|
165
|
+
q_r = binom.sf(s - 1, n_total, p)
|
|
166
|
+
np.minimum(q_min, q_r, out=q_min)
|
|
167
|
+
return q_min
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
def scanStatisticTest(data, n_sims: int = 2000000, random_state=None, return_rect: bool = False):
|
|
171
|
+
"""
|
|
172
|
+
Monte Carlo p-value P(Q <= q_obs) for the scan statistic test.
|
|
173
|
+
|
|
174
|
+
Q = min over all valid "2-by-n" rectangles r of q_r, where q_r is the
|
|
175
|
+
upper-tail binomial probability P(count in r >= observed count in r)
|
|
176
|
+
under the null that N = sum(data) observations are i.i.d. uniform over
|
|
177
|
+
the 25 cells.
|
|
178
|
+
|
|
179
|
+
Returns the p-value, z-score, log-p-value, q_obs, and optionally the
|
|
180
|
+
rectangle with the minimum q-value.
|
|
181
|
+
"""
|
|
182
|
+
data = np.asarray(data)
|
|
183
|
+
if data.shape != (GRID_SIZE, GRID_SIZE):
|
|
184
|
+
raise ValueError(f"expected a {GRID_SIZE}x{GRID_SIZE} array")
|
|
185
|
+
|
|
186
|
+
n_total = int(data.sum())
|
|
187
|
+
flat_obs = data.reshape(-1)
|
|
188
|
+
q_obs, min_rect = _compute_Q(flat_obs, n_total)
|
|
189
|
+
|
|
190
|
+
if n_total == 0:
|
|
191
|
+
# Every rectangle sum is trivially 0 >= 0, so q_r = 1 for all r,
|
|
192
|
+
# Q = 1 always, under the null too -> degenerate p-value of 1.
|
|
193
|
+
result = (q_obs, 1.0)
|
|
194
|
+
if return_rect:
|
|
195
|
+
return (1.0, 0.0, 0.0, q_obs, min_rect)
|
|
196
|
+
return result
|
|
197
|
+
|
|
198
|
+
rng = np.random.default_rng(random_state)
|
|
199
|
+
flat_p = np.full(GRID_SIZE * GRID_SIZE, 1.0 / (GRID_SIZE * GRID_SIZE))
|
|
200
|
+
sims = rng.multinomial(n_total, flat_p, size=n_sims) # shape (n_sims, 25)
|
|
201
|
+
|
|
202
|
+
q_sims = _compute_Q_batch(sims, n_total)
|
|
203
|
+
p_value = float(np.mean(q_sims <= q_obs))
|
|
204
|
+
|
|
205
|
+
z_score = stats.norm.isf(p_value)
|
|
206
|
+
lp = np.inf if p_value == 0 else -np.log(p_value)
|
|
207
|
+
|
|
208
|
+
if return_rect:
|
|
209
|
+
return (p_value, z_score, lp, q_obs, min_rect)
|
|
210
|
+
return (p_value, z_score, lp, q_obs)
|
|
211
|
+
|
|
212
|
+
def _min_tail_prob_at_least_as_extreme(n_total: int, p: float, q_threshold: float) -> float:
|
|
213
|
+
"""
|
|
214
|
+
For S ~ Binomial(n_total, p), let q(s) = P(S >= s) be the upper-tail
|
|
215
|
+
p-value function (non-increasing, right-continuous step function of s).
|
|
216
|
+
Returns P(q(S) <= q_threshold), exactly, with no simulation: this is
|
|
217
|
+
just sf(s* - 1) where s* is the smallest integer count whose own tail
|
|
218
|
+
probability already drops to or below q_threshold.
|
|
219
|
+
"""
|
|
220
|
+
s_values = np.arange(0, n_total + 1)
|
|
221
|
+
tail = binom.sf(s_values - 1, n_total, p) # monotone non-increasing in s
|
|
222
|
+
mask = tail <= q_threshold
|
|
223
|
+
if not mask.any():
|
|
224
|
+
return 0.0
|
|
225
|
+
s_star = int(s_values[mask][0])
|
|
226
|
+
return float(binom.sf(s_star - 1, n_total, p))
|
|
227
|
+
|
|
228
|
+
|
|
229
|
+
def scanStatisticTestBonferroni(data: np.ndarray, return_rect: bool = False):
|
|
230
|
+
"""
|
|
231
|
+
Exact (analytic, no simulation) UPPER BOUND on P(Q <= q_obs), via the
|
|
232
|
+
union bound over the fixed set of 38 rectangles:
|
|
233
|
+
|
|
234
|
+
P(Q <= q) = P(exists r: q_r <= q) <= sum_r P(q_r <= q)
|
|
235
|
+
|
|
236
|
+
Unlike the Monte Carlo version, this has no floor on how small a
|
|
237
|
+
p-value it can report -- useful when the true p-value is far smaller
|
|
238
|
+
than 1/n_sims. It is conservative: the true p-value is <= this bound.
|
|
239
|
+
|
|
240
|
+
Returns (bound, z_score, lp, q_obs) by default, or includes the
|
|
241
|
+
rectangle with the minimum q-value when return_rect=True.
|
|
242
|
+
"""
|
|
243
|
+
data = np.asarray(data)
|
|
244
|
+
if data.shape != (GRID_SIZE, GRID_SIZE):
|
|
245
|
+
raise ValueError(f"expected a {GRID_SIZE}x{GRID_SIZE} array")
|
|
246
|
+
|
|
247
|
+
n_total = int(data.sum())
|
|
248
|
+
flat_obs = data.reshape(-1)
|
|
249
|
+
q_obs, min_rect = _compute_Q(flat_obs, n_total)
|
|
250
|
+
|
|
251
|
+
if n_total == 0:
|
|
252
|
+
raise ValueError("cannot compute scan statistic for empty data (n_total=0)")
|
|
253
|
+
|
|
254
|
+
bound = 0.0
|
|
255
|
+
for rect in _RECTS:
|
|
256
|
+
p = rect[5]
|
|
257
|
+
bound += _min_tail_prob_at_least_as_extreme(n_total, p, q_obs)
|
|
258
|
+
bound = min(bound, 1.0) # a union bound can nominally exceed 1
|
|
259
|
+
|
|
260
|
+
z_score = stats.norm.isf(bound)
|
|
261
|
+
lp = np.inf if bound == 0 else -np.log(bound)
|
|
262
|
+
if return_rect:
|
|
263
|
+
return (bound, z_score, lp, q_obs, min_rect)
|
|
264
|
+
return (bound, z_score, lp, q_obs)
|
|
265
|
+
|
|
266
|
+
# ---------------------------------------------------------------------------
|
|
267
|
+
# Demo
|
|
268
|
+
# ---------------------------------------------------------------------------
|
|
269
|
+
|
|
270
|
+
# if __name__ == "__main__":
|
|
271
|
+
# rng = np.random.default_rng(0)
|
|
272
|
+
|
|
273
|
+
# # Example: 40 observations thrown uniformly at random (should look "null-ish")
|
|
274
|
+
# example = rng.multinomial(40, np.full(25, 1 / 25)).reshape(5, 5)
|
|
275
|
+
# print("Example data:\n", example)
|
|
276
|
+
|
|
277
|
+
# t_obs = int(np.sum(example == 0))
|
|
278
|
+
# p_occ = occupancyStatisticTest(example)
|
|
279
|
+
# print(f"\nOccupancy test: t_obs = {t_obs}, P(T >= t_obs) = {p_occ:.4f}")
|
|
280
|
+
|
|
281
|
+
# q_obs, p_scan = scanStatisticTest(example, n_sims=2000000, random_state=1)
|
|
282
|
+
# print(f"Scan statistic test: q_obs = {q_obs:.4f}, P(Q <= q_obs) = {p_scan:.4f}")
|
|
283
|
+
|
|
284
|
+
# # Example designed to trip the tests: a clustered/skewed pattern
|
|
285
|
+
# skewed = np.array([
|
|
286
|
+
# [10, 10, 0, 0, 0],
|
|
287
|
+
# [10, 10, 0, 0, 0],
|
|
288
|
+
# [0, 0, 0, 0, 0],
|
|
289
|
+
# [0, 0, 0, 0, 0],
|
|
290
|
+
# [0, 0, 0, 0, 0],
|
|
291
|
+
# ])
|
|
292
|
+
# print("\nSkewed data:\n", skewed)
|
|
293
|
+
# t_obs2 = int(np.sum(skewed == 0))
|
|
294
|
+
# p_occ2 = occupancy_test(skewed)
|
|
295
|
+
# print(f"\nOccupancy test: t_obs = {t_obs2}, P(T >= t_obs) = {p_occ2:.6f}")
|
|
296
|
+
|
|
297
|
+
# q_obs2, p_scan2 = scan_statistic_test(skewed, n_sims=20000, random_state=1)
|
|
298
|
+
# print(f"Scan statistic test: q_obs = {q_obs2:.6f}, P(Q <= q_obs) = {p_scan2:.6f}")
|