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.
@@ -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,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,5 @@
1
+ """SSTT visualisation helpers for scan-statistic and occupancy summaries."""
2
+
3
+ from .plot import statisticsVisual
4
+
5
+ __all__ = ["statisticsVisual"]
@@ -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}")