sqsketch 0.2.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.
sqsketch-0.2.0/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Abderrahmane Sghairi
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,204 @@
1
+ Metadata-Version: 2.4
2
+ Name: sqsketch
3
+ Version: 0.2.0
4
+ Summary: Fixed-size, abundance-preserving sketches of count profiles over unbounded alphabets
5
+ Author: Abderrahmane Sghairi
6
+ License: MIT License
7
+
8
+ Copyright (c) 2026 Abderrahmane Sghairi
9
+
10
+ Permission is hereby granted, free of charge, to any person obtaining a copy
11
+ of this software and associated documentation files (the "Software"), to deal
12
+ in the Software without restriction, including without limitation the rights
13
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
14
+ copies of the Software, and to permit persons to whom the Software is
15
+ furnished to do so, subject to the following conditions:
16
+
17
+ The above copyright notice and this permission notice shall be included in all
18
+ copies or substantial portions of the Software.
19
+
20
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
21
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
22
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
23
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
24
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
25
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
26
+ SOFTWARE.
27
+
28
+ Project-URL: Homepage, https://github.com/riscoss63/sqsketch
29
+ Project-URL: Paper, https://github.com/riscoss63/sqsketch/tree/main/paper
30
+ Project-URL: DOI, https://doi.org/10.5281/zenodo.22214969
31
+ Keywords: sketching,Bhattacharyya,Hellinger,random projection,hyperdimensional computing,vector symbolic architectures
32
+ Classifier: Development Status :: 3 - Alpha
33
+ Classifier: Intended Audience :: Science/Research
34
+ Classifier: License :: OSI Approved :: MIT License
35
+ Classifier: Programming Language :: Python :: 3
36
+ Classifier: Topic :: Scientific/Engineering :: Mathematics
37
+ Requires-Python: >=3.9
38
+ Description-Content-Type: text/markdown
39
+ License-File: LICENSE
40
+ Requires-Dist: numpy>=1.21
41
+ Requires-Dist: scipy>=1.7
42
+ Provides-Extra: experiments
43
+ Requires-Dist: scikit-learn>=1.0; extra == "experiments"
44
+ Requires-Dist: torch>=2.0; extra == "experiments"
45
+ Requires-Dist: transformers>=4.30; extra == "experiments"
46
+ Provides-Extra: dev
47
+ Requires-Dist: pytest>=7.0; extra == "dev"
48
+ Dynamic: license-file
49
+
50
+ # sqsketch
51
+
52
+ [![DOI](https://zenodo.org/badge/DOI/10.5281/zenodo.22214969.svg)](https://doi.org/10.5281/zenodo.22214969)
53
+ [![tests](https://github.com/riscoss63/sqsketch/actions/workflows/ci.yml/badge.svg)](https://github.com/riscoss63/sqsketch/actions/workflows/ci.yml)
54
+ [![licence: MIT](https://img.shields.io/badge/licence-MIT-blue.svg)](LICENSE)
55
+
56
+ **Compare two probability or count profiles from a fixed number of bytes, however large the
57
+ alphabet.**
58
+
59
+ ```python
60
+ from sqsketch import Sketch
61
+
62
+ a = Sketch.from_dict({"apple": 12, "pear": 3, "quince": 1})
63
+ b = Sketch.from_dense(probability_vector, D=1024)
64
+
65
+ a.similarity(b) # Bhattacharyya coefficient
66
+ a.hellinger(b) # Hellinger distance
67
+ a.confidence_interval(b) # computed from the two sketches alone, without the profiles
68
+ a.kl_lower_bound(b) # certified: the KL divergence is at least this
69
+ a.merge(b) # a sketch of the pooled profile
70
+ ```
71
+
72
+ Each sketch is `D` numbers. No vocabulary, no codebook, no inverted index; encoding is one
73
+ pass over the items. The accuracy depends on `D` alone — the number of *possible* items never
74
+ enters the error, so the same width serves an alphabet of a thousand or of 4³¹.
75
+
76
+ ```bash
77
+ pip install -e . # numpy and scipy, nothing else
78
+ pytest # 26 tests, one per claim in the paper, ~18 s
79
+ ```
80
+
81
+ ## Should you use it? One number decides
82
+
83
+ The thing you are already doing — keeping the `k` heaviest items and lumping the rest into
84
+ one bucket — is the competitor. Top-`k` logprobs, frequent-item tables, truncated term
85
+ vectors are all this. What truncation cannot represent is the mass it throws away, so
86
+ measure that:
87
+
88
+ ```python
89
+ from sqsketch.baselines import tail_mass
90
+ tail_mass(your_profiles, k) # mass outside the top k, at your byte budget
91
+ ```
92
+
93
+ | tail mass at your budget | verdict |
94
+ |--------------------------|---------|
95
+ | below ≈ 0.10 | keep the top `k` — simpler, and more accurate |
96
+ | 0.15 – 0.4 | sketch wins, by 1.4× to 4× |
97
+ | above 0.7 | sketch wins, by 4× to 7× |
98
+
99
+ The *effective support* `1/Σp²` is **not** the predictor: across the sweep that produced this
100
+ table it ranged from 4 to 800 000 without changing the verdict. Measured on real data, the
101
+ criterion called 14 of 14 cases correctly:
102
+
103
+ | domain | alphabet | tail mass | outcome |
104
+ |--------|---------:|----------:|---------|
105
+ | 21-mer abundance profiles (10 NCBI genomes) | 1 432 940 | 0.992 | sketch, 6.7× |
106
+ | USDT transfer counts per address (live chain) | 27 049 | 0.589 | sketch, 10× |
107
+ | personalised PageRank, 200 000-node graph | 200 000 | 0.502 | sketch, 2.1× |
108
+ | GPT-2 output aggregated over a corpus | 50 257 | 0.412 | sketch, 6.3× |
109
+ | GPT-2 next token, one position | 50 257 | 0.105 | truncation |
110
+ | USDT transfer *value* per address | 27 049 | 0.037 | truncation |
111
+ | document term counts | 45 969 | 0.039 | tie |
112
+ | binned returns, trade sizes (Binance) | 400 / 300 | 0.000 | truncation, exactly |
113
+
114
+ Value-weighted flows are dominated by a handful of addresses; activity counts are spread
115
+ over tens of thousands. Same data, opposite verdicts — which is why the criterion is worth
116
+ measuring rather than guessing.
117
+
118
+ ## What it will not do
119
+
120
+ - **Exact top-1 retrieval among near-identical neighbours.** Accuracy is governed by the gap
121
+ between the true nearest neighbour and the runner-up, against the noise floor `√(2/D)`.
122
+ On a real text corpus that gap is ~0.03 and recall@1 falls apart; recall@10 stays at 97 %.
123
+ Use `Index.search` as a candidate generator and rerank the shortlist exactly.
124
+ - **Sampling-noise-dominated histograms.** If each profile is a small sample from a much
125
+ larger alphabet, Hellinger between two empirical histograms mostly measures sample
126
+ overlap. That is a property of the statistic, not of the sketch, but it rules the approach
127
+ out there.
128
+ - **Upper-bounding the KL divergence.** `kl_lower_bound` is one-sided by construction: it
129
+ certifies that two profiles are far apart, never that they are close.
130
+ - **Forecasting anything.** It measures a distance between two distributions. It has no
131
+ notion of time, and confers no predictive edge.
132
+
133
+ ## Accuracy
134
+
135
+ Unbiased at every width, with variance `σ²/D` where `σ² = 1 + BC² − 2⟨Q,P⟩ < 2` for every
136
+ pair and every alphabet size, so the standard error is at most `√(2/D)`:
137
+
138
+ | `D` | bytes (float32) | standard error at most |
139
+ |-----|-----------------|------------------------|
140
+ | 256 | 1 KB | 0.088 |
141
+ | 1024 | 4 KB | 0.044 |
142
+ | 4096 | 16 KB | 0.022 |
143
+
144
+ `confidence_interval` is asymptotic in `D` and under-covers below `D ≈ 256`; above that it is
145
+ valid and deliberately conservative, since its width is calibrated for the raw inner product
146
+ while `similarity` returns the lower-variance self-normalised cosine.
147
+
148
+ ## How to audit this
149
+
150
+ Every claim is checked twice: as a unit test, and as an end-to-end reproduction.
151
+
152
+ ```bash
153
+ pytest # 26 tests, one per proposition
154
+ cd experiments
155
+ python reproduce.py > outputs/reproduce_output.txt # 14 sections
156
+ python survey.py > outputs/survey_output.txt # the decision criterion
157
+ python verify.py # 36 checks, 5 batteries
158
+ ```
159
+
160
+ `verify.py` is the part worth knowing about. Beyond checking the mathematics, **battery 3
161
+ extracts every experimental number printed in the paper and requires it to appear in a
162
+ script's output.** The manuscript this work supersedes reported a correlation from one
163
+ column of a table as though it came from another; that class of error is invisible to
164
+ proofreading, so it is checked mechanically. It currently matches 142 of 142.
165
+
166
+ ## Repository
167
+
168
+ ```
169
+ sqsketch/ the library: core.py, hashing.py, baselines.py
170
+ adapters: genomics.py (k-mers, FASTA, MinHash baselines), llm.py
171
+ tests/ one test per proposition
172
+ paper/ square_root_sketch.tex, and the superseded v1 draft it retracts
173
+ experiments/ everything that produces a number in the paper, plus verify.py
174
+ data/ reference genomes, downloaded on demand (not in git)
175
+ ```
176
+
177
+ ## The paper
178
+
179
+ *Norm-Invariance in Vector-Symbolic Encodings of Probability Distributions* — why the square
180
+ root is the only exponent that makes the error independent of the alphabet size, what the
181
+ vector's magnitude therefore cannot encode, and how to read one bit.
182
+
183
+ It carries three explicit retractions of earlier claims, an eight-item limitations section,
184
+ and 20 references each checked against the publisher record. The variance formula it uses is
185
+ **not new** and is attributed throughout to Li, Hastie and Church (2006).
186
+
187
+ ## Citing
188
+
189
+ ```bibtex
190
+ @software{sghairi2026sqsketch,
191
+ author = {Sghairi, Abderrahmane},
192
+ title = {sqsketch: alphabet-independent sketches of discrete
193
+ probability and count profiles},
194
+ year = {2026},
195
+ version = {0.2.0},
196
+ doi = {10.5281/zenodo.22214969},
197
+ url = {https://github.com/riscoss63/sqsketch}
198
+ }
199
+ ```
200
+
201
+ ## Licence
202
+
203
+ MIT for the code. The Zenodo record is deposited under the same terms; note that the
204
+ manuscript in `paper/` is part of the same deposit.
@@ -0,0 +1,155 @@
1
+ # sqsketch
2
+
3
+ [![DOI](https://zenodo.org/badge/DOI/10.5281/zenodo.22214969.svg)](https://doi.org/10.5281/zenodo.22214969)
4
+ [![tests](https://github.com/riscoss63/sqsketch/actions/workflows/ci.yml/badge.svg)](https://github.com/riscoss63/sqsketch/actions/workflows/ci.yml)
5
+ [![licence: MIT](https://img.shields.io/badge/licence-MIT-blue.svg)](LICENSE)
6
+
7
+ **Compare two probability or count profiles from a fixed number of bytes, however large the
8
+ alphabet.**
9
+
10
+ ```python
11
+ from sqsketch import Sketch
12
+
13
+ a = Sketch.from_dict({"apple": 12, "pear": 3, "quince": 1})
14
+ b = Sketch.from_dense(probability_vector, D=1024)
15
+
16
+ a.similarity(b) # Bhattacharyya coefficient
17
+ a.hellinger(b) # Hellinger distance
18
+ a.confidence_interval(b) # computed from the two sketches alone, without the profiles
19
+ a.kl_lower_bound(b) # certified: the KL divergence is at least this
20
+ a.merge(b) # a sketch of the pooled profile
21
+ ```
22
+
23
+ Each sketch is `D` numbers. No vocabulary, no codebook, no inverted index; encoding is one
24
+ pass over the items. The accuracy depends on `D` alone — the number of *possible* items never
25
+ enters the error, so the same width serves an alphabet of a thousand or of 4³¹.
26
+
27
+ ```bash
28
+ pip install -e . # numpy and scipy, nothing else
29
+ pytest # 26 tests, one per claim in the paper, ~18 s
30
+ ```
31
+
32
+ ## Should you use it? One number decides
33
+
34
+ The thing you are already doing — keeping the `k` heaviest items and lumping the rest into
35
+ one bucket — is the competitor. Top-`k` logprobs, frequent-item tables, truncated term
36
+ vectors are all this. What truncation cannot represent is the mass it throws away, so
37
+ measure that:
38
+
39
+ ```python
40
+ from sqsketch.baselines import tail_mass
41
+ tail_mass(your_profiles, k) # mass outside the top k, at your byte budget
42
+ ```
43
+
44
+ | tail mass at your budget | verdict |
45
+ |--------------------------|---------|
46
+ | below ≈ 0.10 | keep the top `k` — simpler, and more accurate |
47
+ | 0.15 – 0.4 | sketch wins, by 1.4× to 4× |
48
+ | above 0.7 | sketch wins, by 4× to 7× |
49
+
50
+ The *effective support* `1/Σp²` is **not** the predictor: across the sweep that produced this
51
+ table it ranged from 4 to 800 000 without changing the verdict. Measured on real data, the
52
+ criterion called 14 of 14 cases correctly:
53
+
54
+ | domain | alphabet | tail mass | outcome |
55
+ |--------|---------:|----------:|---------|
56
+ | 21-mer abundance profiles (10 NCBI genomes) | 1 432 940 | 0.992 | sketch, 6.7× |
57
+ | USDT transfer counts per address (live chain) | 27 049 | 0.589 | sketch, 10× |
58
+ | personalised PageRank, 200 000-node graph | 200 000 | 0.502 | sketch, 2.1× |
59
+ | GPT-2 output aggregated over a corpus | 50 257 | 0.412 | sketch, 6.3× |
60
+ | GPT-2 next token, one position | 50 257 | 0.105 | truncation |
61
+ | USDT transfer *value* per address | 27 049 | 0.037 | truncation |
62
+ | document term counts | 45 969 | 0.039 | tie |
63
+ | binned returns, trade sizes (Binance) | 400 / 300 | 0.000 | truncation, exactly |
64
+
65
+ Value-weighted flows are dominated by a handful of addresses; activity counts are spread
66
+ over tens of thousands. Same data, opposite verdicts — which is why the criterion is worth
67
+ measuring rather than guessing.
68
+
69
+ ## What it will not do
70
+
71
+ - **Exact top-1 retrieval among near-identical neighbours.** Accuracy is governed by the gap
72
+ between the true nearest neighbour and the runner-up, against the noise floor `√(2/D)`.
73
+ On a real text corpus that gap is ~0.03 and recall@1 falls apart; recall@10 stays at 97 %.
74
+ Use `Index.search` as a candidate generator and rerank the shortlist exactly.
75
+ - **Sampling-noise-dominated histograms.** If each profile is a small sample from a much
76
+ larger alphabet, Hellinger between two empirical histograms mostly measures sample
77
+ overlap. That is a property of the statistic, not of the sketch, but it rules the approach
78
+ out there.
79
+ - **Upper-bounding the KL divergence.** `kl_lower_bound` is one-sided by construction: it
80
+ certifies that two profiles are far apart, never that they are close.
81
+ - **Forecasting anything.** It measures a distance between two distributions. It has no
82
+ notion of time, and confers no predictive edge.
83
+
84
+ ## Accuracy
85
+
86
+ Unbiased at every width, with variance `σ²/D` where `σ² = 1 + BC² − 2⟨Q,P⟩ < 2` for every
87
+ pair and every alphabet size, so the standard error is at most `√(2/D)`:
88
+
89
+ | `D` | bytes (float32) | standard error at most |
90
+ |-----|-----------------|------------------------|
91
+ | 256 | 1 KB | 0.088 |
92
+ | 1024 | 4 KB | 0.044 |
93
+ | 4096 | 16 KB | 0.022 |
94
+
95
+ `confidence_interval` is asymptotic in `D` and under-covers below `D ≈ 256`; above that it is
96
+ valid and deliberately conservative, since its width is calibrated for the raw inner product
97
+ while `similarity` returns the lower-variance self-normalised cosine.
98
+
99
+ ## How to audit this
100
+
101
+ Every claim is checked twice: as a unit test, and as an end-to-end reproduction.
102
+
103
+ ```bash
104
+ pytest # 26 tests, one per proposition
105
+ cd experiments
106
+ python reproduce.py > outputs/reproduce_output.txt # 14 sections
107
+ python survey.py > outputs/survey_output.txt # the decision criterion
108
+ python verify.py # 36 checks, 5 batteries
109
+ ```
110
+
111
+ `verify.py` is the part worth knowing about. Beyond checking the mathematics, **battery 3
112
+ extracts every experimental number printed in the paper and requires it to appear in a
113
+ script's output.** The manuscript this work supersedes reported a correlation from one
114
+ column of a table as though it came from another; that class of error is invisible to
115
+ proofreading, so it is checked mechanically. It currently matches 142 of 142.
116
+
117
+ ## Repository
118
+
119
+ ```
120
+ sqsketch/ the library: core.py, hashing.py, baselines.py
121
+ adapters: genomics.py (k-mers, FASTA, MinHash baselines), llm.py
122
+ tests/ one test per proposition
123
+ paper/ square_root_sketch.tex, and the superseded v1 draft it retracts
124
+ experiments/ everything that produces a number in the paper, plus verify.py
125
+ data/ reference genomes, downloaded on demand (not in git)
126
+ ```
127
+
128
+ ## The paper
129
+
130
+ *Norm-Invariance in Vector-Symbolic Encodings of Probability Distributions* — why the square
131
+ root is the only exponent that makes the error independent of the alphabet size, what the
132
+ vector's magnitude therefore cannot encode, and how to read one bit.
133
+
134
+ It carries three explicit retractions of earlier claims, an eight-item limitations section,
135
+ and 20 references each checked against the publisher record. The variance formula it uses is
136
+ **not new** and is attributed throughout to Li, Hastie and Church (2006).
137
+
138
+ ## Citing
139
+
140
+ ```bibtex
141
+ @software{sghairi2026sqsketch,
142
+ author = {Sghairi, Abderrahmane},
143
+ title = {sqsketch: alphabet-independent sketches of discrete
144
+ probability and count profiles},
145
+ year = {2026},
146
+ version = {0.2.0},
147
+ doi = {10.5281/zenodo.22214969},
148
+ url = {https://github.com/riscoss63/sqsketch}
149
+ }
150
+ ```
151
+
152
+ ## Licence
153
+
154
+ MIT for the code. The Zenodo record is deposited under the same terms; note that the
155
+ manuscript in `paper/` is part of the same deposit.
@@ -0,0 +1,39 @@
1
+ [build-system]
2
+ requires = ["setuptools>=61"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "sqsketch"
7
+ version = "0.2.0"
8
+ description = "Fixed-size, abundance-preserving sketches of count profiles over unbounded alphabets"
9
+ readme = "README.md"
10
+ requires-python = ">=3.9"
11
+ license = { file = "LICENSE" }
12
+ authors = [{ name = "Abderrahmane Sghairi" }]
13
+ keywords = ["sketching", "Bhattacharyya", "Hellinger", "random projection",
14
+ "hyperdimensional computing", "vector symbolic architectures"]
15
+ classifiers = [
16
+ "Development Status :: 3 - Alpha",
17
+ "Intended Audience :: Science/Research",
18
+ "License :: OSI Approved :: MIT License",
19
+ "Programming Language :: Python :: 3",
20
+ "Topic :: Scientific/Engineering :: Mathematics",
21
+ ]
22
+ dependencies = ["numpy>=1.21", "scipy>=1.7"]
23
+
24
+ [project.optional-dependencies]
25
+ # Only the experiments need these. The library itself needs numpy and scipy.
26
+ experiments = ["scikit-learn>=1.0", "torch>=2.0", "transformers>=4.30"]
27
+ dev = ["pytest>=7.0"]
28
+
29
+ [project.urls]
30
+ Homepage = "https://github.com/riscoss63/sqsketch"
31
+ Paper = "https://github.com/riscoss63/sqsketch/tree/main/paper"
32
+ DOI = "https://doi.org/10.5281/zenodo.22214969"
33
+
34
+ [tool.setuptools]
35
+ packages = ["sqsketch"]
36
+
37
+ [tool.pytest.ini_options]
38
+ testpaths = ["tests"]
39
+ addopts = "-q"
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,21 @@
1
+ """sqsketch -- fixed-size, abundance-preserving sketches of count profiles.
2
+
3
+ The core is domain-agnostic: items are 64-bit codes. Adapters turn domain objects into
4
+ those codes.
5
+
6
+ from sqsketch import Sketch, Index # the core
7
+ from sqsketch import genomics # k-mers from FASTA
8
+ from sqsketch import llm # fingerprints of model output distributions
9
+
10
+ a = Sketch.from_dict({"apple": 12, "pear": 3}, D=1024)
11
+ b = Sketch.from_dense(probability_vector, D=1024)
12
+ a.similarity(b); a.hellinger(b); a.confidence_interval(b); a.kl_lower_bound(b)
13
+ a.merge(b)
14
+ """
15
+ from .core import Sketch, Index
16
+ from .hashing import mix64, stable_code, stable_codes
17
+ from .baselines import topk_bc, topk_bytes
18
+
19
+ __version__ = "0.2.0"
20
+ __all__ = ["Sketch", "Index", "mix64", "stable_code", "stable_codes",
21
+ "topk_bc", "topk_bytes"]
@@ -0,0 +1,61 @@
1
+ """The incumbent a sketch has to beat, stated once so every benchmark uses the same one.
2
+
3
+ Keeping the k heaviest items and lumping the rest into a single bucket is what almost
4
+ every system already does, under one name or another: top-k logprobs, frequent-items
5
+ tables, truncated term vectors. It is simple, exact on the head, and blind to the tail.
6
+ """
7
+ from __future__ import annotations
8
+
9
+ import numpy as np
10
+
11
+ __all__ = ["topk_bc", "topk_bytes", "sketch_bytes", "effective_support", "tail_mass"]
12
+
13
+
14
+ def topk_bytes(k: int, id_bits: int = 20, value_bits: int = 16) -> int:
15
+ """Bytes to store k (id, value) pairs. Defaults suit a million-item alphabet."""
16
+ return int(np.ceil(k * (id_bits + value_bits) / 8))
17
+
18
+
19
+ def sketch_bytes(D: int, dtype=np.float32) -> int:
20
+ return int(D * np.dtype(dtype).itemsize)
21
+
22
+
23
+ def topk_bc(P, Q, k):
24
+ """Bhattacharyya coefficient between two profiles, each truncated to its own top k.
25
+
26
+ Both sides keep one extra bucket for the mass they dropped, which is the fairest
27
+ version of the baseline: without it the truncated vectors would not be distributions.
28
+ """
29
+ P = np.atleast_2d(np.asarray(P, dtype=np.float64))
30
+ Q = np.atleast_2d(np.asarray(Q, dtype=np.float64))
31
+ out = np.empty(P.shape[0])
32
+ for i in range(P.shape[0]):
33
+ k_i = min(k, P.shape[1] - 1)
34
+ tp = np.argpartition(-P[i], k_i)[:k_i]
35
+ tq = np.argpartition(-Q[i], k_i)[:k_i]
36
+ keys = np.union1d(tp, tq)
37
+ a, b = P[i][keys], Q[i][keys]
38
+ a = np.append(a, max(0.0, P[i].sum() - a.sum()))
39
+ b = np.append(b, max(0.0, Q[i].sum() - b.sum()))
40
+ sa, sb = a.sum(), b.sum()
41
+ a = a / (sa or 1.0)
42
+ b = b / (sb or 1.0)
43
+ out[i] = float(np.sum(np.sqrt(a * b)))
44
+ return out if out.size > 1 else float(out[0])
45
+
46
+
47
+ def effective_support(P):
48
+ """1 / sum(p_i^2): how many items are 'really' present. The first of the two numbers
49
+ that decide whether a sketch is the right tool."""
50
+ P = np.atleast_2d(np.asarray(P, dtype=np.float64))
51
+ P = P / P.sum(1, keepdims=True)
52
+ return 1.0 / np.sum(P ** 2, 1)
53
+
54
+
55
+ def tail_mass(P, k):
56
+ """Mass left outside the top k. The second deciding number: a long tail is what
57
+ truncation cannot represent and a sketch can."""
58
+ P = np.atleast_2d(np.asarray(P, dtype=np.float64))
59
+ P = P / P.sum(1, keepdims=True)
60
+ kk = min(k, P.shape[1])
61
+ return 1.0 - np.sort(P, 1)[:, -kk:].sum(1)